功能:安全整合封边条上游数据
This commit is contained in:
parent
d9590ae28d
commit
5045fadfe8
@ -9,11 +9,21 @@ use App\Utilities\SidebarPermissions;
|
||||
use Illuminate\Routing\Controller;
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException;
|
||||
use Throwable;
|
||||
|
||||
class Pages extends Controller
|
||||
{
|
||||
private const EDGE_BANDING_SOURCE = 'jiansun';
|
||||
|
||||
public function __construct(
|
||||
private readonly EdgeBandingRepository $edgeBanding,
|
||||
private readonly UpstreamQueryCacheFactory $upstreamCaches,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的营销账户平台列表
|
||||
@ -92,6 +102,7 @@ class Pages extends Controller
|
||||
'tabs' => $tabs,
|
||||
'activeTab' => null,
|
||||
'content' => $content,
|
||||
'edgeBanding' => $this->edgeBandingContent($page),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -111,6 +122,7 @@ class Pages extends Controller
|
||||
'tabs' => $tabs,
|
||||
'activeTab' => $activeTab,
|
||||
'content' => $content,
|
||||
'edgeBanding' => $this->edgeBandingContent($page),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -126,6 +138,103 @@ class Pages extends Controller
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 封边条页面只读取上游快照;未启用或无快照时将错误交给页面显示,
|
||||
* 避免工作台因上游暂不可用直接返回 500。
|
||||
*/
|
||||
protected function edgeBandingContent(string $page): ?array
|
||||
{
|
||||
$scope = match ($page) {
|
||||
'edge-banding-prices' => 'edge-banding.prices',
|
||||
'details' => 'edge-banding.details',
|
||||
'edge-banding-payables' => 'edge-banding.payables',
|
||||
default => null,
|
||||
};
|
||||
|
||||
if ($scope === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$filters = match ($page) {
|
||||
'edge-banding-prices' => $this->edgeBandingFilters([
|
||||
'raw_name', 'raw_code', 'production_code', 'supplier',
|
||||
]),
|
||||
'details' => $this->edgeBandingFilters([
|
||||
'date', 'production_code', 'raw_name', 'return_flag',
|
||||
'outbound_supplier', 'arrival_supplier', 'outbound_no',
|
||||
]),
|
||||
'edge-banding-payables' => $this->edgeBandingFilters(['period', 'supplier']),
|
||||
default => [],
|
||||
};
|
||||
|
||||
if ($page === 'edge-banding-payables' && ! isset($filters['period'])) {
|
||||
$filters['period'] = '2026-06';
|
||||
}
|
||||
|
||||
$pageNumber = max((int) request('data_page', 1), 1);
|
||||
$limit = min(max((int) request('data_limit', 25), 1), 100);
|
||||
$cache = $this->upstreamCaches->forSource(self::EDGE_BANDING_SOURCE);
|
||||
|
||||
try {
|
||||
$result = $cache->remember(
|
||||
$scope,
|
||||
['filters' => $filters, 'page' => $pageNumber, 'limit' => $limit],
|
||||
fn () => match ($scope) {
|
||||
'edge-banding.prices' => $this->edgeBanding->prices($filters, $pageNumber, $limit),
|
||||
'edge-banding.details' => $this->edgeBanding->details($filters, $pageNumber, $limit),
|
||||
'edge-banding.payables' => $this->edgeBanding->payables($filters, $pageNumber, $limit),
|
||||
},
|
||||
);
|
||||
|
||||
return [
|
||||
'filters' => $filters,
|
||||
'result' => $result,
|
||||
'page' => $pageNumber,
|
||||
'limit' => $limit,
|
||||
'last_page' => max((int) ceil(max((int) ($result['total'] ?? 0), 0) / $limit), 1),
|
||||
'status' => $cache->responseHeaders(),
|
||||
'error' => null,
|
||||
];
|
||||
} catch (ServiceUnavailableHttpException $exception) {
|
||||
return [
|
||||
'filters' => $filters,
|
||||
'result' => ['data' => [], 'total' => 0],
|
||||
'page' => $pageNumber,
|
||||
'limit' => $limit,
|
||||
'last_page' => 1,
|
||||
'status' => [],
|
||||
'error' => $exception->getMessage(),
|
||||
];
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
return [
|
||||
'filters' => $filters,
|
||||
'result' => ['data' => [], 'total' => 0],
|
||||
'page' => $pageNumber,
|
||||
'limit' => $limit,
|
||||
'last_page' => 1,
|
||||
'status' => [],
|
||||
'error' => '封边条数据暂时无法读取,请稍后重试。',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function edgeBandingFilters(array $fields): array
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$value = trim((string) request($field, ''));
|
||||
|
||||
if ($value !== '') {
|
||||
$filters[$field] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 种菜应收对账 - 加载对账数据(21字段)
|
||||
*/
|
||||
@ -208,7 +317,7 @@ class Pages extends Controller
|
||||
$pageSize = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||||
|
||||
$repository = app(PinduoduoSpreadBillRepository::class);
|
||||
$cache = app(UpstreamQueryCacheFactory::class)->forSource('jiansun');
|
||||
$cache = $this->upstreamCaches->forSource('jiansun');
|
||||
$filters = [
|
||||
'store_name' => $storeName !== '' ? $storeName : null,
|
||||
'from' => $fromDate !== '' ? $fromDate : null,
|
||||
|
||||
@ -25,7 +25,10 @@ return [
|
||||
'database' => env('DB_JIANSUN_DATABASE'),
|
||||
'username' => env('DB_JIANSUN_USERNAME'),
|
||||
'password' => env('DB_JIANSUN_PASSWORD'),
|
||||
'odbc_driver' => env('DB_JIANSUN_ODBC_DRIVER', 'FreeTDS'),
|
||||
'odbc_driver' => env(
|
||||
'DB_JIANSUN_ODBC_DRIVER',
|
||||
PHP_OS_FAMILY === 'Windows' ? 'ODBC Driver 18 for SQL Server' : 'FreeTDS'
|
||||
),
|
||||
'tds_version' => env('DB_JIANSUN_TDS_VERSION', '7.4'),
|
||||
// 旧 SQL Server 的可选 TLS 预登录不稳定;仅本地开发默认关闭协商。
|
||||
'encryption' => env(
|
||||
|
||||
@ -24,7 +24,10 @@ return [
|
||||
'database' => env('DB_OPERATIONS_REPLICA_DATABASE'),
|
||||
'username' => env('DB_OPERATIONS_REPLICA_USERNAME'),
|
||||
'password' => env('DB_OPERATIONS_REPLICA_PASSWORD'),
|
||||
'odbc_driver' => env('DB_OPERATIONS_REPLICA_ODBC_DRIVER', 'FreeTDS'),
|
||||
'odbc_driver' => env(
|
||||
'DB_OPERATIONS_REPLICA_ODBC_DRIVER',
|
||||
PHP_OS_FAMILY === 'Windows' ? 'ODBC Driver 18 for SQL Server' : 'FreeTDS'
|
||||
),
|
||||
'tds_version' => env('DB_OPERATIONS_REPLICA_TDS_VERSION', '7.4'),
|
||||
// 旧 SQL Server 的可选 TLS 预登录不稳定;仅本地开发默认关闭协商。
|
||||
'encryption' => env(
|
||||
|
||||
293
modules/EcommerceFinance/Repositories/EdgeBandingRepository.php
Normal file
293
modules/EcommerceFinance/Repositories/EdgeBandingRepository.php
Normal file
@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
|
||||
/**
|
||||
* 原材料-封边条只读数据仓库。
|
||||
*
|
||||
* 数据源只暴露固定的 SELECT,页面请求通过 JianSunQueryCache 做快照、限流
|
||||
* 和熔断。源表中不存在的展示字段使用空值,不从演示数据倒灌真实业务数据。
|
||||
*/
|
||||
class EdgeBandingRepository
|
||||
{
|
||||
private const SOURCE = 'jiansun';
|
||||
|
||||
private ?UpstreamReadOnlyQuery $query = null;
|
||||
|
||||
public function __construct(private readonly UpstreamReadOnlyQueryFactory $queries)
|
||||
{
|
||||
}
|
||||
|
||||
/** @return array{data:array<int,array<string,mixed>>,total:int} */
|
||||
public function prices(array $filters, int $page, int $limit): array
|
||||
{
|
||||
$where = ["p.number LIKE 'FBT%'"];
|
||||
$bindings = [];
|
||||
|
||||
if (($filters['raw_name'] ?? '') !== '') {
|
||||
$where[] = 'p.name LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['raw_name']);
|
||||
}
|
||||
if (($filters['raw_code'] ?? '') !== '') {
|
||||
$where[] = 'p.number LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['raw_code']);
|
||||
}
|
||||
if (($filters['production_code'] ?? '') !== '') {
|
||||
$where[] = "CONCAT(p.number, 'F01') LIKE ? ESCAPE '\\'";
|
||||
$bindings[] = $this->contains($filters['production_code']);
|
||||
}
|
||||
if (($filters['supplier'] ?? '') !== '') {
|
||||
$where[] = 'p.supplier LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['supplier']);
|
||||
}
|
||||
|
||||
$whereSql = implode(' AND ', $where);
|
||||
$page = max($page, 1);
|
||||
$limit = min(max($limit, 1), 100);
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$sql = "WITH source_rows AS (
|
||||
SELECT
|
||||
p.name AS raw_name,
|
||||
p.number AS raw_code,
|
||||
CONCAT(p.number, 'F01') AS production_code,
|
||||
CAST(p.price AS decimal(18,2)) AS latest_price,
|
||||
p.supplier AS target_supplier,
|
||||
ROW_NUMBER() OVER (ORDER BY p.number, p.supplier) AS row_no,
|
||||
COUNT(*) OVER () AS total_count
|
||||
FROM [dbo].[pm_averageUnitPrice] p
|
||||
WHERE {$whereSql}
|
||||
)
|
||||
SELECT raw_name, raw_code, production_code,
|
||||
latest_price, target_supplier, total_count
|
||||
FROM source_rows
|
||||
WHERE row_no BETWEEN ? AND ?
|
||||
ORDER BY row_no";
|
||||
|
||||
$rows = $this->query()->select($sql, array_merge($bindings, [$offset + 1, $offset + $limit]));
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['price_type'] = '采购价';
|
||||
$row['factory_type'] = '封边条厂';
|
||||
}
|
||||
|
||||
return $this->pageResult($rows);
|
||||
}
|
||||
|
||||
/** @return array{data:array<int,array<string,mixed>>,total:int} */
|
||||
public function details(array $filters, int $page, int $limit): array
|
||||
{
|
||||
$where = ["s.procode LIKE 'FBT%'"];
|
||||
$bindings = [];
|
||||
|
||||
if (($filters['date'] ?? '') !== '') {
|
||||
$where[] = 'CONVERT(date, s.sendTime) = ?';
|
||||
$bindings[] = $filters['date'];
|
||||
}
|
||||
if (($filters['production_code'] ?? '') !== '') {
|
||||
$where[] = 's.procode LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['production_code']);
|
||||
}
|
||||
if (($filters['raw_name'] ?? '') !== '') {
|
||||
$where[] = 's.skuname LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['raw_name']);
|
||||
}
|
||||
if (($filters['return_flag'] ?? '') === 'return') {
|
||||
$where[] = 'ISNULL(s.settlementStock, 0) < 0';
|
||||
} elseif (($filters['return_flag'] ?? '') === 'normal') {
|
||||
$where[] = 'ISNULL(s.settlementStock, 0) >= 0';
|
||||
}
|
||||
if (($filters['outbound_supplier'] ?? '') !== '') {
|
||||
$where[] = 's.supplier LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['outbound_supplier']);
|
||||
}
|
||||
if (($filters['arrival_supplier'] ?? '') !== '') {
|
||||
$where[] = 's.whName LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['arrival_supplier']);
|
||||
}
|
||||
if (($filters['outbound_no'] ?? '') !== '') {
|
||||
// 现有结算表没有单独的出库单号,快递/送货号作为可检索的外部单号。
|
||||
$where[] = 'ISNULL(s.expressNo, \'\') LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['outbound_no']);
|
||||
}
|
||||
|
||||
$whereSql = implode(' AND ', $where);
|
||||
$page = max($page, 1);
|
||||
$limit = min(max($limit, 1), 100);
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$sql = "WITH source_rows AS (
|
||||
SELECT
|
||||
CONVERT(varchar(19), s.sendTime, 120) AS inbound_date,
|
||||
s.procode AS production_code,
|
||||
s.skuname AS raw_name,
|
||||
CAST(ISNULL(s.settlementStock, 0) AS decimal(18,2)) AS outbound_quantity,
|
||||
s.supplier AS outbound_supplier,
|
||||
ISNULL(s.whName, '') AS arrival_supplier,
|
||||
CONVERT(varchar(10), s.sendTime, 120) AS batch_date,
|
||||
'' AS outbound_no,
|
||||
ISNULL(s.remark, '') AS outbound_remark,
|
||||
'' AS requisition_no,
|
||||
ISNULL(s.expressNo, '') AS delivery_no,
|
||||
CAST(ISNULL(s.settlementStock, 0) AS decimal(18,2)) AS received_quantity,
|
||||
CAST(ISNULL(s.settlementPrice, 0) AS decimal(18,2)) AS reconciliation_amount,
|
||||
CAST(0 AS decimal(18,2)) AS freight,
|
||||
CAST(ISNULL(a.price, 0) AS decimal(18,2)) AS edge_band_unit_price,
|
||||
CAST(ISNULL(s.settlementStock, 0) * ISNULL(a.price, 0) AS decimal(18,2)) AS payable_amount,
|
||||
CAST(ISNULL(s.singlePrice, 0) AS decimal(18,2)) AS furniture_unit_price,
|
||||
CAST(ISNULL(s.settlementPrice, 0) AS decimal(18,2)) AS furniture_amount,
|
||||
CAST(ISNULL(s.singlePrice, 0) - ISNULL(a.price, 0) AS decimal(18,2)) AS price_difference,
|
||||
CAST(ISNULL(s.settlementPrice, 0) - (ISNULL(s.settlementStock, 0) * ISNULL(a.price, 0)) AS decimal(18,2)) AS profit,
|
||||
ROW_NUMBER() OVER (ORDER BY s.sendTime DESC, s.Id DESC) AS row_no,
|
||||
COUNT(*) OVER () AS total_count
|
||||
FROM [dbo].[fys_supplierGoods_sttlement_sale] s
|
||||
LEFT JOIN [dbo].[pm_averageUnitPrice] a
|
||||
ON a.number = s.procode AND a.supplier = s.supplier
|
||||
WHERE {$whereSql}
|
||||
)
|
||||
SELECT inbound_date, production_code, raw_name, outbound_quantity,
|
||||
outbound_supplier, arrival_supplier, batch_date,
|
||||
outbound_no, outbound_remark, requisition_no, delivery_no,
|
||||
received_quantity, reconciliation_amount, freight, edge_band_unit_price,
|
||||
payable_amount, furniture_unit_price, furniture_amount, price_difference,
|
||||
profit, total_count
|
||||
FROM source_rows
|
||||
WHERE row_no BETWEEN ? AND ?
|
||||
ORDER BY row_no";
|
||||
|
||||
$rows = $this->query()->select($sql, array_merge($bindings, [$offset + 1, $offset + $limit]));
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
$row['unit'] = '米';
|
||||
$row['return_flag'] = (float) ($row['outbound_quantity'] ?? 0) < 0 ? '退货' : '正常出库';
|
||||
$row['number_type'] = '申领';
|
||||
}
|
||||
|
||||
return $this->pageResult($rows);
|
||||
}
|
||||
|
||||
/** @return array{data:array<int,array<string,mixed>>,total:int} */
|
||||
public function payables(array $filters, int $page, int $limit): array
|
||||
{
|
||||
$where = [
|
||||
"EXISTS (
|
||||
SELECT 1
|
||||
FROM [dbo].[pm_averageUnitPrice] edge_price
|
||||
WHERE edge_price.number LIKE 'FBT%'
|
||||
AND edge_price.supplier = p.supplier
|
||||
)",
|
||||
];
|
||||
$bindings = [];
|
||||
|
||||
if (($filters['period'] ?? '') !== '') {
|
||||
$where[] = 'p.Month = ?';
|
||||
$bindings[] = $filters['period'];
|
||||
}
|
||||
if (($filters['supplier'] ?? '') !== '') {
|
||||
$where[] = 'p.supplier LIKE ? ESCAPE \'\\\'';
|
||||
$bindings[] = $this->contains($filters['supplier']);
|
||||
}
|
||||
|
||||
$whereSql = implode(' AND ', $where);
|
||||
$page = max($page, 1);
|
||||
$limit = min(max($limit, 1), 100);
|
||||
$offset = ($page - 1) * $limit;
|
||||
|
||||
$sql = "WITH source_rows AS (
|
||||
SELECT
|
||||
p.supplier,
|
||||
CAST(SUM(ISNULL(p.bginMonthPay, 0)) AS decimal(18,2)) AS month_start_unpaid,
|
||||
CAST(SUM(ISNULL(p.loan, 0)) AS decimal(18,2)) AS goods_amount,
|
||||
CAST(SUM(ISNULL(p.ExpFieght, 0)) AS decimal(18,2)) AS prepaid_freight,
|
||||
CAST(SUM(ISNULL(p.Totalindemnity, 0)) AS decimal(18,2)) AS compensation,
|
||||
CAST(SUM(ISNULL(p.supplySup, 0)) AS decimal(18,2)) AS supplier_refund,
|
||||
CAST(SUM(ISNULL(p.ShouldPay, 0)) AS decimal(18,2)) AS payable_amount,
|
||||
CAST(SUM(ISNULL(p.factPay, 0)) AS decimal(18,2)) AS paid_amount,
|
||||
CAST(SUM(ISNULL(p.cashierbypaid, 0)) AS decimal(18,2)) AS actual_payment,
|
||||
CAST(SUM(ISNULL(p.toPrivate, 0)) AS decimal(18,2)) AS private_payment,
|
||||
CAST(SUM(ISNULL(p.toCorporate, 0)) AS decimal(18,2)) AS corporate_payment,
|
||||
MAX(ISNULL(p.remarks, '')) AS remark,
|
||||
CAST(SUM(SUM(ISNULL(p.bginMonthPay, 0))) OVER () AS decimal(18,2)) AS summary_month_start_unpaid,
|
||||
CAST(SUM(SUM(ISNULL(p.loan, 0))) OVER () AS decimal(18,2)) AS summary_goods_amount,
|
||||
CAST(SUM(SUM(ISNULL(p.ExpFieght, 0))) OVER () AS decimal(18,2)) AS summary_prepaid_freight,
|
||||
CAST(SUM(SUM(ISNULL(p.Totalindemnity, 0))) OVER () AS decimal(18,2)) AS summary_compensation,
|
||||
CAST(SUM(SUM(ISNULL(p.supplySup, 0))) OVER () AS decimal(18,2)) AS summary_supplier_refund,
|
||||
CAST(SUM(SUM(ISNULL(p.ShouldPay, 0))) OVER () AS decimal(18,2)) AS summary_payable_amount,
|
||||
CAST(SUM(SUM(ISNULL(p.factPay, 0))) OVER () AS decimal(18,2)) AS summary_paid_amount,
|
||||
CAST(SUM(SUM(ISNULL(p.cashierbypaid, 0))) OVER () AS decimal(18,2)) AS summary_actual_payment,
|
||||
CAST(SUM(SUM(ISNULL(p.toPrivate, 0))) OVER () AS decimal(18,2)) AS summary_private_payment,
|
||||
CAST(SUM(SUM(ISNULL(p.toCorporate, 0))) OVER () AS decimal(18,2)) AS summary_corporate_payment,
|
||||
ROW_NUMBER() OVER (ORDER BY p.supplier) AS row_no,
|
||||
COUNT(*) OVER () AS total_count
|
||||
FROM [dbo].[fys_supplierPaymentSummary] p
|
||||
WHERE {$whereSql}
|
||||
GROUP BY p.supplier
|
||||
)
|
||||
SELECT supplier, month_start_unpaid, goods_amount, prepaid_freight,
|
||||
compensation, supplier_refund, payable_amount, paid_amount,
|
||||
actual_payment, private_payment, corporate_payment, remark,
|
||||
summary_month_start_unpaid, summary_goods_amount,
|
||||
summary_prepaid_freight, summary_compensation,
|
||||
summary_supplier_refund, summary_payable_amount,
|
||||
summary_paid_amount, summary_actual_payment,
|
||||
summary_private_payment, summary_corporate_payment,
|
||||
total_count
|
||||
FROM source_rows
|
||||
WHERE row_no BETWEEN ? AND ?
|
||||
ORDER BY row_no";
|
||||
|
||||
$rows = $this->query()->select($sql, array_merge($bindings, [$offset + 1, $offset + $limit]));
|
||||
|
||||
return $this->payablesPageResult($rows);
|
||||
}
|
||||
|
||||
private function query(): UpstreamReadOnlyQuery
|
||||
{
|
||||
return $this->query ??= $this->queries->forSource(self::SOURCE);
|
||||
}
|
||||
|
||||
private function contains(string $value): string
|
||||
{
|
||||
return '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], trim($value)) . '%';
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $rows */
|
||||
private function pageResult(array $rows): array
|
||||
{
|
||||
$total = (int) ($rows[0]['total_count'] ?? 0);
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
unset($row['total_count']);
|
||||
}
|
||||
|
||||
return ['data' => $rows, 'total' => $total];
|
||||
}
|
||||
|
||||
/** @param array<int,array<string,mixed>> $rows */
|
||||
private function payablesPageResult(array $rows): array
|
||||
{
|
||||
$summaryKeys = [
|
||||
'month_start_unpaid', 'goods_amount', 'prepaid_freight', 'compensation',
|
||||
'supplier_refund', 'payable_amount', 'paid_amount', 'actual_payment',
|
||||
'private_payment', 'corporate_payment',
|
||||
];
|
||||
$totals = array_fill_keys($summaryKeys, 0.0);
|
||||
|
||||
if ($rows !== []) {
|
||||
foreach ($summaryKeys as $key) {
|
||||
$totals[$key] = (float) ($rows[0]['summary_' . $key] ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rows as &$row) {
|
||||
foreach ($summaryKeys as $key) {
|
||||
unset($row['summary_' . $key]);
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($this->pageResult($rows), ['totals' => $totals]);
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,7 @@ namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Modules\EcommerceFinance\Contracts\UpstreamSnapshotRefresher;
|
||||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||||
use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierPayableRepository;
|
||||
@ -15,6 +16,7 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly JianSunQueryCache $cache,
|
||||
private readonly EdgeBandingRepository $edgeBanding,
|
||||
private readonly GoodsReconciliationRepository $goods,
|
||||
private readonly PinduoduoSpreadBillRepository $pinduoduo,
|
||||
private readonly SupplierPayableRepository $supplier,
|
||||
@ -50,6 +52,10 @@ class JianSunSnapshotRefresher implements UpstreamSnapshotRefresher
|
||||
$flowType = in_array($flowType, ['income', 'expense'], true) ? $flowType : null;
|
||||
|
||||
return match ($scope) {
|
||||
'edge-banding.prices' => $this->edgeBanding->prices($filters, $page, $limit),
|
||||
'edge-banding.details' => $this->edgeBanding->details($filters, $page, $limit),
|
||||
'edge-banding.payables' => $this->edgeBanding->payables($filters, $page, $limit),
|
||||
|
||||
'goods.list' => $this->goods->list($filters, $page, $limit),
|
||||
'goods.summary' => $this->goods->summary($filters),
|
||||
'goods.options' => $this->goods->filterOptions(),
|
||||
|
||||
@ -55,6 +55,16 @@ class UpstreamSourceRegistry
|
||||
return $configured;
|
||||
}
|
||||
|
||||
if ($suffix === 'connection') {
|
||||
return $this->connection($definition['key'], (array) $configured);
|
||||
}
|
||||
|
||||
// Hosts and credentials come from the shared record, while the ODBC
|
||||
// driver must match the operating system that runs this application.
|
||||
if ($suffix === 'connection.odbc_driver' && trim((string) $configured) !== '') {
|
||||
return $configured;
|
||||
}
|
||||
|
||||
return $this->storedConfiguration->value($definition['key'], $suffix, $configured);
|
||||
}
|
||||
|
||||
@ -67,4 +77,30 @@ class UpstreamSourceRegistry
|
||||
{
|
||||
return (bool) config('upstreams.local_configuration.enabled', false);
|
||||
}
|
||||
|
||||
private function connection(string $source, array $configured): array
|
||||
{
|
||||
$fields = array_values(array_unique(array_merge(array_keys($configured), [
|
||||
'host',
|
||||
'port',
|
||||
'database',
|
||||
'username',
|
||||
'password',
|
||||
'odbc_driver',
|
||||
'tds_version',
|
||||
'connect_timeout',
|
||||
'query_timeout',
|
||||
])));
|
||||
$resolved = $configured;
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$resolved[$field] = $this->value(
|
||||
$source,
|
||||
'connection.' . $field,
|
||||
$configured[$field] ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
<style>
|
||||
.edge-banding-page {
|
||||
color: #1f2937;
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.edge-banding-toolbar {
|
||||
@ -10,9 +12,9 @@
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 0.75rem;
|
||||
background: #ffffff;
|
||||
background: var(--vip-surface, #ffffff);
|
||||
}
|
||||
|
||||
.edge-banding-field {
|
||||
@ -23,46 +25,118 @@
|
||||
}
|
||||
|
||||
.edge-banding-field label {
|
||||
color: #6b7280;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-weight: 500;
|
||||
line-height: 1.333;
|
||||
}
|
||||
|
||||
.edge-banding-field input,
|
||||
.edge-banding-field select {
|
||||
height: 2.25rem;
|
||||
height: 2.125rem;
|
||||
min-width: 9rem;
|
||||
padding: 0 0.65rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.375rem;
|
||||
background: #ffffff;
|
||||
color: #374151;
|
||||
font-size: 0.8125rem;
|
||||
padding: 0 0.625rem;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--vip-surface, #ffffff);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font: inherit;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.edge-banding-field input:focus,
|
||||
.edge-banding-field select:focus {
|
||||
border-color: #6366f1;
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.12);
|
||||
border-color: var(--vip-content-accent, #856632);
|
||||
box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2);
|
||||
}
|
||||
|
||||
.edge-banding-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.edge-banding-button {
|
||||
display: inline-flex;
|
||||
height: 2.125rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0 0.875rem;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--vip-surface, #ffffff);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font: inherit;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edge-banding-button--primary {
|
||||
border-color: var(--vip-content-accent, #856632);
|
||||
background: var(--vip-content-accent, #856632);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.edge-banding-button:hover {
|
||||
border-color: var(--vip-content-accent, #856632);
|
||||
background: var(--vip-content-accent-soft, #f7f0de);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
}
|
||||
|
||||
.edge-banding-button--primary:hover {
|
||||
border-color: #6a5129;
|
||||
background: #6a5129;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.edge-banding-button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2);
|
||||
}
|
||||
|
||||
.edge-banding-button .material-icons-outlined {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.edge-banding-alert {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.625rem;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--vip-danger, #b80000);
|
||||
border-radius: 0.5rem;
|
||||
background: #fff5f5;
|
||||
color: var(--vip-danger, #b80000);
|
||||
}
|
||||
|
||||
.edge-banding-alert .material-icons-outlined {
|
||||
flex: 0 0 auto;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.edge-banding-table-card {
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
border: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-radius: 0.75rem;
|
||||
background: #ffffff;
|
||||
background: var(--vip-surface, #ffffff);
|
||||
}
|
||||
|
||||
.edge-banding-table-scroll {
|
||||
overflow-x: auto;
|
||||
scrollbar-color: var(--vip-content-accent, #b9954f) #efede8;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.edge-banding-table {
|
||||
width: 100%;
|
||||
min-width: 68rem;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.8125rem;
|
||||
font-size: 0.78125rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.edge-banding-table--details {
|
||||
@ -71,10 +145,10 @@
|
||||
|
||||
.edge-banding-table th,
|
||||
.edge-banding-table td {
|
||||
height: 2.75rem;
|
||||
padding: 0.55rem 0.7rem;
|
||||
border-right: 1px solid #e5e7eb;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
height: 2.5rem;
|
||||
padding: 0.5rem 0.625rem;
|
||||
border-right: 1px solid var(--vip-border, #d8d2c7);
|
||||
border-bottom: 1px solid var(--vip-border, #d8d2c7);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@ -85,13 +159,14 @@
|
||||
}
|
||||
|
||||
.edge-banding-table thead th {
|
||||
background: #f4f6fa;
|
||||
color: #374151;
|
||||
font-weight: 700;
|
||||
background: var(--vip-content-accent-soft, #f7f0de);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.edge-banding-table tbody tr:hover {
|
||||
background: #f9fafb;
|
||||
background: #fbf8f0;
|
||||
}
|
||||
|
||||
.edge-banding-table .is-number {
|
||||
@ -100,13 +175,13 @@
|
||||
|
||||
.edge-banding-table .is-empty td {
|
||||
height: 8rem;
|
||||
color: #9ca3af;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.edge-banding-table--compensation thead th:last-child {
|
||||
background: #fff200;
|
||||
color: #111827;
|
||||
background: var(--vip-content-accent-soft, #f7f0de);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
}
|
||||
|
||||
.edge-banding-table--payables {
|
||||
@ -115,17 +190,17 @@
|
||||
|
||||
.edge-banding-table--payables thead th,
|
||||
.edge-banding-table--payables tfoot td {
|
||||
border-color: #064b2a;
|
||||
background: #08ad4f;
|
||||
color: #062b19;
|
||||
border-color: var(--vip-border, #d8d2c7);
|
||||
background: var(--vip-content-accent-soft, #f7f0de);
|
||||
color: var(--vip-content-ink, #171717);
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.edge-banding-table--payables thead tr:first-child th {
|
||||
height: 3.25rem;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
height: 3rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.edge-banding-table--payables tbody td {
|
||||
@ -137,7 +212,7 @@
|
||||
}
|
||||
|
||||
.edge-banding-table--payables tfoot td {
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@ -148,12 +223,29 @@
|
||||
|
||||
.edge-banding-pagination {
|
||||
display: flex;
|
||||
min-height: 3rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: #6b7280;
|
||||
font-size: 0.8125rem;
|
||||
padding: 0.625rem 1rem;
|
||||
color: var(--vip-content-muted, #595959);
|
||||
font-size: 0.78125rem;
|
||||
}
|
||||
|
||||
.edge-banding-pagination__controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.edge-banding-pagination__controls .edge-banding-button {
|
||||
width: 2.125rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.edge-banding-pagination__controls .is-disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@ -167,6 +259,10 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edge-banding-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.edge-banding-pagination {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
|
||||
@ -1,40 +1,69 @@
|
||||
@php
|
||||
$edgeData = $edgeBanding ?? [];
|
||||
$result = $edgeData['result'] ?? ['data' => [], 'total' => 0];
|
||||
$rows = $result['data'] ?? [];
|
||||
$filters = $edgeData['filters'] ?? [];
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
$currentPage = (int) ($edgeData['page'] ?? 1);
|
||||
$lastPage = (int) ($edgeData['last_page'] ?? 1);
|
||||
$error = $edgeData['error'] ?? null;
|
||||
$number = static fn ($value) => $value === null || $value === '' ? '-' : number_format((float) $value, 2);
|
||||
@endphp
|
||||
|
||||
<div class="edge-banding-page">
|
||||
@include('business.pages.edge-banding-materials._styles')
|
||||
|
||||
<div class="edge-banding-toolbar" aria-label="封边条明细筛选">
|
||||
@if ($error)
|
||||
<div class="edge-banding-alert" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
<span>{{ $error }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form class="edge-banding-toolbar" method="get" action="{{ request()->url() }}" aria-label="封边条明细筛选">
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-details-in-date">入库日期</label>
|
||||
<input id="edge-details-in-date" type="date">
|
||||
<label for="edge-details-date">入库日期</label>
|
||||
<input id="edge-details-date" name="date" type="date" value="{{ $filters['date'] ?? '' }}">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-details-production-code">原料生产编号</label>
|
||||
<input id="edge-details-production-code" type="text" placeholder="请输入生产编号">
|
||||
<input id="edge-details-production-code" name="production_code" type="text" value="{{ $filters['production_code'] ?? '' }}" placeholder="请输入生产编号">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-details-raw-name">原料名称</label>
|
||||
<input id="edge-details-raw-name" type="text" placeholder="请输入原料名称">
|
||||
<input id="edge-details-raw-name" name="raw_name" type="text" value="{{ $filters['raw_name'] ?? '' }}" placeholder="请输入原料名称">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-details-return-flag">退货标识</label>
|
||||
<select id="edge-details-return-flag">
|
||||
<option>全部</option>
|
||||
<option>正常出库</option>
|
||||
<option>退货</option>
|
||||
<select id="edge-details-return-flag" name="return_flag">
|
||||
<option value="">全部</option>
|
||||
<option value="normal" @selected(($filters['return_flag'] ?? '') === 'normal')>正常出库</option>
|
||||
<option value="return" @selected(($filters['return_flag'] ?? '') === 'return')>退货</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-details-out-factory">出库厂商</label>
|
||||
<input id="edge-details-out-factory" type="text" placeholder="请输入出库厂商">
|
||||
<input id="edge-details-out-factory" name="outbound_supplier" type="text" value="{{ $filters['outbound_supplier'] ?? '' }}" placeholder="请输入出库厂商">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-details-arrival-factory">到货厂商</label>
|
||||
<input id="edge-details-arrival-factory" type="text" placeholder="请输入到货厂商">
|
||||
<input id="edge-details-arrival-factory" name="arrival_supplier" type="text" value="{{ $filters['arrival_supplier'] ?? '' }}" placeholder="请输入到货厂商">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-details-out-number">出库单号</label>
|
||||
<input id="edge-details-out-number" type="text" placeholder="请输入出库单号">
|
||||
<input id="edge-details-out-number" name="outbound_no" type="text" value="{{ $filters['outbound_no'] ?? '' }}" placeholder="请输入出库单号或送货号">
|
||||
</div>
|
||||
</div>
|
||||
<div class="edge-banding-actions">
|
||||
<button class="edge-banding-button edge-banding-button--primary" type="submit">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<a class="edge-banding-button" href="{{ request()->url() }}" title="清除筛选条件">
|
||||
<span class="material-icons-outlined" aria-hidden="true">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="edge-banding-table-card">
|
||||
<div class="edge-banding-table-scroll">
|
||||
@ -67,15 +96,55 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="is-empty">
|
||||
<td colspan="23">暂无数据</td>
|
||||
</tr>
|
||||
@forelse ($rows as $row)
|
||||
<tr>
|
||||
<td>{{ $row['inbound_date'] ?: '-' }}</td>
|
||||
<td>{{ $row['production_code'] ?: '-' }}</td>
|
||||
<td>{{ $row['raw_name'] ?: '-' }}</td>
|
||||
<td class="is-number">{{ $number($row['outbound_quantity'] ?? null) }}</td>
|
||||
<td>{{ $row['unit'] ?: '-' }}</td>
|
||||
<td>{{ $row['return_flag'] ?: '-' }}</td>
|
||||
<td>{{ $row['outbound_supplier'] ?: '-' }}</td>
|
||||
<td>{{ $row['arrival_supplier'] ?: '-' }}</td>
|
||||
<td>{{ $row['batch_date'] ?: '-' }}</td>
|
||||
<td>{{ $row['outbound_no'] ?: '-' }}</td>
|
||||
<td>{{ $row['outbound_remark'] ?: '-' }}</td>
|
||||
<td>{{ $row['requisition_no'] ?: '-' }}</td>
|
||||
<td>{{ $row['number_type'] ?: '-' }}</td>
|
||||
<td>{{ $row['delivery_no'] ?: '-' }}</td>
|
||||
<td class="is-number">{{ $number($row['received_quantity'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['reconciliation_amount'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['freight'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['edge_band_unit_price'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['payable_amount'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['furniture_unit_price'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['furniture_amount'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['price_difference'] ?? null) }}</td>
|
||||
<td class="is-number">{{ $number($row['profit'] ?? null) }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="is-empty">
|
||||
<td colspan="23">{{ $error ? '数据源暂不可用' : '当前筛选条件下没有匹配的封边条明细' }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="edge-banding-pagination">
|
||||
<span>共 0 条记录</span>
|
||||
<span>第 1 / 1 页</span>
|
||||
<span>共 {{ number_format($total) }} 条记录</span>
|
||||
<div class="edge-banding-pagination__controls" aria-label="明细数据分页">
|
||||
@if ($currentPage <= 1)
|
||||
<span class="edge-banding-button is-disabled" aria-disabled="true" title="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></span>
|
||||
@else
|
||||
<a class="edge-banding-button" href="{{ request()->fullUrlWithQuery(['data_page' => $currentPage - 1]) }}" aria-label="上一页" title="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
@endif
|
||||
<span>第 {{ $currentPage }} / {{ max($lastPage, 1) }} 页</span>
|
||||
@if ($currentPage >= $lastPage)
|
||||
<span class="edge-banding-button is-disabled" aria-disabled="true" title="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></span>
|
||||
@else
|
||||
<a class="edge-banding-button" href="{{ request()->fullUrlWithQuery(['data_page' => $currentPage + 1]) }}" aria-label="下一页" title="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,23 +1,58 @@
|
||||
@php
|
||||
$edgeData = $edgeBanding ?? [];
|
||||
$result = $edgeData['result'] ?? ['data' => [], 'total' => 0, 'totals' => []];
|
||||
$rows = $result['data'] ?? [];
|
||||
$totals = $result['totals'] ?? [];
|
||||
$filters = $edgeData['filters'] ?? [];
|
||||
$period = $filters['period'] ?? '2026-06';
|
||||
[$periodYear, $periodMonth] = array_pad(explode('-', $period, 2), 2, '');
|
||||
$periodTitle = ctype_digit($periodYear) && ctype_digit($periodMonth)
|
||||
? $periodYear . '年公司应付封边条厂' . (int) $periodMonth . '月份货款'
|
||||
: '公司应付封边条厂货款';
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
$currentPage = (int) ($edgeData['page'] ?? 1);
|
||||
$lastPage = (int) ($edgeData['last_page'] ?? 1);
|
||||
$error = $edgeData['error'] ?? null;
|
||||
$money = static fn ($value) => $value === null || $value === '' ? '-' : number_format((float) $value, 2);
|
||||
@endphp
|
||||
|
||||
<div class="edge-banding-page">
|
||||
@include('business.pages.edge-banding-materials._styles')
|
||||
|
||||
<div class="edge-banding-toolbar" aria-label="封边条应付筛选">
|
||||
@if ($error)
|
||||
<div class="edge-banding-alert" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
<span>{{ $error }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form class="edge-banding-toolbar" method="get" action="{{ request()->url() }}" aria-label="封边条应付筛选">
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-payables-period">应付月份</label>
|
||||
<input id="edge-payables-period" type="month" value="2026-06">
|
||||
<input id="edge-payables-period" name="period" type="month" value="{{ $period }}">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-payables-supplier">供货单位</label>
|
||||
<input id="edge-payables-supplier" type="text" placeholder="请输入供货单位">
|
||||
<input id="edge-payables-supplier" name="supplier" type="text" value="{{ $filters['supplier'] ?? '' }}" placeholder="请输入供货单位">
|
||||
</div>
|
||||
</div>
|
||||
<div class="edge-banding-actions">
|
||||
<button class="edge-banding-button edge-banding-button--primary" type="submit">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<a class="edge-banding-button" href="{{ request()->url() }}" title="恢复默认月份并清除筛选条件">
|
||||
<span class="material-icons-outlined" aria-hidden="true">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="edge-banding-table-card">
|
||||
<div class="edge-banding-table-scroll">
|
||||
<table class="edge-banding-table edge-banding-table--payables">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="13">2026年公司应付封边条厂6月份货款</th>
|
||||
<th colspan="13">{{ $periodTitle }}</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th rowspan="2">序号</th>
|
||||
@ -47,32 +82,62 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="is-empty">
|
||||
<td colspan="13">暂无数据</td>
|
||||
</tr>
|
||||
@forelse ($rows as $index => $row)
|
||||
<tr>
|
||||
<td>{{ (($currentPage - 1) * ($edgeData['limit'] ?? 25)) + $index + 1 }}</td>
|
||||
<td>{{ $row['supplier'] ?: '-' }}</td>
|
||||
<td>{{ $money($row['month_start_unpaid'] ?? null) }}</td>
|
||||
<td>{{ $money($row['goods_amount'] ?? null) }}</td>
|
||||
<td>{{ $money($row['prepaid_freight'] ?? null) }}</td>
|
||||
<td>{{ $money($row['compensation'] ?? null) }}</td>
|
||||
<td>{{ $money($row['supplier_refund'] ?? null) }}</td>
|
||||
<td>{{ $money($row['payable_amount'] ?? null) }}</td>
|
||||
<td>{{ $money($row['paid_amount'] ?? null) }}</td>
|
||||
<td>{{ $money($row['actual_payment'] ?? null) }}</td>
|
||||
<td>{{ $money($row['private_payment'] ?? null) }}</td>
|
||||
<td>{{ $money($row['corporate_payment'] ?? null) }}</td>
|
||||
<td>{{ $row['remark'] ?: '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="is-empty">
|
||||
<td colspan="13">{{ $error ? '数据源暂不可用' : '当前月份没有匹配的封边条应付数据' }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>合计</td>
|
||||
<td></td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>-</td>
|
||||
<td>全部对公</td>
|
||||
<td>{{ $money($totals['month_start_unpaid'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['goods_amount'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['prepaid_freight'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['compensation'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['supplier_refund'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['payable_amount'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['paid_amount'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['actual_payment'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['private_payment'] ?? null) }}</td>
|
||||
<td>{{ $money($totals['corporate_payment'] ?? null) }}</td>
|
||||
<td>{{ (float) ($totals['private_payment'] ?? 0) === 0.0 ? '全部对公' : '-' }}</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div class="edge-banding-pagination">
|
||||
<span>共 0 条记录</span>
|
||||
<span>第 1 / 1 页</span>
|
||||
<span>共 {{ number_format($total) }} 条记录</span>
|
||||
<div class="edge-banding-pagination__controls" aria-label="应付数据分页">
|
||||
@if ($currentPage <= 1)
|
||||
<span class="edge-banding-button is-disabled" aria-disabled="true" title="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></span>
|
||||
@else
|
||||
<a class="edge-banding-button" href="{{ request()->fullUrlWithQuery(['data_page' => $currentPage - 1]) }}" aria-label="上一页" title="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
@endif
|
||||
<span>第 {{ $currentPage }} / {{ max($lastPage, 1) }} 页</span>
|
||||
@if ($currentPage >= $lastPage)
|
||||
<span class="edge-banding-button is-disabled" aria-disabled="true" title="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></span>
|
||||
@else
|
||||
<a class="edge-banding-button" href="{{ request()->fullUrlWithQuery(['data_page' => $currentPage + 1]) }}" aria-label="下一页" title="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,40 +1,52 @@
|
||||
@php
|
||||
$edgeData = $edgeBanding ?? [];
|
||||
$result = $edgeData['result'] ?? ['data' => [], 'total' => 0];
|
||||
$rows = $result['data'] ?? [];
|
||||
$filters = $edgeData['filters'] ?? [];
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
$currentPage = (int) ($edgeData['page'] ?? 1);
|
||||
$lastPage = (int) ($edgeData['last_page'] ?? 1);
|
||||
$error = $edgeData['error'] ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="edge-banding-page">
|
||||
@include('business.pages.edge-banding-materials._styles')
|
||||
|
||||
<div class="edge-banding-toolbar" aria-label="封边条价格筛选">
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-price-type">价格类型</label>
|
||||
<select id="edge-price-type">
|
||||
<option>全部价格</option>
|
||||
<option>封边条厂价格</option>
|
||||
<option>家具厂价格</option>
|
||||
</select>
|
||||
@if ($error)
|
||||
<div class="edge-banding-alert" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
<span>{{ $error }}</span>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form class="edge-banding-toolbar" method="get" action="{{ request()->url() }}" aria-label="封边条价格筛选">
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-price-raw-name">原料名称</label>
|
||||
<input id="edge-price-raw-name" type="text" placeholder="请输入原料名称">
|
||||
<input id="edge-price-raw-name" name="raw_name" type="text" value="{{ $filters['raw_name'] ?? '' }}" placeholder="请输入原料名称">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-price-raw-code">原料编号</label>
|
||||
<input id="edge-price-raw-code" type="text" placeholder="请输入原料编号">
|
||||
<input id="edge-price-raw-code" name="raw_code" type="text" value="{{ $filters['raw_code'] ?? '' }}" placeholder="请输入原料编号">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-price-production-code">生产编号</label>
|
||||
<input id="edge-price-production-code" type="text" placeholder="请输入生产编号">
|
||||
<input id="edge-price-production-code" name="production_code" type="text" value="{{ $filters['production_code'] ?? '' }}" placeholder="请输入生产编号">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-price-target-factory">目标厂商</label>
|
||||
<input id="edge-price-target-factory" type="text" placeholder="请输入目标厂商">
|
||||
<label for="edge-price-supplier">目标厂商</label>
|
||||
<input id="edge-price-supplier" name="supplier" type="text" value="{{ $filters['supplier'] ?? '' }}" placeholder="请输入目标厂商">
|
||||
</div>
|
||||
<div class="edge-banding-field">
|
||||
<label for="edge-price-factory-type">厂商类型</label>
|
||||
<select id="edge-price-factory-type">
|
||||
<option>全部厂商</option>
|
||||
<option>封边条厂</option>
|
||||
<option>家具厂</option>
|
||||
</select>
|
||||
<div class="edge-banding-actions">
|
||||
<button class="edge-banding-button edge-banding-button--primary" type="submit">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<a class="edge-banding-button" href="{{ request()->url() }}" title="清除筛选条件">
|
||||
<span class="material-icons-outlined" aria-hidden="true">refresh</span>
|
||||
<span>重置</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="edge-banding-table-card">
|
||||
<div class="edge-banding-table-scroll">
|
||||
@ -51,15 +63,39 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="is-empty">
|
||||
<td colspan="7">暂无数据</td>
|
||||
</tr>
|
||||
@forelse ($rows as $row)
|
||||
<tr>
|
||||
<td>{{ $row['price_type'] ?: '-' }}</td>
|
||||
<td>{{ $row['raw_name'] ?: '-' }}</td>
|
||||
<td>{{ $row['raw_code'] ?: '-' }}</td>
|
||||
<td>{{ $row['production_code'] ?: '-' }}</td>
|
||||
<td class="is-number">{{ $row['latest_price'] === null ? '-' : number_format((float) $row['latest_price'], 2) }}</td>
|
||||
<td>{{ $row['target_supplier'] ?: '-' }}</td>
|
||||
<td>{{ $row['factory_type'] ?: '-' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="is-empty">
|
||||
<td colspan="7">{{ $error ? '数据源暂不可用' : '当前筛选条件下没有匹配的封边条价格' }}</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="edge-banding-pagination">
|
||||
<span>共 0 条记录</span>
|
||||
<span>第 1 / 1 页</span>
|
||||
<span>共 {{ number_format($total) }} 条记录</span>
|
||||
<div class="edge-banding-pagination__controls" aria-label="价格数据分页">
|
||||
@if ($currentPage <= 1)
|
||||
<span class="edge-banding-button is-disabled" aria-disabled="true" title="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></span>
|
||||
@else
|
||||
<a class="edge-banding-button" href="{{ request()->fullUrlWithQuery(['data_page' => $currentPage - 1]) }}" aria-label="上一页" title="上一页"><span class="material-icons-outlined" aria-hidden="true">chevron_left</span></a>
|
||||
@endif
|
||||
<span>第 {{ $currentPage }} / {{ max($lastPage, 1) }} 页</span>
|
||||
@if ($currentPage >= $lastPage)
|
||||
<span class="edge-banding-button is-disabled" aria-disabled="true" title="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></span>
|
||||
@else
|
||||
<a class="edge-banding-button" href="{{ request()->fullUrlWithQuery(['data_page' => $currentPage + 1]) }}" aria-label="下一页" title="下一页"><span class="material-icons-outlined" aria-hidden="true">chevron_right</span></a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -82,7 +82,7 @@ class PagesTest extends FeatureTestCase
|
||||
];
|
||||
|
||||
foreach ($pages as $page => $expectedHeader) {
|
||||
$this->loginAs()
|
||||
$response = $this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'edge-banding-materials',
|
||||
'page' => $page,
|
||||
@ -91,6 +91,10 @@ class PagesTest extends FeatureTestCase
|
||||
->assertSee($expectedHeader)
|
||||
->assertSee('class="edge-banding-table', false)
|
||||
->assertDontSee('class="business-blank-page"', false);
|
||||
|
||||
if ($page !== 'compensation') {
|
||||
$response->assertSeeText('该上游数据同步尚未启用,请联系管理员。');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -186,7 +186,7 @@ class UpstreamDatabasesTest extends FeatureTestCase
|
||||
}
|
||||
}
|
||||
|
||||
public function test_whole_connection_comes_from_shared_system_database(): void
|
||||
public function test_complete_connection_uses_shared_fields_and_local_runtime_options(): void
|
||||
{
|
||||
$connection = $this->connection('shared-secret');
|
||||
$connection->update([
|
||||
@ -195,6 +195,7 @@ class UpstreamDatabasesTest extends FeatureTestCase
|
||||
'port' => 14330,
|
||||
'database_name' => 'shared_database',
|
||||
'username' => 'shared_reader',
|
||||
'odbc_driver' => 'Stored Driver',
|
||||
]);
|
||||
config([
|
||||
'upstreams.local_configuration.enabled' => false,
|
||||
@ -209,7 +210,7 @@ class UpstreamDatabasesTest extends FeatureTestCase
|
||||
'encryption' => 'off',
|
||||
'connect_timeout' => 5,
|
||||
'query_timeout' => 15,
|
||||
'application_intent' => '',
|
||||
'application_intent' => 'ReadOnly',
|
||||
],
|
||||
]);
|
||||
|
||||
@ -221,7 +222,9 @@ class UpstreamDatabasesTest extends FeatureTestCase
|
||||
$this->assertSame('shared_database', $resolved['database']);
|
||||
$this->assertSame('shared_reader', $resolved['username']);
|
||||
$this->assertSame('shared-secret', $resolved['password']);
|
||||
$this->assertSame('FreeTDS', $resolved['odbc_driver']);
|
||||
$this->assertSame('off', $resolved['encryption']);
|
||||
$this->assertSame('ReadOnly', $resolved['application_intent']);
|
||||
}
|
||||
|
||||
public function test_global_wildcard_scope_is_rejected(): void
|
||||
|
||||
97
tests/Unit/EcommerceFinance/EdgeBandingRepositoryTest.php
Normal file
97
tests/Unit/EcommerceFinance/EdgeBandingRepositoryTest.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
|
||||
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class EdgeBandingRepositoryTest extends TestCase
|
||||
{
|
||||
public function test_price_rows_are_normalized_and_paginated(): void
|
||||
{
|
||||
$query = $this->createMock(UpstreamReadOnlyQuery::class);
|
||||
$query->expects($this->once())->method('select')->willReturnCallback(
|
||||
function (string $sql): array {
|
||||
UpstreamReadOnlyQuery::assertReadOnlySql($sql);
|
||||
$this->assertStringContainsString('[dbo].[pm_averageUnitPrice]', $sql);
|
||||
|
||||
return [[
|
||||
'raw_code' => 'FBT442',
|
||||
'latest_price' => '0.10',
|
||||
'total_count' => 1,
|
||||
]];
|
||||
}
|
||||
);
|
||||
|
||||
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
|
||||
$factory->expects($this->once())->method('forSource')->with('jiansun')->willReturn($query);
|
||||
|
||||
$result = (new EdgeBandingRepository($factory))->prices([], 1, 25);
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('FBT442', $result['data'][0]['raw_code']);
|
||||
$this->assertSame('采购价', $result['data'][0]['price_type']);
|
||||
$this->assertSame('封边条厂', $result['data'][0]['factory_type']);
|
||||
$this->assertArrayNotHasKey('total_count', $result['data'][0]);
|
||||
}
|
||||
|
||||
public function test_detail_query_uses_the_read_only_guard(): void
|
||||
{
|
||||
$query = $this->createMock(UpstreamReadOnlyQuery::class);
|
||||
$query->expects($this->once())->method('select')->willReturnCallback(
|
||||
function (string $sql): array {
|
||||
UpstreamReadOnlyQuery::assertReadOnlySql($sql);
|
||||
$this->assertStringContainsString('[dbo].[fys_supplierGoods_sttlement_sale]', $sql);
|
||||
|
||||
return [[
|
||||
'production_code' => 'FBT440',
|
||||
'outbound_quantity' => '14000.00',
|
||||
'total_count' => 1,
|
||||
]];
|
||||
}
|
||||
);
|
||||
|
||||
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
|
||||
$factory->expects($this->once())->method('forSource')->with('jiansun')->willReturn($query);
|
||||
|
||||
$result = (new EdgeBandingRepository($factory))->details([], 1, 25);
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('FBT440', $result['data'][0]['production_code']);
|
||||
$this->assertSame('米', $result['data'][0]['unit']);
|
||||
$this->assertSame('正常出库', $result['data'][0]['return_flag']);
|
||||
$this->assertSame('申领', $result['data'][0]['number_type']);
|
||||
}
|
||||
|
||||
public function test_payable_rows_expose_full_result_totals(): void
|
||||
{
|
||||
$query = $this->createMock(UpstreamReadOnlyQuery::class);
|
||||
$query->expects($this->once())->method('select')->willReturnCallback(
|
||||
function (string $sql): array {
|
||||
UpstreamReadOnlyQuery::assertReadOnlySql($sql);
|
||||
$this->assertStringContainsString('[dbo].[fys_supplierPaymentSummary]', $sql);
|
||||
|
||||
return [[
|
||||
'supplier' => '封边条厂',
|
||||
'payable_amount' => '100.00',
|
||||
'summary_payable_amount' => '100.00',
|
||||
'summary_corporate_payment' => '100.00',
|
||||
'summary_private_payment' => '0.00',
|
||||
'total_count' => 1,
|
||||
]];
|
||||
}
|
||||
);
|
||||
|
||||
$factory = $this->createMock(UpstreamReadOnlyQueryFactory::class);
|
||||
$factory->expects($this->once())->method('forSource')->with('jiansun')->willReturn($query);
|
||||
|
||||
$result = (new EdgeBandingRepository($factory))->payables(['period' => '2026-06'], 1, 25);
|
||||
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame(100.0, $result['totals']['payable_amount']);
|
||||
$this->assertSame(100.0, $result['totals']['corporate_payment']);
|
||||
$this->assertArrayNotHasKey('summary_payable_amount', $result['data'][0]);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
namespace Tests\Unit\EcommerceFinance;
|
||||
|
||||
use Closure;
|
||||
use Modules\EcommerceFinance\Repositories\EdgeBandingRepository;
|
||||
use Modules\EcommerceFinance\Repositories\GoodsReconciliationRepository;
|
||||
use Modules\EcommerceFinance\Repositories\PinduoduoSpreadBillRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierPayableRepository;
|
||||
@ -51,9 +52,10 @@ class JianSunSnapshotRefresherTest extends TestCase
|
||||
}
|
||||
};
|
||||
|
||||
$edgeBanding = (new ReflectionClass(EdgeBandingRepository::class))->newInstanceWithoutConstructor();
|
||||
$goods = (new ReflectionClass(GoodsReconciliationRepository::class))->newInstanceWithoutConstructor();
|
||||
$supplier = (new ReflectionClass(SupplierPayableRepository::class))->newInstanceWithoutConstructor();
|
||||
$refresher = new JianSunSnapshotRefresher($cache, $goods, $pinduoduo, $supplier);
|
||||
$refresher = new JianSunSnapshotRefresher($cache, $edgeBanding, $goods, $pinduoduo, $supplier);
|
||||
|
||||
$status = $refresher->refresh([
|
||||
'scope' => 'pinduoduo-bills.list',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user