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))),
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 12))),
|
||||||
'warehouse' => DataCell(Text(item.warehouseName.isEmpty ? '-' : item.warehouseName)),
|
'warehouse' => DataCell(Text(item.warehouseName.isEmpty ? '-' : item.warehouseName)),
|
||||||
'price' => DataCell(Text(
|
'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 ?? '-')),
|
'prodDate' => DataCell(Text(item.productionDate ?? '-')),
|
||||||
'inTime' => DataCell(Text(
|
'inTime' => DataCell(Text(
|
||||||
item.createdAt != null && item.createdAt!.length >= 10
|
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.series.isNotEmpty) MobileCardField('系列', item.series),
|
||||||
if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo),
|
if (item.batchNo.isNotEmpty) MobileCardField('批次', item.batchNo),
|
||||||
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
|
MobileCardField('仓库', item.warehouseName.isEmpty ? '-' : item.warehouseName),
|
||||||
if (item.unitPrice != null)
|
MobileCardField('单价',
|
||||||
MobileCardField('单价', '¥${item.unitPrice!.toStringAsFixed(2)}'),
|
item.unitPrice != null ? '¥${item.unitPrice!.toStringAsFixed(2)}' : '待定价'),
|
||||||
if (item.supplierName.isNotEmpty)
|
if (item.supplierName.isNotEmpty)
|
||||||
MobileCardField('供应商', item.supplierName),
|
MobileCardField('供应商', item.supplierName),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import '../../core/utils/dialog_util.dart';
|
import '../../core/utils/dialog_util.dart';
|
||||||
import '../../core/errors/error_reporter.dart';
|
import '../../core/errors/error_reporter.dart';
|
||||||
|
import '../../core/exceptions.dart';
|
||||||
import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, LabelData;
|
import '../../core/utils/print_util.dart' show safePrint, printStockInOrder, LabelData;
|
||||||
import '../../widgets/label_preview_dialog.dart';
|
import '../../widgets/label_preview_dialog.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -897,7 +898,11 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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);
|
if (mounted) setState(() => _loadedOrder = order);
|
||||||
try {
|
try {
|
||||||
final result = await ref
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Dialog(
|
return Dialog(
|
||||||
@@ -1034,10 +1127,10 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
|||||||
MobileCardField('系列', item.productSeries!),
|
MobileCardField('系列', item.productSeries!),
|
||||||
MobileCardField('规格', item.productSpec ?? '-'),
|
MobileCardField('规格', item.productSpec ?? '-'),
|
||||||
MobileCardField('数量', item.quantity.toStringAsFixed(3)),
|
MobileCardField('数量', item.quantity.toStringAsFixed(3)),
|
||||||
MobileCardField(
|
MobileCardField('单价',
|
||||||
'单价', '¥${item.unitPrice.toStringAsFixed(2)}'),
|
item.unitPrice == 0 ? '待定价' : '¥${item.unitPrice.toStringAsFixed(2)}'),
|
||||||
MobileCardField(
|
MobileCardField('金额',
|
||||||
'金额', '¥${item.totalPrice.toStringAsFixed(2)}'),
|
item.totalPrice == 0 ? '待定' : '¥${item.totalPrice.toStringAsFixed(2)}'),
|
||||||
if (item.batchNo?.isNotEmpty ?? false)
|
if (item.batchNo?.isNotEmpty ?? false)
|
||||||
MobileCardField('批次号', item.batchNo!),
|
MobileCardField('批次号', item.batchNo!),
|
||||||
if (item.productionDate?.isNotEmpty ?? false)
|
if (item.productionDate?.isNotEmpty ?? false)
|
||||||
@@ -1131,13 +1224,31 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
|||||||
_TableCell(
|
_TableCell(
|
||||||
(item.batchNo?.isNotEmpty ?? false) ? item.batchNo! : '-'),
|
(item.batchNo?.isNotEmpty ?? false) ? item.batchNo! : '-'),
|
||||||
_TableCell(item.quantity.toStringAsFixed(3)),
|
_TableCell(item.quantity.toStringAsFixed(3)),
|
||||||
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
|
_TableCell(item.unitPrice == 0 ? '待定价' : '¥${item.unitPrice.toStringAsFixed(2)}'),
|
||||||
_TableCell('¥${item.totalPrice.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