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 package handler
import ( import (
"fmt"
"net/http" "net/http"
"strconv" "strconv"
@@ -51,6 +52,18 @@ func (h *PartnerHandler) Create(c *gin.Context) {
return return
} }
p.ShopID = shopID 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 { if err := h.db.Create(&p).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return return
@@ -14,14 +14,14 @@ class FinanceScreen extends ConsumerWidget {
return PageScaffold( return PageScaffold(
title: '财务管理', title: '财务管理',
tabs: const [ tabs: const [
Tab(text: '全部记录'),
Tab(text: '应付账款'), Tab(text: '应付账款'),
Tab(text: '应收账款'), Tab(text: '应收账款'),
Tab(text: '全部记录'),
], ],
tabViews: [ tabViews: [
_FinanceTab(typeFilter: ''),
_FinanceTab(typeFilter: 'payable'), _FinanceTab(typeFilter: 'payable'),
_FinanceTab(typeFilter: 'receivable'), _FinanceTab(typeFilter: 'receivable'),
_FinanceTab(typeFilter: ''),
], ],
); );
} }
@@ -416,13 +416,15 @@ class _PartnerFormDialogState extends ConsumerState<_PartnerFormDialog> {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row(children: [ Row(children: [
Expanded( if (isEdit) ...[
child: TextFormField( Expanded(
controller: _codeCtrl, child: TextFormField(
decoration: const InputDecoration(labelText: '编码'), controller: _codeCtrl,
decoration: const InputDecoration(labelText: '编码'),
),
), ),
), const SizedBox(width: 12),
const SizedBox(width: 12), ],
Expanded( Expanded(
child: TextFormField( child: TextFormField(
controller: _contactCtrl, controller: _contactCtrl,
@@ -51,7 +51,7 @@ class _StockInListScreenState extends ConsumerState<StockInListScreen> {
], ],
tabViews: [ tabViews: [
_buildListTab(filterStatus: 'pending', showNewButton: true), _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) { data: (result) {
final orders = filterStatus != null final List<StockInOrder> orders;
? result.data if (filterStatus == 'pending') {
.where((o) => o.status == filterStatus) orders = result.data.where((o) => o.status == 'pending').toList();
.toList() } else if (filterStatus == 'exclude_pending') {
: result.data; orders = result.data.where((o) => o.status != 'pending').toList();
} else {
orders = result.data;
}
return _buildOrderTable( return _buildOrderTable(
orders: orders, orders: orders,
totalCount: filterStatus != null ? orders.length : result.total, totalCount: orders.length,
page: result.page, page: result.page,
showStatusFilter: filterStatus == null, showStatusFilter: filterStatus == 'exclude_pending',
showNewButton: showNewButton, showNewButton: showNewButton,
); );
}, },
@@ -611,7 +614,6 @@ class _StatusFilterDropdown extends StatelessWidget {
items: [ items: [
const DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))), const DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'draft', 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: 'approved', child: Text('已审核', style: TextStyle(fontSize: 13))),
const DropdownMenuItem(value: 'rejected', 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 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart'; import '../../core/theme/app_theme.dart';
import '../../models/product.dart'; import '../../models/product.dart';
import '../../providers/inventory_provider.dart';
import '../../providers/partner_provider.dart'; import '../../providers/partner_provider.dart';
import '../../providers/product_provider.dart'; import '../../providers/product_provider.dart';
import '../../providers/stock_out_provider.dart'; import '../../providers/stock_out_provider.dart';
@@ -23,6 +24,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
int? _partnerId; int? _partnerId;
DateTime _orderDate = DateTime.now(); DateTime _orderDate = DateTime.now();
bool _submitting = false; bool _submitting = false;
// productId → available quantity in selected warehouse
Map<int, double> _inventoryMap = {};
final List<_ItemRow> _items = []; final List<_ItemRow> _items = [];
@@ -51,6 +54,19 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
return total; 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() { void _addItem() {
setState(() => _items.add(_ItemRow())); setState(() => _items.add(_ItemRow()));
} }
@@ -219,8 +235,10 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
style: const TextStyle( style: const TextStyle(
fontSize: 13)))) fontSize: 13))))
.toList(), .toList(),
onChanged: (v) => setState( onChanged: (v) {
() => _warehouseId = v), setState(() => _warehouseId = v);
if (v != null) _loadInventory(v);
},
validator: (v) => validator: (v) =>
v == null ? '不能为空' : null, v == null ? '不能为空' : null,
decoration: const InputDecoration(), decoration: const InputDecoration(),
@@ -435,8 +453,8 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
onChanged: (v) { onChanged: (v) {
setState(() { setState(() {
item.productId = v; item.productId = v;
// Auto-fill sale price if available
if (v != null) { if (v != null) {
// Auto-fill sale price
final found = result.data.firstWhere( final found = result.data.firstWhere(
(p) => p.id == v, (p) => p.id == v,
orElse: () => Product( orElse: () => Product(
@@ -445,6 +463,12 @@ class _StockOutFormScreenState extends ConsumerState<StockOutFormScreen> {
item.priceCtrl.text = item.priceCtrl.text =
found.salePrice!.toStringAsFixed(2); 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: [ tabViews: [
_buildListTab(filterStatus: 'pending', showNewButton: true), _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) { data: (result) {
final orders = filterStatus != null final List<StockOutOrder> orders;
? result.data if (filterStatus == 'pending') {
.where((o) => o.status == filterStatus) orders = result.data.where((o) => o.status == 'pending').toList();
.toList() } else if (filterStatus == 'exclude_pending') {
: result.data; orders = result.data.where((o) => o.status != 'pending').toList();
} else {
orders = result.data;
}
return _buildOrderTable( return _buildOrderTable(
orders: orders, orders: orders,
totalCount: filterStatus != null ? orders.length : result.total, totalCount: orders.length,
page: result.page, page: result.page,
showStatusFilter: filterStatus == null, showStatusFilter: filterStatus == 'exclude_pending',
showNewButton: showNewButton, showNewButton: showNewButton,
); );
}, },
@@ -615,7 +618,6 @@ class _StatusFilterDropdown extends StatelessWidget {
items: const [ items: const [
DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))), DropdownMenuItem(value: '', child: Text('全部状态', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'draft', 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: 'approved', child: Text('已审核', style: TextStyle(fontSize: 13))),
DropdownMenuItem(value: 'rejected', child: Text('已拒绝', style: TextStyle(fontSize: 13))), DropdownMenuItem(value: 'rejected', child: Text('已拒绝', style: TextStyle(fontSize: 13))),
], ],