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:
@@ -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))),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user