Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b0b2645ce |
@@ -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
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -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)}'),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user