| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353 |
- <?php
- declare(strict_types=1);
- namespace App\Services;
- use App\Enums\ProductionOrderExecutionType;
- use App\Models\File;
- use App\Models\ProductionOrder;
- use App\Models\ProductionOrderDelivery;
- use App\Models\ProductionOrderInstallation;
- use Illuminate\Support\Collection;
- use Illuminate\Support\Facades\Storage;
- use Illuminate\Support\Str;
- use PhpOffice\PhpSpreadsheet\Cell\DataType;
- use PhpOffice\PhpSpreadsheet\IOFactory;
- use PhpOffice\PhpSpreadsheet\Spreadsheet;
- use PhpOffice\PhpSpreadsheet\Style\Alignment;
- use PhpOffice\PhpSpreadsheet\Style\Border;
- use PhpOffice\PhpSpreadsheet\Writer\Xls;
- use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
- use RuntimeException;
- use ZipArchive;
- class ProductionOrderDocumentService
- {
- public const TYPE_ITEMS = 'items';
- public const TYPE_DELIVERY = 'delivery';
- public const TYPE_INSTALLATION = 'installation';
- public const TYPE_TECHNICAL_DOCUMENTS = 'technical_documents';
- public function generate(
- string $type,
- int $orderId,
- int $userId,
- ?int $relatedId = null,
- array $itemIds = [],
- ): File {
- $order = ProductionOrder::query()
- ->with([
- 'manager',
- 'items.catalogItem.documents',
- 'items.passportFile',
- 'documents',
- ])
- ->findOrFail($orderId);
- return match ($type) {
- self::TYPE_ITEMS => $this->exportItems($order, $userId),
- self::TYPE_DELIVERY => $this->deliveryRequest($order, (int) $relatedId, $userId),
- self::TYPE_INSTALLATION => $this->installationPack($order, (int) $relatedId, $userId),
- self::TYPE_TECHNICAL_DOCUMENTS => $this->technicalDocuments($order, $itemIds, $userId),
- default => throw new RuntimeException('Неизвестный тип документа заказа.'),
- };
- }
- private function exportItems(ProductionOrder $order, int $userId): File
- {
- $spreadsheet = new Spreadsheet;
- $sheet = $spreadsheet->getActiveSheet();
- $sheet->setTitle('МАФ');
- $sheet->mergeCells('A1:F1');
- $sheet->setCellValue('A1', 'Оборудование заказа №'.$order->order_number);
- $sheet->mergeCells('A2:F2');
- $sheet->setCellValue('A2', sprintf(
- 'Заказчик: %s; адрес: %s; счёт №%s',
- $order->customer_name,
- $order->object_address,
- $order->invoice_number,
- ));
- $sheet->fromArray([
- 'Артикул',
- 'Наименование',
- 'Номер заказа МАФ',
- 'Заводской номер',
- 'Дата производства',
- 'Паспорт',
- ], null, 'A4');
- $row = 5;
- foreach ($order->items as $item) {
- $sheet->setCellValueExplicit("A{$row}", (string) $item->catalogItem?->article, DataType::TYPE_STRING);
- $sheet->setCellValue("B{$row}", $item->catalogItem?->calculator_name ?: $item->catalogItem?->print_name);
- $sheet->setCellValueExplicit("C{$row}", (string) $item->order_item_number, DataType::TYPE_STRING);
- $sheet->setCellValueExplicit("D{$row}", (string) $item->factory_number, DataType::TYPE_STRING);
- $sheet->setCellValue("E{$row}", $item->manufacture_date?->format('d.m.Y'));
- $sheet->setCellValue("F{$row}", $item->passport_file_id ? 'Да' : 'Нет');
- $row++;
- }
- $lastRow = max(4, $row - 1);
- $sheet->getStyle("A4:F{$lastRow}")->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
- $sheet->getStyle("A1:F{$lastRow}")->getAlignment()->setWrapText(true)->setVertical(Alignment::VERTICAL_TOP);
- foreach (['A' => 18, 'B' => 42, 'C' => 24, 'D' => 22, 'E' => 20, 'F' => 14] as $column => $width) {
- $sheet->getColumnDimension($column)->setWidth($width);
- }
- $filename = $this->safeFilename("МАФ_заказа_{$order->order_number}.xlsx");
- $file = $this->writeSpreadsheet($spreadsheet, $filename, $userId, new Xlsx($spreadsheet));
- $spreadsheet->disconnectWorksheets();
- return $file;
- }
- private function deliveryRequest(ProductionOrder $order, int $deliveryId, int $userId): File
- {
- $delivery = $order->deliveries()->with('driver')->findOrFail($deliveryId);
- $template = base_path('templates/DeliveryRequest.xls');
- if (! is_file($template)) {
- throw new RuntimeException('Шаблон заявки на доставку не найден.');
- }
- $spreadsheet = IOFactory::load($template);
- $sheet = $spreadsheet->getActiveSheet();
- $sheet->getPageSetup()->setPrintArea('B1:AG94');
- $sheet->setCellValue('B2', 'Заявка на доставку/самовывоз оборудования от '.now()->format('d.m.Y'));
- $sheet->setCellValue('G4', $order->invoice_number);
- $sheet->setCellValue('N4', $order->invoice_date?->format('d.m.Y'));
- $sheet->setCellValue('G5', $order->contract_number);
- $sheet->setCellValue('N5', $order->contract_date?->format('d.m.Y'));
- $sheet->setCellValue('G6', $order->order_number);
- $sheet->setCellValue('F8', $order->manager?->name);
- $sheet->setCellValue('V8', $order->manager?->phone);
- $sheet->setCellValue('B11', $delivery->carrier ?: $delivery->driver?->name);
- $sheet->setCellValue('O11', trim(implode(' / ', array_filter([
- $delivery->request_number,
- $delivery->request_date?->format('d.m.Y'),
- ]))));
- $sheet->setCellValue('S11', $this->shortTime($delivery->delivery_time));
- $sheet->setCellValue('W11', $delivery->carrier_note);
- $sheet->setCellValue('L12', $order->customer_name);
- $sheet->setCellValue('C13', $delivery->contact_position);
- $sheet->setCellValue('L13', $delivery->contact_name);
- $sheet->setCellValue('Y13', $delivery->contact_phone);
- $sheet->setCellValue('L16', $order->execution_type === ProductionOrderExecutionType::Pickup ? 'Самовывоз' : 'Доставка');
- $sheet->setCellValue('P16', $order->execution_type === ProductionOrderExecutionType::Pickup ? '' : 'Х');
- $sheet->setCellValue('Q16', $order->execution_type === ProductionOrderExecutionType::Pickup ? 'Х' : '');
- $sheet->setCellValue('L17', $order->object_address);
- $sheet->setCellValue('L18', $delivery->delivery_date->format('d.m.Y'));
- $sheet->setCellValue('L19', $this->shortTime($delivery->desired_time ?: $delivery->delivery_time));
- $sheet->setCellValue('L20', $delivery->access_system);
- $sheet->setCellValue('L21', $delivery->unloading_place);
- $sheet->setCellValue('L22', $delivery->advance_notice);
- $sheet->setCellValue('L23', $delivery->document_signing_method);
- $sheet->setCellValue('L24', $delivery->has_passports_and_certificates ? 'Да' : 'Нет');
- $sheet->setCellValue('G25', $this->equipmentSummary($order->items));
- $sheet->setCellValue('G26', $delivery->note);
- $sheet->setCellValue('B27', $order->manager?->name);
- $sheet->setCellValue('S28', now()->format('d.m.Y'));
- $filename = $this->safeFilename("Заявка_на_доставку_{$order->order_number}_{$delivery->delivery_date->format('Y-m-d')}.xls");
- $file = $this->writeSpreadsheet($spreadsheet, $filename, $userId, new Xls($spreadsheet));
- $spreadsheet->disconnectWorksheets();
- return $file;
- }
- private function installationPack(ProductionOrder $order, int $installationId, int $userId): File
- {
- $installation = $order->installations()->with('brigadier')->findOrFail($installationId);
- $template = base_path('templates/OrderForMount.xlsx');
- if (! is_file($template)) {
- throw new RuntimeException('Шаблон заявки на монтаж не найден.');
- }
- $temporaryDirectory = $this->temporaryDirectory();
- Storage::disk('local')->makeDirectory($temporaryDirectory);
- $requestFilename = $this->safeFilename("Заявка_на_монтаж_{$order->order_number}.xlsx");
- $requestPath = $temporaryDirectory.'/'.$requestFilename;
- $spreadsheet = IOFactory::load($template);
- $sheet = $spreadsheet->getActiveSheet();
- $sheet->setCellValue('F8', $order->manager?->name);
- $sheet->setCellValue('X8', $order->manager?->phone);
- $sheet->setCellValue('C12', $order->customer_name);
- $sheet->setCellValue('L14', $order->object_address);
- $sheet->setCellValue('L15', $installation->installation_date->format('d.m.Y'));
- $sheet->setCellValue('G33', $this->equipmentSummary($order->items));
- (new Xlsx($spreadsheet))->save(Storage::disk('local')->path($requestPath));
- $spreadsheet->disconnectWorksheets();
- $filename = $this->safeFilename("Монтажный_пакет_{$order->order_number}.zip");
- try {
- return $this->zipFiles(
- $order,
- $order->items,
- $userId,
- $filename,
- [$requestPath => $requestFilename],
- includeOrderDocuments: true,
- );
- } finally {
- Storage::disk('local')->deleteDirectory($temporaryDirectory);
- }
- }
- private function technicalDocuments(ProductionOrder $order, array $itemIds, int $userId): File
- {
- $ids = collect($itemIds)->map(static fn ($id): int => (int) $id)->filter()->unique();
- $items = $order->items->whereIn('id', $ids);
- if ($items->isEmpty() || $items->count() !== $ids->count()) {
- throw new RuntimeException('Выбранные МАФ не найдены в заказе.');
- }
- return $this->zipFiles(
- $order,
- $items,
- $userId,
- $this->safeFilename("Техдокументация_{$order->order_number}.zip"),
- );
- }
- /**
- * @param Collection<int, \App\Models\ProductionOrderItem> $items
- * @param array<string, string> $localFiles
- */
- private function zipFiles(
- ProductionOrder $order,
- Collection $items,
- int $userId,
- string $filename,
- array $localFiles = [],
- bool $includeOrderDocuments = false,
- ): File {
- $path = $this->generatedPath($userId, $filename);
- Storage::disk('local')->makeDirectory(dirname($path));
- $zip = new ZipArchive;
- if ($zip->open(Storage::disk('local')->path($path), ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
- throw new RuntimeException('Не удалось создать ZIP-архив.');
- }
- $added = 0;
- foreach ($localFiles as $filePath => $archiveName) {
- if (Storage::disk('local')->exists($filePath)) {
- $zip->addFile(Storage::disk('local')->path($filePath), $archiveName);
- $added++;
- }
- }
- foreach ($items->pluck('catalogItem')->filter()->unique('id') as $catalogItem) {
- foreach ($catalogItem->documents as $document) {
- if (! $document->path || ! Storage::disk('public')->exists($document->path)) {
- continue;
- }
- $archiveName = 'Техдокументация/'.$this->safeFilename((string) $catalogItem->article).'/'.$this->safeFilename($document->original_name);
- $zip->addFile(Storage::disk('public')->path($document->path), $this->uniqueArchiveName($zip, $archiveName));
- $added++;
- }
- }
- if ($includeOrderDocuments) {
- foreach ($order->documents as $document) {
- if (! $document->path || ! Storage::disk('public')->exists($document->path)) {
- continue;
- }
- $archiveName = 'Документы заказа/'.$this->safeFilename($document->original_name);
- $zip->addFile(Storage::disk('public')->path($document->path), $this->uniqueArchiveName($zip, $archiveName));
- $added++;
- }
- }
- $zip->close();
- if ($added === 0) {
- Storage::disk('local')->delete($path);
- throw new RuntimeException('Для выбранных МАФ нет технической документации.');
- }
- return $this->registerFile($path, $filename, 'application/zip', $userId);
- }
- private function writeSpreadsheet(Spreadsheet $spreadsheet, string $filename, int $userId, object $writer): File
- {
- $path = $this->generatedPath($userId, $filename);
- Storage::disk('local')->makeDirectory(dirname($path));
- $writer->save(Storage::disk('local')->path($path));
- $mimeType = Str::endsWith($filename, '.xls')
- ? 'application/vnd.ms-excel'
- : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
- return $this->registerFile($path, $filename, $mimeType, $userId);
- }
- private function registerFile(string $path, string $filename, string $mimeType, int $userId): File
- {
- $file = File::query()->create([
- 'link' => '',
- 'path' => $path,
- 'user_id' => $userId,
- 'original_name' => $filename,
- 'mime_type' => $mimeType,
- 'is_generated' => true,
- ]);
- $file->update(['link' => route('schedule.orders.files.download', $file)]);
- return $file;
- }
- private function equipmentSummary(Collection $items): string
- {
- return $items
- ->groupBy(static fn ($item): string => implode('|', [
- $item->catalogItem?->article,
- $item->catalogItem?->calculator_name ?: $item->catalogItem?->print_name,
- ]))
- ->map(static function (Collection $group): string {
- $item = $group->first();
- $name = $item->catalogItem?->calculator_name ?: $item->catalogItem?->print_name;
- return trim((string) $item->catalogItem?->article.' — '.$name).' — '.$group->count().' шт.';
- })
- ->implode("\n");
- }
- private function shortTime(mixed $value): string
- {
- return $value ? substr((string) $value, 0, 5) : '';
- }
- private function generatedPath(int $userId, string $filename): string
- {
- return "generated/production-orders/{$userId}/".Str::uuid()."/{$filename}";
- }
- private function temporaryDirectory(): string
- {
- return 'generated/production-orders/tmp/'.Str::uuid();
- }
- private function safeFilename(string $value): string
- {
- $value = preg_replace('/[^\pL\pN ._()-]+/u', '_', trim($value)) ?? '';
- return mb_substr($value === '' ? 'файл' : $value, 0, 180);
- }
- private function uniqueArchiveName(ZipArchive $zip, string $name): string
- {
- if ($zip->locateName($name) === false) {
- return $name;
- }
- $extension = pathinfo($name, PATHINFO_EXTENSION);
- $base = $extension === '' ? $name : substr($name, 0, -strlen($extension) - 1);
- for ($index = 2; $index < 1000; $index++) {
- $candidate = $base." ({$index})".($extension === '' ? '' : ".{$extension}");
- if ($zip->locateName($candidate) === false) {
- return $candidate;
- }
- }
- return $name;
- }
- }
|