feat(client): 出库明细成本/售价/利润三列重设计 + 建单利润列 + 入库列头统一
- 出库详情抽屉(管理员):数量/成本价/售价/利润 + 合计金额/合计利润 双行; operator 见 售价/小计(服务端抹零+前端隐藏双保险);利润负数 danger - 出库建单:商品名称下带编码(字体同系列列);金额列→利润列(实时); 底栏合计利润(仅管理员);进价列仅管理员;提交发新 key(cost_price/sale_price) - 入库建单列头:进价(单瓶)/ 参考售价 / 总进价 - 退单弹窗金额改售价口径(与冲应收一致);打印保留待定价回退成本兼容 - models 字段随后端消歧改名;golden 重打;新增抽屉双形态 widget 测试 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
@@ -1045,7 +1045,7 @@ Future<Uint8List> buildStockInOrderPdfImpl(
|
|||||||
final rows = <List<String>>[];
|
final rows = <List<String>>[];
|
||||||
for (final it in order.items) {
|
for (final it in order.items) {
|
||||||
totalQty += it.quantity;
|
totalQty += it.quantity;
|
||||||
totalAmt += it.totalPrice;
|
totalAmt += it.costAmount;
|
||||||
rows.add([
|
rows.add([
|
||||||
it.productCode ?? '',
|
it.productCode ?? '',
|
||||||
it.productName ?? '',
|
it.productName ?? '',
|
||||||
@@ -1054,8 +1054,8 @@ Future<Uint8List> buildStockInOrderPdfImpl(
|
|||||||
it.batchNo ?? '',
|
it.batchNo ?? '',
|
||||||
_d10(it.productionDate),
|
_d10(it.productionDate),
|
||||||
_qtyStr(it.quantity),
|
_qtyStr(it.quantity),
|
||||||
it.unitPrice.toStringAsFixed(2),
|
it.costPrice.toStringAsFixed(2),
|
||||||
it.totalPrice.toStringAsFixed(2),
|
it.costAmount.toStringAsFixed(2),
|
||||||
'',
|
'',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -1112,9 +1112,9 @@ Future<Uint8List> buildStockOutOrderPdfImpl(
|
|||||||
final rows = <List<String>>[];
|
final rows = <List<String>>[];
|
||||||
for (final it in order.items) {
|
for (final it in order.items) {
|
||||||
// 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
|
// 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
|
||||||
// 实际售价存在 unit_price/total_price,故 sale_price 为空时回退到 unit_price。
|
// 实际售价存在 cost_price/cost_amount(源列直存),故售价为空时回退成本列。
|
||||||
final unit = it.salePrice > 0 ? it.salePrice : it.unitPrice;
|
final unit = it.salePrice > 0 ? it.salePrice : it.costPrice;
|
||||||
final amt = it.salePrice > 0 ? it.salePrice * it.quantity : it.totalPrice;
|
final amt = it.salePrice > 0 ? it.saleAmount : it.costAmount;
|
||||||
totalQty += it.quantity;
|
totalQty += it.quantity;
|
||||||
totalAmt += amt;
|
totalAmt += amt;
|
||||||
rows.add([
|
rows.add([
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ Future<void> printStockInOrderImpl(
|
|||||||
double totalAmt = 0;
|
double totalAmt = 0;
|
||||||
for (final item in order.items) {
|
for (final item in order.items) {
|
||||||
totalQty += item.quantity;
|
totalQty += item.quantity;
|
||||||
totalAmt += item.totalPrice;
|
totalAmt += item.costAmount;
|
||||||
rows.write('''<tr>
|
rows.write('''<tr>
|
||||||
<td>${item.productCode ?? ''}</td>
|
<td>${item.productCode ?? ''}</td>
|
||||||
<td>${item.productName ?? ''}</td>
|
<td>${item.productName ?? ''}</td>
|
||||||
@@ -249,8 +249,8 @@ Future<void> printStockInOrderImpl(
|
|||||||
<td class="c">${item.batchNo ?? ''}</td>
|
<td class="c">${item.batchNo ?? ''}</td>
|
||||||
<td class="c">${_d10(item.productionDate)}</td>
|
<td class="c">${_d10(item.productionDate)}</td>
|
||||||
<td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td>
|
<td class="r">${item.quantity % 1 == 0 ? item.quantity.toStringAsFixed(0) : item.quantity.toStringAsFixed(3)}</td>
|
||||||
<td class="r">${item.unitPrice.toStringAsFixed(2)}</td>
|
<td class="r">${item.costPrice.toStringAsFixed(2)}</td>
|
||||||
<td class="r">${item.totalPrice.toStringAsFixed(2)}</td>
|
<td class="r">${item.costAmount.toStringAsFixed(2)}</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
</tr>''');
|
</tr>''');
|
||||||
}
|
}
|
||||||
@@ -324,10 +324,9 @@ Future<void> printStockOutOrderImpl(
|
|||||||
double totalAmt = 0;
|
double totalAmt = 0;
|
||||||
for (final item in order.items) {
|
for (final item in order.items) {
|
||||||
// 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
|
// 出库单「单价」= 售价:App 单据存 sale_price;历史导入单 sale_price=0、
|
||||||
// 实际售价存在 unit_price/total_price,故 sale_price 为空时回退到 unit_price。
|
// 实际售价存在 cost_price/cost_amount(源列直存),故售价为空时回退成本列。
|
||||||
final unit = item.salePrice > 0 ? item.salePrice : item.unitPrice;
|
final unit = item.salePrice > 0 ? item.salePrice : item.costPrice;
|
||||||
final amt =
|
final amt = item.salePrice > 0 ? item.saleAmount : item.costAmount;
|
||||||
item.salePrice > 0 ? item.salePrice * item.quantity : item.totalPrice;
|
|
||||||
totalQty += item.quantity;
|
totalQty += item.quantity;
|
||||||
totalAmt += amt;
|
totalAmt += amt;
|
||||||
rows.write('''<tr>
|
rows.write('''<tr>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ class StockInItem {
|
|||||||
final int? orderId;
|
final int? orderId;
|
||||||
final int productId;
|
final int productId;
|
||||||
final double quantity;
|
final double quantity;
|
||||||
final double unitPrice;
|
final double costPrice; // 进价(单瓶)
|
||||||
final double totalPrice;
|
final double costAmount; // 总进价 = quantity × costPrice
|
||||||
final double returnedQuantity; // 0=未退;>=quantity 表示整行已退单
|
final double returnedQuantity; // 0=未退;>=quantity 表示整行已退单
|
||||||
final String? batchNo;
|
final String? batchNo;
|
||||||
final String? productionDate;
|
final String? productionDate;
|
||||||
@@ -26,8 +26,8 @@ class StockInItem {
|
|||||||
this.orderId,
|
this.orderId,
|
||||||
required this.productId,
|
required this.productId,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
required this.unitPrice,
|
required this.costPrice,
|
||||||
required this.totalPrice,
|
required this.costAmount,
|
||||||
this.returnedQuantity = 0,
|
this.returnedQuantity = 0,
|
||||||
this.batchNo,
|
this.batchNo,
|
||||||
this.productionDate,
|
this.productionDate,
|
||||||
@@ -57,8 +57,8 @@ class StockInItem {
|
|||||||
json['order_id'] != null ? (json['order_id'] as num).toInt() : null,
|
json['order_id'] != null ? (json['order_id'] as num).toInt() : null,
|
||||||
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(),
|
costPrice: (json['cost_price'] as num?)?.toDouble() ?? 0,
|
||||||
totalPrice: (json['total_price'] as num).toDouble(),
|
costAmount: (json['cost_amount'] as num?)?.toDouble() ?? 0,
|
||||||
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
|
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
|
||||||
batchNo: json['batch_no'] as String?,
|
batchNo: json['batch_no'] as String?,
|
||||||
productionDate: json['production_date'] as String?,
|
productionDate: json['production_date'] as String?,
|
||||||
@@ -81,8 +81,8 @@ class StockInItem {
|
|||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'product_id': productId,
|
'product_id': productId,
|
||||||
'quantity': quantity,
|
'quantity': quantity,
|
||||||
'unit_price': unitPrice,
|
'cost_price': costPrice,
|
||||||
'total_price': totalPrice,
|
'cost_amount': costAmount,
|
||||||
if (batchNo != null) 'batch_no': batchNo,
|
if (batchNo != null) 'batch_no': batchNo,
|
||||||
if (productionDate != null) 'production_date': productionDate,
|
if (productionDate != null) 'production_date': productionDate,
|
||||||
};
|
};
|
||||||
@@ -104,7 +104,7 @@ class StockInOrder {
|
|||||||
final String returnState; // none | partial | full(退单状态)
|
final String returnState; // none | partial | full(退单状态)
|
||||||
final String? orderDate;
|
final String? orderDate;
|
||||||
final String? reviewedAt;
|
final String? reviewedAt;
|
||||||
final double? totalAmount;
|
final double? costTotal; // 应付合计 = Σ 总进价
|
||||||
final String? remark;
|
final String? remark;
|
||||||
final List<StockInItem> items;
|
final List<StockInItem> items;
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ class StockInOrder {
|
|||||||
this.returnState = 'none',
|
this.returnState = 'none',
|
||||||
this.orderDate,
|
this.orderDate,
|
||||||
this.reviewedAt,
|
this.reviewedAt,
|
||||||
this.totalAmount,
|
this.costTotal,
|
||||||
this.remark,
|
this.remark,
|
||||||
this.items = const [],
|
this.items = const [],
|
||||||
});
|
});
|
||||||
@@ -155,8 +155,8 @@ class StockInOrder {
|
|||||||
returnState: json['return_state'] as String? ?? 'none',
|
returnState: json['return_state'] as String? ?? 'none',
|
||||||
orderDate: json['order_date'] as String?,
|
orderDate: json['order_date'] as String?,
|
||||||
reviewedAt: json['reviewed_at'] as String?,
|
reviewedAt: json['reviewed_at'] as String?,
|
||||||
totalAmount: json['total_amount'] != null
|
costTotal: json['cost_total'] != null
|
||||||
? (json['total_amount'] as num).toDouble()
|
? (json['cost_total'] as num).toDouble()
|
||||||
: null,
|
: null,
|
||||||
remark: json['remark'] as String?,
|
remark: json['remark'] as String?,
|
||||||
items: json['items'] != null
|
items: json['items'] != null
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ class StockOutItem {
|
|||||||
final int? orderId;
|
final int? orderId;
|
||||||
final int productId;
|
final int productId;
|
||||||
final double quantity;
|
final double quantity;
|
||||||
final double unitPrice;
|
final double costPrice; // 成本单价(入库成本快照,仅管理员可见——operator 响应被服务端抹零)
|
||||||
final double salePrice;
|
final double salePrice; // 销售单价
|
||||||
final double totalPrice;
|
final double costAmount; // 成本小计 = quantity × costPrice
|
||||||
|
final double saleAmount; // 售价小计 = quantity × salePrice(待定价=0)
|
||||||
final double returnedQuantity;
|
final double returnedQuantity;
|
||||||
final String? productName;
|
final String? productName;
|
||||||
final String? productCode;
|
final String? productCode;
|
||||||
@@ -22,9 +23,10 @@ class StockOutItem {
|
|||||||
this.orderId,
|
this.orderId,
|
||||||
required this.productId,
|
required this.productId,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
required this.unitPrice,
|
this.costPrice = 0,
|
||||||
this.salePrice = 0,
|
this.salePrice = 0,
|
||||||
required this.totalPrice,
|
this.costAmount = 0,
|
||||||
|
this.saleAmount = 0,
|
||||||
this.returnedQuantity = 0,
|
this.returnedQuantity = 0,
|
||||||
this.productName,
|
this.productName,
|
||||||
this.productCode,
|
this.productCode,
|
||||||
@@ -51,9 +53,10 @@ class StockOutItem {
|
|||||||
json['order_id'] != null ? (json['order_id'] as num).toInt() : null,
|
json['order_id'] != null ? (json['order_id'] as num).toInt() : null,
|
||||||
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(),
|
costPrice: (json['cost_price'] as num?)?.toDouble() ?? 0,
|
||||||
salePrice: (json['sale_price'] as num?)?.toDouble() ?? 0,
|
salePrice: (json['sale_price'] as num?)?.toDouble() ?? 0,
|
||||||
totalPrice: (json['total_price'] as num).toDouble(),
|
costAmount: (json['cost_amount'] as num?)?.toDouble() ?? 0,
|
||||||
|
saleAmount: (json['sale_amount'] as num?)?.toDouble() ?? 0,
|
||||||
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'),
|
||||||
productCode: lineOrProduct('product_code', 'code'),
|
productCode: lineOrProduct('product_code', 'code'),
|
||||||
@@ -85,7 +88,8 @@ class StockOutOrder {
|
|||||||
final String? orderDate;
|
final String? orderDate;
|
||||||
final String? reviewedAt;
|
final String? reviewedAt;
|
||||||
final String? createdAt;
|
final String? createdAt;
|
||||||
final double? totalAmount;
|
final double? saleTotal; // 应收合计 = Σ 售价小计
|
||||||
|
final double profitTotal; // 总利润(仅管理员可见,operator 被抹零)
|
||||||
final String? remark;
|
final String? remark;
|
||||||
final List<StockOutItem> items;
|
final List<StockOutItem> items;
|
||||||
|
|
||||||
@@ -106,7 +110,8 @@ class StockOutOrder {
|
|||||||
this.orderDate,
|
this.orderDate,
|
||||||
this.reviewedAt,
|
this.reviewedAt,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.totalAmount,
|
this.saleTotal,
|
||||||
|
this.profitTotal = 0,
|
||||||
this.remark,
|
this.remark,
|
||||||
this.items = const [],
|
this.items = const [],
|
||||||
});
|
});
|
||||||
@@ -138,9 +143,10 @@ class StockOutOrder {
|
|||||||
orderDate: json['order_date'] as String?,
|
orderDate: json['order_date'] as String?,
|
||||||
reviewedAt: json['reviewed_at'] as String?,
|
reviewedAt: json['reviewed_at'] as String?,
|
||||||
createdAt: json['created_at'] as String?,
|
createdAt: json['created_at'] as String?,
|
||||||
totalAmount: json['total_amount'] != null
|
saleTotal: json['sale_total'] != null
|
||||||
? (json['total_amount'] as num).toDouble()
|
? (json['sale_total'] as num).toDouble()
|
||||||
: null,
|
: null,
|
||||||
|
profitTotal: (json['profit_total'] as num?)?.toDouble() ?? 0,
|
||||||
remark: json['remark'] as String?,
|
remark: json['remark'] as String?,
|
||||||
items: json['items'] != null
|
items: json['items'] != null
|
||||||
? (json['items'] as List)
|
? (json['items'] as List)
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class OrderFormShell extends StatelessWidget {
|
|||||||
final Widget detail;
|
final Widget detail;
|
||||||
final String rowsLabel; // 明细 N 行
|
final String rowsLabel; // 明细 N 行
|
||||||
final String totalText; // 已格式化 ¥
|
final String totalText; // 已格式化 ¥
|
||||||
|
final String? profitText; // 合计利润(出库·仅管理员;null 不渲染)
|
||||||
final bool loading;
|
final bool loading;
|
||||||
|
|
||||||
const OrderFormShell({
|
const OrderFormShell({
|
||||||
@@ -45,6 +46,7 @@ class OrderFormShell extends StatelessWidget {
|
|||||||
required this.detail,
|
required this.detail,
|
||||||
required this.rowsLabel,
|
required this.rowsLabel,
|
||||||
required this.totalText,
|
required this.totalText,
|
||||||
|
this.profitText,
|
||||||
this.statusBadge,
|
this.statusBadge,
|
||||||
this.notice,
|
this.notice,
|
||||||
this.loading = false,
|
this.loading = false,
|
||||||
@@ -123,6 +125,20 @@ class OrderFormShell extends StatelessWidget {
|
|||||||
Text(rowsLabel,
|
Text(rowsLabel,
|
||||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
if (profitText != null) ...[
|
||||||
|
Text('合计利润 ',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||||
|
Text(profitText!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsTitle,
|
||||||
|
color:
|
||||||
|
profitText!.contains('-') ? t.danger : t.success,
|
||||||
|
fontFamily: AppFonts.mono,
|
||||||
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
const SizedBox(width: 18),
|
||||||
|
],
|
||||||
Text('合计金额 ',
|
Text('合计金额 ',
|
||||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||||
Text(totalText,
|
Text(totalText,
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
final row = _ItemRow();
|
final row = _ItemRow();
|
||||||
row.productId = item.productId;
|
row.productId = item.productId;
|
||||||
row.qtyCtrl.text = item.quantity.toStringAsFixed(0);
|
row.qtyCtrl.text = item.quantity.toStringAsFixed(0);
|
||||||
row.priceCtrl.text = item.unitPrice.toStringAsFixed(2);
|
row.priceCtrl.text = item.costPrice.toStringAsFixed(2);
|
||||||
row.selectedNameId =
|
row.selectedNameId =
|
||||||
nameOpts.where((o) => o.name == item.productName).firstOrNull?.id;
|
nameOpts.where((o) => o.name == item.productName).firstOrNull?.id;
|
||||||
row.selectedSeriesId = seriesOpts
|
row.selectedSeriesId = seriesOpts
|
||||||
@@ -375,9 +375,8 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
'series': optName(seriesOpts, it.selectedSeriesId),
|
'series': optName(seriesOpts, it.selectedSeriesId),
|
||||||
'spec': optName(specOpts, it.selectedSpecId),
|
'spec': optName(specOpts, it.selectedSpecId),
|
||||||
'quantity': qty,
|
'quantity': qty,
|
||||||
'unit_price': price,
|
'cost_price': price,
|
||||||
if (sale > 0) 'sale_price': sale,
|
if (sale > 0) 'sale_price': sale,
|
||||||
'total_price': qty * price,
|
|
||||||
if (batchNo.isNotEmpty) 'batch_no': batchNo,
|
if (batchNo.isNotEmpty) 'batch_no': batchNo,
|
||||||
if (productionDate.isNotEmpty) 'production_date': productionDate,
|
if (productionDate.isNotEmpty) 'production_date': productionDate,
|
||||||
};
|
};
|
||||||
@@ -649,9 +648,9 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
GridCol('prodDate', '生产日期', width: 150, req: true),
|
GridCol('prodDate', '生产日期', width: 150, req: true),
|
||||||
GridCol('batch', '批次号', width: 116, req: true),
|
GridCol('batch', '批次号', width: 116, req: true),
|
||||||
GridCol('qty', '数量', width: 82, num: true, req: true),
|
GridCol('qty', '数量', width: 82, num: true, req: true),
|
||||||
GridCol('price', '进价', width: 100, num: true),
|
GridCol('price', '进价(单瓶)', width: 108, num: true),
|
||||||
GridCol('sale', '售价', width: 108, num: true, req: true),
|
GridCol('sale', '参考售价', width: 108, num: true, req: true),
|
||||||
GridCol('amount', '金额', width: 110, num: true),
|
GridCol('amount', '总进价', width: 110, num: true),
|
||||||
GridCol('act', '', width: 78),
|
GridCol('act', '', width: 78),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -912,14 +911,14 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
num: true,
|
num: true,
|
||||||
hint: '0',
|
hint: '0',
|
||||||
onChanged: (_) => setState(() {}))),
|
onChanged: (_) => setState(() {}))),
|
||||||
MobileCardField('进价(可留空/0)', null,
|
MobileCardField('进价·单瓶(可留空/0)', null,
|
||||||
valueWidget: GciField(
|
valueWidget: GciField(
|
||||||
controller: item.priceCtrl,
|
controller: item.priceCtrl,
|
||||||
num: true,
|
num: true,
|
||||||
money: true,
|
money: true,
|
||||||
hint: '留空=待定价',
|
hint: '留空=待定价',
|
||||||
onChanged: (_) => setState(() {}))),
|
onChanged: (_) => setState(() {}))),
|
||||||
MobileCardField('售价', null,
|
MobileCardField('参考售价', null,
|
||||||
valueWidget: GciField(
|
valueWidget: GciField(
|
||||||
controller: item.saleCtrl,
|
controller: item.saleCtrl,
|
||||||
num: true,
|
num: true,
|
||||||
@@ -927,7 +926,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
|||||||
hint: '0.00',
|
hint: '0.00',
|
||||||
onChanged: (_) => setState(() {}))),
|
onChanged: (_) => setState(() {}))),
|
||||||
MobileCardField(
|
MobileCardField(
|
||||||
'金额', item.pending ? '待定价' : '¥${item.amount.toStringAsFixed(2)}'),
|
'总进价', item.pending ? '待定价' : '¥${item.amount.toStringAsFixed(2)}'),
|
||||||
MobileCardField('', null,
|
MobileCardField('', null,
|
||||||
valueWidget: TextButton.icon(
|
valueWidget: TextButton.icon(
|
||||||
onPressed: () => setState(() => item.expanded = !item.expanded),
|
onPressed: () => setState(() => item.expanded = !item.expanded),
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
o.partnerName ?? '',
|
o.partnerName ?? '',
|
||||||
o.status,
|
o.status,
|
||||||
o.orderDate,
|
o.orderDate,
|
||||||
o.totalAmount,
|
o.costTotal,
|
||||||
])
|
])
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
@@ -712,8 +712,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
return Text(o.warehouseName ?? '-');
|
return Text(o.warehouseName ?? '-');
|
||||||
case 'amount':
|
case 'amount':
|
||||||
return Text(
|
return Text(
|
||||||
o.totalAmount != null
|
o.costTotal != null
|
||||||
? '¥${NumberFormat('#,##0').format(o.totalAmount)}'
|
? '¥${NumberFormat('#,##0').format(o.costTotal)}'
|
||||||
: '—',
|
: '—',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
@@ -828,11 +828,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
fields: [
|
fields: [
|
||||||
MobileCardField('供应商', o.partnerName ?? '-'),
|
MobileCardField('供应商', o.partnerName ?? '-'),
|
||||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||||
MobileCardField(
|
MobileCardField('合计金额',
|
||||||
'合计金额',
|
o.costTotal != null ? '¥${o.costTotal!.toStringAsFixed(2)}' : '-'),
|
||||||
o.totalAmount != null
|
|
||||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
|
||||||
: '-'),
|
|
||||||
MobileCardField('入库员', o.operatorName ?? '-'),
|
MobileCardField('入库员', o.operatorName ?? '-'),
|
||||||
MobileCardField('审核员', o.reviewerName ?? '-'),
|
MobileCardField('审核员', o.reviewerName ?? '-'),
|
||||||
],
|
],
|
||||||
@@ -899,13 +896,13 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
series: it.productSeries ?? '—',
|
series: it.productSeries ?? '—',
|
||||||
spec: it.productSpec ?? '',
|
spec: it.productSpec ?? '',
|
||||||
qty: _fmtQty(it.quantity),
|
qty: _fmtQty(it.quantity),
|
||||||
price: it.unitPrice == 0 ? '待定价' : _money.format(it.unitPrice),
|
price: it.costPrice == 0 ? '待定价' : _money.format(it.costPrice),
|
||||||
amount:
|
amount:
|
||||||
it.totalPrice == 0 ? '待定价' : _money.format(it.totalPrice),
|
it.costAmount == 0 ? '待定价' : _money.format(it.costAmount),
|
||||||
pending: it.unitPrice == 0,
|
pending: it.costPrice == 0,
|
||||||
))
|
))
|
||||||
.toList(),
|
.toList(),
|
||||||
totalText: o.totalAmount != null ? _money.format(o.totalAmount) : '—',
|
totalText: o.costTotal != null ? _money.format(o.costTotal) : '—',
|
||||||
actionGroups: _detailActionGroups(o),
|
actionGroups: _detailActionGroups(o),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -971,7 +968,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
}
|
}
|
||||||
if (audit.isNotEmpty) groups.add(DrawerActionGroup('单据审核', audit));
|
if (audit.isNotEmpty) groups.add(DrawerActionGroup('单据审核', audit));
|
||||||
|
|
||||||
final pending = o.items.where((it) => it.unitPrice == 0).length;
|
final pending = o.items.where((it) => it.costPrice == 0).length;
|
||||||
if (o.status == 'approved' && isAdmin && pending > 0) {
|
if (o.status == 'approved' && isAdmin && pending > 0) {
|
||||||
groups.add(DrawerActionGroup('成本 · $pending 项待定价', [
|
groups.add(DrawerActionGroup('成本 · $pending 项待定价', [
|
||||||
b('确认进价', () {
|
b('确认进价', () {
|
||||||
@@ -1025,7 +1022,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
/// 确认进价:为 0 价(待定)明细补填真实进价,调后端前向补偿后刷新列表。
|
/// 确认进价:为 0 价(待定)明细补填真实进价,调后端前向补偿后刷新列表。
|
||||||
Future<void> _confirmCost(StockInOrder o) async {
|
Future<void> _confirmCost(StockInOrder o) async {
|
||||||
final pending =
|
final pending =
|
||||||
o.items.where((it) => it.unitPrice == 0 && it.id != null).toList();
|
o.items.where((it) => it.costPrice == 0 && it.id != null).toList();
|
||||||
if (pending.isEmpty) return;
|
if (pending.isEmpty) return;
|
||||||
final controllers = {
|
final controllers = {
|
||||||
for (final it in pending) it.id!: TextEditingController()
|
for (final it in pending) it.id!: TextEditingController()
|
||||||
@@ -1303,8 +1300,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
series: it.productSeries ?? '',
|
series: it.productSeries ?? '',
|
||||||
spec: it.productSpec ?? '',
|
spec: it.productSpec ?? '',
|
||||||
quantity: it.quantity,
|
quantity: it.quantity,
|
||||||
unitPrice: it.unitPrice,
|
unitPrice: it.costPrice,
|
||||||
totalPrice: it.totalPrice,
|
totalPrice: it.costAmount,
|
||||||
alreadyReturned: it.isReturned,
|
alreadyReturned: it.isReturned,
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../core/auth/auth_state.dart';
|
|||||||
import '../../core/config/app_constants.dart';
|
import '../../core/config/app_constants.dart';
|
||||||
import '../../core/responsive/responsive.dart';
|
import '../../core/responsive/responsive.dart';
|
||||||
import '../../core/theme/app_dims.g.dart';
|
import '../../core/theme/app_dims.g.dart';
|
||||||
|
import '../../core/theme/app_fonts.dart';
|
||||||
import '../../core/theme/context_tokens.dart';
|
import '../../core/theme/context_tokens.dart';
|
||||||
import '../../core/utils/date_util.dart';
|
import '../../core/utils/date_util.dart';
|
||||||
import '../../models/inventory.dart';
|
import '../../models/inventory.dart';
|
||||||
@@ -34,7 +35,7 @@ class _PickerItem {
|
|||||||
final String series;
|
final String series;
|
||||||
final String spec;
|
final String spec;
|
||||||
final String unit;
|
final String unit;
|
||||||
final double? unitPrice;
|
final double? costPrice; // 进价(成本,仅管理员可见列)
|
||||||
final double availableQty;
|
final double availableQty;
|
||||||
const _PickerItem({
|
const _PickerItem({
|
||||||
required this.productId,
|
required this.productId,
|
||||||
@@ -43,7 +44,7 @@ class _PickerItem {
|
|||||||
required this.series,
|
required this.series,
|
||||||
required this.spec,
|
required this.spec,
|
||||||
required this.unit,
|
required this.unit,
|
||||||
this.unitPrice,
|
this.costPrice,
|
||||||
required this.availableQty,
|
required this.availableQty,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ List<_PickerItem> _aggregatePickerItems(List<Inventory> rows) {
|
|||||||
series: existing.series,
|
series: existing.series,
|
||||||
spec: existing.spec,
|
spec: existing.spec,
|
||||||
unit: existing.unit,
|
unit: existing.unit,
|
||||||
unitPrice: existing.unitPrice ?? inv.unitPrice,
|
costPrice: existing.costPrice ?? inv.unitPrice,
|
||||||
availableQty: existing.availableQty + inv.quantity,
|
availableQty: existing.availableQty + inv.quantity,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -73,7 +74,7 @@ List<_PickerItem> _aggregatePickerItems(List<Inventory> rows) {
|
|||||||
series: inv.series,
|
series: inv.series,
|
||||||
spec: inv.spec,
|
spec: inv.spec,
|
||||||
unit: inv.unit,
|
unit: inv.unit,
|
||||||
unitPrice: inv.unitPrice,
|
costPrice: inv.unitPrice,
|
||||||
availableQty: inv.quantity,
|
availableQty: inv.quantity,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -87,7 +88,7 @@ class _ItemRow {
|
|||||||
final String productName;
|
final String productName;
|
||||||
final String series;
|
final String series;
|
||||||
final String spec;
|
final String spec;
|
||||||
final double? unitPrice;
|
final double? costPrice; // 进价(成本)
|
||||||
final double? availableQty;
|
final double? availableQty;
|
||||||
final TextEditingController qtyCtrl;
|
final TextEditingController qtyCtrl;
|
||||||
final TextEditingController salePriceCtrl;
|
final TextEditingController salePriceCtrl;
|
||||||
@@ -100,7 +101,7 @@ class _ItemRow {
|
|||||||
this.productName = '',
|
this.productName = '',
|
||||||
this.series = '',
|
this.series = '',
|
||||||
this.spec = '',
|
this.spec = '',
|
||||||
this.unitPrice,
|
this.costPrice,
|
||||||
this.availableQty,
|
this.availableQty,
|
||||||
double? salePrice,
|
double? salePrice,
|
||||||
}) : qtyCtrl = TextEditingController(text: '1'),
|
}) : qtyCtrl = TextEditingController(text: '1'),
|
||||||
@@ -108,7 +109,7 @@ class _ItemRow {
|
|||||||
salePriceCtrl = TextEditingController(
|
salePriceCtrl = TextEditingController(
|
||||||
text: ((salePrice != null && salePrice > 0)
|
text: ((salePrice != null && salePrice > 0)
|
||||||
? salePrice
|
? salePrice
|
||||||
: (unitPrice ?? 0))
|
: (costPrice ?? 0))
|
||||||
.toStringAsFixed(2));
|
.toStringAsFixed(2));
|
||||||
|
|
||||||
FocusNode focusNode(String field) => field == 'sale' ? saleFocus : qtyFocus;
|
FocusNode focusNode(String field) => field == 'sale' ? saleFocus : qtyFocus;
|
||||||
@@ -118,6 +119,13 @@ class _ItemRow {
|
|||||||
(double.tryParse(qtyCtrl.text) ?? 0) *
|
(double.tryParse(qtyCtrl.text) ?? 0) *
|
||||||
(double.tryParse(salePriceCtrl.text) ?? 0);
|
(double.tryParse(salePriceCtrl.text) ?? 0);
|
||||||
|
|
||||||
|
/// 行利润 =(售价 − 进价)× 数量;售价未填(≤0)视为待定不计
|
||||||
|
double get profit {
|
||||||
|
final sale = double.tryParse(salePriceCtrl.text) ?? 0;
|
||||||
|
if (sale <= 0) return 0;
|
||||||
|
return (sale - (costPrice ?? 0)) * (double.tryParse(qtyCtrl.text) ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
qtyCtrl.dispose();
|
qtyCtrl.dispose();
|
||||||
salePriceCtrl.dispose();
|
salePriceCtrl.dispose();
|
||||||
@@ -183,7 +191,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
productName: item.productName ?? '',
|
productName: item.productName ?? '',
|
||||||
series: item.productSeries ?? '',
|
series: item.productSeries ?? '',
|
||||||
spec: item.productSpec ?? '',
|
spec: item.productSpec ?? '',
|
||||||
unitPrice: item.unitPrice,
|
costPrice: item.costPrice,
|
||||||
salePrice: item.salePrice,
|
salePrice: item.salePrice,
|
||||||
);
|
);
|
||||||
row.qtyCtrl.text = item.quantity.toStringAsFixed(0);
|
row.qtyCtrl.text = item.quantity.toStringAsFixed(0);
|
||||||
@@ -217,6 +225,14 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double get _totalProfit {
|
||||||
|
double total = 0;
|
||||||
|
for (final item in _items) {
|
||||||
|
total += item.profit;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _loadInventory(int warehouseId) async {
|
Future<void> _loadInventory(int warehouseId) async {
|
||||||
try {
|
try {
|
||||||
final result = await ref.read(inventoryRepositoryProvider).listInventory(
|
final result = await ref.read(inventoryRepositoryProvider).listInventory(
|
||||||
@@ -254,7 +270,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
productName: item.productName,
|
productName: item.productName,
|
||||||
series: item.series,
|
series: item.series,
|
||||||
spec: item.spec,
|
spec: item.spec,
|
||||||
unitPrice: item.unitPrice,
|
costPrice: item.costPrice,
|
||||||
availableQty: item.availableQty,
|
availableQty: item.availableQty,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -276,7 +292,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
productName: s.productName,
|
productName: s.productName,
|
||||||
series: s.series,
|
series: s.series,
|
||||||
spec: s.spec,
|
spec: s.spec,
|
||||||
unitPrice: s.unitPrice,
|
costPrice: s.costPrice,
|
||||||
availableQty: s.availableQty,
|
availableQty: s.availableQty,
|
||||||
);
|
);
|
||||||
r.salePriceCtrl.text = s.salePriceCtrl.text;
|
r.salePriceCtrl.text = s.salePriceCtrl.text;
|
||||||
@@ -355,14 +371,14 @@ 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.costPrice ?? 0;
|
||||||
final sale = double.tryParse(item.salePriceCtrl.text) ?? 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,
|
'cost_price': price,
|
||||||
'sale_price': sale,
|
'sale_price': sale,
|
||||||
'total_price': qty * price,
|
|
||||||
};
|
};
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@@ -516,6 +532,9 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
mobileActions: mobileActions,
|
mobileActions: mobileActions,
|
||||||
rowsLabel: '明细 ${_items.length} 行',
|
rowsLabel: '明细 ${_items.length} 行',
|
||||||
totalText: '¥${_totalAmount.toStringAsFixed(2)}',
|
totalText: '¥${_totalAmount.toStringAsFixed(2)}',
|
||||||
|
profitText: ref.watch(isAdminProvider)
|
||||||
|
? '¥${_totalProfit.toStringAsFixed(2)}'
|
||||||
|
: null,
|
||||||
docHead: _buildDocHead(currentUser?.realName ?? '-'),
|
docHead: _buildDocHead(currentUser?.realName ?? '-'),
|
||||||
detailHead: DetailHead(actions: [
|
detailHead: DetailHead(actions: [
|
||||||
DsButton('从库存选择',
|
DsButton('从库存选择',
|
||||||
@@ -628,18 +647,23 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 桌面:内联网格 ─────────────────────────────────────────────────────────
|
// ── 桌面:内联网格 ─────────────────────────────────────────────────────────
|
||||||
static const _columns = [
|
// 进价/利润列仅管理员可见(2026-07 定价重设计;operator 由服务端抹零+前端隐藏双保险);
|
||||||
GridCol('idx', '#', width: 38),
|
// 原「金额」列(=售价×数量,qty=1 时与售价重复)改为「利润」实时列。
|
||||||
GridCol('name', '商品名称'),
|
List<GridCol> get _columns {
|
||||||
GridCol('series', '系列', width: 116),
|
final admin = ref.read(isAdminProvider);
|
||||||
GridCol('spec', '规格', width: 128),
|
return [
|
||||||
GridCol('avail', '可用', width: 92, num: true),
|
const GridCol('idx', '#', width: 38),
|
||||||
GridCol('qty', '数量', width: 82, num: true, req: true),
|
const GridCol('name', '商品名称'),
|
||||||
GridCol('price', '进价', width: 100, num: true),
|
const GridCol('series', '系列', width: 116),
|
||||||
GridCol('sale', '售价', width: 108, num: true, req: true),
|
const GridCol('spec', '规格', width: 128),
|
||||||
GridCol('amount', '金额', width: 110, num: true),
|
const GridCol('avail', '可用', width: 92, num: true),
|
||||||
GridCol('act', '', width: 62),
|
const GridCol('qty', '数量', width: 82, num: true, req: true),
|
||||||
];
|
if (admin) const GridCol('price', '进价', width: 100, num: true),
|
||||||
|
const GridCol('sale', '售价', width: 108, num: true, req: true),
|
||||||
|
if (admin) const GridCol('profit', '利润', width: 110, num: true),
|
||||||
|
const GridCol('act', '', width: 62),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildGrid() {
|
Widget _buildGrid() {
|
||||||
if (_items.isEmpty) {
|
if (_items.isEmpty) {
|
||||||
@@ -687,7 +711,21 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||||
);
|
);
|
||||||
case 'name':
|
case 'name':
|
||||||
return RoCell(item.productName, color: t.heading);
|
// 名称 + 编码两行;编码字体样式与系列列一致(RoCell 默认 muted)
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(item.productName,
|
||||||
|
style: TextStyle(fontSize: AppDims.fsBody, color: t.heading)),
|
||||||
|
if (item.productCode.isNotEmpty)
|
||||||
|
Text(item.productCode,
|
||||||
|
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
case 'series':
|
case 'series':
|
||||||
return RoCell(item.series);
|
return RoCell(item.series);
|
||||||
case 'spec':
|
case 'spec':
|
||||||
@@ -711,8 +749,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
);
|
);
|
||||||
case 'price':
|
case 'price':
|
||||||
return RoCell(
|
return RoCell(
|
||||||
(item.unitPrice ?? 0) > 0
|
(item.costPrice ?? 0) > 0
|
||||||
? '¥${item.unitPrice!.toStringAsFixed(2)}'
|
? '¥${item.costPrice!.toStringAsFixed(2)}'
|
||||||
: '—',
|
: '—',
|
||||||
num: true,
|
num: true,
|
||||||
);
|
);
|
||||||
@@ -727,8 +765,10 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
onEnter: () => _advance(i, 'sale'),
|
onEnter: () => _advance(i, 'sale'),
|
||||||
onTab: ({required backward}) => _onTab(i, 'sale', backward: backward),
|
onTab: ({required backward}) => _onTab(i, 'sale', backward: backward),
|
||||||
);
|
);
|
||||||
case 'amount':
|
case 'profit':
|
||||||
return AmountCell(amount: item.amount);
|
return _ProfitCell(
|
||||||
|
profit: item.profit,
|
||||||
|
pending: (double.tryParse(item.salePriceCtrl.text) ?? 0) <= 0);
|
||||||
case 'act':
|
case 'act':
|
||||||
return RowActions(
|
return RowActions(
|
||||||
onCopy: () => _copyRow(i),
|
onCopy: () => _copyRow(i),
|
||||||
@@ -772,11 +812,12 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
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('可用', avail != null ? avail.toStringAsFixed(0) : '—'),
|
MobileCardField('可用', avail != null ? avail.toStringAsFixed(0) : '—'),
|
||||||
MobileCardField(
|
if (ref.read(isAdminProvider))
|
||||||
'进价',
|
MobileCardField(
|
||||||
(item.unitPrice ?? 0) > 0
|
'进价',
|
||||||
? '¥${item.unitPrice!.toStringAsFixed(2)}'
|
(item.costPrice ?? 0) > 0
|
||||||
: '—'),
|
? '¥${item.costPrice!.toStringAsFixed(2)}'
|
||||||
|
: '—'),
|
||||||
MobileCardField('数量', null,
|
MobileCardField('数量', null,
|
||||||
valueWidget: GciField(
|
valueWidget: GciField(
|
||||||
controller: item.qtyCtrl,
|
controller: item.qtyCtrl,
|
||||||
@@ -790,12 +831,36 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
|||||||
money: true,
|
money: true,
|
||||||
hint: '0.00',
|
hint: '0.00',
|
||||||
onChanged: (_) => setState(() {}))),
|
onChanged: (_) => setState(() {}))),
|
||||||
MobileCardField('金额', '¥${item.amount.toStringAsFixed(2)}'),
|
if (ref.read(isAdminProvider))
|
||||||
|
MobileCardField('利润', '¥${item.profit.toStringAsFixed(2)}'),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 利润单元格:正=success 负=danger 等宽粗体;售价未填显示 —
|
||||||
|
class _ProfitCell extends StatelessWidget {
|
||||||
|
final double profit;
|
||||||
|
final bool pending;
|
||||||
|
const _ProfitCell({required this.profit, required this.pending});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final t = context.tokens;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
child: Text(pending ? '—' : '¥${profit.toStringAsFixed(2)}',
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsBody,
|
||||||
|
color: pending ? t.faint : (profit < 0 ? t.danger : t.success),
|
||||||
|
fontFamily: AppFonts.mono,
|
||||||
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
|
fontWeight: FontWeight.w600)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── 意图(键盘快捷键)────────────────────────────────────────────────────────
|
// ── 意图(键盘快捷键)────────────────────────────────────────────────────────
|
||||||
class _SubmitIntent extends Intent {
|
class _SubmitIntent extends Intent {
|
||||||
const _SubmitIntent();
|
const _SubmitIntent();
|
||||||
@@ -1012,8 +1077,8 @@ class _InventoryPickerDialogState
|
|||||||
_dataCell(item.spec, 130,
|
_dataCell(item.spec, 130,
|
||||||
color: context.tokens.muted),
|
color: context.tokens.muted),
|
||||||
_dataCell(
|
_dataCell(
|
||||||
item.unitPrice != null
|
item.costPrice != null
|
||||||
? '¥${item.unitPrice!.toStringAsFixed(2)}'
|
? '¥${item.costPrice!.toStringAsFixed(2)}'
|
||||||
: '-',
|
: '-',
|
||||||
90,
|
90,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -364,8 +364,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
Text(o.partnerName ?? '-'),
|
Text(o.partnerName ?? '-'),
|
||||||
Text(o.warehouseName ?? '-'),
|
Text(o.warehouseName ?? '-'),
|
||||||
Text(
|
Text(
|
||||||
o.totalAmount != null
|
o.saleTotal != null
|
||||||
? '¥${NumberFormat('#,##0').format(o.totalAmount)}'
|
? '¥${NumberFormat('#,##0').format(o.saleTotal)}'
|
||||||
: '—',
|
: '—',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
@@ -725,7 +725,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
o.partnerName ?? '',
|
o.partnerName ?? '',
|
||||||
o.status,
|
o.status,
|
||||||
o.orderDate,
|
o.orderDate,
|
||||||
o.totalAmount,
|
o.saleTotal,
|
||||||
])
|
])
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
@@ -813,11 +813,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
fields: [
|
fields: [
|
||||||
MobileCardField('客户', o.partnerName ?? '-'),
|
MobileCardField('客户', o.partnerName ?? '-'),
|
||||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||||
MobileCardField(
|
MobileCardField('金额',
|
||||||
'金额',
|
o.saleTotal != null ? '¥${o.saleTotal!.toStringAsFixed(2)}' : '-'),
|
||||||
o.totalAmount != null
|
|
||||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
|
||||||
: '-'),
|
|
||||||
MobileCardField('出库时间', o.orderDate?.substring(0, 10) ?? '-'),
|
MobileCardField('出库时间', o.orderDate?.substring(0, 10) ?? '-'),
|
||||||
],
|
],
|
||||||
actions: _orderActions(context, o),
|
actions: _orderActions(context, o),
|
||||||
@@ -876,20 +873,30 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
linesLabel: '出库明细',
|
linesLabel: '出库明细',
|
||||||
lines: o.items
|
// 管理员:成本价/售价/利润 三列 + 合计利润;operator:售价/小计
|
||||||
.map((it) => OrderLine(
|
//(成本与利润由服务端按角色抹零,前端同时不渲染对应列——双保险)
|
||||||
name: it.productName ?? '—',
|
lines: o.items.map((it) {
|
||||||
code: it.productCode ?? '—',
|
final pending = it.salePrice <= 0;
|
||||||
series: it.productSeries ?? '—',
|
final admin = ref.read(isAdminProvider);
|
||||||
spec: it.productSpec ?? '',
|
final profit = (it.salePrice - it.costPrice) * it.quantity;
|
||||||
qty: _fmtQty(it.quantity),
|
return OrderLine(
|
||||||
price: it.salePrice <= 0 ? '待定价' : _money.format(it.salePrice),
|
name: it.productName ?? '—',
|
||||||
amount:
|
code: it.productCode ?? '—',
|
||||||
it.totalPrice == 0 ? '待定价' : _money.format(it.totalPrice),
|
series: it.productSeries ?? '—',
|
||||||
pending: it.salePrice <= 0,
|
spec: it.productSpec ?? '',
|
||||||
))
|
qty: _fmtQty(it.quantity),
|
||||||
.toList(),
|
price: pending ? '待定价' : _money.format(it.salePrice),
|
||||||
totalText: o.totalAmount != null ? _money.format(o.totalAmount) : '—',
|
amount: pending ? '待定价' : _money.format(it.saleAmount),
|
||||||
|
cost: admin ? _money.format(it.costPrice) : null,
|
||||||
|
profit: admin ? (pending ? '待定价' : _money.format(profit)) : null,
|
||||||
|
pending: pending,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
totalText: o.saleTotal != null ? _money.format(o.saleTotal) : '—',
|
||||||
|
profitText:
|
||||||
|
ref.read(isAdminProvider) ? _money.format(o.profitTotal) : null,
|
||||||
|
priceLabel: '售价',
|
||||||
|
amountLabel: '小计',
|
||||||
actionGroups: _detailActionGroups(o),
|
actionGroups: _detailActionGroups(o),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1298,8 +1305,9 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
series: it.productSeries ?? '',
|
series: it.productSeries ?? '',
|
||||||
spec: it.productSpec ?? '',
|
spec: it.productSpec ?? '',
|
||||||
quantity: it.quantity,
|
quantity: it.quantity,
|
||||||
unitPrice: it.unitPrice,
|
// 退单冲应收是售价口径:展示与之一致(待定价单回退成本)
|
||||||
totalPrice: it.totalPrice,
|
unitPrice: it.salePrice > 0 ? it.salePrice : it.costPrice,
|
||||||
|
totalPrice: it.salePrice > 0 ? it.saleAmount : it.costAmount,
|
||||||
alreadyReturned: it.isReturned,
|
alreadyReturned: it.isReturned,
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ class OrderLine {
|
|||||||
final String qty;
|
final String qty;
|
||||||
final String price; // 已算好文案(或「待定价」)
|
final String price; // 已算好文案(或「待定价」)
|
||||||
final String amount;
|
final String amount;
|
||||||
|
// 出库管理员 6 列形态(2026-07 定价重设计):成本价 + 利润;
|
||||||
|
// 两者非空时明细表渲染 数量/成本价/售价(price)/利润 四数值列,amount 不用。
|
||||||
|
final String? cost;
|
||||||
|
final String? profit;
|
||||||
final bool pending; // 单价<=0:单价/金额显示待定价样式
|
final bool pending; // 单价<=0:单价/金额显示待定价样式
|
||||||
const OrderLine({
|
const OrderLine({
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -63,7 +67,9 @@ class OrderLine {
|
|||||||
required this.spec,
|
required this.spec,
|
||||||
required this.qty,
|
required this.qty,
|
||||||
required this.price,
|
required this.price,
|
||||||
required this.amount,
|
this.amount = '',
|
||||||
|
this.cost,
|
||||||
|
this.profit,
|
||||||
this.pending = false,
|
this.pending = false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -83,6 +89,9 @@ class OrderDetailDrawer extends StatelessWidget {
|
|||||||
final String linesLabel; // 入库明细 / 出库明细
|
final String linesLabel; // 入库明细 / 出库明细
|
||||||
final List<OrderLine> lines;
|
final List<OrderLine> lines;
|
||||||
final String totalText; // 合计金额(已格式化)
|
final String totalText; // 合计金额(已格式化)
|
||||||
|
final String? profitText; // 合计利润(仅出库·管理员传入;null 不渲染)
|
||||||
|
final String priceLabel; // 单价列头(入库=进价(单瓶)/出库=售价)
|
||||||
|
final String amountLabel; // 金额列头(入库=总进价/出库 operator=小计)
|
||||||
final List<DrawerActionGroup> actionGroups;
|
final List<DrawerActionGroup> actionGroups;
|
||||||
|
|
||||||
const OrderDetailDrawer({
|
const OrderDetailDrawer({
|
||||||
@@ -92,6 +101,9 @@ class OrderDetailDrawer extends StatelessWidget {
|
|||||||
required this.linesLabel,
|
required this.linesLabel,
|
||||||
required this.lines,
|
required this.lines,
|
||||||
required this.totalText,
|
required this.totalText,
|
||||||
|
this.profitText,
|
||||||
|
this.priceLabel = '单价',
|
||||||
|
this.amountLabel = '金额',
|
||||||
required this.actionGroups,
|
required this.actionGroups,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -138,7 +150,10 @@ class OrderDetailDrawer extends StatelessWidget {
|
|||||||
child: Text(linesLabel,
|
child: Text(linesLabel,
|
||||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||||
),
|
),
|
||||||
_LinesTable(lines: lines),
|
_LinesTable(
|
||||||
|
lines: lines,
|
||||||
|
priceLabel: priceLabel,
|
||||||
|
amountLabel: amountLabel),
|
||||||
// ── 合计(.ltotal)──
|
// ── 合计(.ltotal)──
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(4, 8, 4, 0),
|
padding: const EdgeInsets.fromLTRB(4, 8, 4, 0),
|
||||||
@@ -159,6 +174,28 @@ class OrderDetailDrawer extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (profitText != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(4, 6, 4, 0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('合计利润 ',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsBody, color: t.muted)),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(profitText!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsTitle,
|
||||||
|
fontFamily: AppFonts.mono,
|
||||||
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: profitText!.startsWith('-')
|
||||||
|
? t.danger
|
||||||
|
: t.success)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
// ── 操作组(.dactions)──
|
// ── 操作组(.dactions)──
|
||||||
if (actionGroups.isNotEmpty) const SizedBox(height: 18),
|
if (actionGroups.isNotEmpty) const SizedBox(height: 18),
|
||||||
for (var i = 0; i < actionGroups.length; i++) ...[
|
for (var i = 0; i < actionGroups.length; i++) ...[
|
||||||
@@ -207,7 +244,15 @@ class _DrawerRow extends StatelessWidget {
|
|||||||
|
|
||||||
class _LinesTable extends StatelessWidget {
|
class _LinesTable extends StatelessWidget {
|
||||||
final List<OrderLine> lines;
|
final List<OrderLine> lines;
|
||||||
const _LinesTable({required this.lines});
|
final String priceLabel;
|
||||||
|
final String amountLabel;
|
||||||
|
const _LinesTable(
|
||||||
|
{required this.lines,
|
||||||
|
required this.priceLabel,
|
||||||
|
required this.amountLabel});
|
||||||
|
|
||||||
|
// 任一行带成本/利润 → 出库管理员 6 列形态
|
||||||
|
bool get _withProfit => lines.any((l) => l.cost != null || l.profit != null);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -233,8 +278,17 @@ class _LinesTable extends StatelessWidget {
|
|||||||
style: _hStyle(t), overflow: TextOverflow.ellipsis),
|
style: _hStyle(t), overflow: TextOverflow.ellipsis),
|
||||||
c2: Text('系列 / 规格', style: _hStyle(t)),
|
c2: Text('系列 / 规格', style: _hStyle(t)),
|
||||||
c3: Text('数量', textAlign: TextAlign.right, style: _hStyle(t)),
|
c3: Text('数量', textAlign: TextAlign.right, style: _hStyle(t)),
|
||||||
c4: Text('单价', textAlign: TextAlign.right, style: _hStyle(t)),
|
c4: _withProfit
|
||||||
c5: Text('金额', textAlign: TextAlign.right, style: _hStyle(t)),
|
? Text('成本价', textAlign: TextAlign.right, style: _hStyle(t))
|
||||||
|
: Text(priceLabel,
|
||||||
|
textAlign: TextAlign.right, style: _hStyle(t)),
|
||||||
|
c5: _withProfit
|
||||||
|
? Text('售价', textAlign: TextAlign.right, style: _hStyle(t))
|
||||||
|
: Text(amountLabel,
|
||||||
|
textAlign: TextAlign.right, style: _hStyle(t)),
|
||||||
|
c6: _withProfit
|
||||||
|
? Text('利润', textAlign: TextAlign.right, style: _hStyle(t))
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
for (var i = 0; i < lines.length; i++)
|
for (var i = 0; i < lines.length; i++)
|
||||||
@@ -281,9 +335,18 @@ class _LinesTable extends StatelessWidget {
|
|||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontFamily: AppFonts.mono,
|
fontFamily: AppFonts.mono,
|
||||||
fontFamilyFallback: AppFonts.monoFallback)),
|
fontFamilyFallback: AppFonts.monoFallback)),
|
||||||
c4: _priceCell(context, lines[i].price, lines[i].pending),
|
c4: _withProfit
|
||||||
c5: _priceCell(context, lines[i].amount, lines[i].pending,
|
? _priceCell(
|
||||||
accent: true),
|
context, lines[i].cost ?? '—', lines[i].pending)
|
||||||
|
: _priceCell(context, lines[i].price, lines[i].pending),
|
||||||
|
c5: _withProfit
|
||||||
|
? _priceCell(context, lines[i].price, lines[i].pending)
|
||||||
|
: _priceCell(context, lines[i].amount, lines[i].pending,
|
||||||
|
accent: true),
|
||||||
|
c6: _withProfit
|
||||||
|
? _profitCell(
|
||||||
|
context, lines[i].profit ?? '—', lines[i].pending)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -314,28 +377,56 @@ class _LinesTable extends StatelessWidget {
|
|||||||
color: accent ? t.text : t.text));
|
color: accent ? t.text : t.text));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5 列布局(对齐原型 grid 1fr 124 40 64 78)。
|
// 5 列布局(对齐原型 grid 1fr 124 40 64 78);c6 非空时为出库 6 列形态
|
||||||
|
// (数量/成本价/售价/利润,数值列等宽收窄)。
|
||||||
Widget _lineRow(BuildContext context,
|
Widget _lineRow(BuildContext context,
|
||||||
{required Widget c1,
|
{required Widget c1,
|
||||||
required Widget c2,
|
required Widget c2,
|
||||||
required Widget c3,
|
required Widget c3,
|
||||||
required Widget c4,
|
required Widget c4,
|
||||||
required Widget c5}) {
|
required Widget c5,
|
||||||
|
Widget? c6}) {
|
||||||
|
final six = c6 != null;
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: c1),
|
Expanded(child: c1),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(width: 96, child: c2),
|
SizedBox(width: six ? 84 : 96, child: c2),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(width: 34, child: c3),
|
SizedBox(width: 34, child: c3),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(width: 60, child: c4),
|
SizedBox(width: six ? 56 : 60, child: c4),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
SizedBox(width: 72, child: c5),
|
SizedBox(width: six ? 56 : 72, child: c5),
|
||||||
|
if (six) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(width: 56, child: c6),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 利润列:负数染 danger,正常 success;待定价沿 warn 样式
|
||||||
|
Widget _profitCell(BuildContext context, String text, bool pending) {
|
||||||
|
final t = context.tokens;
|
||||||
|
if (pending) {
|
||||||
|
return Text(text,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsXs,
|
||||||
|
color: t.warn,
|
||||||
|
fontWeight: FontWeight.w600));
|
||||||
|
}
|
||||||
|
return Text(text,
|
||||||
|
textAlign: TextAlign.right,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: AppDims.fsBody,
|
||||||
|
fontFamily: AppFonts.mono,
|
||||||
|
fontFamilyFallback: AppFonts.monoFallback,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: text.startsWith('-') ? t.danger : t.success));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ActionGroup extends StatelessWidget {
|
class _ActionGroup extends StatelessWidget {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 129 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 130 KiB |
@@ -26,7 +26,7 @@ const _orders = [
|
|||||||
reviewerName: '张管理',
|
reviewerName: '张管理',
|
||||||
status: 'approved',
|
status: 'approved',
|
||||||
orderDate: '2026-06-20',
|
orderDate: '2026-06-20',
|
||||||
totalAmount: 34320),
|
costTotal: 34320),
|
||||||
StockInOrder(
|
StockInOrder(
|
||||||
id: 2,
|
id: 2,
|
||||||
orderNo: 'RK-20260618-009',
|
orderNo: 'RK-20260618-009',
|
||||||
@@ -36,7 +36,7 @@ const _orders = [
|
|||||||
operatorName: '李销售',
|
operatorName: '李销售',
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
orderDate: '2026-06-18',
|
orderDate: '2026-06-18',
|
||||||
totalAmount: 6700),
|
costTotal: 6700),
|
||||||
StockInOrder(
|
StockInOrder(
|
||||||
id: 3,
|
id: 3,
|
||||||
orderNo: 'RK-20260615-003',
|
orderNo: 'RK-20260615-003',
|
||||||
@@ -46,7 +46,7 @@ const _orders = [
|
|||||||
operatorName: '王经理',
|
operatorName: '王经理',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
orderDate: '2026-06-15',
|
orderDate: '2026-06-15',
|
||||||
totalAmount: 5000),
|
costTotal: 5000),
|
||||||
StockInOrder(
|
StockInOrder(
|
||||||
id: 4,
|
id: 4,
|
||||||
orderNo: 'RK-20260612-001',
|
orderNo: 'RK-20260612-001',
|
||||||
@@ -57,7 +57,7 @@ const _orders = [
|
|||||||
reviewerName: '张管理',
|
reviewerName: '张管理',
|
||||||
status: 'rejected',
|
status: 'rejected',
|
||||||
orderDate: '2026-06-12',
|
orderDate: '2026-06-12',
|
||||||
totalAmount: 3500),
|
costTotal: 3500),
|
||||||
];
|
];
|
||||||
|
|
||||||
class _FakeStockInNotifier extends StockInListNotifier {
|
class _FakeStockInNotifier extends StockInListNotifier {
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const _orders = [
|
|||||||
reviewerName: '张管理',
|
reviewerName: '张管理',
|
||||||
status: 'approved',
|
status: 'approved',
|
||||||
orderDate: '2026-06-20',
|
orderDate: '2026-06-20',
|
||||||
totalAmount: 12800),
|
saleTotal: 12800),
|
||||||
StockOutOrder(
|
StockOutOrder(
|
||||||
id: 2,
|
id: 2,
|
||||||
orderNo: 'CK-20260618-022',
|
orderNo: 'CK-20260618-022',
|
||||||
@@ -36,7 +36,7 @@ const _orders = [
|
|||||||
operatorName: '张前台',
|
operatorName: '张前台',
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
orderDate: '2026-06-18',
|
orderDate: '2026-06-18',
|
||||||
totalAmount: 5400),
|
saleTotal: 5400),
|
||||||
StockOutOrder(
|
StockOutOrder(
|
||||||
id: 3,
|
id: 3,
|
||||||
orderNo: 'CK-20260615-017',
|
orderNo: 'CK-20260615-017',
|
||||||
@@ -46,7 +46,7 @@ const _orders = [
|
|||||||
operatorName: '李销售',
|
operatorName: '李销售',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
orderDate: '2026-06-15',
|
orderDate: '2026-06-15',
|
||||||
totalAmount: 3200),
|
saleTotal: 3200),
|
||||||
StockOutOrder(
|
StockOutOrder(
|
||||||
id: 4,
|
id: 4,
|
||||||
orderNo: 'CK-20260612-009',
|
orderNo: 'CK-20260612-009',
|
||||||
@@ -57,7 +57,7 @@ const _orders = [
|
|||||||
reviewerName: '张管理',
|
reviewerName: '张管理',
|
||||||
status: 'rejected',
|
status: 'rejected',
|
||||||
orderDate: '2026-06-12',
|
orderDate: '2026-06-12',
|
||||||
totalAmount: 1960),
|
saleTotal: 1960),
|
||||||
];
|
];
|
||||||
|
|
||||||
class _FakeStockOutNotifier extends StockOutListNotifier {
|
class _FakeStockOutNotifier extends StockOutListNotifier {
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:jiu_client/core/theme/themes.dart';
|
||||||
|
import 'package:jiu_client/widgets/order_detail_drawer.dart';
|
||||||
|
|
||||||
|
/// 2026-07 定价重设计:出库详情按角色两种形态——
|
||||||
|
/// 管理员(行带 cost/profit)= 成本价/售价/利润 三数值列 + 合计利润;
|
||||||
|
/// operator(不带)= 售价/小计两列,无成本无利润。
|
||||||
|
void main() {
|
||||||
|
Widget host(OrderDetailDrawer drawer) => MaterialApp(
|
||||||
|
theme: buildTheme('a'),
|
||||||
|
home: Scaffold(body: drawer),
|
||||||
|
);
|
||||||
|
|
||||||
|
const line6 = OrderLine(
|
||||||
|
name: '汾酒青花20年',
|
||||||
|
code: 'ZXZ027232',
|
||||||
|
series: '普通/53度',
|
||||||
|
spec: '500ml*6/件',
|
||||||
|
qty: '2',
|
||||||
|
price: '¥400.00',
|
||||||
|
cost: '¥355.00',
|
||||||
|
profit: '¥90.00',
|
||||||
|
);
|
||||||
|
|
||||||
|
const line4 = OrderLine(
|
||||||
|
name: '汾酒青花20年',
|
||||||
|
code: 'ZXZ027232',
|
||||||
|
series: '普通/53度',
|
||||||
|
spec: '500ml*6/件',
|
||||||
|
qty: '2',
|
||||||
|
price: '¥400.00',
|
||||||
|
amount: '¥800.00',
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('管理员形态:成本价/售价/利润列 + 合计利润', (tester) async {
|
||||||
|
await tester.pumpWidget(host(OrderDetailDrawer(
|
||||||
|
title: 'CK001',
|
||||||
|
infoRows: const [],
|
||||||
|
linesLabel: '出库明细',
|
||||||
|
lines: const [line6],
|
||||||
|
totalText: '¥800.00',
|
||||||
|
profitText: '¥90.00',
|
||||||
|
priceLabel: '售价',
|
||||||
|
amountLabel: '小计',
|
||||||
|
actionGroups: const [],
|
||||||
|
)));
|
||||||
|
expect(find.text('成本价'), findsOneWidget);
|
||||||
|
expect(find.text('售价'), findsOneWidget);
|
||||||
|
expect(find.text('利润'), findsOneWidget);
|
||||||
|
expect(find.text('¥355.00'), findsOneWidget);
|
||||||
|
expect(find.text('¥90.00'), findsNWidgets(2)); // 行利润 + 合计利润
|
||||||
|
expect(find.text('合计利润 '), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('operator 形态:售价/小计,无成本无利润', (tester) async {
|
||||||
|
await tester.pumpWidget(host(OrderDetailDrawer(
|
||||||
|
title: 'CK001',
|
||||||
|
infoRows: const [],
|
||||||
|
linesLabel: '出库明细',
|
||||||
|
lines: const [line4],
|
||||||
|
totalText: '¥800.00',
|
||||||
|
priceLabel: '售价',
|
||||||
|
amountLabel: '小计',
|
||||||
|
actionGroups: const [],
|
||||||
|
)));
|
||||||
|
expect(find.text('售价'), findsOneWidget);
|
||||||
|
expect(find.text('小计'), findsOneWidget);
|
||||||
|
expect(find.text('成本价'), findsNothing);
|
||||||
|
expect(find.text('利润'), findsNothing);
|
||||||
|
expect(find.text('合计利润 '), findsNothing);
|
||||||
|
expect(find.text('¥800.00'), findsNWidgets(2)); // 行小计 + 合计金额
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -136,7 +136,7 @@ StockInOrder _makeOrder({
|
|||||||
warehouseName: warehouseName,
|
warehouseName: warehouseName,
|
||||||
partnerName: partnerName,
|
partnerName: partnerName,
|
||||||
status: status,
|
status: status,
|
||||||
totalAmount: 1000.0,
|
costTotal: 1000.0,
|
||||||
orderDate: '2024-01-10',
|
orderDate: '2024-01-10',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||