diff --git a/CHANGELOG-client.md b/CHANGELOG-client.md index cb25592..2a51fba 100644 --- a/CHANGELOG-client.md +++ b/CHANGELOG-client.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.72] - 2026-06-22 + +### 新功能 +- 出库单录入新增「售价」可编辑列:可按实际销售价录入,金额与单据应收按「售价 × 数量」实时计算,同时保留成本价一栏便于对比 + +### 改进 +- 「退单」按钮改为红色实心样式,更醒目地突出该危险操作(与原型一致) +- 库存列表隐藏「库存量」列:序列号模型下每件商品独立,原数量列易受历史数据误导,故不再展示(缺货/预警逻辑不受影响) +- 出库「添加商品」可一次看到整仓商品(需配合后端 v1.0.75) + ## [1.0.71] - 2026-06-21 ### 新功能 diff --git a/client/lib/models/stock_out.dart b/client/lib/models/stock_out.dart index 7a85d08..437bed7 100644 --- a/client/lib/models/stock_out.dart +++ b/client/lib/models/stock_out.dart @@ -4,6 +4,7 @@ class StockOutItem { final int productId; final double quantity; final double unitPrice; + final double salePrice; final double totalPrice; final double returnedQuantity; final String? productName; @@ -20,6 +21,7 @@ class StockOutItem { required this.productId, required this.quantity, required this.unitPrice, + this.salePrice = 0, required this.totalPrice, this.returnedQuantity = 0, this.productName, @@ -47,6 +49,7 @@ class StockOutItem { productId: (json['product_id'] as num).toInt(), quantity: (json['quantity'] as num).toDouble(), unitPrice: (json['unit_price'] as num).toDouble(), + salePrice: (json['sale_price'] as num?)?.toDouble() ?? 0, totalPrice: (json['total_price'] as num).toDouble(), returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0, productName: lineOrProduct('product_name', 'name'), diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 0b845d4..0a031cf 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -51,7 +51,7 @@ class _InventoryListScreenState extends ConsumerState { ColDef('series', '系列'), ColDef('batch', '批次号'), ColDef('warehouse', '仓库'), - ColDef('qty', '库存量'), + // 库存量列已移除:序列号模型下每件独立,原数量列含历史装箱数脏数据易误导 ColDef('price', '单价'), ColDef('prodDate', '生产日期'), ColDef('inTime', '入库时间'), @@ -311,15 +311,6 @@ class _InventoryListScreenState extends ConsumerState { 'batch' => DataCell(Text(item.batchNo.isEmpty ? '-' : item.batchNo, style: const TextStyle(fontFamily: 'monospace', fontSize: 12))), 'warehouse' => DataCell(Text(item.warehouseName.isEmpty ? '-' : item.warehouseName)), - 'qty' => DataCell(Text( - '${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(), - style: TextStyle( - fontWeight: FontWeight.w600, - color: item.quantity == 0 - ? AppTheme.danger - : (item.minStock != null && item.quantity < item.minStock!) - ? AppTheme.accent - : AppTheme.textPrimary))), 'price' => DataCell(Text( item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '-')), 'prodDate' => DataCell(Text(item.productionDate ?? '-')), @@ -367,19 +358,6 @@ class _InventoryListScreenState extends ConsumerState { if (item.series.isNotEmpty) MobileCardField('系列', item.series), if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo), MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName), - MobileCardField('库存', null, - valueWidget: Text( - '${item.quantity.toStringAsFixed(0)} ${item.unit}'.trim(), - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: item.quantity == 0 - ? AppTheme.danger - : (item.minStock != null && item.quantity < item.minStock!) - ? AppTheme.accent - : AppTheme.textPrimary, - ), - )), if (item.unitPrice != null) MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'), if (item.supplierName.isNotEmpty) @@ -763,7 +741,7 @@ class _InventoryListScreenState extends ConsumerState { }; return DataColumn( label: label, - numeric: c.key == 'qty' || c.key == 'price', + numeric: c.key == 'price', ); }).toList(), rows: items.isEmpty diff --git a/client/lib/screens/stock_out/stock_out_form_screen.dart b/client/lib/screens/stock_out/stock_out_form_screen.dart index 70c5478..82ad80c 100644 --- a/client/lib/screens/stock_out/stock_out_form_screen.dart +++ b/client/lib/screens/stock_out/stock_out_form_screen.dart @@ -83,6 +83,7 @@ class _ItemRow { final String spec; final double? unitPrice; final TextEditingController qtyCtrl; + final TextEditingController salePriceCtrl; _ItemRow({ this.productId, @@ -91,9 +92,19 @@ class _ItemRow { this.series = '', this.spec = '', this.unitPrice, - }) : qtyCtrl = TextEditingController(text: '1'); + double? salePrice, + }) : qtyCtrl = TextEditingController(text: '1'), + // 售价默认带出成本单价(无历史售价时),可手工改成实际卖价 + salePriceCtrl = TextEditingController( + text: ((salePrice != null && salePrice > 0) + ? salePrice + : (unitPrice ?? 0)) + .toStringAsFixed(2)); - void dispose() => qtyCtrl.dispose(); + void dispose() { + qtyCtrl.dispose(); + salePriceCtrl.dispose(); + } } class StockOutFormScreen extends ConsumerStatefulWidget { @@ -152,6 +163,7 @@ class _StockOutFormScreenState extends ConsumerState { series: item.productSeries ?? '', spec: item.productSpec ?? '', unitPrice: item.unitPrice, + salePrice: item.salePrice, ); row.qtyCtrl.text = item.quantity.toStringAsFixed(0); _items.add(row); @@ -178,11 +190,13 @@ class _StockOutFormScreenState extends ConsumerState { super.dispose(); } + // 出库单总金额 = 应收 = Σ(售价×数量) double get _totalAmount { double total = 0; for (final item in _items) { final qty = double.tryParse(item.qtyCtrl.text) ?? 0; - total += qty * (item.unitPrice ?? 0); + final sale = double.tryParse(item.salePriceCtrl.text) ?? 0; + total += qty * sale; } return total; } @@ -280,10 +294,12 @@ class _StockOutFormScreenState extends ConsumerState { final itemsData = _items.map((item) { final qty = double.tryParse(item.qtyCtrl.text) ?? 0; final price = item.unitPrice ?? 0; + final sale = double.tryParse(item.salePriceCtrl.text) ?? 0; return { 'product_id': item.productId ?? 0, 'quantity': qty, 'unit_price': price, + 'sale_price': sale, 'total_price': qty * price, }; }).toList(); @@ -647,10 +663,11 @@ class _StockOutFormScreenState extends ConsumerState { 2: FlexColumnWidth(2.0), // 商品名称 3: FlexColumnWidth(1.2), // 系列 4: FlexColumnWidth(1.2), // 规格 - 5: FlexColumnWidth(1.0), // 单价 - 6: FlexColumnWidth(0.8), // 数量 - 7: FlexColumnWidth(1.0), // 金额 - 8: FixedColumnWidth(48), // 操作 + 5: FlexColumnWidth(1.0), // 成本价 + 6: FlexColumnWidth(1.0), // 售价 + 7: FlexColumnWidth(0.8), // 数量 + 8: FlexColumnWidth(1.0), // 金额 + 9: FixedColumnWidth(48), // 操作 }, children: [ TableRow( @@ -662,7 +679,8 @@ class _StockOutFormScreenState extends ConsumerState { '商品名称', '系列', '规格', - '单价', + '成本价', + '售价', '数量', '金额', '操作', @@ -734,7 +752,8 @@ class _StockOutFormScreenState extends ConsumerState { final item = _items[index]; final qty = double.tryParse(item.qtyCtrl.text) ?? 0; final price = item.unitPrice ?? 0; - final amount = qty * price; + final sale = double.tryParse(item.salePriceCtrl.text) ?? 0; + final amount = qty * sale; return TableRow( decoration: BoxDecoration( @@ -761,12 +780,16 @@ class _StockOutFormScreenState extends ConsumerState { _cell(Text(item.spec, style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary))), - // 单价 + // 成本价 _cell(Text(price > 0 ? '¥${price.toStringAsFixed(2)}' : '-', - style: const TextStyle(fontSize: 13))), + style: const TextStyle( + fontSize: 13, color: AppTheme.textSecondary))), + // 售价(可编辑) + Padding( + padding: const EdgeInsets.all(4), child: _salePriceField(item)), // 数量 Padding(padding: const EdgeInsets.all(4), child: _qtyField(item)), - // 金额 + // 金额(=售价×数量) _cell(Text('¥${amount.toStringAsFixed(2)}', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))), // 操作 @@ -803,12 +826,27 @@ class _StockOutFormScreenState extends ConsumerState { ); } + // 售价输入:可编辑,金额(应收)按 售价×数量 联动 + Widget _salePriceField(_ItemRow item) { + return TextFormField( + controller: item.salePriceCtrl, + decoration: const InputDecoration(hintText: '0.00', isDense: true), + style: const TextStyle(fontSize: 13), + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'^\d+\.?\d{0,2}')) + ], + onChanged: (_) => setState(() {}), + ); + } + /// 窄屏(手机):每个明细行渲染为一张卡片,字段竖排,避免表格横向溢出。 Widget _buildItemCard(int index) { final item = _items[index]; final qty = double.tryParse(item.qtyCtrl.text) ?? 0; final price = item.unitPrice ?? 0; - final amount = qty * price; + final sale = double.tryParse(item.salePriceCtrl.text) ?? 0; + final amount = qty * sale; return MobileListCard( title: Text(item.productName), @@ -824,7 +862,9 @@ class _StockOutFormScreenState extends ConsumerState { fields: [ if (item.series.isNotEmpty) MobileCardField('系列', item.series), if (item.spec.isNotEmpty) MobileCardField('规格', item.spec), - MobileCardField('单价', price > 0 ? '¥${price.toStringAsFixed(2)}' : '-'), + MobileCardField( + '成本价', price > 0 ? '¥${price.toStringAsFixed(2)}' : '-'), + MobileCardField('售价', null, valueWidget: _salePriceField(item)), MobileCardField('数量', null, valueWidget: _qtyField(item)), MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'), ],