Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51acee2705 |
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.71] - 2026-06-21
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- 已审核单据「退单」:入库/出库列表点红色「退单」→ 弹出退单明细窗,可逐行退或一键「全部退单」,确认框显示名称/系列/规格;已退明细以红色「已退单」醒目标注,列表状态旁显示「部分退单/已退单」徽章
|
||||||
|
- 数据导入页新增「入库单/出库单」导入(老系统单据打印格式,每个文件一张单)
|
||||||
|
|
||||||
## [1.0.70] - 2026-06-21
|
## [1.0.70] - 2026-06-21
|
||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
class StockInItem {
|
class StockInItem {
|
||||||
|
final int? id;
|
||||||
final int? orderId;
|
final int? orderId;
|
||||||
final int productId;
|
final int productId;
|
||||||
final double quantity;
|
final double quantity;
|
||||||
final double unitPrice;
|
final double unitPrice;
|
||||||
final double totalPrice;
|
final double totalPrice;
|
||||||
|
final double returnedQuantity; // 0=未退;>=quantity 表示整行已退单
|
||||||
final String? batchNo;
|
final String? batchNo;
|
||||||
final String? productionDate;
|
final String? productionDate;
|
||||||
|
|
||||||
|
/// 该明细是否已退单(整行已退)。
|
||||||
|
bool get isReturned => returnedQuantity >= quantity && quantity > 0;
|
||||||
// Denormalized for display
|
// Denormalized for display
|
||||||
final String? productName;
|
final String? productName;
|
||||||
final String? productCode;
|
final String? productCode;
|
||||||
@@ -17,11 +22,13 @@ class StockInItem {
|
|||||||
final String? productStorage;
|
final String? productStorage;
|
||||||
|
|
||||||
const StockInItem({
|
const StockInItem({
|
||||||
|
this.id,
|
||||||
this.orderId,
|
this.orderId,
|
||||||
required this.productId,
|
required this.productId,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
required this.totalPrice,
|
required this.totalPrice,
|
||||||
|
this.returnedQuantity = 0,
|
||||||
this.batchNo,
|
this.batchNo,
|
||||||
this.productionDate,
|
this.productionDate,
|
||||||
this.productName,
|
this.productName,
|
||||||
@@ -45,6 +52,7 @@ class StockInItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return StockInItem(
|
return StockInItem(
|
||||||
|
id: json['id'] != null ? (json['id'] as num).toInt() : null,
|
||||||
orderId: json['order_id'] != null
|
orderId: json['order_id'] != null
|
||||||
? (json['order_id'] as num).toInt()
|
? (json['order_id'] as num).toInt()
|
||||||
: null,
|
: null,
|
||||||
@@ -52,6 +60,7 @@ class StockInItem {
|
|||||||
quantity: (json['quantity'] as num).toDouble(),
|
quantity: (json['quantity'] as num).toDouble(),
|
||||||
unitPrice: (json['unit_price'] as num).toDouble(),
|
unitPrice: (json['unit_price'] as num).toDouble(),
|
||||||
totalPrice: (json['total_price'] as num).toDouble(),
|
totalPrice: (json['total_price'] as num).toDouble(),
|
||||||
|
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
|
||||||
batchNo: json['batch_no'] as String?,
|
batchNo: json['batch_no'] as String?,
|
||||||
productionDate: json['production_date'] as String?,
|
productionDate: json['production_date'] as String?,
|
||||||
productName: lineOrProduct('product_name', 'name'),
|
productName: lineOrProduct('product_name', 'name'),
|
||||||
@@ -91,6 +100,7 @@ class StockInOrder {
|
|||||||
final int? reviewerId;
|
final int? reviewerId;
|
||||||
final String? reviewerName;
|
final String? reviewerName;
|
||||||
final String status; // draft | pending | approved | rejected
|
final String status; // draft | pending | approved | rejected
|
||||||
|
final String returnState; // none | partial | full(退单状态)
|
||||||
final String? orderDate;
|
final String? orderDate;
|
||||||
final String? reviewedAt;
|
final String? reviewedAt;
|
||||||
final double? totalAmount;
|
final double? totalAmount;
|
||||||
@@ -110,6 +120,7 @@ class StockInOrder {
|
|||||||
this.reviewerId,
|
this.reviewerId,
|
||||||
this.reviewerName,
|
this.reviewerName,
|
||||||
required this.status,
|
required this.status,
|
||||||
|
this.returnState = 'none',
|
||||||
this.orderDate,
|
this.orderDate,
|
||||||
this.reviewedAt,
|
this.reviewedAt,
|
||||||
this.totalAmount,
|
this.totalAmount,
|
||||||
@@ -136,6 +147,7 @@ class StockInOrder {
|
|||||||
: null,
|
: null,
|
||||||
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name'] as String?,
|
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name'] as String?,
|
||||||
status: json['status'] as String,
|
status: json['status'] as String,
|
||||||
|
returnState: json['return_state'] as String? ?? 'none',
|
||||||
orderDate: json['order_date'] as String?,
|
orderDate: json['order_date'] as String?,
|
||||||
reviewedAt: json['reviewed_at'] as String?,
|
reviewedAt: json['reviewed_at'] as String?,
|
||||||
totalAmount: json['total_amount'] != null
|
totalAmount: json['total_amount'] != null
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
class StockOutItem {
|
class StockOutItem {
|
||||||
|
final int? id;
|
||||||
final int? orderId;
|
final int? orderId;
|
||||||
final int productId;
|
final int productId;
|
||||||
final double quantity;
|
final double quantity;
|
||||||
final double unitPrice;
|
final double unitPrice;
|
||||||
final double totalPrice;
|
final double totalPrice;
|
||||||
|
final double returnedQuantity;
|
||||||
final String? productName;
|
final String? productName;
|
||||||
final String? productCode;
|
final String? productCode;
|
||||||
final String? productSeries;
|
final String? productSeries;
|
||||||
final String? productSpec;
|
final String? productSpec;
|
||||||
final String? productUnit;
|
final String? productUnit;
|
||||||
|
|
||||||
|
bool get isReturned => returnedQuantity >= quantity && quantity > 0;
|
||||||
|
|
||||||
const StockOutItem({
|
const StockOutItem({
|
||||||
|
this.id,
|
||||||
this.orderId,
|
this.orderId,
|
||||||
required this.productId,
|
required this.productId,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
required this.totalPrice,
|
required this.totalPrice,
|
||||||
|
this.returnedQuantity = 0,
|
||||||
this.productName,
|
this.productName,
|
||||||
this.productCode,
|
this.productCode,
|
||||||
this.productSeries,
|
this.productSeries,
|
||||||
@@ -34,6 +40,7 @@ class StockOutItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return StockOutItem(
|
return StockOutItem(
|
||||||
|
id: json['id'] != null ? (json['id'] as num).toInt() : null,
|
||||||
orderId: json['order_id'] != null
|
orderId: json['order_id'] != null
|
||||||
? (json['order_id'] as num).toInt()
|
? (json['order_id'] as num).toInt()
|
||||||
: null,
|
: null,
|
||||||
@@ -41,6 +48,7 @@ class StockOutItem {
|
|||||||
quantity: (json['quantity'] as num).toDouble(),
|
quantity: (json['quantity'] as num).toDouble(),
|
||||||
unitPrice: (json['unit_price'] as num).toDouble(),
|
unitPrice: (json['unit_price'] as num).toDouble(),
|
||||||
totalPrice: (json['total_price'] as num).toDouble(),
|
totalPrice: (json['total_price'] as num).toDouble(),
|
||||||
|
returnedQuantity: (json['returned_quantity'] as num?)?.toDouble() ?? 0,
|
||||||
productName: lineOrProduct('product_name', 'name'),
|
productName: lineOrProduct('product_name', 'name'),
|
||||||
productCode: lineOrProduct('product_code', 'code'),
|
productCode: lineOrProduct('product_code', 'code'),
|
||||||
productSeries: lineOrProduct('series', 'series'),
|
productSeries: lineOrProduct('series', 'series'),
|
||||||
@@ -63,6 +71,7 @@ class StockOutOrder {
|
|||||||
final int? reviewerId;
|
final int? reviewerId;
|
||||||
final String? reviewerName;
|
final String? reviewerName;
|
||||||
final String status; // draft | pending | approved | rejected
|
final String status; // draft | pending | approved | rejected
|
||||||
|
final String returnState; // none | partial | full
|
||||||
final String? orderDate;
|
final String? orderDate;
|
||||||
final String? reviewedAt;
|
final String? reviewedAt;
|
||||||
final String? createdAt;
|
final String? createdAt;
|
||||||
@@ -83,6 +92,7 @@ class StockOutOrder {
|
|||||||
this.reviewerId,
|
this.reviewerId,
|
||||||
this.reviewerName,
|
this.reviewerName,
|
||||||
required this.status,
|
required this.status,
|
||||||
|
this.returnState = 'none',
|
||||||
this.orderDate,
|
this.orderDate,
|
||||||
this.reviewedAt,
|
this.reviewedAt,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
@@ -110,6 +120,7 @@ class StockOutOrder {
|
|||||||
: null,
|
: null,
|
||||||
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name'] as String?,
|
reviewerName: (json['reviewer'] as Map<String, dynamic>?)?['real_name'] as String?,
|
||||||
status: json['status'] as String,
|
status: json['status'] as String,
|
||||||
|
returnState: json['return_state'] as String? ?? 'none',
|
||||||
orderDate: json['order_date'] as String?,
|
orderDate: json['order_date'] as String?,
|
||||||
reviewedAt: json['reviewed_at'] as String?,
|
reviewedAt: json['reviewed_at'] as String?,
|
||||||
createdAt: json['created_at'] as String?,
|
createdAt: json['created_at'] as String?,
|
||||||
|
|||||||
@@ -128,4 +128,10 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
|
|||||||
await ref.read(stockInRepositoryProvider).withdraw(id);
|
await ref.read(stockInRepositoryProvider).withdraw(id);
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> returnItems(int id, List<int> itemIds) async {
|
||||||
|
await ref.read(stockInRepositoryProvider).returnItems(id, itemIds);
|
||||||
|
reload();
|
||||||
|
ref.invalidate(inventoryListProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,4 +128,10 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
|
|||||||
await ref.read(stockOutRepositoryProvider).withdraw(id);
|
await ref.read(stockOutRepositoryProvider).withdraw(id);
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> returnItems(int id, List<int> itemIds) async {
|
||||||
|
await ref.read(stockOutRepositoryProvider).returnItems(id, itemIds);
|
||||||
|
reload();
|
||||||
|
ref.invalidate(inventoryListProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,4 +130,16 @@ class StockInRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 退单:退掉指定明细(item ids)。入库退单对应库存从库存删除。
|
||||||
|
Future<void> returnItems(int id, List<int> itemIds) async {
|
||||||
|
try {
|
||||||
|
await _client.post('/stock-in/orders/$id/return', data: {'item_ids': itemIds});
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw AppException(
|
||||||
|
e.response?.data?['error'] as String? ?? '退单失败',
|
||||||
|
statusCode: e.response?.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,4 +130,16 @@ class StockOutRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 退单:退掉指定明细(item ids)。出库退单数量加回库存。
|
||||||
|
Future<void> returnItems(int id, List<int> itemIds) async {
|
||||||
|
try {
|
||||||
|
await _client.post('/stock-out/orders/$id/return', data: {'item_ids': itemIds});
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw AppException(
|
||||||
|
e.response?.data?['error'] as String? ?? '退单失败',
|
||||||
|
statusCode: e.response?.statusCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import '../../providers/finance_provider.dart' show financeRepositoryProvider;
|
|||||||
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
import '../../providers/shop_provider.dart' show shopInfoProvider;
|
||||||
import '../../widgets/write_guard.dart';
|
import '../../widgets/write_guard.dart';
|
||||||
import '../../widgets/order_row_actions.dart';
|
import '../../widgets/order_row_actions.dart';
|
||||||
|
import '../../widgets/order_return_dialog.dart';
|
||||||
import '../../core/auth/auth_state.dart'
|
import '../../core/auth/auth_state.dart'
|
||||||
show isAdminProvider, currentUserIdProvider;
|
show isAdminProvider, currentUserIdProvider;
|
||||||
|
|
||||||
@@ -268,7 +269,11 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
||||||
: '-'));
|
: '-'));
|
||||||
case 'status':
|
case 'status':
|
||||||
return DataCell(StatusBadge(_apiStatusToEnum(o.status)));
|
final rb = returnStateBadge(o.returnState);
|
||||||
|
return DataCell(Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
StatusBadge(_apiStatusToEnum(o.status)),
|
||||||
|
if (rb != null) ...[const SizedBox(width: 4), rb],
|
||||||
|
]));
|
||||||
case 'date':
|
case 'date':
|
||||||
return DataCell(Text(o.orderDate?.substring(0, 10) ?? '-'));
|
return DataCell(Text(o.orderDate?.substring(0, 10) ?? '-'));
|
||||||
case 'operator':
|
case 'operator':
|
||||||
@@ -531,6 +536,9 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
onApprove: () => _confirmApprove(context, o),
|
onApprove: () => _confirmApprove(context, o),
|
||||||
onReject: () => _confirmReject(context, o),
|
onReject: () => _confirmReject(context, o),
|
||||||
onWithdraw: () => _confirmWithdraw(context, o),
|
onWithdraw: () => _confirmWithdraw(context, o),
|
||||||
|
// 入库退单:仅管理员/超管
|
||||||
|
canReturn: ref.watch(isAdminProvider),
|
||||||
|
onReturn: () => _confirmReturn(context, o),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,7 +548,13 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
onTap: () => _showDetail(context, o.id),
|
onTap: () => _showDetail(context, o.id),
|
||||||
title: Text(o.orderNo,
|
title: Text(o.orderNo,
|
||||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
|
||||||
trailing: StatusBadge(_apiStatusToEnum(o.status)),
|
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
StatusBadge(_apiStatusToEnum(o.status)),
|
||||||
|
if (returnStateBadge(o.returnState) != null) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
returnStateBadge(o.returnState)!,
|
||||||
|
],
|
||||||
|
]),
|
||||||
fields: [
|
fields: [
|
||||||
MobileCardField('供应商', o.partnerName ?? '-'),
|
MobileCardField('供应商', o.partnerName ?? '-'),
|
||||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||||
@@ -797,6 +811,68 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmReturn(BuildContext context, StockInOrder o) async {
|
||||||
|
final go = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('退单'),
|
||||||
|
content: Text('确认对已审核入库单「${o.orderNo}」发起退单?将打开退单明细窗口选择要退的商品。'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(false),
|
||||||
|
child: const Text('取消')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(true),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||||
|
child: const Text('确认'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (go != true || !mounted) return;
|
||||||
|
final StockInOrder full;
|
||||||
|
try {
|
||||||
|
full = await ref.read(stockInRepositoryProvider).get(o.id);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('加载明细失败:$e'), backgroundColor: AppTheme.danger));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
final lines = full.items
|
||||||
|
.where((it) => it.id != null)
|
||||||
|
.map((it) => ReturnLine(
|
||||||
|
itemId: it.id!,
|
||||||
|
code: it.productCode ?? '',
|
||||||
|
name: it.productName ?? '',
|
||||||
|
series: it.productSeries ?? '',
|
||||||
|
spec: it.productSpec ?? '',
|
||||||
|
quantity: it.quantity,
|
||||||
|
unitPrice: it.unitPrice,
|
||||||
|
totalPrice: it.totalPrice,
|
||||||
|
alreadyReturned: it.isReturned,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
await showOrderReturnDialog(
|
||||||
|
context: context,
|
||||||
|
title: '入库单退单 — ${o.orderNo}',
|
||||||
|
isOut: false,
|
||||||
|
meta: [
|
||||||
|
('供应商', o.partnerName ?? '-'),
|
||||||
|
('仓库', o.warehouseName ?? '-'),
|
||||||
|
('入库日期', o.orderDate != null && o.orderDate!.length >= 10
|
||||||
|
? o.orderDate!.substring(0, 10)
|
||||||
|
: (o.orderDate ?? '-')),
|
||||||
|
],
|
||||||
|
lines: lines,
|
||||||
|
onSubmit: (ids) =>
|
||||||
|
ref.read(stockInListProvider.notifier).returnItems(o.id, ids),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detail dialog — fetches full order with items
|
// Detail dialog — fetches full order with items
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import '../../providers/product_provider.dart';
|
|||||||
import '../../providers/finance_provider.dart' show financeRepositoryProvider;
|
import '../../providers/finance_provider.dart' show financeRepositoryProvider;
|
||||||
import '../../widgets/write_guard.dart';
|
import '../../widgets/write_guard.dart';
|
||||||
import '../../widgets/order_row_actions.dart';
|
import '../../widgets/order_row_actions.dart';
|
||||||
|
import '../../widgets/order_return_dialog.dart';
|
||||||
import '../../core/auth/auth_state.dart'
|
import '../../core/auth/auth_state.dart'
|
||||||
show isAdminProvider, currentUserIdProvider;
|
show isAdminProvider, currentUserIdProvider;
|
||||||
|
|
||||||
@@ -269,7 +270,11 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
? '¥${o.totalAmount!.toStringAsFixed(2)}'
|
||||||
: '-'));
|
: '-'));
|
||||||
case 'status':
|
case 'status':
|
||||||
return DataCell(StatusBadge(_apiStatusToEnum(o.status)));
|
final rb = returnStateBadge(o.returnState);
|
||||||
|
return DataCell(Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
StatusBadge(_apiStatusToEnum(o.status)),
|
||||||
|
if (rb != null) ...[const SizedBox(width: 4), rb],
|
||||||
|
]));
|
||||||
case 'date':
|
case 'date':
|
||||||
return DataCell(Text(o.orderDate?.substring(0, 10) ?? '-'));
|
return DataCell(Text(o.orderDate?.substring(0, 10) ?? '-'));
|
||||||
case 'created_at':
|
case 'created_at':
|
||||||
@@ -505,6 +510,11 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
onApprove: () => _confirmApprove(context, o),
|
onApprove: () => _confirmApprove(context, o),
|
||||||
onReject: () => _confirmReject(context, o),
|
onReject: () => _confirmReject(context, o),
|
||||||
onWithdraw: () => _confirmWithdraw(context, o),
|
onWithdraw: () => _confirmWithdraw(context, o),
|
||||||
|
// 出库退单:管理员/超管 或 本人单
|
||||||
|
canReturn: ref.watch(isAdminProvider) ||
|
||||||
|
(o.operatorId != null &&
|
||||||
|
o.operatorId == ref.watch(currentUserIdProvider)),
|
||||||
|
onReturn: () => _confirmReturn(context, o),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +524,13 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
onTap: () => _showDetail(context, o.id),
|
onTap: () => _showDetail(context, o.id),
|
||||||
title: Text(o.orderNo,
|
title: Text(o.orderNo,
|
||||||
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
|
style: const TextStyle(fontFamily: 'monospace', fontSize: 14)),
|
||||||
trailing: StatusBadge(_apiStatusToEnum(o.status)),
|
trailing: Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
StatusBadge(_apiStatusToEnum(o.status)),
|
||||||
|
if (returnStateBadge(o.returnState) != null) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
returnStateBadge(o.returnState)!,
|
||||||
|
],
|
||||||
|
]),
|
||||||
fields: [
|
fields: [
|
||||||
MobileCardField('客户', o.partnerName ?? '-'),
|
MobileCardField('客户', o.partnerName ?? '-'),
|
||||||
MobileCardField('仓库', o.warehouseName ?? '-'),
|
MobileCardField('仓库', o.warehouseName ?? '-'),
|
||||||
@@ -776,6 +792,68 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmReturn(BuildContext context, StockOutOrder o) async {
|
||||||
|
final go = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (ctx) => AlertDialog(
|
||||||
|
title: const Text('退单'),
|
||||||
|
content: Text('确认对已审核出库单「${o.orderNo}」发起退单?将打开退单明细窗口选择要退的商品。'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(false),
|
||||||
|
child: const Text('取消')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(ctx).pop(true),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||||
|
child: const Text('确认'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (go != true || !mounted) return;
|
||||||
|
final StockOutOrder full;
|
||||||
|
try {
|
||||||
|
full = await ref.read(stockOutRepositoryProvider).get(o.id);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('加载明细失败:$e'), backgroundColor: AppTheme.danger));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!mounted) return;
|
||||||
|
final lines = full.items
|
||||||
|
.where((it) => it.id != null)
|
||||||
|
.map((it) => ReturnLine(
|
||||||
|
itemId: it.id!,
|
||||||
|
code: it.productCode ?? '',
|
||||||
|
name: it.productName ?? '',
|
||||||
|
series: it.productSeries ?? '',
|
||||||
|
spec: it.productSpec ?? '',
|
||||||
|
quantity: it.quantity,
|
||||||
|
unitPrice: it.unitPrice,
|
||||||
|
totalPrice: it.totalPrice,
|
||||||
|
alreadyReturned: it.isReturned,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
await showOrderReturnDialog(
|
||||||
|
context: context,
|
||||||
|
title: '出库单退单 — ${o.orderNo}',
|
||||||
|
isOut: true,
|
||||||
|
meta: [
|
||||||
|
('客户', o.partnerName ?? '-'),
|
||||||
|
('仓库', o.warehouseName ?? '-'),
|
||||||
|
('出库日期', o.orderDate != null && o.orderDate!.length >= 10
|
||||||
|
? o.orderDate!.substring(0, 10)
|
||||||
|
: (o.orderDate ?? '-')),
|
||||||
|
],
|
||||||
|
lines: lines,
|
||||||
|
onSubmit: (ids) =>
|
||||||
|
ref.read(stockOutListProvider.notifier).returnItems(o.id, ids),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detail dialog — fetches full order with items
|
// Detail dialog — fetches full order with items
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../core/responsive/responsive.dart';
|
||||||
|
import '../core/theme/app_theme.dart';
|
||||||
|
import '../core/utils/dialog_util.dart';
|
||||||
|
|
||||||
|
/// 退单状态小徽章(none 返回 null 不显示)。partial=部分退单(amber),full=已退单(红)。
|
||||||
|
Widget? returnStateBadge(String state) {
|
||||||
|
Color? c;
|
||||||
|
String? t;
|
||||||
|
if (state == 'partial') {
|
||||||
|
c = AppTheme.warning;
|
||||||
|
t = '部分退单';
|
||||||
|
} else if (state == 'full') {
|
||||||
|
c = AppTheme.danger;
|
||||||
|
t = '已退单';
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: c.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: Text(t,
|
||||||
|
style: TextStyle(fontSize: 10, color: c, fontWeight: FontWeight.w600)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 退单弹窗的一条明细。
|
||||||
|
class ReturnLine {
|
||||||
|
final int itemId;
|
||||||
|
final String code;
|
||||||
|
final String name;
|
||||||
|
final String series;
|
||||||
|
final String spec;
|
||||||
|
final double quantity;
|
||||||
|
final double unitPrice;
|
||||||
|
final double totalPrice;
|
||||||
|
final bool alreadyReturned; // 之前已退过(不可再选)
|
||||||
|
|
||||||
|
const ReturnLine({
|
||||||
|
required this.itemId,
|
||||||
|
required this.code,
|
||||||
|
required this.name,
|
||||||
|
required this.series,
|
||||||
|
required this.spec,
|
||||||
|
required this.quantity,
|
||||||
|
required this.unitPrice,
|
||||||
|
required this.totalPrice,
|
||||||
|
this.alreadyReturned = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 打开退单弹窗。提交成功返回 true(调用方据此刷新列表),取消返回 null。
|
||||||
|
/// - [isOut] true=出库(退回库存)/ false=入库(从库存删除),影响文案。
|
||||||
|
/// - [onSubmit] 收到选中的 itemId 列表,向后端提交;抛异常表示失败。
|
||||||
|
Future<bool?> showOrderReturnDialog({
|
||||||
|
required BuildContext context,
|
||||||
|
required String title,
|
||||||
|
required List<(String, String)> meta,
|
||||||
|
required bool isOut,
|
||||||
|
required List<ReturnLine> lines,
|
||||||
|
required Future<void> Function(List<int> itemIds) onSubmit,
|
||||||
|
}) {
|
||||||
|
return showAppDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => _OrderReturnDialog(
|
||||||
|
title: title,
|
||||||
|
meta: meta,
|
||||||
|
isOut: isOut,
|
||||||
|
lines: lines,
|
||||||
|
onSubmit: onSubmit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrderReturnDialog extends StatefulWidget {
|
||||||
|
final String title;
|
||||||
|
final List<(String, String)> meta;
|
||||||
|
final bool isOut;
|
||||||
|
final List<ReturnLine> lines;
|
||||||
|
final Future<void> Function(List<int> itemIds) onSubmit;
|
||||||
|
|
||||||
|
const _OrderReturnDialog({
|
||||||
|
required this.title,
|
||||||
|
required this.meta,
|
||||||
|
required this.isOut,
|
||||||
|
required this.lines,
|
||||||
|
required this.onSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_OrderReturnDialog> createState() => _OrderReturnDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _OrderReturnDialogState extends State<_OrderReturnDialog> {
|
||||||
|
final Set<int> _staged = {}; // 本次暂存要退的 itemId
|
||||||
|
bool _submitting = false;
|
||||||
|
|
||||||
|
bool _isReturned(ReturnLine l) => l.alreadyReturned || _staged.contains(l.itemId);
|
||||||
|
|
||||||
|
List<ReturnLine> get _selectable =>
|
||||||
|
widget.lines.where((l) => !l.alreadyReturned).toList();
|
||||||
|
|
||||||
|
Future<void> _confirmLine(ReturnLine l) async {
|
||||||
|
final ok = await showAppDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('退单确认', style: TextStyle(fontSize: 16)),
|
||||||
|
content: RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: const TextStyle(fontSize: 13, color: AppTheme.textPrimary, height: 1.6),
|
||||||
|
children: [
|
||||||
|
const TextSpan(text: '确认退掉以下明细?\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: l.name,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.w700)),
|
||||||
|
TextSpan(text: ' · ${l.series} · ${l.spec}\n'),
|
||||||
|
TextSpan(
|
||||||
|
text: '编码 ${l.code} · 数量 ${_q(l.quantity)}',
|
||||||
|
style: const TextStyle(color: AppTheme.textSecondary, fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('取消')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||||
|
child: const Text('退单'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ok == true) setState(() => _staged.add(l.itemId));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _confirmAll() async {
|
||||||
|
final pending = _selectable.where((l) => !_staged.contains(l.itemId)).toList();
|
||||||
|
if (pending.isEmpty) return;
|
||||||
|
final ok = await showAppDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('全部退单确认', style: TextStyle(fontSize: 16)),
|
||||||
|
content: Text('确认退掉本单全部 ${pending.length} 条未退明细?',
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('取消')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||||
|
child: const Text('全部退单'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ok == true) {
|
||||||
|
setState(() => _staged.addAll(pending.map((e) => e.itemId)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
if (_staged.isEmpty) return;
|
||||||
|
final ok = await showAppDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('提交退单', style: TextStyle(fontSize: 16)),
|
||||||
|
content: Text(
|
||||||
|
'确认提交退单?${widget.isOut ? '退回库存' : '从库存删除'} 将立即生效,且不可撤销。',
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context, false),
|
||||||
|
child: const Text('取消')),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.danger, foregroundColor: Colors.white),
|
||||||
|
child: const Text('提交退单'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (ok != true) return;
|
||||||
|
setState(() => _submitting = true);
|
||||||
|
try {
|
||||||
|
await widget.onSubmit(_staged.toList());
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('退单成功'), backgroundColor: AppTheme.success));
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _submitting = false);
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('退单失败:$e'), backgroundColor: AppTheme.danger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final stagedQty = widget.lines
|
||||||
|
.where((l) => _staged.contains(l.itemId))
|
||||||
|
.fold<double>(0, (s, l) => s + l.quantity);
|
||||||
|
|
||||||
|
return Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: SizedBox(
|
||||||
|
width: context.dialogWidth(820),
|
||||||
|
height: 560,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 头部
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 12, 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(widget.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15, fontWeight: FontWeight.w600),
|
||||||
|
overflow: TextOverflow.ellipsis),
|
||||||
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _submitting ? null : _confirmAll,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppTheme.danger,
|
||||||
|
side: const BorderSide(color: AppTheme.danger),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
child: const Text('全部退单', style: TextStyle(fontSize: 12)),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 20),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
// 单据信息
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 20,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
for (final (k, v) in widget.meta)
|
||||||
|
Text.rich(TextSpan(
|
||||||
|
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
|
||||||
|
children: [
|
||||||
|
TextSpan(text: '$k:'),
|
||||||
|
TextSpan(text: v, style: const TextStyle(color: AppTheme.textPrimary)),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 行为提示
|
||||||
|
Container(
|
||||||
|
width: double.infinity,
|
||||||
|
margin: const EdgeInsets.fromLTRB(16, 2, 16, 8),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF0F6FF),
|
||||||
|
border: const Border(left: BorderSide(color: AppTheme.primary, width: 3)),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.isOut
|
||||||
|
? '提交后,已退单明细的数量将「加回库存」。退单的明细不会删除,以红色「已退单」标注。'
|
||||||
|
: '提交后,已退单明细对应的库存将「从库存删除」。退单的明细不会删除,以红色「已退单」标注。',
|
||||||
|
style: const TextStyle(fontSize: 12, color: AppTheme.primaryDark),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 明细列表
|
||||||
|
Expanded(
|
||||||
|
child: ListView.separated(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
itemCount: widget.lines.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (_, i) => _lineTile(widget.lines[i], i),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1),
|
||||||
|
// 底部
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 10, 16, 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
_staged.isEmpty
|
||||||
|
? '未选择退单明细'
|
||||||
|
: '已退单 ${_staged.length} 项 · ${widget.isOut ? '退回库存 +' : '从库存移除 −'}${_q(stagedQty)} 件',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: _staged.isEmpty
|
||||||
|
? AppTheme.textSecondary
|
||||||
|
: AppTheme.danger,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: _submitting ? null : () => Navigator.pop(context),
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: (_staged.isEmpty || _submitting) ? null : _submit,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppTheme.danger,
|
||||||
|
foregroundColor: Colors.white),
|
||||||
|
child: _submitting
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: Colors.white))
|
||||||
|
: const Text('提交退单'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _lineTile(ReturnLine l, int idx) {
|
||||||
|
final returned = _isReturned(l);
|
||||||
|
final nameStyle = TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: returned ? AppTheme.danger : AppTheme.textPrimary,
|
||||||
|
decoration: returned ? TextDecoration.lineThrough : null,
|
||||||
|
);
|
||||||
|
final subStyle = TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: returned ? AppTheme.danger : AppTheme.textSecondary,
|
||||||
|
decoration: returned ? TextDecoration.lineThrough : null,
|
||||||
|
);
|
||||||
|
return Container(
|
||||||
|
color: returned ? const Color(0xFFFFF4F4) : null,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 9),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
child: Text('${idx + 1}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: AppTheme.textSecondary))),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(children: [
|
||||||
|
Flexible(child: Text(l.name, style: nameStyle, overflow: TextOverflow.ellipsis)),
|
||||||
|
if (returned) ...[
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppTheme.danger,
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
|
child: const Text('已退单',
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
'${l.code} · ${l.series} · ${l.spec} · ${_q(l.quantity)}×¥${l.unitPrice.toStringAsFixed(2)} = ¥${l.totalPrice.toStringAsFixed(2)}',
|
||||||
|
style: subStyle,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: 64,
|
||||||
|
child: l.alreadyReturned
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: (_staged.contains(l.itemId)
|
||||||
|
? TextButton(
|
||||||
|
onPressed: _submitting
|
||||||
|
? null
|
||||||
|
: () => setState(() => _staged.remove(l.itemId)),
|
||||||
|
child: const Text('撤销',
|
||||||
|
style: TextStyle(fontSize: 12, color: AppTheme.primary)),
|
||||||
|
)
|
||||||
|
: OutlinedButton(
|
||||||
|
onPressed: _submitting ? null : () => _confirmLine(l),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppTheme.danger,
|
||||||
|
side: const BorderSide(color: AppTheme.danger),
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
),
|
||||||
|
child: const Text('退单', style: TextStyle(fontSize: 12)),
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _q(double v) =>
|
||||||
|
v == v.roundToDouble() ? v.toInt().toString() : v.toString();
|
||||||
|
}
|
||||||
@@ -24,7 +24,9 @@ List<Widget> buildOrderRowActions({
|
|||||||
required VoidCallback onApprove,
|
required VoidCallback onApprove,
|
||||||
required VoidCallback onReject,
|
required VoidCallback onReject,
|
||||||
required VoidCallback onWithdraw,
|
required VoidCallback onWithdraw,
|
||||||
|
required VoidCallback onReturn,
|
||||||
bool canWithdraw = false,
|
bool canWithdraw = false,
|
||||||
|
bool canReturn = false,
|
||||||
List<Widget> afterPrint = const [],
|
List<Widget> afterPrint = const [],
|
||||||
}) {
|
}) {
|
||||||
TextButton btn(String text, Color color, VoidCallback onPressed, {Key? key}) =>
|
TextButton btn(String text, Color color, VoidCallback onPressed, {Key? key}) =>
|
||||||
@@ -39,6 +41,11 @@ List<Widget> buildOrderRowActions({
|
|||||||
...afterPrint,
|
...afterPrint,
|
||||||
if (!readonly && status == 'approved')
|
if (!readonly && status == 'approved')
|
||||||
WriteGuard(child: btn('结清', AppTheme.accent, onSettle)),
|
WriteGuard(child: btn('结清', AppTheme.accent, onSettle)),
|
||||||
|
// 退单(已审核单退货):error 样式,按权限显示
|
||||||
|
if (!readonly && status == 'approved' && canReturn)
|
||||||
|
WriteGuard(
|
||||||
|
child: btn('退单', AppTheme.danger, onReturn,
|
||||||
|
key: Key('btn_return_$orderId'))),
|
||||||
if (!readonly && status == 'draft') ...[
|
if (!readonly && status == 'draft') ...[
|
||||||
WriteGuard(child: btn('修改', AppTheme.primary, onEdit)),
|
WriteGuard(child: btn('修改', AppTheme.primary, onEdit)),
|
||||||
WriteGuard(child: btn('删除', AppTheme.danger, onDelete)),
|
WriteGuard(child: btn('删除', AppTheme.danger, onDelete)),
|
||||||
|
|||||||
Reference in New Issue
Block a user