Compare commits

..

3 Commits

Author SHA1 Message Date
xinadmin 7a895c143e 对账标记 2026-09-06 22:48:45 +08:00
xinadmin f9eb6f7d32 导出时间 2026-09-06 22:37:46 +08:00
xinadmin f8e81cb830 导出 2026-09-06 22:34:25 +08:00
5 changed files with 83 additions and 18 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -94,7 +94,7 @@ class BillExport implements FromCollection, WithStyles
->all(); ->all();
$rows[] = ['账单号:' . $billNoText]; $rows[] = ['账单号:' . $billNoText];
$this->specialRows[++$rowIndex] = 'scope'; $this->specialRows[++$rowIndex] = 'scope';
$rows[] = ['门店:' . implode('、', $storeNames) . ' 分类:' . $categoryName . ' 导出时间:' . now()->format('Y-m-d H:i:s')]; $rows[] = ['门店:' . implode('、', $storeNames) . ' 分类:' . $categoryName . ' 导出时间:' . now('Asia/Shanghai')->format('Y-m-d H:i:s')];
$this->specialRows[++$rowIndex] = 'scope'; $this->specialRows[++$rowIndex] = 'scope';
// 空行 // 空行
+1 -1
View File
@@ -92,7 +92,7 @@ class ContainerReturnExport implements FromCollection, WithStyles, WithStrictNul
$this->specialRows[++$rowIndex] = 'title'; $this->specialRows[++$rowIndex] = 'title';
// 范围行:门店 / 导出时间 // 范围行:门店 / 导出时间
$rows[] = ['门店:' . implode('、', array_values($this->storeNames)) . ' 导出时间:' . now()->format('Y-m-d H:i:s')]; $rows[] = ['门店:' . implode('、', array_values($this->storeNames)) . ' 导出时间:' . now('Asia/Shanghai')->format('Y-m-d H:i:s')];
$this->specialRows[++$rowIndex] = 'scope'; $this->specialRows[++$rowIndex] = 'scope';
// 空行 // 空行
+17 -7
View File
@@ -22,7 +22,8 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
/** /**
* 采购单商品明细导出:系统全部未删除商品行(含本采购单无订货的商品,数量 0), * 采购单商品明细导出:系统全部未删除商品行(含本采购单无订货的商品,数量 0),
* 支持按供应商筛选;行尾合计 + 门店列合计;数量/金额/门店数量按值条件填色; * 支持按供应商筛选;行尾合计 + 门店列合计;数量/金额/门店数量按值条件填色;
* 序号/分类/包规/单位/成本/单价/实际称重/金额列在表格中默认隐藏(数据照常导出,Excel 中可取消隐藏) * 序号/分类/包规/单位/成本/单价/实际称重/金额列在表格中默认隐藏(数据照常导出,Excel 中可取消隐藏)
* 市场/数量/门店列列宽减半、列头自动换行;门店数量无数据显示空白(不显示 0)
*/ */
class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, WithStyles class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, WithStyles
{ {
@@ -183,7 +184,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
if ($this->supplierId > 0) { if ($this->supplierId > 0) {
$scopeSupplier = $supplierNames[$this->supplierId] ?? ('供应商#' . $this->supplierId); $scopeSupplier = $supplierNames[$this->supplierId] ?? ('供应商#' . $this->supplierId);
} }
$rows[] = ['供应商:' . $scopeSupplier . ' 导出时间:' . now()->format('Y-m-d H:i:s')]; $rows[] = ['供应商:' . $scopeSupplier . ' 导出时间:' . now('Asia/Shanghai')->format('Y-m-d H:i:s')];
$this->specialRows[++$rowIndex] = 'scope'; $this->specialRows[++$rowIndex] = 'scope';
// 空行 // 空行
@@ -222,7 +223,10 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
$item['quantity'], $item['quantity'],
$item['weight'], $item['weight'],
$item['amount'], $item['amount'],
], array_values($item['store_quantities'])); ], array_map(
static fn (int $qty): int|string => $qty > 0 ? $qty : '',
array_values($item['store_quantities']),
));
$totalQuantity = bcadd($totalQuantity, (string) $item['quantity'], 2); $totalQuantity = bcadd($totalQuantity, (string) $item['quantity'], 2);
$totalWeight = bcadd($totalWeight, (string) $item['weight'], 3); $totalWeight = bcadd($totalWeight, (string) $item['weight'], 3);
$totalAmount = bcadd($totalAmount, (string) $item['amount'], 2); $totalAmount = bcadd($totalAmount, (string) $item['amount'], 2);
@@ -231,10 +235,10 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
} }
} }
// 合计行(行合计 + 门店列合计) // 合计行(行合计 + 门店列合计;门店无数据显示空白
$rows[] = array_merge( $rows[] = array_merge(
['', '', '合计', '', '', '', '', '', '', (float) $totalQuantity, (float) $totalWeight, (float) $totalAmount], ['', '', '合计', '', '', '', '', '', '', (float) $totalQuantity, (float) $totalWeight, (float) $totalAmount],
array_values($storeTotals), array_map(static fn (int $qty): int|string => $qty > 0 ? $qty : '', array_values($storeTotals)),
); );
$this->specialRows[++$rowIndex] = 'summary'; $this->specialRows[++$rowIndex] = 'summary';
$this->lastRow = $rowIndex; $this->lastRow = $rowIndex;
@@ -256,7 +260,8 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
$this->collection(); $this->collection();
$sheet->freezePane('A' . ($this->headerRow + 1)); $sheet->freezePane('A' . ($this->headerRow + 1));
$widths = [6, 10, 20, 12, 12, 12, 8, 10, 12, 10, 12, 12]; // 市场/数量列宽减半
$widths = [6, 10, 20, 12, 6, 12, 8, 10, 12, 5, 12, 12];
foreach ($widths as $index => $width) { foreach ($widths as $index => $width) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width); $sheet->getColumnDimension(Coordinate::stringFromColumnIndex($index + 1))->setWidth($width);
} }
@@ -265,7 +270,7 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
$storeCount = count($storeIds); $storeCount = count($storeIds);
$lastColumn = Coordinate::stringFromColumnIndex(12 + max($storeCount, 1)); $lastColumn = Coordinate::stringFromColumnIndex(12 + max($storeCount, 1));
foreach ($storeIds as $i => $storeId) { foreach ($storeIds as $i => $storeId) {
$sheet->getColumnDimension(Coordinate::stringFromColumnIndex(13 + $i))->setWidth(12); $sheet->getColumnDimension(Coordinate::stringFromColumnIndex(13 + $i))->setWidth(6);
} }
// 默认隐藏列:序号/分类/包规/单位/成本/单价/实际称重/金额(数据照常导出,Excel 中可取消隐藏) // 默认隐藏列:序号/分类/包规/单位/成本/单价/实际称重/金额(数据照常导出,Excel 中可取消隐藏)
@@ -279,6 +284,11 @@ class PurchaseOrderExport implements FromCollection, WithStrictNullComparison, W
->setHorizontal(Alignment::HORIZONTAL_CENTER) ->setHorizontal(Alignment::HORIZONTAL_CENTER)
->setVertical(Alignment::VERTICAL_CENTER); ->setVertical(Alignment::VERTICAL_CENTER);
// 列头自动换行(市场/数量/门店列较窄,表头文字折行显示)
$sheet->getStyle('A' . $this->headerRow . ':' . $lastColumn . $this->headerRow)
->getAlignment()
->setWrapText(true);
// 表格区域(列头 → 合计行)添加所有边框 // 表格区域(列头 → 合计行)添加所有边框
$sheet->getStyle('A' . $this->headerRow . ':' . $lastColumn . $this->lastRow) $sheet->getStyle('A' . $this->headerRow . ':' . $lastColumn . $this->lastRow)
->getBorders() ->getBorders()
+63 -8
View File
@@ -17,9 +17,10 @@ import {
Table, Table,
Tabs, Tabs,
Tag, Tag,
Tooltip,
Typography, Typography,
} from 'antd'; } from 'antd';
import {AccountBookOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, PlusOutlined, UnorderedListOutlined} from '@ant-design/icons'; import {AccountBookOutlined, CheckOutlined, DeleteOutlined, DownloadOutlined, EditOutlined, PlusOutlined, UnorderedListOutlined} from '@ant-design/icons';
import type { TableProps } from 'antd'; import type { TableProps } from 'antd';
import XinTable from '@/components/XinTable'; import XinTable from '@/components/XinTable';
import type { import type {
@@ -98,6 +99,9 @@ const PurchaseOrderPage: React.FC = () => {
const [detailLoading, setDetailLoading] = useState(false); const [detailLoading, setDetailLoading] = useState(false);
const [completing, setCompleting] = useState(false); const [completing, setCompleting] = useState(false);
// 商品明细:单品「已对账」勾选标记(前端本地控制不入库,按采购单持久化到 localStorage
const [checkedItems, setCheckedItems] = useState<Set<number>>(new Set());
// 行内编辑:供应商选项(品名/供应商/包规/单位/成本与各门店订货量直接在表格中编辑) // 行内编辑:供应商选项(品名/供应商/包规/单位/成本与各门店订货量直接在表格中编辑)
const [suppliers, setSuppliers] = useState<ISupplier[]>([]); const [suppliers, setSuppliers] = useState<ISupplier[]>([]);
@@ -246,6 +250,37 @@ const PurchaseOrderPage: React.FC = () => {
} }
}, [detailOpen, detailTab, storeId, detail?.purchase.id]); }, [detailOpen, detailTab, storeId, detail?.purchase.id]);
// 切换采购单时从 localStorage 恢复单品「已对账」标记
useEffect(() => {
if (!detail) {
setCheckedItems(new Set());
return;
}
try {
const raw = localStorage.getItem(`purchase-checked-items:${detail.purchase.id}`);
setCheckedItems(new Set(raw ? (JSON.parse(raw) as number[]) : []));
} catch {
setCheckedItems(new Set());
}
}, [detail]);
/** 切换单品「已对账」标记(前端本地控制,不入库) */
const toggleItemChecked = (productId: number) => {
if (!detail) {
return;
}
setCheckedItems((prev) => {
const next = new Set(prev);
if (next.has(productId)) {
next.delete(productId);
} else {
next.add(productId);
}
localStorage.setItem(`purchase-checked-items:${detail.purchase.id}`, JSON.stringify([...next]));
return next;
});
};
const loadDetail = async (id: number) => { const loadDetail = async (id: number) => {
setDetailLoading(true); setDetailLoading(true);
try { try {
@@ -403,7 +438,8 @@ const PurchaseOrderPage: React.FC = () => {
const res = await updatePurchaseRow(detail.purchase.id!, row.product_id, params); const res = await updatePurchaseRow(detail.purchase.id!, row.product_id, params);
message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`); message.success(`已更新 ${res.data.data?.count ?? 0} 条订货明细`);
await loadDetail(detail.purchase.id!); const res1 = await getPurchaseDetail(detail.purchase.id!);
setDetail(res1.data.data ?? null);
await tableRef.current?.reload(); await tableRef.current?.reload();
}; };
@@ -565,17 +601,36 @@ const PurchaseOrderPage: React.FC = () => {
title: '市场', title: '市场',
fixed: 'left', fixed: 'left',
dataIndex: 'market', dataIndex: 'market',
width: 110, width: 90,
align: 'center', align: 'center',
filters: itemMarketOptions.map((m) => ({ text: m, value: m })), filters: itemMarketOptions.map((m) => ({ text: m, value: m })),
filteredValue: itemColumnFilters.market ?? null, filteredValue: itemColumnFilters.market ?? null,
onFilter: (value, row) => (row.market ?? '') === value, onFilter: (value, row) => (row.market ?? '') === value,
render: (v) => v || '-', render: (v, row) => {
// ✔ 按钮标记本单品已对账(前端本地标记,不入库)
const checked = checkedItems.has(row.product_id);
return (
<Space size={4}>
<span>{v || '-'}</span>
<Tooltip title={checked ? '取消对账标记' : '标记本单品已对账'}>
<Button
size="small"
shape="circle"
style={{ minWidth: 16, height: 16, width: 16 }}
icon={<CheckOutlined />}
color={checked ? 'green' : 'default'}
variant={checked ? 'solid' : 'outlined'}
onClick={() => toggleItemChecked(row.product_id)}
/>
</Tooltip>
</Space>
);
},
}, },
{ {
title: '单价', title: '单价',
key: 'retail_price', key: 'retail_price',
width: 110, width: 90,
align: 'center', align: 'center',
render: (_, row) => { render: (_, row) => {
// 加权平均售价 ÷ 包规数值(无订货数量时无参考价) // 加权平均售价 ÷ 包规数值(无订货数量时无参考价)
@@ -589,20 +644,20 @@ const PurchaseOrderPage: React.FC = () => {
title: '包规', title: '包规',
dataIndex: 'product_spec', dataIndex: 'product_spec',
align: 'center', align: 'center',
width: 110, width: 90,
render: (v, row) => renderEditableCell(row, 'product_spec', v || '-'), render: (v, row) => renderEditableCell(row, 'product_spec', v || '-'),
}, },
{ {
title: '单位', title: '单位',
dataIndex: 'unit', dataIndex: 'unit',
width: 110, width: 90,
align: 'center', align: 'center',
render: (v, row) => renderEditableCell(row, 'unit', v || '-'), render: (v, row) => renderEditableCell(row, 'unit', v || '-'),
}, },
{ {
title: '成本', title: '成本',
dataIndex: 'cost_price', dataIndex: 'cost_price',
width: 110, width: 120,
align: 'center', align: 'center',
render: (v, row) => renderEditableCell(row, 'cost_price', `¥${Number(v).toFixed(2)}`), render: (v, row) => renderEditableCell(row, 'cost_price', `¥${Number(v).toFixed(2)}`),
}, },