144 lines
7.0 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Repositories;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQuery;
use Modules\EcommerceFinance\Services\UpstreamReadOnlyQueryFactory;
/**
* 原材料库存管理只读数据仓库。
*/
class InventoryRepository
{
private const SOURCE = 'jiansun';
private const SUMMARY_KEYS = [
'opening_balance',
'stock_in',
'lamination_out',
'inventory',
'remaining_value',
'stock_in_amount',
'closing_remaining_value',
'previous_remaining_value',
'stock_out_amount',
];
private ?UpstreamReadOnlyQuery $query = null;
public function __construct(private readonly UpstreamReadOnlyQueryFactory $queries)
{
}
/** @return array{data:array<int,array<string,mixed>>,total:int,totals:array<string,float>} */
public function boardInventoryDetails(array $filters, int $page, int $limit): array
{
$where = [
's.stockType = ?',
's.[month] = (SELECT MAX(latest.[month]) FROM [dbo].[pm_boardInventoryDetail_snapshot] latest WHERE latest.stockType = ?)',
];
$bindings = ['素板', '素板'];
if (($filters['manufacturer'] ?? '') !== '') {
$where[] = 's.supplier LIKE ? ESCAPE \'\\\'';
$bindings[] = $this->contains($filters['manufacturer']);
}
if (($filters['specification'] ?? '') !== '') {
$where[] = 's.name LIKE ? ESCAPE \'\\\'';
$bindings[] = $this->contains($filters['specification']);
}
if (($filters['matching_value'] ?? '') !== '') {
$where[] = 's.number LIKE ? ESCAPE \'\\\'';
$bindings[] = $this->contains($filters['matching_value']);
}
if (($filters['date_from'] ?? '') !== '') {
$where[] = 'CONVERT(date, COALESCE(s.[date], s.[month])) >= ?';
$bindings[] = $filters['date_from'];
}
if (($filters['date_to'] ?? '') !== '') {
$where[] = 'CONVERT(date, COALESCE(s.[date], s.[month])) <= ?';
$bindings[] = $filters['date_to'];
}
$page = max($page, 1);
$limit = min(max($limit, 1), 100);
$offset = ($page - 1) * $limit;
$whereSql = implode(' AND ', $where);
$sql = "WITH source_rows AS (
SELECT
ISNULL(s.supplier, '') AS manufacturer,
CONVERT(varchar(10), COALESCE(s.[date], s.[month]), 120) AS [date],
ISNULL(s.name, '') AS specification,
ISNULL(s.number, '') AS matching_value,
CAST(ISNULL(s.lastBalance, 0) AS decimal(18,2)) AS opening_balance,
CAST(ISNULL(s.averagePrice, 0) AS decimal(18,2)) AS weighted_average_price,
CAST(ISNULL(s.inCount, 0) AS decimal(18,2)) AS stock_in,
CAST(ISNULL(s.deliveryCount, 0) AS decimal(18,2)) AS lamination_out,
CAST(ISNULL(s.balance, 0) AS decimal(18,2)) AS inventory,
CAST(ISNULL(s.surplusPrice, 0) AS decimal(18,2)) AS remaining_value,
CAST(ISNULL(s.inCount, 0) * ISNULL(s.averagePrice, 0) AS decimal(18,2)) AS stock_in_amount,
CAST(ISNULL(s.balance, 0) * ISNULL(s.averagePrice, 0) AS decimal(18,2)) AS closing_remaining_value,
CAST(ISNULL(s.lastBalance, 0) * ISNULL(s.averagePrice, 0) AS decimal(18,2)) AS previous_remaining_value,
CAST(ISNULL(s.deliveryCount, 0) * ISNULL(s.averagePrice, 0) AS decimal(18,2)) AS stock_out_amount,
CAST(SUM(CAST(ISNULL(s.lastBalance, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_opening_balance,
CAST(SUM(CAST(ISNULL(s.inCount, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_stock_in,
CAST(SUM(CAST(ISNULL(s.deliveryCount, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_lamination_out,
CAST(SUM(CAST(ISNULL(s.balance, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_inventory,
CAST(SUM(CAST(ISNULL(s.surplusPrice, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_remaining_value,
CAST(SUM(CAST(ISNULL(s.inCount, 0) * ISNULL(s.averagePrice, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_stock_in_amount,
CAST(SUM(CAST(ISNULL(s.balance, 0) * ISNULL(s.averagePrice, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_closing_remaining_value,
CAST(SUM(CAST(ISNULL(s.lastBalance, 0) * ISNULL(s.averagePrice, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_previous_remaining_value,
CAST(SUM(CAST(ISNULL(s.deliveryCount, 0) * ISNULL(s.averagePrice, 0) AS decimal(38,2))) OVER () AS decimal(18,2)) AS summary_stock_out_amount,
ROW_NUMBER() OVER (
ORDER BY COALESCE(s.[date], s.[month]) DESC, s.supplier, s.name, s.number, s.id DESC
) AS row_no,
COUNT(*) OVER () AS total_count
FROM [dbo].[pm_boardInventoryDetail_snapshot] s
WHERE {$whereSql}
)
SELECT manufacturer, [date], specification, matching_value,
opening_balance, weighted_average_price, stock_in, lamination_out,
inventory, remaining_value, stock_in_amount, closing_remaining_value,
previous_remaining_value, stock_out_amount,
summary_opening_balance, summary_stock_in, summary_lamination_out,
summary_inventory, summary_remaining_value, summary_stock_in_amount,
summary_closing_remaining_value, summary_previous_remaining_value,
summary_stock_out_amount, 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]));
$totals = array_fill_keys(self::SUMMARY_KEYS, 0.0);
if ($rows !== []) {
foreach (self::SUMMARY_KEYS as $key) {
$totals[$key] = (float) ($rows[0]['summary_' . $key] ?? 0);
}
}
$total = (int) ($rows[0]['total_count'] ?? 0);
foreach ($rows as &$row) {
unset($row['total_count']);
foreach (self::SUMMARY_KEYS as $key) {
unset($row['summary_' . $key]);
}
}
return ['data' => $rows, 'total' => $total, 'totals' => $totals];
}
private function query(): UpstreamReadOnlyQuery
{
return $this->query ??= $this->queries->forSource(self::SOURCE);
}
private function contains(string $value): string
{
return '%' . str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], trim($value)) . '%';
}
}