feat: 4项需求实现

1. 供应商/客户编码自动生成(S001/C001...),新建表单隐藏编码字段,编辑保留
2. 财务管理"全部记录"移为第一个 tab
3. 入库单列表 tab 排除 pending 单据,提交后自动移入审核 tab
4. 出库单列表同上;出库表单按仓库+商品自动填最大可用库存数量

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-04-10 22:37:19 +08:00
parent 66e54af8c6
commit 53a60d230f
6 changed files with 72 additions and 29 deletions
+13
View File
@@ -1,6 +1,7 @@
package handler
import (
"fmt"
"net/http"
"strconv"
@@ -51,6 +52,18 @@ func (h *PartnerHandler) Create(c *gin.Context) {
return
}
p.ShopID = shopID
// 编码未提供时自动生成:供应商 S001/S002…,客户 C001/C002…
if p.Code == "" {
prefix := "S"
if p.Type == "customer" {
prefix = "C"
}
var count int64
h.db.Model(&model.Partner{}).
Where("shop_id = ? AND type = ? AND deleted_at IS NULL", shopID, p.Type).
Count(&count)
p.Code = fmt.Sprintf("%s%03d", prefix, count+1)
}
if err := h.db.Create(&p).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
@@ -14,14 +14,14 @@ class FinanceScreen extends ConsumerWidget {
return PageScaffold(
title: '财务管理',
tabs: const [
Tab(text: '全部记录'),
Tab(text: '应付账款'),
Tab(text: '应收账款'),
Tab(text: '全部记录'),
],
tabViews: [
_FinanceTab(typeFilter: ''),
_FinanceTab(typeFilter: 'payable'),
_FinanceTab(typeFilter: 'receivable'),
_FinanceTab(typeFilter: ''),
],
);
}
@@ -416,6 +416,7 @@ class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
),
const SizedBox(height: 12),
Row(children: [
if (isEdit) ...[
Expanded(
child: TextFormField(
controller: _codeCtrl,
@@ -423,6 +424,7 @@ class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
),
),
const SizedBox(width: 12),
],
Expanded(
child: TextFormField(
controller: _contactCtrl,
@@ -51,7 +51,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
],
tabViews: [
_buildListTab(filterStatus: 'pending', showNewButton: true),
_buildListTab(filterStatus: null, showNewButton: false),
_buildListTab(filterStatus: 'exclude_pending', showNewButton: false),
],
);
}
@@ -76,16 +76,19 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
),
),
data: (result) {
final orders = filterStatus != null
? result.data
.where((o) => o.status == filterStatus)
.toList()
: result.data;
final List<StockInOrder> orders;
if (filterStatus == 'pending') {
orders = result.data.where((o) => o.status == 'pending').toList();
} else if (filterStatus == 'exclude_pending') {
orders = result.data.where((o) => o.status != 'pending').toList();
} else {
orders = result.data;
}
return _buildOrderTable(
orders: orders,
totalCount: filterStatus != null ? orders.length : result.total,
totalCount: orders.length,
page: result.page,
showStatusFilter: filterStatus == null,
showStatusFilter: filterStatus == 'exclude_pending',
showNewButton: showNewButton,
);
},
@@ -611,7 +614,6 @@ class _StatusFilterDropdown extends StatelessWidget {
items: [
const DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'draft', child: Text('草稿', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'pending', child: Text('待审核', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'approved', child: Text('已审核', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'rejected', child: Text('已拒绝', style: TextStyle(fontSize: 13))),
],
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../models/product.dart';
import '../../providers/inventory_provider.dart';
import '../../providers/partner_provider.dart';
import '../../providers/product_provider.dart';
import '../../providers/stock_out_provider.dart';
@@ -23,6 +24,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
int? _partnerId;
DateTime _orderDate = DateTime.now();
bool _submitting = false;
// productId → available quantity in selected warehouse
Map<int, double> _inventoryMap = {};
final List<_ItemRow> _items = [];
@@ -51,6 +54,19 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
return total;
}
Future<void> _loadInventory(int warehouseId) async {
try {
final result = await ref
.read(inventoryRepositoryProvider)
.listInventory(warehouseId: warehouseId, pageSize: 500);
setState(() {
_inventoryMap = {
for (final inv in result.data) inv.productId: inv.quantity
};
});
} catch (_) {}
}
void _addItem() {
setState(() => _items.add(_ItemRow()));
}
@@ -219,8 +235,10 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
style: const TextStyle(
fontSize: 13))))
.toList(),
onChanged: (v) => setState(
() => _warehouseId = v),
onChanged: (v) {
setState(() => _warehouseId = v);
if (v != null) _loadInventory(v);
},
validator: (v) =>
v == null ? '不能为空' : null,
decoration: const InputDecoration(),
@@ -435,8 +453,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
onChanged: (v) {
setState(() {
item.productId = v;
// Auto-fill sale price if available
if (v != null) {
// Auto-fill sale price
final found = result.data.firstWhere(
(p) => p.id == v,
orElse: () => Product(
@@ -445,6 +463,12 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
item.priceCtrl.text =
found.salePrice!.toStringAsFixed(2);
}
// Auto-fill max available quantity from inventory
final available = _inventoryMap[v] ?? 0;
if (available > 0) {
item.qtyCtrl.text =
available.toStringAsFixed(0);
}
}
});
},
@@ -52,7 +52,7 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
],
tabViews: [
_buildListTab(filterStatus: 'pending', showNewButton: true),
_buildListTab(filterStatus: null, showNewButton: false),
_buildListTab(filterStatus: 'exclude_pending', showNewButton: false),
],
);
}
@@ -77,16 +77,19 @@ class _StockOutListScreenState extends ConsumerState<StockOutListScreen> {
),
),
data: (result) {
final orders = filterStatus != null
? result.data
.where((o) => o.status == filterStatus)
.toList()
: result.data;
final List<StockOutOrder> orders;
if (filterStatus == 'pending') {
orders = result.data.where((o) => o.status == 'pending').toList();
} else if (filterStatus == 'exclude_pending') {
orders = result.data.where((o) => o.status != 'pending').toList();
} else {
orders = result.data;
}
return _buildOrderTable(
orders: orders,
totalCount: filterStatus != null ? orders.length : result.total,
totalCount: orders.length,
page: result.page,
showStatusFilter: filterStatus == null,
showStatusFilter: filterStatus == 'exclude_pending',
showNewButton: showNewButton,
);
},
@@ -615,7 +618,6 @@ class _StatusFilterDropdown extends StatelessWidget {
items: const [
DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'draft', child: Text('草稿', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'pending', child: Text('待审核', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'approved', child: Text('已审核', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'rejected', child: Text('已拒绝', style: TextStyle(fontSize: 13))),
],