feat(client): 出入库单详情抽屉明细增加生产日期+批次号列(两端,抽屉加宽 660,原型同步)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,8 +60,9 @@ class StockInItem {
|
||||
costPrice: (json['cost_price'] as num?)?.toDouble() ?? 0,
|
||||
costAmount: (json['cost_amount'] as num?)?.toDouble() ?? 0,
|
||||
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
|
||||
batchNo: json['batch_no'] as String?,
|
||||
productionDate: json['production_date'] as String?,
|
||||
// 批次号/生产日期:优先明细行快照,回退 product 关联(对齐 stock_out.dart 写法)
|
||||
batchNo: lineOrProduct('batch_no', 'batch_no'),
|
||||
productionDate: lineOrProduct('production_date', 'production_date'),
|
||||
productName: lineOrProduct('product_name', 'name'),
|
||||
productCode: lineOrProduct('product_code', 'code'),
|
||||
productSeries: lineOrProduct('series', 'series'),
|
||||
|
||||
@@ -414,7 +414,8 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
// 下拉刷新 = 桌面「刷新」
|
||||
onRefresh: () async {
|
||||
ref.read(stockInListProvider.notifier).reload();
|
||||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||||
await Future<void>.delayed(
|
||||
const Duration(milliseconds: 600));
|
||||
},
|
||||
mobileCards:
|
||||
orders.map((o) => _orderCard(context, o)).toList(),
|
||||
@@ -995,6 +996,10 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
||||
price: it.costPrice == 0 ? '待定价' : _num.format(it.costPrice),
|
||||
amount: it.costAmount == 0 ? '待定价' : _num.format(it.costAmount),
|
||||
pending: it.costPrice == 0,
|
||||
productionDate: it.productionDate != null
|
||||
? it.productionDate!.substring(0, 10)
|
||||
: null,
|
||||
batchNo: it.batchNo,
|
||||
))
|
||||
.toList(),
|
||||
totalText: o.costTotal != null ? _money.format(o.costTotal) : '—',
|
||||
|
||||
@@ -1076,6 +1076,10 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
||||
cost: admin ? _num.format(it.costPrice) : null,
|
||||
profit: admin ? (pending ? '待定价' : _num.format(profit)) : null,
|
||||
pending: pending,
|
||||
productionDate: it.productionDate != null
|
||||
? it.productionDate!.substring(0, 10)
|
||||
: null,
|
||||
batchNo: it.batchNo,
|
||||
);
|
||||
}).toList(),
|
||||
totalText: o.saleTotal != null ? _money.format(o.saleTotal) : '—',
|
||||
|
||||
@@ -41,7 +41,7 @@ Future<T?> showOrderDetailDrawer<T>(
|
||||
barrierColor: _kDrawerMask,
|
||||
transitionDuration: const Duration(milliseconds: 250),
|
||||
pageBuilder: (ctx, _, __) {
|
||||
final w = math.min(600.0, MediaQuery.of(ctx).size.width * 0.94);
|
||||
final w = math.min(660.0, MediaQuery.of(ctx).size.width * 0.94);
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Material(
|
||||
@@ -63,7 +63,8 @@ Future<T?> showOrderDetailDrawer<T>(
|
||||
);
|
||||
}
|
||||
|
||||
/// 抽屉明细行(对齐原型 .lines .lr,5 列:商品·编码 / 系列·规格 / 数量 / 单价 / 金额)。
|
||||
/// 抽屉明细行(对齐原型 .lines .lr,入库 6 列 / 出库管理员 8 列:
|
||||
/// 商品·编码 / 系列·规格 / 生产日期·批次号 / 数量 / 单价(/成本价/售价/总售价) / 利润)。
|
||||
class OrderLine {
|
||||
final String name;
|
||||
final String code;
|
||||
@@ -77,6 +78,9 @@ class OrderLine {
|
||||
final String? cost;
|
||||
final String? profit;
|
||||
final bool pending; // 单价<=0:单价/金额显示待定价样式
|
||||
// 生产日期 / 批次号(2026-07-14,出入库单详情明细新增双行列)
|
||||
final String? productionDate;
|
||||
final String? batchNo;
|
||||
const OrderLine({
|
||||
required this.name,
|
||||
required this.code,
|
||||
@@ -88,6 +92,8 @@ class OrderLine {
|
||||
this.cost,
|
||||
this.profit,
|
||||
this.pending = false,
|
||||
this.productionDate,
|
||||
this.batchNo,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -327,8 +333,11 @@ class OrderDetailDrawer extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// 窄屏明细行(原型:左 商品名 + 系列·×数量 小字,右 金额 mono / 待定价徽章)。
|
||||
/// 窄屏明细行(原型:左 商品名 + 系列·×数量 小字,中 生产日期/批次号双行列
|
||||
/// (两值都空则不渲染,避免空占位),右 金额 mono / 待定价徽章)。
|
||||
Widget _mLineRow(dynamic t, OrderLine l, bool last) {
|
||||
final hasDateBatch = (l.productionDate?.isNotEmpty ?? false) ||
|
||||
(l.batchNo?.isNotEmpty ?? false);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 11),
|
||||
decoration: last
|
||||
@@ -351,6 +360,30 @@ class OrderDetailDrawer extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (hasDateBatch) ...[
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
(l.productionDate?.isNotEmpty ?? false)
|
||||
? l.productionDate!
|
||||
: '—',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
color: t.text)),
|
||||
Text((l.batchNo?.isNotEmpty ?? false) ? l.batchNo! : '—',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsXs,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
color: t.faint)),
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 10),
|
||||
l.pending
|
||||
? const DsTextIconBadge('待定价')
|
||||
@@ -450,6 +483,7 @@ class _LinesTable extends StatelessWidget {
|
||||
c1: Text('商品 · 编码',
|
||||
style: _hStyle(t), overflow: TextOverflow.ellipsis),
|
||||
c2: Text('系列 / 规格', style: _hStyle(t)),
|
||||
cDate: Text('生产日期 / 批次号', style: _hStyle(t)),
|
||||
c3: Text('数量', textAlign: TextAlign.right, style: _hStyle(t)),
|
||||
c4: _withProfit
|
||||
? Text('成本价', textAlign: TextAlign.right, style: _hStyle(t))
|
||||
@@ -506,6 +540,30 @@ class _LinesTable extends StatelessWidget {
|
||||
TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
|
||||
],
|
||||
),
|
||||
cDate: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
(lines[i].productionDate?.isNotEmpty ?? false)
|
||||
? lines[i].productionDate!
|
||||
: '—',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsSm,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
color: t.text)),
|
||||
Text(
|
||||
(lines[i].batchNo?.isNotEmpty ?? false)
|
||||
? lines[i].batchNo!
|
||||
: '—',
|
||||
style: TextStyle(
|
||||
fontSize: AppDims.fsXs,
|
||||
fontFamily: AppFonts.mono,
|
||||
fontFamilyFallback: AppFonts.monoFallback,
|
||||
color: t.faint)),
|
||||
],
|
||||
),
|
||||
c3: Text(lines[i].qty,
|
||||
textAlign: TextAlign.right,
|
||||
style: const TextStyle(
|
||||
@@ -557,11 +615,12 @@ class _LinesTable extends StatelessWidget {
|
||||
color: accent ? t.text : t.text));
|
||||
}
|
||||
|
||||
// 5 列布局(对齐原型 grid 1fr 124 40 64 78);c6/c7 非空时为出库 7 列形态
|
||||
// (数量/成本价/售价/总售价/利润,数值列等宽收窄)。
|
||||
// 6 列布局(入库,对齐原型 grid 1fr 110 72 40 64 78);c6/c7 非空时为出库
|
||||
// 8 列形态(商品/系列规格/生产日期批次号/数量/成本价/售价/总售价/利润,数值列等宽收窄)。
|
||||
Widget _lineRow(BuildContext context,
|
||||
{required Widget c1,
|
||||
required Widget c2,
|
||||
required Widget cDate,
|
||||
required Widget c3,
|
||||
required Widget c4,
|
||||
required Widget c5,
|
||||
@@ -573,15 +632,18 @@ class _LinesTable extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(child: c1),
|
||||
const SizedBox(width: 8),
|
||||
// 5 列(入库)时中间列加宽左移:商品列不再吃掉全部余宽,
|
||||
// 6 列(入库)时中间列加宽左移:商品列不再吃掉全部余宽,
|
||||
// 「进价(单瓶)」表头一行放得下(2026-07-03 用户反馈)。
|
||||
SizedBox(width: wide ? 100 : 124, child: c2),
|
||||
// 2026-07-14:新增「生产日期/批次号」列,入库侧挤压 124→110、88→78 腾位。
|
||||
SizedBox(width: wide ? 100 : 110, child: c2),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 72, child: cDate),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: wide ? 32 : 40, child: c3),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: wide ? 62 : 88, child: c4),
|
||||
SizedBox(width: wide ? 62 : 78, child: c4),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: wide ? 62 : 88, child: c5),
|
||||
SizedBox(width: wide ? 62 : 78, child: c5),
|
||||
if (c6 != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(width: 62, child: c6),
|
||||
|
||||
@@ -6,6 +6,9 @@ import 'package:jiu_client/widgets/order_detail_drawer.dart';
|
||||
/// 2026-07 定价重设计:出库详情按角色两种形态——
|
||||
/// 管理员(行带 cost/profit)= 成本价/售价/利润 三数值列 + 合计利润;
|
||||
/// operator(不带)= 售价/小计两列,无成本无利润。
|
||||
///
|
||||
/// 2026-07-14:明细新增「生产日期 / 批次号」双行列(入库 5→6 列 / 出库管理员
|
||||
/// 7→8 列),两种形态下都固定渲染,与成本/利润的角色可见性无关。
|
||||
void main() {
|
||||
Widget host(OrderDetailDrawer drawer) => MaterialApp(
|
||||
theme: buildTheme('a'),
|
||||
@@ -22,6 +25,8 @@ void main() {
|
||||
amount: '¥800.00',
|
||||
cost: '¥355.00',
|
||||
profit: '¥90.00',
|
||||
productionDate: '2021-01-27',
|
||||
batchNo: '20260506',
|
||||
);
|
||||
|
||||
const line4 = OrderLine(
|
||||
@@ -32,9 +37,11 @@ void main() {
|
||||
qty: '2',
|
||||
price: '¥400.00',
|
||||
amount: '¥800.00',
|
||||
productionDate: '2021-01-27',
|
||||
batchNo: '20260506',
|
||||
);
|
||||
|
||||
testWidgets('管理员形态:成本价/售价/利润列 + 合计利润', (tester) async {
|
||||
testWidgets('管理员形态:成本价/售价/利润列 + 生产日期/批次号列 + 合计利润', (tester) async {
|
||||
await tester.pumpWidget(host(OrderDetailDrawer(
|
||||
title: 'CK001',
|
||||
infoRows: const [],
|
||||
@@ -54,9 +61,12 @@ void main() {
|
||||
expect(find.text('¥355.00'), findsOneWidget);
|
||||
expect(find.text('¥90.00'), findsNWidgets(2)); // 行利润 + 合计利润
|
||||
expect(find.text('合计利润 '), findsOneWidget);
|
||||
expect(find.text('生产日期 / 批次号'), findsOneWidget);
|
||||
expect(find.text('2021-01-27'), findsOneWidget);
|
||||
expect(find.text('20260506'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('operator 形态:售价/小计,无成本无利润', (tester) async {
|
||||
testWidgets('operator 形态:售价/小计 + 生产日期/批次号列,无成本无利润', (tester) async {
|
||||
await tester.pumpWidget(host(OrderDetailDrawer(
|
||||
title: 'CK001',
|
||||
infoRows: const [],
|
||||
@@ -73,5 +83,32 @@ void main() {
|
||||
expect(find.text('利润'), findsNothing);
|
||||
expect(find.text('合计利润 '), findsNothing);
|
||||
expect(find.text('¥800.00'), findsNWidgets(2)); // 行小计 + 合计金额
|
||||
expect(find.text('生产日期 / 批次号'), findsOneWidget);
|
||||
expect(find.text('2021-01-27'), findsOneWidget);
|
||||
expect(find.text('20260506'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('生产日期/批次号两值都空时,桌面列仍渲染但单元格显示 —', (tester) async {
|
||||
const lineEmpty = OrderLine(
|
||||
name: '汾酒青花20年',
|
||||
code: 'ZXZ027232',
|
||||
series: '普通/53度',
|
||||
spec: '500ml*6/件',
|
||||
qty: '2',
|
||||
price: '¥400.00',
|
||||
amount: '¥800.00',
|
||||
);
|
||||
await tester.pumpWidget(host(OrderDetailDrawer(
|
||||
title: 'RK001',
|
||||
infoRows: const [],
|
||||
linesLabel: '入库明细',
|
||||
lines: const [lineEmpty],
|
||||
totalText: '¥800.00',
|
||||
priceLabel: '进价(单瓶)',
|
||||
amountLabel: '总价',
|
||||
actionGroups: const [],
|
||||
)));
|
||||
expect(find.text('生产日期 / 批次号'), findsOneWidget);
|
||||
expect(find.text('—'), findsNWidgets(2)); // 生产日期 + 批次号均兜底
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user