feat(client): 入库确认进价入口与「待定价」标注
入库单详情弹窗为已审核+含 0 价明细+管理员时显示「确认进价」, 逐行补填真实进价调后端;库存列表与入库明细 0 价/NULL 显示 「待定价」而非 ¥0.00。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -142,4 +142,17 @@ class StockInRepository {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 确认进价:把指定明细的暂估 0 价补成真实进价(前向补偿成本与应付)。
|
||||
/// items 形如 [{'item_id': 1, 'unit_price': 50.0}]。
|
||||
Future<void> confirmCost(int id, List<Map<String, dynamic>> 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,10 @@ class _InventoryListScreenState extends ConsumerState<InventoryListScreen> {
|
||||
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<InventoryListScreen> {
|
||||
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),
|
||||
],
|
||||
|
||||
@@ -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<StockInOrder> _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<void> _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<bool>(
|
||||
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 = <Map<String, dynamic>>[];
|
||||
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),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user