From 836848f431e94e4459cb6c1fbd4988fd3816479e Mon Sep 17 00:00:00 2001 From: renshen <1005457301@qq.com> Date: Tue, 22 Sep 2026 14:49:09 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=BE=9B=E5=BA=94?= =?UTF-8?q?=E5=95=86=E5=BA=94=E4=BB=98=E4=BA=94=E9=A1=B9=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Controllers/Business/Pages.php | 24 +- .../Controllers/Business/ProductPrices.php | 7 + .../Business/SupplierPayableActions.php | 196 ++++++++++++++ config/database.php | 30 +++ ...reate_supplier_payable_override_tables.php | 50 ++++ deploy/local-init.sh | 5 + docker-compose.local.yml | 12 + .../Controllers/Api/GoodsReconciliations.php | 5 +- .../Http/Controllers/Api/SupplierPayables.php | 11 +- .../SupplierPayableRepository.php | 17 +- .../Services/ProductPriceWorkbookImporter.php | 19 ++ .../SupplierPayableImportTemplate.php | 89 +++++++ .../SupplierPayableMaintenanceRegistry.php | 167 ++++++++++++ .../Services/SupplierPayableOverrideStore.php | 107 ++++++++ .../SupplierPayableWorkbookImporter.php | 129 +++++++++ .../ecommerce-finance/advance-details.js | 4 +- .../ecommerce-finance/compensation-details.js | 4 +- .../ecommerce-finance/delivery-unpriced.js | 4 +- .../ecommerce-finance/dropshipping-express.js | 4 +- .../goods-reconciliations.js | 15 +- .../ecommerce-finance/invoice-deductions.js | 4 +- .../ecommerce-finance/other-adjustments.js | 4 +- .../return-reconciliations.js | 4 +- .../ecommerce-finance/return-unpriced.js | 4 +- .../ecommerce-finance/sample-deductions.js | 4 +- .../supplier-payable-maintenance.js | 166 ++++++++++++ .../advance-details.blade.php | 7 +- .../board-deductions.blade.php | 27 +- .../compensation-details.blade.php | 5 +- .../delivery-unpriced.blade.php | 7 +- .../dropshipping-express.blade.php | 7 +- .../invoice-deductions.blade.php | 7 +- .../maintenance-actions.blade.php | 10 + .../maintenance-blade-modal.blade.php | 109 ++++++++ .../maintenance-edit-button.blade.php | 6 + .../maintenance-import-modal.blade.php | 96 +++++++ .../maintenance-toolbar.blade.php | 43 +++ .../supplier-payables/navigation.blade.php | 101 +++++++ .../other-adjustments.blade.php | 7 +- .../payment-reconciliation.blade.php | 5 +- .../product-prices.blade.php | 224 +++++++--------- .../return-reconciliation.blade.php | 7 +- .../return-unpriced.blade.php | 7 +- .../sample-deductions.blade.php | 7 +- .../supplier-payables-report.blade.php | 27 +- routes/admin.php | 23 ++ tests/CreatesApplication.php | 1 + .../Business/ProductPriceActionsTest.php | 27 ++ .../Business/SupplierPayableActionsTest.php | 247 ++++++++++++++++++ .../Business/SupplierPayablePagesTest.php | 73 +++++- 50 files changed, 1975 insertions(+), 190 deletions(-) create mode 100644 app/Http/Controllers/Business/SupplierPayableActions.php create mode 100644 database/migrations/2026_09_17_000000_create_supplier_payable_override_tables.php create mode 100644 modules/EcommerceFinance/Services/SupplierPayableImportTemplate.php create mode 100644 modules/EcommerceFinance/Services/SupplierPayableMaintenanceRegistry.php create mode 100644 modules/EcommerceFinance/Services/SupplierPayableOverrideStore.php create mode 100644 modules/EcommerceFinance/Services/SupplierPayableWorkbookImporter.php create mode 100644 resources/assets/js/views/ecommerce-finance/supplier-payable-maintenance.js create mode 100644 resources/views/business/pages/supplier-payables/maintenance-actions.blade.php create mode 100644 resources/views/business/pages/supplier-payables/maintenance-blade-modal.blade.php create mode 100644 resources/views/business/pages/supplier-payables/maintenance-edit-button.blade.php create mode 100644 resources/views/business/pages/supplier-payables/maintenance-import-modal.blade.php create mode 100644 resources/views/business/pages/supplier-payables/maintenance-toolbar.blade.php create mode 100644 tests/Feature/Business/SupplierPayableActionsTest.php diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index e816e066..78ba800f 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -53,6 +53,8 @@ use Modules\EcommerceFinance\Repositories\SmallPaymentWorkOrderMirrorRepository; use Modules\EcommerceFinance\Repositories\SupplierPayableReportRepository; use Modules\EcommerceFinance\Repositories\TmallTaobaoSpreadBillRepository; use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory; +use Modules\EcommerceFinance\Services\SupplierPayableMaintenanceRegistry; +use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore; use Modules\EcommerceFinance\View\PlatformPromotionAccountPageData; use Modules\EcommerceFinance\View\OrderDataPageData; use RuntimeException; @@ -94,6 +96,8 @@ class Pages extends Controller private readonly DingPayableWorkflowMirrorRepository $dingPayableWorkflows, private readonly DingOnlineDaifaPriceMirrorRepository $onlineDaifaPrices, private readonly SupplierPayableReportRepository $supplierPayableReports, + private readonly SupplierPayableOverrideStore $supplierPayableOverrides, + private readonly SupplierPayableMaintenanceRegistry $supplierPayableMaintenance, private readonly UpstreamQueryCacheFactory $upstreamCaches, private readonly CourierSurchargeQuoteRepository $courierSurchargeQuotes, private readonly CourierSurchargeTypeRepository $courierSurchargeTypes, @@ -2410,10 +2414,16 @@ class Pages extends Controller ['filters' => $filters, 'page' => $page, 'limit' => $limit], fn () => $this->supplierPayableReports->boardDeductions($filters, $page, $limit), ); + $rows = $this->supplierPayableOverrides->decorate( + 'board-deductions', + (array) ($result['rows'] ?? []), + (int) (company_id() ?: 1), + ); return [ - 'boardDeductions' => $this->supplierPayablePaginator((array) ($result['rows'] ?? []), $limit, $page), + 'boardDeductions' => $this->supplierPayablePaginator($rows, $limit, $page), 'boardDeductionFilters' => $filters, + 'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('board-deductions'), 'boardDeductionStatus' => $cache->responseHeaders(), 'boardDeductionError' => null, ]; @@ -2421,6 +2431,7 @@ class Pages extends Controller return [ 'boardDeductions' => $this->supplierPayablePaginator([], $limit, $page), 'boardDeductionFilters' => $filters, + 'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('board-deductions'), 'boardDeductionStatus' => [], 'boardDeductionError' => $exception->getMessage(), ]; @@ -2430,6 +2441,7 @@ class Pages extends Controller return [ 'boardDeductions' => $this->supplierPayablePaginator([], $limit, $page), 'boardDeductionFilters' => $filters, + 'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('board-deductions'), 'boardDeductionStatus' => [], 'boardDeductionError' => '板材扣款数据暂时无法读取,请稍后重试。', ]; @@ -2455,10 +2467,16 @@ class Pages extends Controller ['filters' => $filters, 'page' => $page, 'limit' => $limit], fn () => $this->supplierPayableReports->supplierPayables($filters, $page, $limit), ); + $rows = $this->supplierPayableOverrides->decorate( + 'supplier-payables-report', + (array) ($result['rows'] ?? []), + (int) (company_id() ?: 1), + ); return [ - 'supplierPayables' => $this->supplierPayablePaginator((array) ($result['rows'] ?? []), $limit, $page), + 'supplierPayables' => $this->supplierPayablePaginator($rows, $limit, $page), 'supplierPayableFilters' => $filters, + 'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('supplier-payables-report'), 'supplierPayableStatus' => $cache->responseHeaders(), 'supplierPayableError' => null, ]; @@ -2466,6 +2484,7 @@ class Pages extends Controller return [ 'supplierPayables' => $this->supplierPayablePaginator([], $limit, $page), 'supplierPayableFilters' => $filters, + 'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('supplier-payables-report'), 'supplierPayableStatus' => [], 'supplierPayableError' => $exception->getMessage(), ]; @@ -2475,6 +2494,7 @@ class Pages extends Controller return [ 'supplierPayables' => $this->supplierPayablePaginator([], $limit, $page), 'supplierPayableFilters' => $filters, + 'supplierPayableMaintenance' => $this->supplierPayableMaintenance->publicDefinition('supplier-payables-report'), 'supplierPayableStatus' => [], 'supplierPayableError' => '供应商应付数据暂时无法读取,请稍后重试。', ]; diff --git a/app/Http/Controllers/Business/ProductPrices.php b/app/Http/Controllers/Business/ProductPrices.php index d506478d..44a90f55 100644 --- a/app/Http/Controllers/Business/ProductPrices.php +++ b/app/Http/Controllers/Business/ProductPrices.php @@ -8,6 +8,7 @@ use Illuminate\Routing\Controller; use Illuminate\Validation\ValidationException; use Modules\EcommerceFinance\Repositories\ProductPriceMirrorRepository; use Modules\EcommerceFinance\Services\ProductPriceWorkbookImporter; +use Modules\EcommerceFinance\Services\SupplierPayableImportTemplate; use RuntimeException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -16,6 +17,7 @@ class ProductPrices extends Controller public function __construct( private readonly ProductPriceMirrorRepository $repository, private readonly ProductPriceWorkbookImporter $importer, + private readonly SupplierPayableImportTemplate $template, ) { $this->middleware('auth'); $this->middleware('company.identify'); @@ -93,6 +95,11 @@ class ProductPrices extends Controller ); } + public function template(): StreamedResponse + { + return $this->template->download('商品价格', $this->importer->templateFields()); + } + public function update(Request $request, string $upstreamKey): RedirectResponse { $values = $request->validate( diff --git a/app/Http/Controllers/Business/SupplierPayableActions.php b/app/Http/Controllers/Business/SupplierPayableActions.php new file mode 100644 index 00000000..520315f8 --- /dev/null +++ b/app/Http/Controllers/Business/SupplierPayableActions.php @@ -0,0 +1,196 @@ +middleware('auth'); + $this->middleware('company.identify'); + } + + public function definition(Request $request): JsonResponse + { + return response()->json($this->registry->publicDefinition($this->dataset($request))); + } + + public function update(Request $request, string $recordKey): JsonResponse|RedirectResponse + { + $dataset = $this->dataset($request); + $values = $this->registry->normalize($dataset, (array) $request->input('values', [])); + $result = $this->overrides->save( + $dataset, + $recordKey, + $values, + (int) (company_id() ?: 1), + user_id() ? (int) user_id() : null, + ); + + return $request->expectsJson() + ? response()->json(['message' => '保存成功。'] + $result) + : back()->with('success', '保存成功。'); + } + + public function import(Request $request): JsonResponse|RedirectResponse + { + $request->validate(['import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240']]); + $result = $this->importer->import( + $this->dataset($request), + $request->file('import'), + (int) (company_id() ?: 1), + user_id() ? (int) user_id() : null, + ); + $message = "导入完成:新增维护 {$result['created']} 条,更新维护 {$result['updated']} 条。"; + + return $request->expectsJson() + ? response()->json(['message' => $message] + $result) + : back()->with('success', $message); + } + + public function template(Request $request): StreamedResponse + { + $dataset = $this->dataset($request); + + return $this->template->download($this->title($dataset), $this->registry->fields($dataset)); + } + + public function export(Request $request): StreamedResponse + { + $dataset = $this->dataset($request); + $fields = $this->registry->fields($dataset); + $filters = $this->filters($dataset, $request); + $companyId = (int) (company_id() ?: 1); + + return response()->streamDownload(function () use ($dataset, $fields, $filters, $companyId): void { + $csv = fopen('php://output', 'w'); + if ($csv === false) { + throw new RuntimeException('无法创建导出文件。'); + } + fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF)); + fputcsv($csv, array_merge(['记录标识'], array_column($fields, 'label'))); + + foreach ($this->allRows($dataset, $filters) as $batch) { + foreach ($this->overrides->decorate($dataset, $batch, $companyId) as $row) { + $values = [$this->csvText($row['_action_key'] ?? '')]; + foreach ($fields as $field => $definition) { + $values[] = $definition['type'] === 'number' && is_numeric($row[$field] ?? null) + ? $row[$field] + : $this->csvText($row[$field] ?? ''); + } + fputcsv($csv, $values); + } + } + fclose($csv); + }, $this->title($dataset) . '_' . now()->format('Ymd_His') . '.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Cache-Control' => 'private, no-store', + ]); + } + + /** @return iterable>> */ + private function allRows(string $dataset, array $filters): iterable + { + if ($dataset === 'payment-reconciliation' && ($filters['record_type'] ?? 'all') === 'all') { + foreach (['purchase', 'sale'] as $type) { + yield from $this->allRows($dataset, array_replace($filters, ['record_type' => $type])); + } + return; + } + + $page = 1; + do { + $result = $this->page($dataset, $filters, $page, 100); + $rows = array_slice((array) ($result['data'] ?? $result['rows'] ?? []), 0, 100); + if ($rows !== []) { + yield $rows; + } + $total = (int) ($result['total'] ?? -1); + $more = $rows !== [] && ($total < 0 ? count($rows) === 100 : $page * 100 < $total); + $page++; + } while ($more); + } + + private function page(string $dataset, array $filters, int $page, int $limit): array + { + return match ($dataset) { + 'payment-reconciliation' => $this->goods->list($filters, $page, $limit), + 'return-reconciliation' => $this->supplier->returnReconciliations($filters, $page, $limit), + 'invoice-deductions' => $this->supplier->invoiceDeductions($filters, $page, $limit), + 'compensation-details' => $this->supplier->compensationDetails($filters, $page, $limit), + 'advance-details' => $this->supplier->advanceDetails($filters, $page, $limit), + 'sample-deductions' => $this->supplier->sampleDeductions($filters, $page, $limit), + 'delivery-unpriced' => $this->supplier->deliveryUnpriced($filters, $page, $limit), + 'return-unpriced' => $this->supplier->returnUnpriced($filters, $page, $limit), + 'other-adjustments' => $this->supplier->otherAdjustments($filters, $page, $limit), + 'dropshipping-express' => $this->supplier->dropshippingExpress($filters, $page, $limit), + 'board-deductions' => $this->reports->boardDeductions($filters, $page, $limit), + 'supplier-payables-report' => $this->reports->supplierPayables($filters, $page, $limit), + }; + } + + private function filters(string $dataset, Request $request): array + { + $allowed = match ($dataset) { + 'payment-reconciliation' => ['record_type', 'supplier', 'warehouse', 'stock_no', 'purchase_no', 'product_code', 'express_no', 'send_time_from', 'send_time_to', 'create_time_from', 'create_time_to', 'sent_status', 'has_diff'], + 'return-reconciliation', 'return-unpriced' => ['supplier', 'warehouse', 'sku_id', 'express_no', 'date_from', 'date_to'], + 'invoice-deductions', 'compensation-details', 'advance-details', 'other-adjustments' => ['supplier', 'month_from', 'month_to'], + 'sample-deductions' => ['supplier', 'department', 'creator', 'sku_id', 'date_from', 'date_to'], + 'delivery-unpriced' => ['supplier', 'warehouse', 'purchase', 'barcode', 'express_no', 'date_from', 'date_to'], + 'dropshipping-express' => ['supplier', 'express_name', 'month_from', 'month_to'], + 'board-deductions' => ['stock_in_date', 'stock_out_date', 'furniture_factory', 'lamination_factory', 'deduction_type', 'material_name', 'claim_number'], + 'supplier-payables-report' => ['month', 'supplier', 'checker', 'settlement_method'], + }; + + return collect($request->only($allowed)) + ->map(static fn (mixed $value): mixed => is_string($value) ? trim($value) : $value) + ->reject(static fn (mixed $value): bool => $value === '' || $value === null || $value === false) + ->all(); + } + + private function dataset(Request $request): string + { + $dataset = (string) $request->route('dataset'); + $this->registry->fields($dataset); + return $dataset; + } + + private function title(string $dataset): string + { + return [ + 'payment-reconciliation' => '货款对账', 'return-reconciliation' => '退货对账', + 'invoice-deductions' => '发票扣款', 'compensation-details' => '赔偿明细', + 'advance-details' => '借支明细', 'sample-deductions' => '样品扣款', + 'delivery-unpriced' => '送货未定价', 'return-unpriced' => '退货未定价', + 'other-adjustments' => '其他扣款补款', 'dropshipping-express' => '代发快递', + 'board-deductions' => '板材扣款', 'supplier-payables-report' => '供应商应付表', + ][$dataset]; + } + + private function csvText(mixed $value): string + { + $value = (string) $value; + return preg_match('/^[=+\-@\t\r]/u', $value) === 1 ? "\t" . $value : $value; + } +} diff --git a/config/database.php b/config/database.php index 723b4175..36b64fc1 100644 --- a/config/database.php +++ b/config/database.php @@ -45,6 +45,13 @@ return [ 'default' => env('DB_CONNECTION', 'mysql'), + // Supplier payable edits and imports must never write to the shared + // system database used by the rest of the application. + 'supplier_payable_maintenance_connection' => env( + 'SUPPLIER_PAYABLE_MAINTENANCE_DB_CONNECTION', + 'supplier_payable_maintenance' + ), + /* |-------------------------------------------------------------------------- | Database Connections @@ -153,6 +160,29 @@ return [ 'options' => $mysqlSslOptions(), ], + 'supplier_payable_maintenance' => [ + 'driver' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_DRIVER', 'mysql'), + 'host' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_HOST'), + 'port' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_PORT', '3306'), + 'database' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_DATABASE'), + 'username' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_USERNAME'), + 'password' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_PASSWORD'), + 'unix_socket' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_SOCKET', ''), + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', + 'prefix' => env('SUPPLIER_PAYABLE_MAINTENANCE_DB_PREFIX', 'ak_'), + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => 'InnoDB ROW_FORMAT=DYNAMIC', + 'modes' => [ + 'STRICT_TRANS_TABLES', + 'NO_ZERO_IN_DATE', + 'NO_ZERO_DATE', + 'ERROR_FOR_DIVISION_BY_ZERO', + 'NO_ENGINE_SUBSTITUTION', + ], + ], + 'mariadb' => [ 'driver' => 'mysql', 'url' => env('DATABASE_URL'), diff --git a/database/migrations/2026_09_17_000000_create_supplier_payable_override_tables.php b/database/migrations/2026_09_17_000000_create_supplier_payable_override_tables.php new file mode 100644 index 00000000..22fc7ad5 --- /dev/null +++ b/database/migrations/2026_09_17_000000_create_supplier_payable_override_tables.php @@ -0,0 +1,50 @@ +hasTable('supplier_payable_overrides')) { + $schema->create('supplier_payable_overrides', function (Blueprint $table): void { + $table->bigIncrements('id'); + $table->unsignedInteger('company_id'); + $table->string('dataset', 64); + $table->string('record_key', 191); + $table->json('values'); + $table->unsignedInteger('created_by')->nullable(); + $table->unsignedInteger('updated_by')->nullable(); + $table->timestamps(); + $table->unique(['company_id', 'dataset', 'record_key'], 'supplier_payable_override_unique'); + $table->index(['company_id', 'dataset'], 'supplier_payable_override_dataset'); + }); + } + + if (! $schema->hasTable('supplier_payable_override_events')) { + $schema->create('supplier_payable_override_events', function (Blueprint $table): void { + $table->bigIncrements('id'); + $table->unsignedInteger('company_id'); + $table->string('dataset', 64); + $table->string('record_key', 191); + $table->string('source', 24); + $table->json('before_values')->nullable(); + $table->json('after_values'); + $table->unsignedInteger('user_id')->nullable(); + $table->timestamp('created_at')->useCurrent(); + $table->index(['company_id', 'dataset', 'record_key'], 'supplier_payable_override_event_lookup'); + }); + } + } + + public function down(): void + { + $schema = Schema::connection(config('database.supplier_payable_maintenance_connection')); + $schema->dropIfExists('supplier_payable_override_events'); + $schema->dropIfExists('supplier_payable_overrides'); + } +}; diff --git a/deploy/local-init.sh b/deploy/local-init.sh index 6dfa66b8..3b8b9b1a 100644 --- a/deploy/local-init.sh +++ b/deploy/local-init.sh @@ -384,6 +384,11 @@ fi echo "==> 自动检查本机临时引导库结构..." docker compose -f "$compose_file" exec -T -u www-data app php artisan optimize:clear >/dev/null +echo "==> 自动检查供应商应付本地维护库结构..." +docker compose -f "$compose_file" exec -T -u www-data app \ + php artisan migrate --database=supplier_payable_maintenance \ + --path=database/migrations/2026_09_17_000000_create_supplier_payable_override_tables.php \ + --force --no-ansi docker compose -f "$compose_file" exec -T -u www-data app \ php artisan local-bootstrap:update --no-ansi diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 45276cf0..2c90773c 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -20,6 +20,18 @@ services: depends_on: mysql: condition: service_healthy + environment: + # Local test data entry uses PATCH requests; do not let the global + # firewall heuristics block the maintenance-only test environment. + FIREWALL_ENABLED: "false" + SUPPLIER_PAYABLE_MAINTENANCE_DB_CONNECTION: supplier_payable_maintenance + SUPPLIER_PAYABLE_MAINTENANCE_DB_DRIVER: mysql + SUPPLIER_PAYABLE_MAINTENANCE_DB_HOST: mysql + SUPPLIER_PAYABLE_MAINTENANCE_DB_PORT: 3306 + SUPPLIER_PAYABLE_MAINTENANCE_DB_DATABASE: akaunting + SUPPLIER_PAYABLE_MAINTENANCE_DB_USERNAME: akaunting + SUPPLIER_PAYABLE_MAINTENANCE_DB_PASSWORD: akaunting123 + SUPPLIER_PAYABLE_MAINTENANCE_DB_PREFIX: ak_ ports: - "127.0.0.1:18088:18088" volumes: diff --git a/modules/EcommerceFinance/Http/Controllers/Api/GoodsReconciliations.php b/modules/EcommerceFinance/Http/Controllers/Api/GoodsReconciliations.php index dfa53775..9cc94795 100644 --- a/modules/EcommerceFinance/Http/Controllers/Api/GoodsReconciliations.php +++ b/modules/EcommerceFinance/Http/Controllers/Api/GoodsReconciliations.php @@ -5,6 +5,7 @@ namespace Modules\EcommerceFinance\Http\Controllers\Api; use App\Abstracts\Http\ApiController; use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository; use Modules\EcommerceFinance\Services\JianSunQueryCache; +use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore; /** * 货款对账 API @@ -20,7 +21,7 @@ class GoodsReconciliations extends ApiController private JianSunQueryCache $cache; - public function __construct(GoodsReconciliationRepository $repo, JianSunQueryCache $cache) + public function __construct(GoodsReconciliationRepository $repo, JianSunQueryCache $cache, private readonly SupplierPayableOverrideStore $overrides) { $this->repo = $repo; $this->cache = $cache; @@ -41,6 +42,8 @@ class GoodsReconciliations extends ApiController fn () => $this->repo->list($filters, $page, $limit) ); + $result['data'] = $this->overrides->decorate('payment-reconciliation', $result['data'] ?? [], (int) (company_id() ?: 1)); + return $this->snapshotResponse([ 'data' => $result['data'], 'total' => $result['total'], diff --git a/modules/EcommerceFinance/Http/Controllers/Api/SupplierPayables.php b/modules/EcommerceFinance/Http/Controllers/Api/SupplierPayables.php index c8401e4d..8d84c164 100644 --- a/modules/EcommerceFinance/Http/Controllers/Api/SupplierPayables.php +++ b/modules/EcommerceFinance/Http/Controllers/Api/SupplierPayables.php @@ -6,6 +6,7 @@ use App\Abstracts\Http\ApiController; use Closure; use Modules\EcommerceFinance\Repositories\SupplierPayableRepository; use Modules\EcommerceFinance\Services\JianSunQueryCache; +use Modules\EcommerceFinance\Services\SupplierPayableOverrideStore; /** * 供应商应付 - 子功能页面 API @@ -23,7 +24,7 @@ class SupplierPayables extends ApiController private JianSunQueryCache $cache; - public function __construct(SupplierPayableRepository $repo, JianSunQueryCache $cache) + public function __construct(SupplierPayableRepository $repo, JianSunQueryCache $cache, private readonly SupplierPayableOverrideStore $overrides) { $this->repo = $repo; $this->cache = $cache; @@ -333,12 +334,18 @@ class SupplierPayables extends ApiController { $page = $this->page(); $limit = $this->limit(); + $cacheParameters = ['filters' => $filters, 'page' => $page, 'limit' => $limit]; + if ($scope === 'other-adjustments') { + $cacheParameters['format_version'] = 2; + } $result = $this->cache->remember( $scope . '.list', - ['filters' => $filters, 'page' => $page, 'limit' => $limit], + $cacheParameters, fn () => $resolver($page, $limit) ); + $result['data'] = $this->overrides->decorate($scope, $result['data'] ?? [], (int) (company_id() ?: 1)); + return $this->snapshotResponse([ 'data' => $result['data'], 'total' => $result['total'], diff --git a/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php b/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php index 96dfc4f1..7f3130d4 100644 --- a/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php +++ b/modules/EcommerceFinance/Repositories/SupplierPayableRepository.php @@ -135,6 +135,7 @@ class SupplierPayableRepository $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT + id AS id, CONVERT(varchar(10), date, 120) AS send_time, supplierName AS supplier, sku_id AS barcode, @@ -225,6 +226,7 @@ class SupplierPayableRepository $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT + id AS id, CONVERT(varchar(7), month, 120) AS month_str, '' AS category, supplier AS payee, @@ -281,6 +283,7 @@ class SupplierPayableRepository $rows = $this->query->select( "SELECT + [id] AS id, [Month] AS month, [supplier] AS supplier, CAST([Totalindemnity] AS decimal(18,2)) AS amount, @@ -369,6 +372,7 @@ class SupplierPayableRepository $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT + id AS id, supplier, '' AS contract_name, CAST(price AS decimal(18,2)) AS advance_amount, @@ -454,6 +458,7 @@ class SupplierPayableRepository $whereSql = $where ? 'WHERE ' . implode(' AND ', $where) : ''; $sql = "SELECT + id AS id, CONVERT(varchar(10), finishTime, 120) AS finish_time, department, creator, @@ -619,6 +624,7 @@ class SupplierPayableRepository $whereSql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT + id AS id, CONVERT(varchar(10), date, 120) AS send_time, supplierName AS supplier, sku_id AS barcode, @@ -759,7 +765,7 @@ class SupplierPayableRepository $whereSql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT - '其他扣款补款' AS project, + id AS id, Month AS month_str, supplier, CAST(other AS decimal(18,2)) AS supplement_amount, @@ -770,7 +776,13 @@ class SupplierPayableRepository FROM fys_supplierPaymentSummary $whereSql"; - return $this->runQuery($sql, $params, $page, $pageSize, 'month_str DESC, supplier'); + $result = $this->runQuery($sql, $params, $page, $pageSize, 'month_str DESC, supplier'); + foreach ($result['data'] as &$row) { + $row['project'] = '其他扣款补款'; + } + unset($row); + + return $result; } public function otherAdjustmentsSummary(array $filters): array @@ -825,6 +837,7 @@ class SupplierPayableRepository $whereSql = 'WHERE ' . implode(' AND ', $where); $sql = "SELECT + id AS id, reconciliation_person, month AS month_str, courier_company_name AS supplier, diff --git a/modules/EcommerceFinance/Services/ProductPriceWorkbookImporter.php b/modules/EcommerceFinance/Services/ProductPriceWorkbookImporter.php index 48394b17..8d77feae 100644 --- a/modules/EcommerceFinance/Services/ProductPriceWorkbookImporter.php +++ b/modules/EcommerceFinance/Services/ProductPriceWorkbookImporter.php @@ -39,6 +39,25 @@ class ProductPriceWorkbookImporter { } + /** @return array */ + public function templateFields(): array + { + $fields = []; + + foreach (self::HEADERS as $label => $field) { + if ($field === 'upstream_key') { + continue; + } + + $fields[$field] = [ + 'label' => $label, + 'type' => isset(self::TEXT_LIMITS[$field]) ? 'text' : 'number', + ]; + } + + return $fields; + } + /** @return array{created:int,updated:int,rows:int} */ public function import(UploadedFile $file, int $companyId, ?int $userId): array { diff --git a/modules/EcommerceFinance/Services/SupplierPayableImportTemplate.php b/modules/EcommerceFinance/Services/SupplierPayableImportTemplate.php new file mode 100644 index 00000000..837813c5 --- /dev/null +++ b/modules/EcommerceFinance/Services/SupplierPayableImportTemplate.php @@ -0,0 +1,89 @@ + $fields */ + public function download(string $title, array $fields): StreamedResponse + { + return response()->streamDownload(function () use ($title, $fields): void { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setTitle($this->sheetTitle($title)); + + $headers = array_merge(['记录标识'], array_column($fields, 'label')); + $sheet->fromArray($headers, null, 'A1'); + $lastColumn = Coordinate::stringFromColumnIndex(count($headers)); + $headerRange = 'A1:' . $lastColumn . '1'; + + $sheet->getStyle($headerRange)->applyFromArray([ + 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']], + 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']], + 'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER, 'vertical' => Alignment::VERTICAL_CENTER], + 'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'D8D2C7']]], + ]); + $sheet->getRowDimension(1)->setRowHeight(24); + $sheet->freezePane('A2'); + $sheet->setAutoFilter($headerRange); + $sheet->getColumnDimension('A')->setWidth(38); + + $columnIndex = 2; + foreach ($fields as $definition) { + $column = Coordinate::stringFromColumnIndex($columnIndex); + $width = max(14, min(30, mb_strlen($definition['label']) * 2 + 4)); + $sheet->getColumnDimension($column)->setWidth($width); + if ($definition['type'] === 'number') { + $sheet->getStyle($column . '2:' . $column . '10001')->getNumberFormat()->setFormatCode('0.0000'); + } else { + $sheet->getStyle($column . '2:' . $column . '10001')->getNumberFormat()->setFormatCode('@'); + } + $columnIndex++; + } + $sheet->getStyle('A2:A10001')->getNumberFormat()->setFormatCode('@'); + + $instructions = $spreadsheet->createSheet(); + $instructions->setTitle('填写说明'); + $instructions->fromArray([ + ['项目', '说明'], + ['记录标识', '必填。请从本页面导出的文件中复制,不能修改。'], + ['日期', '请填写 YYYY-MM-DD,例如 2026-09-17。'], + ['月份', '请填写 YYYY-MM,例如 2026-09。'], + ['数字', '最多支持 4 位小数;金额可直接填写数字。'], + ['空白单元格', '导入后将清空该字段,请确认后再导入。'], + ], null, 'A1'); + $instructions->getStyle('A1:B1')->applyFromArray([ + 'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']], + 'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']], + ]); + $instructions->getColumnDimension('A')->setWidth(18); + $instructions->getColumnDimension('B')->setWidth(58); + $instructions->getStyle('A1:B6')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setWrapText(true); + foreach (range(1, 6) as $row) { + $instructions->getRowDimension($row)->setRowHeight($row === 1 ? 24 : 32); + } + + $spreadsheet->setActiveSheetIndex(0); + (new Xlsx($spreadsheet))->save('php://output'); + $spreadsheet->disconnectWorksheets(); + }, $title . '_导入模板_' . now()->format('Ymd') . '.xlsx', [ + 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'Cache-Control' => 'private, no-store', + ]); + } + + private function sheetTitle(string $title): string + { + $title = str_replace(['\\', '/', '?', '*', '[', ']', ':'], '', $title) ?: '导入模板'; + + return mb_substr($title, 0, 31); + } +} diff --git a/modules/EcommerceFinance/Services/SupplierPayableMaintenanceRegistry.php b/modules/EcommerceFinance/Services/SupplierPayableMaintenanceRegistry.php new file mode 100644 index 00000000..df1d9b3f --- /dev/null +++ b/modules/EcommerceFinance/Services/SupplierPayableMaintenanceRegistry.php @@ -0,0 +1,167 @@ + */ + public function fields(string $dataset): array + { + $money = static fn (string $label): array => ['label' => $label, 'type' => 'number']; + $text = static fn (string $label): array => ['label' => $label, 'type' => 'text']; + $date = static fn (string $label): array => ['label' => $label, 'type' => 'date']; + $month = static fn (string $label): array => ['label' => $label, 'type' => 'month']; + + return match ($dataset) { + 'payment-reconciliation' => [ + 'supplier' => $text('供应商'), 'skuCode' => $text('货号'), 'productCode' => $text('商品编码'), + 'skuName' => $text('规格名称'), 'qty' => $money('数量'), 'unitPrice' => $money('单价'), + 'amount' => $money('金额'), 'guessUnitPrice' => $money('预估单价'), 'guessAmount' => $money('预估金额'), + 'supplierGoodsCode' => $text('供应商商品编码'), 'remark' => $text('备注'), + 'warehouse' => $text('仓库名称'), 'expressName' => $text('快递名称'), 'expressNo' => $text('快递单号'), + ], + 'return-reconciliation', 'return-unpriced' => [ + 'send_time' => $date('发货时间'), 'supplier' => $text('明细供应商'), 'barcode' => $text('条形码'), + 'sku_name' => $text('商品名称'), 'qty' => $money('发货量'), 'unit_price' => $money('单价'), + 'amount' => $money('金额'), 'guess_unit_price' => $money('预估单价'), 'guess_amount' => $money('预估金额'), + 'supplier_barcode' => $text('供应商条码'), 'remark' => $text('备注'), 'title' => $text('标题'), + 'warehouse' => $text('仓库名称'), 'express_no' => $text('快递单号'), + 'express_name' => $text('快递名称'), 'labels' => $text('标记多标签'), 'reconciler' => $text('对账人'), + ], + 'invoice-deductions' => [ + 'month_str' => $month('日期'), 'category' => $text('类别'), 'payee' => $text('打款抬头'), + 'receiver' => $text('收款方'), 'board_factory' => $text('供板材厂家/代扣厂家'), + 'subsidy_fee' => $money('补贴费用'), 'remark' => $text('备注'), 'price' => $money('对私打款金额'), + 'detail' => $text('明细'), 'note' => $text('注明'), 'person_charge' => $text('负责人'), + ], + 'compensation-details' => [ + 'month' => $month('日期'), 'supplier' => $text('供应商'), 'amount' => $money('赔付金额'), + 'reason' => $text('赔付原因'), 'remark' => $text('备注'), + ], + 'advance-details' => [ + 'supplier' => $text('供应商'), 'contract_name' => $text('协议签订名称'), + 'advance_amount' => $money('借支金额'), 'actual_pay' => $money('实际打款金额'), + 'month_deduct' => $money('本月扣款金额'), 'should_deduct' => $money('应扣金额'), + 'deduct_start_month' => $month('分期起始扣款月份'), 'deduct_end_month' => $month('分期最后扣款月份'), + 'remain_periods' => $money('分期剩余期数'), 'month_str' => $month('借支日期'), + 'loan_term' => $text('借款期限'), 'remark' => $text('备注'), 'is_paid' => $text('是否打款'), + 'pay_month' => $month('打款月份'), 'serial_no' => $text('流水号'), + 'loan_agreement' => $text('借支协议'), 'person_charge' => $text('负责人'), + ], + 'sample-deductions' => [ + 'finish_time' => $date('日期'), 'department' => $text('部门'), 'creator' => $text('申请人'), + 'sku_id' => $text('商品代码'), 'skuName' => $text('商品名称'), 'num' => $money('数量'), + 'supplier' => $text('厂家'), 'purchase' => $text('对应采购'), 'productCode' => $text('扣款厂家商品代码'), + 'shipping_fee' => $money('运费'), 'cost_price' => $money('成本价'), + 'total_amount' => $money('合计扣款金额'), 'estimated_price' => $money('预估价'), + 'estimated_price_all' => $money('预估金额'), 'remark' => $text('备注'), + ], + 'delivery-unpriced' => [ + 'transactionNo' => $text('来源单号'), 'distributionNo' => $text('单据编号(采购单号)'), + 'date' => $date('创建时间'), 'supplier' => $text('供应商'), 'sku' => $text('货号'), + 'barCode' => $text('条码'), 'skuName' => $text('规格名'), 'supplier_barcode' => $text('供应商条码'), + 'remark' => $text('备注'), 'warehouse' => $text('仓库'), 'expressName' => $text('快递名称'), + 'expressNo' => $text('快递单号'), 'purchase' => $text('负责人'), + ], + 'other-adjustments' => [ + 'project' => $text('项目'), 'month_str' => $month('日期'), 'supplier' => $text('厂家'), + 'supplement_amount' => $money('补款金额'), 'supplement_reason' => $text('补款原因'), + 'remark' => $text('备注'), 'remark2' => $text('备注2'), 'person_charge' => $text('负责人'), + ], + 'dropshipping-express' => [ + 'reconciliation_person' => $text('对账人'), 'month_str' => $month('日期'), 'supplier' => $text('供应商'), + 'express_freight' => $money('快递运费'), 'express_account' => $money('快递记账'), + 'replenish_freight' => $money('补运费'), 'replenish_account' => $money('补记账'), + 'return_fee' => $money('退回费'), 'intercept_fee' => $money('拦截'), + 'change_address_fee' => $money('改地址'), 'turnover_fee' => $money('翻箱费'), + 'overlong_fee' => $money('超长费'), 'total_freight' => $money('合计运费'), + 'remark' => $text('备注'), 'express_name' => $text('快递'), 'remark2' => $text('备注2'), + ], + 'board-deductions' => [ + 'stock_in_date' => $date('入库日期'), 'stock_out_date' => $date('出库日期'), + 'furniture_factory' => $text('家具厂'), 'lamination_factory' => $text('压贴厂'), + 'deduction_type' => $text('类型'), 'specification' => $text('规格'), 'material_name' => $text('原料名称'), + 'received_quantity' => $money('实收数量'), 'unit_price' => $money('单价'), 'amount' => $money('金额'), + 'unit' => $text('单位'), 'batch_date' => $date('批次日期'), 'stock_out_number' => $text('出库单号'), + 'stock_out_remark' => $text('出库备注'), 'claim_number' => $text('申领单号'), + 'number_type' => $text('单号类型'), 'delivery_number' => $text('送货单号'), + ], + 'supplier-payables-report' => [ + 'checker' => $text('对账人'), 'settlement_method' => $text('结算方式'), 'company' => $text('供应商公司主体名称'), + 'supplier' => $text('供应商简称'), 'express_freight' => $money('快递运费'), + 'beginning_payable' => $money('月初未付'), 'unpriced' => $money('之前未定价-本月定价'), + 'advance_loan' => $money('预支借款'), 'loan' => $money('货款金额'), 'refund' => $money('退货金额'), + 'procurement' => $money('采购小计'), 'invoice_deduction' => $money('发票扣款'), + 'board_deduction' => $money('板材扣款'), 'sample_deduction' => $money('样品扣款'), + 'total_compensation' => $money('总赔偿金额'), 'other_adjustment' => $money('其他费用'), + 'supplier_supplement' => $money('补给厂家'), 'supplier_balance' => $money('供应商余额'), + 'should_pay' => $money('应付余额'), 'estimated_price' => $money('预估金额'), + 'to_corporate' => $money('对公打款金额'), 'to_private' => $money('对私打款金额'), + 'cashier_paid' => $money('已销账金额'), 'remarks' => $text('备注'), + 'private_reason' => $text('对私原因'), 'deposit' => $money('押款金额'), + 'negative_reason' => $text('负数原因'), + ], + default => throw ValidationException::withMessages(['dataset' => '不支持该供应商应付页面。']), + }; + } + + public function normalize(string $dataset, array $input): array + { + $fields = $this->fields($dataset); + $values = []; + + foreach ($fields as $field => $definition) { + if (! array_key_exists($field, $input)) { + continue; + } + + $value = $input[$field]; + if ($value === null || trim((string) $value) === '') { + $values[$field] = null; + continue; + } + + if ($definition['type'] === 'number') { + $number = str_replace([',', ',', '¥', '¥'], '', trim((string) $value)); + if (preg_match('/^-?(?:\d+(?:\.\d{1,4})?|\.\d{1,4})$/', $number) !== 1 + || bccomp(ltrim($number, '-'), '9999999999999999.9999', 4) === 1) { + throw ValidationException::withMessages([ + $field => $definition['label'] . '必须是最多 4 位小数的有效数字。', + ]); + } + $values[$field] = $number; + continue; + } + + $text = trim((string) $value); + if (mb_strlen($text) > 5000) { + throw ValidationException::withMessages([$field => $definition['label'] . '内容过长。']); + } + if ($definition['type'] === 'date' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $text) !== 1) { + throw ValidationException::withMessages([$field => $definition['label'] . '日期格式无效。']); + } + if ($definition['type'] === 'month' && preg_match('/^\d{4}-\d{2}$/', $text) !== 1) { + throw ValidationException::withMessages([$field => $definition['label'] . '月份格式无效。']); + } + $values[$field] = $text; + } + + if ($values === []) { + throw ValidationException::withMessages(['values' => '没有可保存的字段。']); + } + + return $values; + } + + public function publicDefinition(string $dataset): array + { + return [ + 'dataset' => $dataset, + 'fields' => collect($this->fields($dataset))->map( + static fn (array $definition, string $name): array => ['name' => $name] + $definition + )->values()->all(), + ]; + } +} diff --git a/modules/EcommerceFinance/Services/SupplierPayableOverrideStore.php b/modules/EcommerceFinance/Services/SupplierPayableOverrideStore.php new file mode 100644 index 00000000..30718531 --- /dev/null +++ b/modules/EcommerceFinance/Services/SupplierPayableOverrideStore.php @@ -0,0 +1,107 @@ +> $rows */ + public function decorate(string $dataset, array $rows, int $companyId): array + { + if ($rows === []) { + return []; + } + + $keys = []; + foreach ($rows as $row) { + $keys[] = $this->key($dataset, $row); + } + + $overrides = $this->connection()->table('supplier_payable_overrides') + ->where('company_id', $companyId) + ->where('dataset', $dataset) + ->whereIn('record_key', $keys) + ->get() + ->keyBy('record_key'); + + foreach ($rows as $index => &$row) { + $key = $keys[$index]; + $row['_action_key'] = $key; + $override = $overrides->get($key); + if ($override) { + $values = json_decode((string) $override->values, true) ?: []; + $row = array_merge($row, $values); + $row['_overridden'] = true; + } else { + $row['_overridden'] = false; + } + } + unset($row); + + return $rows; + } + + /** @return array{created:int,updated:int} */ + public function save(string $dataset, string $recordKey, array $values, int $companyId, ?int $userId, string $source = 'manual'): array + { + $connection = $this->connection(); + $existing = $connection->table('supplier_payable_overrides') + ->where(['company_id' => $companyId, 'dataset' => $dataset, 'record_key' => $recordKey]) + ->first(); + $before = $existing ? (json_decode((string) $existing->values, true) ?: []) : null; + $now = now(); + + $connection->transaction(function () use ($connection, $existing, $dataset, $recordKey, $values, $companyId, $userId, $now, $source, $before): void { + $connection->table('supplier_payable_overrides')->updateOrInsert( + ['company_id' => $companyId, 'dataset' => $dataset, 'record_key' => $recordKey], + [ + 'values' => json_encode($values, JSON_UNESCAPED_UNICODE), + 'updated_by' => $userId, + 'created_by' => $existing->created_by ?? $userId, + 'created_at' => $existing->created_at ?? $now, + 'updated_at' => $now, + ], + ); + $connection->table('supplier_payable_override_events')->insert([ + 'company_id' => $companyId, + 'dataset' => $dataset, + 'record_key' => $recordKey, + 'source' => $source, + 'before_values' => $before === null ? null : json_encode($before, JSON_UNESCAPED_UNICODE), + 'after_values' => json_encode($values, JSON_UNESCAPED_UNICODE), + 'user_id' => $userId, + 'created_at' => $now, + ]); + }); + + return ['created' => $existing ? 0 : 1, 'updated' => $existing ? 1 : 0]; + } + + public function transaction(Closure $callback): mixed + { + return $this->connection()->transaction($callback); + } + + private function connection(): ConnectionInterface + { + return DB::connection(config('database.supplier_payable_maintenance_connection')); + } +} diff --git a/modules/EcommerceFinance/Services/SupplierPayableWorkbookImporter.php b/modules/EcommerceFinance/Services/SupplierPayableWorkbookImporter.php new file mode 100644 index 00000000..727e90cc --- /dev/null +++ b/modules/EcommerceFinance/Services/SupplierPayableWorkbookImporter.php @@ -0,0 +1,129 @@ +registry->fields($dataset); + $lookup = ['记录标识' => '_action_key', 'recordkey' => '_action_key', '_action_key' => '_action_key']; + foreach ($definitions as $field => $definition) { + $lookup[$this->header($field)] = $field; + $lookup[$this->header($definition['label'])] = $field; + } + + try { + $readerType = match (mb_strtolower($file->getClientOriginalExtension())) { + 'xlsx' => 'Xlsx', + 'xls' => 'Xls', + 'csv' => 'Csv', + default => throw new \RuntimeException('Unsupported workbook format.'), + }; + $spreadsheet = IOFactory::createReader($readerType)->load($file->getRealPath()); + [$rows, $headerRow, $columns] = $this->bestSheet($spreadsheet, $lookup); + } catch (Throwable) { + throw ValidationException::withMessages([ + 'import' => '无法读取导入文件,请确认文件未损坏且格式为 xlsx、xls 或 csv。', + ]); + } finally { + if (isset($spreadsheet)) { + $spreadsheet->disconnectWorksheets(); + } + } + + if ($rows === null || ! in_array('_action_key', $columns, true)) { + throw ValidationException::withMessages([ + 'import' => '未找到“记录标识”列,请先导出当前数据,再使用导出的表格修改。', + ]); + } + + $records = []; + foreach (array_slice($rows, $headerRow + 1, null, true) as $rowIndex => $row) { + $line = $rowIndex + 1; + $key = ''; + $rawValues = []; + foreach ($columns as $columnIndex => $field) { + if ($field === '_action_key') { + $key = trim((string) ($row[$columnIndex] ?? '')); + } else { + $rawValues[$field] = $row[$columnIndex] ?? null; + } + } + if ($key === '' && collect($rawValues)->every(static fn (mixed $value): bool => trim((string) $value) === '')) { + continue; + } + if (! str_starts_with($key, 'id:') && ! preg_match('/^hash:[a-f0-9]{64}$/', $key)) { + throw ValidationException::withMessages(['import' => "第 {$line} 行“记录标识”无效,请勿修改该列。"]); + } + if (isset($records[$key])) { + throw ValidationException::withMessages(['import' => "第 {$line} 行“记录标识”重复。"]); + } + try { + $records[$key] = $this->registry->normalize($dataset, $rawValues); + } catch (ValidationException $exception) { + throw ValidationException::withMessages([ + 'import' => "第 {$line} 行:" . collect($exception->errors())->flatten()->first(), + ]); + } + if (count($records) > 10000) { + throw ValidationException::withMessages(['import' => '单次最多导入 10,000 条记录,请拆分文件后重试。']); + } + } + + if ($records === []) { + throw ValidationException::withMessages(['import' => '表格中没有可导入的数据行。']); + } + + $counts = ['created' => 0, 'updated' => 0]; + $this->overrides->transaction(function () use ($records, $dataset, $companyId, $userId, &$counts): void { + foreach ($records as $key => $values) { + $result = $this->overrides->save($dataset, $key, $values, $companyId, $userId, 'import'); + $counts['created'] += $result['created']; + $counts['updated'] += $result['updated']; + } + }); + + return $counts + ['rows' => count($records)]; + } + + private function bestSheet(mixed $spreadsheet, array $lookup): array + { + $best = [null, null, []]; + $score = 0; + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $rows = $worksheet->toArray(null, false, false, false); + foreach (array_slice($rows, 0, 20, true) as $rowIndex => $row) { + $columns = []; + foreach ($row as $columnIndex => $heading) { + $key = $this->header($heading); + if ($key !== '' && isset($lookup[$key])) { + $columns[$columnIndex] = $lookup[$key]; + } + } + if (count($columns) > $score) { + $best = [$rows, $rowIndex, $columns]; + $score = count($columns); + } + } + } + return $best; + } + + private function header(mixed $value): string + { + return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim((string) $value))); + } +} diff --git a/resources/assets/js/views/ecommerce-finance/advance-details.js b/resources/assets/js/views/ecommerce-finance/advance-details.js index d6629fba..07bec1b8 100644 --- a/resources/assets/js/views/ecommerce-finance/advance-details.js +++ b/resources/assets/js/views/ecommerce-finance/advance-details.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message @@ -21,10 +22,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#advance-detail-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'advance-details', baseUrl: url + '/workspace-data/supplier-payables/advance-details', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/compensation-details.js b/resources/assets/js/views/ecommerce-finance/compensation-details.js index ae67fbbb..5d11d8eb 100644 --- a/resources/assets/js/views/ecommerce-finance/compensation-details.js +++ b/resources/assets/js/views/ecommerce-finance/compensation-details.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message @@ -21,10 +22,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#compensation-detail-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'compensation-details', baseUrl: url + '/workspace-data/supplier-payables/compensation-details', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/delivery-unpriced.js b/resources/assets/js/views/ecommerce-finance/delivery-unpriced.js index c5347cd6..4ad90b17 100644 --- a/resources/assets/js/views/ecommerce-finance/delivery-unpriced.js +++ b/resources/assets/js/views/ecommerce-finance/delivery-unpriced.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Select, Option, Message @@ -23,10 +24,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#delivery-unpriced-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'delivery-unpriced', baseUrl: url + '/workspace-data/supplier-payables/delivery-unpriced', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/dropshipping-express.js b/resources/assets/js/views/ecommerce-finance/dropshipping-express.js index 8b022f91..daf41317 100644 --- a/resources/assets/js/views/ecommerce-finance/dropshipping-express.js +++ b/resources/assets/js/views/ecommerce-finance/dropshipping-express.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Select, Option, Message @@ -23,10 +24,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#dropshipping-express-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'dropshipping-express', baseUrl: url + '/workspace-data/supplier-payables/dropshipping-express', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/goods-reconciliations.js b/resources/assets/js/views/ecommerce-finance/goods-reconciliations.js index 28cefa94..43c3166b 100644 --- a/resources/assets/js/views/ecommerce-finance/goods-reconciliations.js +++ b/resources/assets/js/views/ecommerce-finance/goods-reconciliations.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, @@ -24,10 +25,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#goods-reconciliation-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'payment-reconciliation', baseUrl: url + '/workspace-data/supplier-payables/payment-reconciliation', loading: false, tableData: [], @@ -234,6 +236,17 @@ const app = new Vue({ this.fetchData(); }, + onReset() { + this.filters = { + record_type: 'all', supplier: '', warehouse: '', stock_no: '', purchase_no: '', + product_code: '', express_no: '', send_time_range: null, create_time_range: null, + sent_status: 'all', has_diff: false + }; + this.activeTab = 'all'; + this.currentPage = 1; + this.fetchData(); + }, + /** * Pagination page change */ diff --git a/resources/assets/js/views/ecommerce-finance/invoice-deductions.js b/resources/assets/js/views/ecommerce-finance/invoice-deductions.js index a959757b..d7197a94 100644 --- a/resources/assets/js/views/ecommerce-finance/invoice-deductions.js +++ b/resources/assets/js/views/ecommerce-finance/invoice-deductions.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message @@ -21,10 +22,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#invoice-deduction-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'invoice-deductions', baseUrl: url + '/workspace-data/supplier-payables/invoice-deductions', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/other-adjustments.js b/resources/assets/js/views/ecommerce-finance/other-adjustments.js index 3108b4d0..aecdfef4 100644 --- a/resources/assets/js/views/ecommerce-finance/other-adjustments.js +++ b/resources/assets/js/views/ecommerce-finance/other-adjustments.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message @@ -21,10 +22,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#other-adjustment-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'other-adjustments', baseUrl: url + '/workspace-data/supplier-payables/other-adjustments', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/return-reconciliations.js b/resources/assets/js/views/ecommerce-finance/return-reconciliations.js index 7f193bfa..76ab3686 100644 --- a/resources/assets/js/views/ecommerce-finance/return-reconciliations.js +++ b/resources/assets/js/views/ecommerce-finance/return-reconciliations.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, @@ -32,10 +33,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#return-reconciliation-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'return-reconciliation', baseUrl: url + '/workspace-data/supplier-payables/return-reconciliation', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/return-unpriced.js b/resources/assets/js/views/ecommerce-finance/return-unpriced.js index 44293c4c..13f56613 100644 --- a/resources/assets/js/views/ecommerce-finance/return-unpriced.js +++ b/resources/assets/js/views/ecommerce-finance/return-unpriced.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Select, Option, Message @@ -23,10 +24,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#return-unpriced-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'return-unpriced', baseUrl: url + '/workspace-data/supplier-payables/return-unpriced', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/sample-deductions.js b/resources/assets/js/views/ecommerce-finance/sample-deductions.js index 21e07f49..c5c0e436 100644 --- a/resources/assets/js/views/ecommerce-finance/sample-deductions.js +++ b/resources/assets/js/views/ecommerce-finance/sample-deductions.js @@ -3,6 +3,7 @@ require('./../../bootstrap'); import Vue from 'vue'; import DashboardPlugin from './../../plugins/dashboard-plugin'; import Global from './../../mixins/global'; +import SupplierPayableMaintenance from './supplier-payable-maintenance'; import { Table, TableColumn, Pagination, DatePicker, Tag, Button, Input, Message @@ -21,10 +22,11 @@ Vue.prototype.$message = Message; const app = new Vue({ el: '#sample-deduction-app', - mixins: [Global], + mixins: [Global, SupplierPayableMaintenance], data() { return { + maintenanceDataset: 'sample-deductions', baseUrl: url + '/workspace-data/supplier-payables/sample-deductions', loading: false, tableData: [], diff --git a/resources/assets/js/views/ecommerce-finance/supplier-payable-maintenance.js b/resources/assets/js/views/ecommerce-finance/supplier-payable-maintenance.js new file mode 100644 index 00000000..e60dc514 --- /dev/null +++ b/resources/assets/js/views/ecommerce-finance/supplier-payable-maintenance.js @@ -0,0 +1,166 @@ +import Vue from 'vue'; +import { Dialog, Form, FormItem, Input, DatePicker, Button } from 'element-ui'; + +[Dialog, Form, FormItem, Input, DatePicker, Button].forEach(component => Vue.component(component.name, component)); + +export default { + data() { + return { + maintenance: { + fields: [], + form: {}, + row: null, + selectedRow: null, + visible: false, + saving: false, + importing: false, + importVisible: false, + importFile: null, + importFileName: '' + } + }; + }, + + computed: { + maintenanceBaseUrl() { + return url + '/workspace/supplier-payables/actions/' + this.maintenanceDataset; + } + }, + + watch: { + tableData() { + this.maintenanceSelect(null); + } + }, + + mounted() { + axios.get(this.maintenanceBaseUrl + '/definition') + .then(response => { this.maintenance.fields = response.data.fields || []; }) + .catch(() => this.$message.error('获取维护字段失败')); + }, + + methods: { + maintenanceSelect(row) { + this.maintenance.selectedRow = row || null; + }, + + maintenanceEditSelected() { + const row = this.maintenance.selectedRow; + if (!row || !row._action_key) { + this.$message.warning('请先选择一条记录'); + return; + } + if (!this.maintenance.fields.length) { + this.$message.warning('编辑字段正在加载,请稍后重试'); + return; + } + this.maintenanceOpen(row); + }, + + maintenanceOpen(row) { + if (!row || !row._action_key || !this.maintenance.fields.length) return; + const form = {}; + this.maintenance.fields.forEach(field => { + const value = row[field.name]; + form[field.name] = value == null ? '' : String(value); + }); + this.maintenance.row = row; + this.maintenance.form = form; + this.maintenance.visible = true; + }, + + maintenanceClose() { + this.maintenance.row = null; + this.maintenance.form = {}; + this.maintenance.visible = false; + this.maintenance.saving = false; + }, + + maintenanceSave() { + if (!this.maintenance.row || !this.maintenance.row._action_key) { + this.$message.error('当前记录缺少维护标识,请刷新后重试'); + return; + } + this.maintenance.saving = true; + axios.patch( + this.maintenanceBaseUrl + '/' + this.maintenanceRecordKey(this.maintenance.row._action_key), + { values: this.maintenance.form }, + { headers: { Accept: 'application/json', 'Content-Type': 'application/json' } } + ).then(response => { + this.$message.success(response.data.message || '保存成功'); + this.maintenance.visible = false; + this.fetchData(); + }).catch(error => this.$message.error(this.maintenanceError(error, '保存失败'))) + .finally(() => { this.maintenance.saving = false; }); + }, + + maintenanceRecordKey(recordKey) { + return String(recordKey).split(':').map(encodeURIComponent).join(':'); + }, + + maintenanceOpenImport() { + this.maintenance.importFile = null; + this.maintenance.importFileName = ''; + this.maintenance.importVisible = true; + }, + + maintenanceCloseImport() { + if (this.maintenance.importing) return; + this.maintenance.importVisible = false; + this.maintenance.importFile = null; + this.maintenance.importFileName = ''; + if (this.$refs.maintenanceImport) this.$refs.maintenanceImport.value = ''; + }, + + maintenanceChooseImport() { + this.$refs.maintenanceImport.click(); + }, + + maintenanceSelectImport(event) { + const file = event.target.files && event.target.files[0]; + if (!file) return; + this.maintenance.importFile = file; + this.maintenance.importFileName = file.name; + }, + + maintenanceImport() { + const file = this.maintenance.importFile; + if (!file) { + this.$message.warning('请先选择导入文件'); + return; + } + + const body = new FormData(); + body.append('import', file); + this.maintenance.importing = true; + axios.post(this.maintenanceBaseUrl + '/import', body, { + headers: { Accept: 'application/json', 'Content-Type': 'multipart/form-data' } + }).then(response => { + this.$message.success(response.data.message || '导入完成'); + this.maintenance.importVisible = false; + this.maintenance.importFile = null; + this.maintenance.importFileName = ''; + if (this.$refs.maintenanceImport) this.$refs.maintenanceImport.value = ''; + this.fetchData(); + }).catch(error => this.$message.error(this.maintenanceError(error, '导入失败'))) + .finally(() => { this.maintenance.importing = false; }); + }, + + maintenanceExport() { + const params = Object.assign({}, this.buildParams ? this.buildParams() : {}); + delete params.page; + delete params.limit; + const query = new URLSearchParams(params).toString(); + window.location.href = this.maintenanceBaseUrl + '/export' + (query ? '?' + query : ''); + }, + + maintenanceError(error, fallback) { + const errors = error && error.response && error.response.data && error.response.data.errors; + if (errors) { + const first = Object.values(errors)[0]; + if (Array.isArray(first) && first[0]) return first[0]; + } + return (error && error.response && error.response.data && error.response.data.message) || fallback; + } + } +}; diff --git a/resources/views/business/pages/supplier-payables/advance-details.blade.php b/resources/views/business/pages/supplier-payables/advance-details.blade.php index b713bc12..a20fcb40 100644 --- a/resources/views/business/pages/supplier-payables/advance-details.blade.php +++ b/resources/views/business/pages/supplier-payables/advance-details.blade.php @@ -6,7 +6,7 @@ {{-- 统计卡片 --}} -
+
借支总金额
@{{ formatMoney(summary.totalAmount) }}
@@ -25,8 +25,7 @@ start-placeholder="月份开始" end-placeholder="月份结束" size="small" style="width: 240px" value-format="yyyy-MM" @change="onFilterChange"> - 查询 - 重置 + @include('business.pages.supplier-payables.maintenance-actions')
@@ -35,7 +34,7 @@
加载中...
- + diff --git a/resources/views/business/pages/supplier-payables/board-deductions.blade.php b/resources/views/business/pages/supplier-payables/board-deductions.blade.php index a17c48c9..aa49074b 100644 --- a/resources/views/business/pages/supplier-payables/board-deductions.blade.php +++ b/resources/views/business/pages/supplier-payables/board-deductions.blade.php @@ -3,6 +3,7 @@ $boardDeductionFilters = $content['boardDeductionFilters'] ?? []; $boardDeductionError = $content['boardDeductionError'] ?? null; $boardDeductionStatus = data_get($content, 'boardDeductionStatus.X-Caiwu-Data-Status'); + $boardActions = url('/' . (company_id() ?: 1) . '/workspace/supplier-payables/actions/board-deductions'); @endphp
@@ -53,6 +54,7 @@ .supplier-board-actions { display: flex; + flex-wrap: wrap; gap: 0.5rem; } @@ -78,6 +80,8 @@ color: #ffffff; } + .supplier-board-button .material-icons-outlined { font-size: 1.125rem; } + .supplier-board-alert { margin-bottom: 1rem; padding: 0.75rem 1rem; @@ -189,17 +193,31 @@
-
- + + 导出 + + - + 重置 +
+ @include('business.pages.supplier-payables.maintenance-import-modal', [ + 'supplierImportAction' => $boardActions . '/import', + 'supplierTemplateUrl' => $boardActions . '/template', + ]) @if ($boardDeductionError) @@ -231,7 +249,7 @@ @forelse ($boardDeductions ?? [] as $row) - + {{ $row->stock_in_date ?: '-' }} {{ $row->stock_out_date ?: '-' }} {{ $row->furniture_factory ?: '-' }} @@ -277,3 +295,4 @@
+@include('business.pages.supplier-payables.maintenance-blade-modal') diff --git a/resources/views/business/pages/supplier-payables/compensation-details.blade.php b/resources/views/business/pages/supplier-payables/compensation-details.blade.php index ddfce9dd..030e290d 100644 --- a/resources/views/business/pages/supplier-payables/compensation-details.blade.php +++ b/resources/views/business/pages/supplier-payables/compensation-details.blade.php @@ -46,8 +46,7 @@ start-placeholder="月份开始" end-placeholder="月份结束" size="small" style="width: 240px" value-format="yyyy-MM" @change="onFilterChange"> - 查询 - 重置 + @include('business.pages.supplier-payables.maintenance-actions') @@ -56,7 +55,7 @@
加载中...
- + diff --git a/resources/views/business/pages/supplier-payables/delivery-unpriced.blade.php b/resources/views/business/pages/supplier-payables/delivery-unpriced.blade.php index adee8b68..46fb957e 100644 --- a/resources/views/business/pages/supplier-payables/delivery-unpriced.blade.php +++ b/resources/views/business/pages/supplier-payables/delivery-unpriced.blade.php @@ -1,7 +1,7 @@
{{-- 统计卡片 --}} -
+
未定价记录数
@{{ (summary.totalCount || 0).toLocaleString() }}
@@ -29,8 +29,7 @@ start-placeholder="日期开始" end-placeholder="日期结束" size="small" style="width: 260px" value-format="yyyy-MM-dd" @change="onFilterChange"> - 查询 - 重置 + @include('business.pages.supplier-payables.maintenance-actions')
@@ -39,7 +38,7 @@
加载中...
- + diff --git a/resources/views/business/pages/supplier-payables/dropshipping-express.blade.php b/resources/views/business/pages/supplier-payables/dropshipping-express.blade.php index 692745e2..86daeccd 100644 --- a/resources/views/business/pages/supplier-payables/dropshipping-express.blade.php +++ b/resources/views/business/pages/supplier-payables/dropshipping-express.blade.php @@ -1,7 +1,7 @@
{{-- 统计卡片 --}} -
+
合计运费
@{{ formatMoney(summary.totalAmount) }}
@@ -23,8 +23,7 @@ start-placeholder="月份开始" end-placeholder="月份结束" size="small" style="width: 240px" value-format="yyyy-MM" @change="onFilterChange"> - 查询 - 重置 + @include('business.pages.supplier-payables.maintenance-actions')
@@ -33,7 +32,7 @@
加载中...
- + diff --git a/resources/views/business/pages/supplier-payables/invoice-deductions.blade.php b/resources/views/business/pages/supplier-payables/invoice-deductions.blade.php index a7cd58eb..2fc0d9dd 100644 --- a/resources/views/business/pages/supplier-payables/invoice-deductions.blade.php +++ b/resources/views/business/pages/supplier-payables/invoice-deductions.blade.php @@ -1,7 +1,7 @@
{{-- 统计卡片 --}} -
+
扣款总金额
@{{ formatMoney(summary.totalPrice) }}
@@ -20,8 +20,7 @@ start-placeholder="月份开始" end-placeholder="月份结束" size="small" style="width: 240px" value-format="yyyy-MM" @change="onFilterChange"> - 查询 - 重置 + @include('business.pages.supplier-payables.maintenance-actions')
@@ -30,7 +29,7 @@
加载中...
- + diff --git a/resources/views/business/pages/supplier-payables/maintenance-actions.blade.php b/resources/views/business/pages/supplier-payables/maintenance-actions.blade.php new file mode 100644 index 00000000..3b72a117 --- /dev/null +++ b/resources/views/business/pages/supplier-payables/maintenance-actions.blade.php @@ -0,0 +1,10 @@ +
+ @include('business.pages.supplier-payables.maintenance-toolbar') + + 查询 + + + 重置 + + @include('business.pages.supplier-payables.maintenance-edit-button') +
diff --git a/resources/views/business/pages/supplier-payables/maintenance-blade-modal.blade.php b/resources/views/business/pages/supplier-payables/maintenance-blade-modal.blade.php new file mode 100644 index 00000000..26e7dee2 --- /dev/null +++ b/resources/views/business/pages/supplier-payables/maintenance-blade-modal.blade.php @@ -0,0 +1,109 @@ +@php + $maintenance = $content['supplierPayableMaintenance'] ?? ['dataset' => '', 'fields' => []]; + $maintenanceAction = url('/' . (company_id() ?: 1) . '/workspace/supplier-payables/actions/' . $maintenance['dataset']); +@endphp + + + +@once + +@endonce + +@push('body_end') + +@endpush diff --git a/resources/views/business/pages/supplier-payables/maintenance-edit-button.blade.php b/resources/views/business/pages/supplier-payables/maintenance-edit-button.blade.php new file mode 100644 index 00000000..2f9eb8dc --- /dev/null +++ b/resources/views/business/pages/supplier-payables/maintenance-edit-button.blade.php @@ -0,0 +1,6 @@ + + 编辑 + diff --git a/resources/views/business/pages/supplier-payables/maintenance-import-modal.blade.php b/resources/views/business/pages/supplier-payables/maintenance-import-modal.blade.php new file mode 100644 index 00000000..1dbe9a30 --- /dev/null +++ b/resources/views/business/pages/supplier-payables/maintenance-import-modal.blade.php @@ -0,0 +1,96 @@ + + +@once + +@endonce + +@push('body_end') + +@endpush diff --git a/resources/views/business/pages/supplier-payables/maintenance-toolbar.blade.php b/resources/views/business/pages/supplier-payables/maintenance-toolbar.blade.php new file mode 100644 index 00000000..35d7e671 --- /dev/null +++ b/resources/views/business/pages/supplier-payables/maintenance-toolbar.blade.php @@ -0,0 +1,43 @@ + + 导入 + + + 导出 + + + +
+ + + 下载导入模板当前页面字段模板(Excel) + + + +
+ + 取消 + 开始导入 + +
+ + + + + + + + + + + 取消 + 保存 + + diff --git a/resources/views/business/pages/supplier-payables/navigation.blade.php b/resources/views/business/pages/supplier-payables/navigation.blade.php index ef119914..3fece263 100644 --- a/resources/views/business/pages/supplier-payables/navigation.blade.php +++ b/resources/views/business/pages/supplier-payables/navigation.blade.php @@ -33,5 +33,106 @@ .supplier-page-navigation { display:flex; flex-wrap:wrap; gap:4px 16px; margin-bottom:16px; } .supplier-page-navigation .business-tab-link { flex:0 0 auto; padding:10px 0; font-size:0.75rem; color:var(--vip-content-muted, #595959); } .supplier-page-navigation .business-tab-link.is-active { color:var(--vip-content-accent-dark, #856632); } + .supplier-maintenance-file { display: none; } + .supplier-maintenance-form { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0 1rem; + max-height: 60vh; + overflow-y: auto; + padding-right: 0.25rem; + } + .supplier-maintenance-form .el-form-item { margin-bottom: 0.75rem; } + .supplier-maintenance-form .el-form-item__label { padding: 0; color: #595959; font-size: 0.75rem; } + .supplier-maintenance-form input { letter-spacing: 0; } + .supplier-maintenance-form input:focus { border-color: #856632; box-shadow: 0 0 0 3px rgba(133, 102, 50, .20); } + .supplier-maintenance-form + * .el-button--primary, + .el-dialog__footer .el-button--primary { border-color: #856632; background: #856632; } + .supplier-import-dialog { display: grid; gap: 12px; } + .supplier-import-template-link, + .supplier-import-file-button { + display: flex; + width: 100%; + min-height: 64px; + box-sizing: border-box; + align-items: center; + gap: 12px; + padding: 12px 14px; + border: 1px solid #D8D2C7; + border-radius: 8px; + background: #fff; + color: #171717; + text-align: left; + text-decoration: none; + cursor: pointer; + } + .supplier-import-template-link:hover, + .supplier-import-template-link:focus, + .supplier-import-file-button:hover, + .supplier-import-file-button:focus { border-color: #856632; background: #F7F0DE; color: #856632; outline: 0; } + .supplier-import-template-link .material-icons-outlined, + .supplier-import-file-button .material-icons-outlined { flex: 0 0 auto; font-size: 22px; } + .supplier-import-template-link > span:last-child { display: grid; gap: 3px; } + .supplier-import-template-link strong { font-size: .875rem; font-weight: 600; } + .supplier-import-template-link small { color: #737373; font-size: .75rem; } + .supplier-import-file-button > span:last-child { min-width: 0; overflow: hidden; font-size: .8125rem; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } + .supplier-page-actions { + display: inline-flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; + } + .supplier-page-actions .supplier-action-button { + display: inline-flex; + box-sizing: border-box; + min-width: 82px; + height: 36px; + margin: 0; + padding: 0 14px; + align-items: center; + justify-content: center; + gap: 6px; + border: 1px solid var(--vip-border, #D8D2C7); + border-radius: 8px; + background: #fff; + color: #171717; + font-size: 0.8125rem; + font-weight: 600; + line-height: 1; + letter-spacing: 0; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + } + .supplier-page-actions .supplier-action-button > span, + .supplier-page-actions .supplier-action-button.el-button > span { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + } + .supplier-page-actions .supplier-action-button .material-icons-outlined { + font-size: 18px; + line-height: 1; + } + .supplier-page-actions .supplier-action-button:hover, + .supplier-page-actions .supplier-action-button:focus { + border-color: var(--vip-content-accent-dark, #856632); + background: var(--vip-content-accent-soft, #F7F0DE); + color: var(--vip-content-accent-dark, #856632); + outline: 0; + } + .supplier-page-actions .supplier-action-button.is-primary { + border-color: var(--vip-content-accent-dark, #856632); + background: var(--vip-content-accent-dark, #856632); + color: #fff; + } + .supplier-page-actions .supplier-action-button.is-primary:hover, + .supplier-page-actions .supplier-action-button.is-primary:focus { + border-color: #6f5428; + background: #6f5428; + color: #fff; + } + @media (max-width: 640px) { .supplier-maintenance-form { grid-template-columns: 1fr; } } @include('business.pages.supplier-payables.local-navigation') diff --git a/resources/views/business/pages/supplier-payables/other-adjustments.blade.php b/resources/views/business/pages/supplier-payables/other-adjustments.blade.php index 953ddea8..3faea24b 100644 --- a/resources/views/business/pages/supplier-payables/other-adjustments.blade.php +++ b/resources/views/business/pages/supplier-payables/other-adjustments.blade.php @@ -1,7 +1,7 @@
{{-- 统计卡片 --}} -
+
补款总金额
@{{ formatMoney(summary.totalAmount) }}
@@ -20,8 +20,7 @@ start-placeholder="月份开始" end-placeholder="月份结束" size="small" style="width: 240px" value-format="yyyy-MM" @change="onFilterChange"> - 查询 - 重置 + @include('business.pages.supplier-payables.maintenance-actions')
@@ -30,7 +29,7 @@
加载中...
- +