Compare commits

...

1 Commits

Author SHA1 Message Date
wangjia 6b0b2645ce chore: release client-v1.0.72
Deploy Client / build-client-web (push) Successful in 41s
Deploy Client / build-windows (push) Successful in 2m34s
Deploy Client / build-macos (push) Successful in 2m17s
Deploy Client / build-android (push) Successful in 1m26s
Deploy Client / build-ios (push) Successful in 2m42s
Deploy Client / release-deploy-client (push) Successful in 1m35s
出库售价可编辑列(应收按售价) + 退单按钮红色实心 + 库存列表隐藏数量列

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
2026-06-22 13:02:29 +08:00
4 changed files with 69 additions and 38 deletions
+10
View File
@@ -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/), 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). 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 ## [1.0.71] - 2026-06-21
### 新功能 ### 新功能
+3
View File
@@ -4,6 +4,7 @@ class StockOutItem {
final int productId; final int productId;
final double quantity; final double quantity;
final double unitPrice; final double unitPrice;
final double salePrice;
final double totalPrice; final double totalPrice;
final double returnedQuantity; final double returnedQuantity;
final String? productName; final String? productName;
@@ -20,6 +21,7 @@ class StockOutItem {
required this.productId, required this.productId,
required this.quantity, required this.quantity,
required this.unitPrice, required this.unitPrice,
this.salePrice = 0,
required this.totalPrice, required this.totalPrice,
this.returnedQuantity = 0, this.returnedQuantity = 0,
this.productName, this.productName,
@@ -47,6 +49,7 @@ class StockOutItem {
productId: (json['product_id'] as num).toInt(), productId: (json['product_id'] as num).toInt(),
quantity: (json['quantity'] as num).toDouble(), quantity: (json['quantity'] as num).toDouble(),
unitPrice: (json['unit_price'] 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(), totalPrice: (json['total_price'] as num).toDouble(),
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0, returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
productName: lineOrProduct('product_name', 'name'), productName: lineOrProduct('product_name', 'name'),
@@ -51,7 +51,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
ColDef('series', '系列'), ColDef('series', '系列'),
ColDef('batch', '批次号'), ColDef('batch', '批次号'),
ColDef('warehouse', '仓库'), ColDef('warehouse', '仓库'),
ColDef('qty', '库存量'), // 库存量列已移除:序列号模型下每件独立,原数量列含历史装箱数脏数据易误导
ColDef('price', '单价'), ColDef('price', '单价'),
ColDef('prodDate', '生产日期'), ColDef('prodDate', '生产日期'),
ColDef('inTime', '入库时间'), ColDef('inTime', '入库时间'),
@@ -311,15 +311,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
'batch' => DataCell(Text(item.batchNo.isEmpty ? '-' : item.batchNo, 'batch' => DataCell(Text(item.batchNo.isEmpty ? '-' : item.batchNo,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12))), style: const TextStyle(fontFamily: 'monospace', fontSize: 12))),
'warehouse' => DataCell(Text(item.warehouseName.isEmpty ? '-' : item.warehouseName)), '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( 'price' => DataCell(Text(
item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '-')), item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '-')),
'prodDate' => DataCell(Text(item.productionDate ?? '-')), 'prodDate' => DataCell(Text(item.productionDate ?? '-')),
@@ -367,19 +358,6 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
if (item.series.isNotEmpty) MobileCardField('系列', item.series), if (item.series.isNotEmpty) MobileCardField('系列', item.series),
if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo), if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo),
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName), 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) if (item.unitPrice != null)
MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'), MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'),
if (item.supplierName.isNotEmpty) if (item.supplierName.isNotEmpty)
@@ -763,7 +741,7 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
}; };
return DataColumn( return DataColumn(
label: label, label: label,
numeric: c.key == 'qty' || c.key == 'price', numeric: c.key == 'price',
); );
}).toList(), }).toList(),
rows: items.isEmpty rows: items.isEmpty
@@ -83,6 +83,7 @@ class _ItemRow {
final String spec; final String spec;
final double? unitPrice; final double? unitPrice;
final TextEditingController qtyCtrl; final TextEditingController qtyCtrl;
final TextEditingController salePriceCtrl;
_ItemRow({ _ItemRow({
this.productId, this.productId,
@@ -91,9 +92,19 @@ class _ItemRow {
this.series = '', this.series = '',
this.spec = '', this.spec = '',
this.unitPrice, 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 { class StockOutFormScreen extends ConsumerStatefulWidget {
@@ -152,6 +163,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
series: item.productSeries ?? '', series: item.productSeries ?? '',
spec: item.productSpec ?? '', spec: item.productSpec ?? '',
unitPrice: item.unitPrice, unitPrice: item.unitPrice,
salePrice: item.salePrice,
); );
row.qtyCtrl.text = item.quantity.toStringAsFixed(0); row.qtyCtrl.text = item.quantity.toStringAsFixed(0);
_items.add(row); _items.add(row);
@@ -178,11 +190,13 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
super.dispose(); super.dispose();
} }
// 出库单总金额 = 应收 = Σ(售价×数量)
double get _totalAmount { double get _totalAmount {
double total = 0; double total = 0;
for (final item in _items) { for (final item in _items) {
final qty = double.tryParse(item.qtyCtrl.text) ?? 0; 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; return total;
} }
@@ -280,10 +294,12 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
final itemsData = _items.map((item) { final itemsData = _items.map((item) {
final qty = double.tryParse(item.qtyCtrl.text) ?? 0; final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = item.unitPrice ?? 0; final price = item.unitPrice ?? 0;
final sale = double.tryParse(item.salePriceCtrl.text) ?? 0;
return { return {
'product_id': item.productId ?? 0, 'product_id': item.productId ?? 0,
'quantity': qty, 'quantity': qty,
'unit_price': price, 'unit_price': price,
'sale_price': sale,
'total_price': qty * price, 'total_price': qty * price,
}; };
}).toList(); }).toList();
@@ -647,10 +663,11 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
2: FlexColumnWidth(2.0), // 商品名称 2: FlexColumnWidth(2.0), // 商品名称
3: FlexColumnWidth(1.2), // 系列 3: FlexColumnWidth(1.2), // 系列
4: FlexColumnWidth(1.2), // 规格 4: FlexColumnWidth(1.2), // 规格
5: FlexColumnWidth(1.0), // 5: FlexColumnWidth(1.0), // 成本
6: FlexColumnWidth(0.8), // 数量 6: FlexColumnWidth(1.0), // 售价
7: FlexColumnWidth(1.0), // 金额 7: FlexColumnWidth(0.8), // 数量
8: FixedColumnWidth(48), // 操作 8: FlexColumnWidth(1.0), // 金额
9: FixedColumnWidth(48), // 操作
}, },
children: [ children: [
TableRow( TableRow(
@@ -662,7 +679,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
'商品名称', '商品名称',
'系列', '系列',
'规格', '规格',
'', '成本',
'售价',
'数量', '数量',
'金额', '金额',
'操作', '操作',
@@ -734,7 +752,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
final item = _items[index]; final item = _items[index];
final qty = double.tryParse(item.qtyCtrl.text) ?? 0; final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = item.unitPrice ?? 0; final price = item.unitPrice ?? 0;
final amount = qty * price; final sale = double.tryParse(item.salePriceCtrl.text) ?? 0;
final amount = qty * sale;
return TableRow( return TableRow(
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -761,12 +780,16 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
_cell(Text(item.spec, _cell(Text(item.spec,
style: style:
const TextStyle(fontSize: 12, color: AppTheme.textSecondary))), const TextStyle(fontSize: 12, color: AppTheme.textSecondary))),
// // 成本
_cell(Text(price > 0 ? '¥${price.toStringAsFixed(2)}' : '-', _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)), Padding(padding: const EdgeInsets.all(4), child: _qtyField(item)),
// 金额 // 金额=售价×数量)
_cell(Text('¥${amount.toStringAsFixed(2)}', _cell(Text('¥${amount.toStringAsFixed(2)}',
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))), style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500))),
// 操作 // 操作
@@ -803,12 +826,27 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
); );
} }
// 售价输入:可编辑,金额(应收)按 售价×数量 联动
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) { Widget _buildItemCard(int index) {
final item = _items[index]; final item = _items[index];
final qty = double.tryParse(item.qtyCtrl.text) ?? 0; final qty = double.tryParse(item.qtyCtrl.text) ?? 0;
final price = item.unitPrice ?? 0; final price = item.unitPrice ?? 0;
final amount = qty * price; final sale = double.tryParse(item.salePriceCtrl.text) ?? 0;
final amount = qty * sale;
return MobileListCard( return MobileListCard(
title: Text(item.productName), title: Text(item.productName),
@@ -824,7 +862,9 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
fields: [ fields: [
if (item.series.isNotEmpty) MobileCardField('系列', item.series), if (item.series.isNotEmpty) MobileCardField('系列', item.series),
if (item.spec.isNotEmpty) MobileCardField('规格', item.spec), 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('数量', null, valueWidget: _qtyField(item)),
MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'), MobileCardField('金额', '¥${amount.toStringAsFixed(2)}'),
], ],