238 lines
8.8 KiB
PHP
238 lines
8.8 KiB
PHP
<?php
|
|
|
|
namespace App\Business;
|
|
|
|
use App\Models\Business\InventoryWarehouseNameMapping;
|
|
use DateTimeImmutable;
|
|
use Illuminate\Support\Facades\DB;
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
use RuntimeException;
|
|
|
|
class InventoryInboundDetailImporter
|
|
{
|
|
private const TABLE = 'inventory_inbound_detail_rows';
|
|
|
|
/**
|
|
* Replace the imported inbound detail snapshot only after the workbook has
|
|
* been opened successfully. The delete and inserts share one transaction,
|
|
* so a malformed file cannot leave the page with a partially imported table.
|
|
*
|
|
* @return array{imported:int,mapped:int,skipped:int,total:int}
|
|
*/
|
|
public function import(string $file, string $forcedMonth = ''): array
|
|
{
|
|
$path = realpath($file);
|
|
if ($path === false || ! is_file($path) || ! is_readable($path)) {
|
|
throw new RuntimeException('入库明细文件不存在或不可读取。');
|
|
}
|
|
|
|
$forcedMonth = $this->normalizeForcedMonth($forcedMonth);
|
|
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
|
|
$now = now();
|
|
$maps = $this->shortNameMap();
|
|
|
|
$reader = IOFactory::createReaderForFile($path);
|
|
$reader->setReadDataOnly(true);
|
|
$spreadsheet = $reader->load($path);
|
|
$sheet = $spreadsheet->getSheet(0);
|
|
|
|
try {
|
|
return DB::connection($connection)->transaction(function () use (
|
|
$connection,
|
|
$sheet,
|
|
$maps,
|
|
$forcedMonth,
|
|
$now,
|
|
): array {
|
|
DB::connection($connection)->table(self::TABLE)->delete();
|
|
|
|
$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 = '';
|
|
if ($warehouse !== '') {
|
|
$shortName = str_contains($warehouse, '代发仓')
|
|
? ($maps['provider'][$warehouse] ?? $maps['warehouse'][$warehouse] ?? '')
|
|
: ($maps['warehouse'][$warehouse] ?? '');
|
|
}
|
|
if ($shortName !== '') {
|
|
$mapped++;
|
|
}
|
|
|
|
$batch[] = [
|
|
'report_month' => $month,
|
|
'row_key' => sha1(substr($month, 0, 7) . '|' . $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(self::TABLE)->insertOrIgnore($batch);
|
|
$batch = [];
|
|
}
|
|
}
|
|
|
|
if ($batch !== []) {
|
|
DB::connection($connection)->table(self::TABLE)->insertOrIgnore($batch);
|
|
}
|
|
if ($imported === 0) {
|
|
throw new RuntimeException('入库明细文件中没有可导入的数据。');
|
|
}
|
|
|
|
return [
|
|
'imported' => $imported,
|
|
'mapped' => $mapped,
|
|
'skipped' => $skipped,
|
|
'total' => DB::connection($connection)->table(self::TABLE)->count(),
|
|
];
|
|
});
|
|
} finally {
|
|
$spreadsheet->disconnectWorksheets();
|
|
}
|
|
}
|
|
|
|
private function normalizeForcedMonth(string $month): string
|
|
{
|
|
$month = trim($month);
|
|
if ($month === '') {
|
|
return '';
|
|
}
|
|
if (preg_match('/^\d{4}-\d{2}$/', $month) === 1) {
|
|
return $month . '-01';
|
|
}
|
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $month) === 1) {
|
|
return $month;
|
|
}
|
|
|
|
throw new RuntimeException('指定月份格式应为 YYYY-MM。');
|
|
}
|
|
|
|
/** @return array{warehouse:array<string,string>,provider: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);
|
|
}
|
|
|
|
$byWarehouse = [];
|
|
foreach ($grouped as $warehouse => $names) {
|
|
$unique = array_values(array_unique($names));
|
|
if (count($unique) === 1 && $unique[0] !== '') {
|
|
$byWarehouse[$warehouse] = $unique[0];
|
|
}
|
|
}
|
|
|
|
$byProvider = [];
|
|
foreach ($rows as $row) {
|
|
$provider = trim((string) $row->warehouse_provider);
|
|
if ($provider !== '') {
|
|
$byProvider[$provider][] = trim((string) $row->warehouse_short_name);
|
|
}
|
|
}
|
|
foreach ($byProvider as $provider => $names) {
|
|
$unique = array_values(array_unique($names));
|
|
if (count($unique) === 1 && $unique[0] !== '') {
|
|
$byProvider[$provider] = $unique[0];
|
|
}
|
|
}
|
|
|
|
return ['warehouse' => $byWarehouse, 'provider' => $byProvider];
|
|
}
|
|
|
|
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') . '-01';
|
|
}
|
|
}
|
|
$timestamp = strtotime($value);
|
|
|
|
return $timestamp === false ? '' : date('Y-m-01', $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;
|
|
}
|
|
}
|