|
|
@@ -0,0 +1,353 @@
|
|
|
+<?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;
|
|
|
+ }
|
|
|
+}
|