diff --git a/client/lib/repositories/stock_in_repository.dart b/client/lib/repositories/stock_in_repository.dart index bbac6b4..293c52a 100644 --- a/client/lib/repositories/stock_in_repository.dart +++ b/client/lib/repositories/stock_in_repository.dart @@ -142,4 +142,17 @@ class StockInRepository { ); } } + + /// 确认进价:把指定明细的暂估 0 价补成真实进价(前向补偿成本与应付)。 + /// items 形如 [{'item_id': 1, 'unit_price': 50.0}]。 + Future confirmCost(int id, List> items) async { + try { + await _client.post('/stock-in/orders/$id/confirm-cost', data: {'items': items}); + } on DioException catch (e) { + throw AppException( + e.response?.data?['error'] as String? ?? '确认进价失败', + statusCode: e.response?.statusCode, + ); + } + } } diff --git a/client/lib/screens/inventory/inventory_list_screen.dart b/client/lib/screens/inventory/inventory_list_screen.dart index 0a031cf..bf13fb4 100644 --- a/client/lib/screens/inventory/inventory_list_screen.dart +++ b/client/lib/screens/inventory/inventory_list_screen.dart @@ -312,7 +312,10 @@ class _InventoryListScreenState extends ConsumerState { style: const TextStyle(fontFamily: 'monospace', fontSize: 12))), 'warehouse' => DataCell(Text(item.warehouseName.isEmpty ? '-' : item.warehouseName)), 'price' => DataCell(Text( - item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '-')), + item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '待定价', + style: item.unitPrice == null + ? const TextStyle(color: AppTheme.warning) + : null)), 'prodDate' => DataCell(Text(item.productionDate ?? '-')), 'inTime' => DataCell(Text( item.createdAt != null && item.createdAt!.length >= 10 @@ -358,8 +361,8 @@ class _InventoryListScreenState extends ConsumerState { if (item.series.isNotEmpty) MobileCardField('系列', item.series), if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo), MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName), - if (item.unitPrice != null) - MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'), + MobileCardField('单价', + item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '待定价'), if (item.supplierName.isNotEmpty) MobileCardField('供应商', item.supplierName), ], diff --git a/client/lib/screens/stock_in/stock_in_list_screen.dart b/client/lib/screens/stock_in/stock_in_list_screen.dart index d16c59b..2b27f1e 100644 --- a/client/lib/screens/stock_in/stock_in_list_screen.dart +++ b/client/lib/screens/stock_in/stock_in_list_screen.dart @@ -1,5 +1,6 @@ import '../../core/utils/dialog_util.dart'; import '../../core/errors/error_reporter.dart'; +import '../../core/exceptions.dart'; import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, LabelData; import '../../widgets/label_preview_dialog.dart'; import 'package:flutter/material.dart'; @@ -897,7 +898,11 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> { @override void initState() { super.initState(); - _future = widget.repository.get(widget.orderId).then((order) async { + _future = _load(); + } + + Future _load() { + return widget.repository.get(widget.orderId).then((order) async { if (mounted) setState(() => _loadedOrder = order); try { final result = await ref @@ -915,6 +920,94 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> { }); } + /// 确认进价:为 0 价(待定)明细补填真实进价,调后端前向补偿后刷新。 + Future _confirmCost(StockInOrder o) async { + final pending = + o.items.where((it) => it.unitPrice == 0 && it.id != null).toList(); + if (pending.isEmpty) return; + final controllers = { + for (final it in pending) it.id!: TextEditingController() + }; + + final ok = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('确认进价'), + content: SizedBox( + width: context.dialogWidth(440), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: pending.map((it) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Expanded( + child: Text( + '${it.productName ?? ''} ${it.productSpec ?? ''} ×${it.quantity.toStringAsFixed(0)}', + style: const TextStyle(fontSize: 13), + ), + ), + const SizedBox(width: 8), + SizedBox( + width: 120, + child: TextField( + controller: controllers[it.id], + keyboardType: const TextInputType.numberWithOptions( + decimal: true), + decoration: const InputDecoration( + prefixText: '¥', + hintText: '进价', + isDense: true), + ), + ), + ], + ), + ); + }).toList(), + ), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx, false), + child: const Text('取消')), + ElevatedButton( + onPressed: () => Navigator.pop(ctx, true), + child: const Text('确认')), + ], + ), + ); + + final items = >[]; + if (ok == true) { + for (final it in pending) { + final v = double.tryParse(controllers[it.id]!.text.trim()); + if (v != null && v > 0) { + items.add({'item_id': it.id, 'unit_price': v}); + } + } + } + for (final c in controllers.values) { + c.dispose(); + } + if (ok != true || items.isEmpty) return; + + try { + await widget.repository.confirmCost(o.id, items); + if (!mounted) return; + ScaffoldMessenger.of(context) + .showSnackBar(const SnackBar(content: Text('进价已确认'))); + setState(() => _future = _load()); + ref.invalidate(stockInListProvider); + } on AppException catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text('确认失败:${e.message}'))); + } + } + @override Widget build(BuildContext context) { return Dialog( @@ -1034,10 +1127,10 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> { MobileCardField('系列', item.productSeries!), MobileCardField('规格', item.productSpec ?? '-'), MobileCardField('数量', item.quantity.toStringAsFixed(3)), - MobileCardField( - '单价', '¥${item.unitPrice.toStringAsFixed(2)}'), - MobileCardField( - '金额', '¥${item.totalPrice.toStringAsFixed(2)}'), + MobileCardField('单价', + item.unitPrice == 0 ? '待定价' : '¥${item.unitPrice.toStringAsFixed(2)}'), + MobileCardField('金额', + item.totalPrice == 0 ? '待定' : '¥${item.totalPrice.toStringAsFixed(2)}'), if (item.batchNo?.isNotEmpty ?? false) MobileCardField('批次号', item.batchNo!), if (item.productionDate?.isNotEmpty ?? false) @@ -1131,13 +1224,31 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> { _TableCell( (item.batchNo?.isNotEmpty ?? false) ? item.batchNo! : '-'), _TableCell(item.quantity.toStringAsFixed(3)), - _TableCell('¥${item.unitPrice.toStringAsFixed(2)}'), - _TableCell('¥${item.totalPrice.toStringAsFixed(2)}'), + _TableCell(item.unitPrice == 0 ? '待定价' : '¥${item.unitPrice.toStringAsFixed(2)}'), + _TableCell(item.totalPrice == 0 ? '待定' : '¥${item.totalPrice.toStringAsFixed(2)}'), ], ); }), ], ), + if (o.status == 'approved' && + ref.watch(isAdminProvider) && + o.items.any((it) => it.unitPrice == 0)) ...[ + const SizedBox(height: 16), + Align( + alignment: Alignment.centerLeft, + child: ElevatedButton.icon( + onPressed: () => _confirmCost(o), + icon: const Icon(Icons.price_check, size: 18), + label: const Text('确认进价'), + ), + ), + const SizedBox(height: 4), + const Text( + '该单含「待定价」明细:价格确定后补填真实进价,将自动回填库存成本、已出库成本快照与应付差额(不影响售价/应收)。', + style: TextStyle(fontSize: 12, color: AppTheme.textSecondary), + ), + ], ], ), );