207 lines
7.5 KiB
PHP
207 lines
7.5 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Models\Business\InventoryWarehouseNameMapping;
|
||
use Carbon\CarbonImmutable;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\DB;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
|
||
/**
|
||
* 进销存报表 → 入库明细数据:按原始 Excel 原样导入,仓库简称用仓库名称映射表按「仓库」唯一匹配。
|
||
*/
|
||
class ImportInventoryInboundDetail extends Command
|
||
{
|
||
protected $signature = 'inventory:inbound-detail:import {file : 入库明细表 Excel} {--month= : 统一指定月份(不传则按创建时间判断)}';
|
||
|
||
protected $description = '导入进销存报表的入库明细数据';
|
||
|
||
public function handle(): int
|
||
{
|
||
$file = (string) $this->argument('file');
|
||
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
|
||
$table = 'inventory_inbound_detail_rows';
|
||
$forcedMonth = trim((string) ($this->option('month') ?? ''));
|
||
$now = now();
|
||
|
||
$shortNames = $this->shortNameMap();
|
||
$this->info('仓库名称映射:' . count($shortNames) . ' 个仓库名可唯一匹配');
|
||
|
||
// 原样导入:以本次文件为准整体重建,不做去重、不做加工。
|
||
DB::connection($connection)->table($table)->delete();
|
||
|
||
$reader = IOFactory::createReaderForFile($file);
|
||
$reader->setReadDataOnly(true);
|
||
$sheet = $reader->load($file)->getSheet(0);
|
||
|
||
$batch = [];
|
||
$imported = 0;
|
||
$mapped = 0;
|
||
$skipped = 0;
|
||
foreach ($sheet->getRowIterator(2) as $row) {
|
||
$rowNumber = $row->getRowIndex();
|
||
$cells = [];
|
||
foreach ($row->getCellIterator('A', 'U') as $cell) {
|
||
$cells[] = $cell->getFormattedValue();
|
||
}
|
||
$warehouse = trim((string) ($cells[14] ?? ''));
|
||
$createdRaw = trim((string) ($cells[16] ?? ''));
|
||
if ($warehouse === '' && $createdRaw === '') {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$month = $forcedMonth !== '' ? $forcedMonth : $this->monthOf($createdRaw);
|
||
if ($month === '') {
|
||
$skipped++;
|
||
continue;
|
||
}
|
||
|
||
$shortName = $warehouse !== '' ? ($shortNames[$warehouse] ?? '') : '';
|
||
if ($shortName !== '') {
|
||
$mapped++;
|
||
}
|
||
|
||
$batch[] = [
|
||
'report_month' => $month . '-01',
|
||
// 唯一键按 Excel 行号生成,保留原表每一行(含重复行)。
|
||
'row_key' => sha1($month . '|' . $rowNumber),
|
||
'source_no' => $this->text($cells[0] ?? ''),
|
||
'document_no' => $this->text($cells[1] ?? ''),
|
||
'created_serial' => $this->text($cells[2] ?? ''),
|
||
'supplier' => $this->text($cells[3] ?? ''),
|
||
'item_no' => $this->text($cells[4] ?? ''),
|
||
'barcode' => $this->text($cells[5] ?? ''),
|
||
'spec_name' => $this->text($cells[6] ?? ''),
|
||
'quantity' => $this->number($cells[7] ?? ''),
|
||
'unit_price' => $this->number($cells[8] ?? ''),
|
||
'amount' => $this->number($cells[9] ?? ''),
|
||
'estimated_unit_price' => $this->number($cells[10] ?? ''),
|
||
'estimated_amount' => $this->number($cells[11] ?? ''),
|
||
'supplier_barcode' => $this->text($cells[12] ?? ''),
|
||
'remark' => $this->text($cells[13] ?? ''),
|
||
'warehouse' => $warehouse !== '' ? $warehouse : null,
|
||
'warehouse_short_name' => $shortName !== '' ? $shortName : null,
|
||
'created_at_raw' => $this->moment($createdRaw),
|
||
'express_name' => $this->text($cells[17] ?? ''),
|
||
'express_no' => $this->text($cells[18] ?? ''),
|
||
'not_sent' => $this->text($cells[19] ?? ''),
|
||
'owner_name' => $this->text($cells[20] ?? ''),
|
||
'imported_at' => $now,
|
||
];
|
||
$imported++;
|
||
|
||
if (count($batch) >= 1000) {
|
||
DB::connection($connection)->table($table)->insertOrIgnore($batch);
|
||
$this->info('已写入 ' . $imported . ' 行');
|
||
$batch = [];
|
||
}
|
||
}
|
||
|
||
if ($batch !== []) {
|
||
DB::connection($connection)->table($table)->insertOrIgnore($batch);
|
||
}
|
||
|
||
$this->info(sprintf(
|
||
'导入完成:%d 行,仓库简称匹配上 %d 行,跳过 %d 行;库中该表共 %d 行',
|
||
$imported,
|
||
$mapped,
|
||
$skipped,
|
||
DB::connection($connection)->table($table)->count(),
|
||
));
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
/** @return array<string,string> 仓库名 => 仓库简称(仅唯一匹配的) */
|
||
private function shortNameMap(): array
|
||
{
|
||
$rows = InventoryWarehouseNameMapping::withoutGlobalScopes()
|
||
->get(['warehouse_provider', 'warehouse', 'warehouse_short_name']);
|
||
$grouped = [];
|
||
foreach ($rows as $row) {
|
||
$warehouse = trim((string) $row->warehouse);
|
||
if ($warehouse === '') {
|
||
continue;
|
||
}
|
||
$grouped[$warehouse][] = trim((string) $row->warehouse_short_name);
|
||
}
|
||
|
||
$map = [];
|
||
foreach ($grouped as $warehouse => $names) {
|
||
$unique = array_values(array_unique($names));
|
||
if (count($unique) === 1 && $unique[0] !== '') {
|
||
$map[$warehouse] = $unique[0];
|
||
}
|
||
}
|
||
|
||
// 明细表里“仓库”一列有时填的是仓储方名称(例如"工厂-沐野竹木代发仓"),
|
||
// 仓库名匹配不到时再按仓储方唯一匹配一次。
|
||
$byProvider = [];
|
||
foreach ($rows as $row) {
|
||
$provider = trim((string) $row->warehouse_provider);
|
||
if ($provider === '') {
|
||
continue;
|
||
}
|
||
$byProvider[$provider][] = trim((string) $row->warehouse_short_name);
|
||
}
|
||
foreach ($byProvider as $provider => $names) {
|
||
if (isset($map[$provider])) {
|
||
continue;
|
||
}
|
||
$unique = array_values(array_unique($names));
|
||
if (count($unique) === 1 && $unique[0] !== '') {
|
||
$map[$provider] = $unique[0];
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
private function monthOf(string $value): string
|
||
{
|
||
$value = trim($value);
|
||
if ($value === '') {
|
||
return '';
|
||
}
|
||
foreach (['Y/m/d H:i:s', 'Y-m-d H:i:s', 'Y/m/d', 'Y-m-d'] as $format) {
|
||
$parsed = \DateTimeImmutable::createFromFormat($format, $value);
|
||
if ($parsed instanceof \DateTimeImmutable) {
|
||
return $parsed->format('Y-m');
|
||
}
|
||
}
|
||
$timestamp = strtotime($value);
|
||
|
||
return $timestamp === false ? '' : date('Y-m', $timestamp);
|
||
}
|
||
|
||
private function moment(string $value): ?string
|
||
{
|
||
$value = trim($value);
|
||
if ($value === '') {
|
||
return null;
|
||
}
|
||
$timestamp = strtotime($value);
|
||
|
||
return $timestamp === false ? null : date('Y-m-d H:i:s', $timestamp);
|
||
}
|
||
|
||
private function text(mixed $value): ?string
|
||
{
|
||
$text = trim((string) $value);
|
||
|
||
return $text === '' ? null : $text;
|
||
}
|
||
|
||
private function number(mixed $value): ?float
|
||
{
|
||
$text = str_replace(',', '', trim((string) $value));
|
||
if ($text === '' || ! is_numeric($text)) {
|
||
return null;
|
||
}
|
||
|
||
return (float) $text;
|
||
}
|
||
}
|