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
This commit is contained in:
@@ -32,6 +32,7 @@ class OrderFormShell extends StatelessWidget {
|
||||
final Widget detail;
|
||||
final String rowsLabel; // 明细 N 行
|
||||
final String totalText; // 已格式化 ¥
|
||||
final String? profitText; // 合计利润(出库·仅管理员;null 不渲染)
|
||||
final bool loading;
|
||||
|
||||
const OrderFormShell({
|
||||
@@ -45,6 +46,7 @@ class OrderFormShell extends StatelessWidget {
|
||||
required this.detail,
|
||||
required this.rowsLabel,
|
||||
required this.totalText,
|
||||
this.profitText,
|
||||
this.statusBadge,
|
||||
this.notice,
|
||||
this.loading = false,
|
||||
@@ -123,6 +125,20 @@ class OrderFormShell extends StatelessWidget {
|
||||
Text(rowsLabel,
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
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('合计金额 ',
|
||||
style: TextStyle(fontSize: AppDims.fsBody, color: t.muted)),
|
||||
Text(totalText,
|
||||
|
||||
@@ -153,7 +153,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
final row = _ItemRow();
|
||||
row.productId = item.productId;
|
||||
row.qtyCtrl.text = item.quantity.toStringAsFixed(0);
|
||||
row.priceCtrl.text = item.unitPrice.toStringAsFixed(2);
|
||||
row.priceCtrl.text = item.costPrice.toStringAsFixed(2);
|
||||
row.selectedNameId =
|
||||
nameOpts.where((o) => o.name == item.productName).firstOrNull?.id;
|
||||
row.selectedSeriesId = seriesOpts
|
||||
@@ -375,9 +375,8 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
'series': optName(seriesOpts, it.selectedSeriesId),
|
||||
'spec': optName(specOpts, it.selectedSpecId),
|
||||
'quantity': qty,
|
||||
'unit_price': price,
|
||||
'cost_price': price,
|
||||
if (sale > 0) 'sale_price': sale,
|
||||
'total_price': qty * price,
|
||||
if (batchNo.isNotEmpty) 'batch_no': batchNo,
|
||||
if (productionDate.isNotEmpty) 'production_date': productionDate,
|
||||
};
|
||||
@@ -649,9 +648,9 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
GridCol('prodDate', '生产日期', width: 150, req: true),
|
||||
GridCol('batch', '批次号', width: 116, req: true),
|
||||
GridCol('qty', '数量', width: 82, num: true, req: true),
|
||||
GridCol('price', '进价', width: 100, num: true),
|
||||
GridCol('sale', '售价', width: 108, num: true, req: true),
|
||||
GridCol('amount', '金额', width: 110, num: true),
|
||||
GridCol('price', '进价(单瓶)', width: 108, num: true),
|
||||
GridCol('sale', '参考售价', width: 108, num: true, req: true),
|
||||
GridCol('amount', '总进价', width: 110, num: true),
|
||||
GridCol('act', '', width: 78),
|
||||
];
|
||||
|
||||
@@ -912,14 +911,14 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
num: true,
|
||||
hint: '0',
|
||||
onChanged: (_) => setState(() {}))),
|
||||
MobileCardField('进价(可留空/0)', null,
|
||||
MobileCardField('进价·单瓶(可留空/0)', null,
|
||||
valueWidget: GciField(
|
||||
controller: item.priceCtrl,
|
||||
num: true,
|
||||
money: true,
|
||||
hint: '留空=待定价',
|
||||
onChanged: (_) => setState(() {}))),
|
||||
MobileCardField('售价', null,
|
||||
MobileCardField('参考售价', null,
|
||||
valueWidget: GciField(
|
||||
controller: item.saleCtrl,
|
||||
num: true,
|
||||
@@ -927,7 +926,7 @@ class _StockInFormScreenState extends ConsumerState<StockInFormScreen> {
|
||||
hint: '0.00',
|
||||
onChanged: (_) => setState(() {}))),
|
||||
MobileCardField(
|
||||
'金额', item.pending ? '待定价' : '¥${item.amount.toStringAsFixed(2)}'),
|
||||
'总进价', item.pending ? '待定价' : '¥${item.amount.toStringAsFixed(2)}'),
|
||||
MobileCardField('', null,
|
||||
valueWidget: TextButton.icon(
|
||||
onPressed: () => setState(() => item.expanded = !item.expanded),
|
||||
|
||||
@@ -688,7 +688,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
o.partnerName ?? '',
|
||||
o.status,
|
||||
o.orderDate,
|
||||
o.totalAmount,
|
||||
o.costTotal,
|
||||
])
|
||||
.toList(),
|
||||
);
|
||||
@@ -712,8 +712,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
return Text(o.warehouseName ?? '-');
|
||||
case 'amount':
|
||||
return Text(
|
||||
o.totalAmount != null
|
||||
? '¥${NumberFormat('#,##0').format(o.totalAmount)}'
|
||||
o.costTotal != null
|
||||
? '¥${NumberFormat('#,##0').format(o.costTotal)}'
|
||||
: '—',
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
@@ -828,11 +828,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
fields: [
|
||||
MobileCardField('供应商', o.partnerName ?? '-'),
|
||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||
MobileCardField(
|
||||
'合计金额',
|
||||
o.totalAmount != null
|
||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
||||
: '-'),
|
||||
MobileCardField('合计金额',
|
||||
o.costTotal != null ? '¥${o.costTotal!.toStringAsFixed(2)}' : '-'),
|
||||
MobileCardField('入库员', o.operatorName ?? '-'),
|
||||
MobileCardField('审核员', o.reviewerName ?? '-'),
|
||||
],
|
||||
@@ -899,13 +896,13 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
series: it.productSeries ?? '—',
|
||||
spec: it.productSpec ?? '',
|
||||
qty: _fmtQty(it.quantity),
|
||||
price: it.unitPrice == 0 ? '待定价' : _money.format(it.unitPrice),
|
||||
price: it.costPrice == 0 ? '待定价' : _money.format(it.costPrice),
|
||||
amount:
|
||||
it.totalPrice == 0 ? '待定价' : _money.format(it.totalPrice),
|
||||
pending: it.unitPrice == 0,
|
||||
it.costAmount == 0 ? '待定价' : _money.format(it.costAmount),
|
||||
pending: it.costPrice == 0,
|
||||
))
|
||||
.toList(),
|
||||
totalText: o.totalAmount != null ? _money.format(o.totalAmount) : '—',
|
||||
totalText: o.costTotal != null ? _money.format(o.costTotal) : '—',
|
||||
actionGroups: _detailActionGroups(o),
|
||||
);
|
||||
}
|
||||
@@ -971,7 +968,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
}
|
||||
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) {
|
||||
groups.add(DrawerActionGroup('成本 · $pending 项待定价', [
|
||||
b('确认进价', () {
|
||||
@@ -1025,7 +1022,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
/// 确认进价:为 0 价(待定)明细补填真实进价,调后端前向补偿后刷新列表。
|
||||
Future<void> _confirmCost(StockInOrder o) async {
|
||||
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;
|
||||
final controllers = {
|
||||
for (final it in pending) it.id!: TextEditingController()
|
||||
@@ -1303,8 +1300,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
series: it.productSeries ?? '',
|
||||
spec: it.productSpec ?? '',
|
||||
quantity: it.quantity,
|
||||
unitPrice: it.unitPrice,
|
||||
totalPrice: it.totalPrice,
|
||||
unitPrice: it.costPrice,
|
||||
totalPrice: it.costAmount,
|
||||
alreadyReturned: it.isReturned,
|
||||
))
|
||||
.toList();
|
||||
|
||||
@@ -10,6 +10,7 @@ import '../../core/auth/auth_state.dart';
|
||||
import '../../core/config/app_constants.dart';
|
||||
import '../../core/responsive/responsive.dart';
|
||||
import '../../core/theme/app_dims.g.dart';
|
||||
import '../../core/theme/app_fonts.dart';
|
||||
import '../../core/theme/context_tokens.dart';
|
||||
import '../../core/utils/date_util.dart';
|
||||
import '../../models/inventory.dart';
|
||||
@@ -34,7 +35,7 @@ class _PickerItem {
|
||||
final String series;
|
||||
final String spec;
|
||||
final String unit;
|
||||
final double? unitPrice;
|
||||
final double? costPrice; // 进价(成本,仅管理员可见列)
|
||||
final double availableQty;
|
||||
const _PickerItem({
|
||||
required this.productId,
|
||||
@@ -43,7 +44,7 @@ class _PickerItem {
|
||||
required this.series,
|
||||
required this.spec,
|
||||
required this.unit,
|
||||
this.unitPrice,
|
||||
this.costPrice,
|
||||
required this.availableQty,
|
||||
});
|
||||
}
|
||||
@@ -62,7 +63,7 @@ List<_PickerItem> _aggregatePickerItems(List<Inventory> rows) {
|
||||
series: existing.series,
|
||||
spec: existing.spec,
|
||||
unit: existing.unit,
|
||||
unitPrice: existing.unitPrice ?? inv.unitPrice,
|
||||
costPrice: existing.costPrice ?? inv.unitPrice,
|
||||
availableQty: existing.availableQty + inv.quantity,
|
||||
);
|
||||
} else {
|
||||
@@ -73,7 +74,7 @@ List<_PickerItem> _aggregatePickerItems(List<Inventory> rows) {
|
||||
series: inv.series,
|
||||
spec: inv.spec,
|
||||
unit: inv.unit,
|
||||
unitPrice: inv.unitPrice,
|
||||
costPrice: inv.unitPrice,
|
||||
availableQty: inv.quantity,
|
||||
);
|
||||
}
|
||||
@@ -87,7 +88,7 @@ class _ItemRow {
|
||||
final String productName;
|
||||
final String series;
|
||||
final String spec;
|
||||
final double? unitPrice;
|
||||
final double? costPrice; // 进价(成本)
|
||||
final double? availableQty;
|
||||
final TextEditingController qtyCtrl;
|
||||
final TextEditingController salePriceCtrl;
|
||||
@@ -100,7 +101,7 @@ class _ItemRow {
|
||||
this.productName = '',
|
||||
this.series = '',
|
||||
this.spec = '',
|
||||
this.unitPrice,
|
||||
this.costPrice,
|
||||
this.availableQty,
|
||||
double? salePrice,
|
||||
}) : qtyCtrl = TextEditingController(text: '1'),
|
||||
@@ -108,7 +109,7 @@ class _ItemRow {
|
||||
salePriceCtrl = TextEditingController(
|
||||
text: ((salePrice != null && salePrice > 0)
|
||||
? salePrice
|
||||
: (unitPrice ?? 0))
|
||||
: (costPrice ?? 0))
|
||||
.toStringAsFixed(2));
|
||||
|
||||
FocusNode focusNode(String field) => field == 'sale' ? saleFocus : qtyFocus;
|
||||
@@ -118,6 +119,13 @@ class _ItemRow {
|
||||
(double.tryParse(qtyCtrl.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() {
|
||||
qtyCtrl.dispose();
|
||||
salePriceCtrl.dispose();
|
||||
@@ -183,7 +191,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
productName: item.productName ?? '',
|
||||
series: item.productSeries ?? '',
|
||||
spec: item.productSpec ?? '',
|
||||
unitPrice: item.unitPrice,
|
||||
costPrice: item.costPrice,
|
||||
salePrice: item.salePrice,
|
||||
);
|
||||
row.qtyCtrl.text = item.quantity.toStringAsFixed(0);
|
||||
@@ -217,6 +225,14 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
return total;
|
||||
}
|
||||
|
||||
double get _totalProfit {
|
||||
double total = 0;
|
||||
for (final item in _items) {
|
||||
total += item.profit;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
Future<void> _loadInventory(int warehouseId) async {
|
||||
try {
|
||||
final result = await ref.read(inventoryRepositoryProvider).listInventory(
|
||||
@@ -254,7 +270,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
productName: item.productName,
|
||||
series: item.series,
|
||||
spec: item.spec,
|
||||
unitPrice: item.unitPrice,
|
||||
costPrice: item.costPrice,
|
||||
availableQty: item.availableQty,
|
||||
));
|
||||
}
|
||||
@@ -276,7 +292,7 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
productName: s.productName,
|
||||
series: s.series,
|
||||
spec: s.spec,
|
||||
unitPrice: s.unitPrice,
|
||||
costPrice: s.costPrice,
|
||||
availableQty: s.availableQty,
|
||||
);
|
||||
r.salePriceCtrl.text = s.salePriceCtrl.text;
|
||||
@@ -355,14 +371,14 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
|
||||
final itemsData = _items.map((item) {
|
||||
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;
|
||||
// 小计/合计/利润由服务端按同口径重算落库,这里只传两侧单价
|
||||
return {
|
||||
'product_id': item.productId ?? 0,
|
||||
'quantity': qty,
|
||||
'unit_price': price,
|
||||
'cost_price': price,
|
||||
'sale_price': sale,
|
||||
'total_price': qty * price,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
@@ -516,6 +532,9 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
mobileActions: mobileActions,
|
||||
rowsLabel: '明细 ${_items.length} 行',
|
||||
totalText: '¥${_totalAmount.toStringAsFixed(2)}',
|
||||
profitText: ref.watch(isAdminProvider)
|
||||
? '¥${_totalProfit.toStringAsFixed(2)}'
|
||||
: null,
|
||||
docHead: _buildDocHead(currentUser?.realName ?? '-'),
|
||||
detailHead: DetailHead(actions: [
|
||||
DsButton('从库存选择',
|
||||
@@ -628,18 +647,23 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
}
|
||||
|
||||
// ── 桌面:内联网格 ─────────────────────────────────────────────────────────
|
||||
static const _columns = [
|
||||
GridCol('idx', '#', width: 38),
|
||||
GridCol('name', '商品名称'),
|
||||
GridCol('series', '系列', width: 116),
|
||||
GridCol('spec', '规格', width: 128),
|
||||
GridCol('avail', '可用', width: 92, num: true),
|
||||
GridCol('qty', '数量', width: 82, num: true, req: true),
|
||||
GridCol('price', '进价', width: 100, num: true),
|
||||
GridCol('sale', '售价', width: 108, num: true, req: true),
|
||||
GridCol('amount', '金额', width: 110, num: true),
|
||||
GridCol('act', '', width: 62),
|
||||
];
|
||||
// 进价/利润列仅管理员可见(2026-07 定价重设计;operator 由服务端抹零+前端隐藏双保险);
|
||||
// 原「金额」列(=售价×数量,qty=1 时与售价重复)改为「利润」实时列。
|
||||
List<GridCol> get _columns {
|
||||
final admin = ref.read(isAdminProvider);
|
||||
return [
|
||||
const GridCol('idx', '#', width: 38),
|
||||
const GridCol('name', '商品名称'),
|
||||
const GridCol('series', '系列', width: 116),
|
||||
const GridCol('spec', '规格', width: 128),
|
||||
const GridCol('avail', '可用', width: 92, num: true),
|
||||
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() {
|
||||
if (_items.isEmpty) {
|
||||
@@ -687,7 +711,21 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
|
||||
);
|
||||
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':
|
||||
return RoCell(item.series);
|
||||
case 'spec':
|
||||
@@ -711,8 +749,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
);
|
||||
case 'price':
|
||||
return RoCell(
|
||||
(item.unitPrice ?? 0) > 0
|
||||
? '¥${item.unitPrice!.toStringAsFixed(2)}'
|
||||
(item.costPrice ?? 0) > 0
|
||||
? '¥${item.costPrice!.toStringAsFixed(2)}'
|
||||
: '—',
|
||||
num: true,
|
||||
);
|
||||
@@ -727,8 +765,10 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
onEnter: () => _advance(i, 'sale'),
|
||||
onTab: ({required backward}) => _onTab(i, 'sale', backward: backward),
|
||||
);
|
||||
case 'amount':
|
||||
return AmountCell(amount: item.amount);
|
||||
case 'profit':
|
||||
return _ProfitCell(
|
||||
profit: item.profit,
|
||||
pending: (double.tryParse(item.salePriceCtrl.text) ?? 0) <= 0);
|
||||
case 'act':
|
||||
return RowActions(
|
||||
onCopy: () => _copyRow(i),
|
||||
@@ -772,11 +812,12 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
if (item.series.isNotEmpty) MobileCardField('系列', item.series),
|
||||
if (item.spec.isNotEmpty) MobileCardField('规格', item.spec),
|
||||
MobileCardField('可用', avail != null ? avail.toStringAsFixed(0) : '—'),
|
||||
MobileCardField(
|
||||
'进价',
|
||||
(item.unitPrice ?? 0) > 0
|
||||
? '¥${item.unitPrice!.toStringAsFixed(2)}'
|
||||
: '—'),
|
||||
if (ref.read(isAdminProvider))
|
||||
MobileCardField(
|
||||
'进价',
|
||||
(item.costPrice ?? 0) > 0
|
||||
? '¥${item.costPrice!.toStringAsFixed(2)}'
|
||||
: '—'),
|
||||
MobileCardField('数量', null,
|
||||
valueWidget: GciField(
|
||||
controller: item.qtyCtrl,
|
||||
@@ -790,12 +831,36 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
|
||||
money: true,
|
||||
hint: '0.00',
|
||||
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 {
|
||||
const _SubmitIntent();
|
||||
@@ -1012,8 +1077,8 @@ class _InventoryPickerDialogState
|
||||
_dataCell(item.spec, 130,
|
||||
color: context.tokens.muted),
|
||||
_dataCell(
|
||||
item.unitPrice != null
|
||||
? '¥${item.unitPrice!.toStringAsFixed(2)}'
|
||||
item.costPrice != null
|
||||
? '¥${item.costPrice!.toStringAsFixed(2)}'
|
||||
: '-',
|
||||
90,
|
||||
),
|
||||
|
||||
@@ -364,8 +364,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
Text(o.partnerName ?? '-'),
|
||||
Text(o.warehouseName ?? '-'),
|
||||
Text(
|
||||
o.totalAmount != null
|
||||
? '¥${NumberFormat('#,##0').format(o.totalAmount)}'
|
||||
o.saleTotal != null
|
||||
? '¥${NumberFormat('#,##0').format(o.saleTotal)}'
|
||||
: '—',
|
||||
style: const TextStyle(
|
||||
fontFamily: AppFonts.mono,
|
||||
@@ -725,7 +725,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
o.partnerName ?? '',
|
||||
o.status,
|
||||
o.orderDate,
|
||||
o.totalAmount,
|
||||
o.saleTotal,
|
||||
])
|
||||
.toList(),
|
||||
);
|
||||
@@ -813,11 +813,8 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
fields: [
|
||||
MobileCardField('客户', o.partnerName ?? '-'),
|
||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||
MobileCardField(
|
||||
'金额',
|
||||
o.totalAmount != null
|
||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
||||
: '-'),
|
||||
MobileCardField('金额',
|
||||
o.saleTotal != null ? '¥${o.saleTotal!.toStringAsFixed(2)}' : '-'),
|
||||
MobileCardField('出库时间', o.orderDate?.substring(0, 10) ?? '-'),
|
||||
],
|
||||
actions: _orderActions(context, o),
|
||||
@@ -876,20 +873,30 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
),
|
||||
],
|
||||
linesLabel: '出库明细',
|
||||
lines: o.items
|
||||
.map((it) => OrderLine(
|
||||
name: it.productName ?? '—',
|
||||
code: it.productCode ?? '—',
|
||||
series: it.productSeries ?? '—',
|
||||
spec: it.productSpec ?? '',
|
||||
qty: _fmtQty(it.quantity),
|
||||
price: it.salePrice <= 0 ? '待定价' : _money.format(it.salePrice),
|
||||
amount:
|
||||
it.totalPrice == 0 ? '待定价' : _money.format(it.totalPrice),
|
||||
pending: it.salePrice <= 0,
|
||||
))
|
||||
.toList(),
|
||||
totalText: o.totalAmount != null ? _money.format(o.totalAmount) : '—',
|
||||
// 管理员:成本价/售价/利润 三列 + 合计利润;operator:售价/小计
|
||||
//(成本与利润由服务端按角色抹零,前端同时不渲染对应列——双保险)
|
||||
lines: o.items.map((it) {
|
||||
final pending = it.salePrice <= 0;
|
||||
final admin = ref.read(isAdminProvider);
|
||||
final profit = (it.salePrice - it.costPrice) * it.quantity;
|
||||
return OrderLine(
|
||||
name: it.productName ?? '—',
|
||||
code: it.productCode ?? '—',
|
||||
series: it.productSeries ?? '—',
|
||||
spec: it.productSpec ?? '',
|
||||
qty: _fmtQty(it.quantity),
|
||||
price: pending ? '待定价' : _money.format(it.salePrice),
|
||||
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),
|
||||
);
|
||||
}
|
||||
@@ -1298,8 +1305,9 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
series: it.productSeries ?? '',
|
||||
spec: it.productSpec ?? '',
|
||||
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,
|
||||
))
|
||||
.toList();
|
||||
|
||||
Reference in New Issue
Block a user