Compare commits

...

2 Commits

Author SHA1 Message Date
wangjia 08f9e20010 chore: release server-v1.0.76
Deploy Server / release-deploy-server (push) Successful in 1m30s
修复编辑入库单草稿后合计金额被清零(Update 漏累加 total)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
2026-06-22 20:15:38 +08:00
wangjia 2fac1aff66 chore: release client-v1.0.73
Deploy Client / build-client-web (push) Successful in 40s
Deploy Client / build-windows (push) Successful in 2m7s
Deploy Client / build-macos (push) Successful in 2m11s
Deploy Client / build-android (push) Successful in 1m8s
Deploy Client / build-ios (push) Successful in 2m48s
Deploy Client / release-deploy-client (push) Successful in 1m26s
出库选货弹窗改为分页加载(默认20条+库存总数+翻页)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YZ4DskSRKsSiheQonFtQvx
2026-06-22 14:26:23 +08:00
6 changed files with 118 additions and 3 deletions
+5
View File
@@ -5,6 +5,11 @@ 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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.73] - 2026-06-22
### 改进
- 出库「添加商品」选货弹窗改为分页加载:默认显示前 20 条,底部显示库存总数并支持上一页/下一页翻页;弹窗打开即加载、按编码/名称/系列搜索时即时分页,避免一次拉取过多导致加载缓慢或看不到商品
## [1.0.72] - 2026-06-22
### 新功能
+5
View File
@@ -5,6 +5,11 @@
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).
## [1.0.76] - 2026-06-22
### 修复
- 修复「编辑入库单草稿后合计金额变成 0」的问题:此前每次编辑草稿会把单据合计金额错误清零(明细的单价/金额正常,仅单据头合计未重算),现已按明细正确重算。
## [1.0.75] - 2026-06-22
### 新功能
+1
View File
@@ -172,6 +172,7 @@ func (h *StockInHandler) Update(c *gin.Context) {
it.ProductID = prod.ID
it.ProductCode = prod.Code
it.TotalPrice = it.Quantity * it.UnitPrice
total += it.TotalPrice
}
updates := map[string]interface{}{
"warehouse_id": req.WarehouseID,
+38
View File
@@ -272,6 +272,44 @@ func TestStockInHandler_TotalAmount(t *testing.T) {
assert.Equal(t, float64(110), data["total_amount"])
}
// 回归:编辑草稿入库单后 total_amount 必须按新明细重算(此前 Update 漏了累加,编辑后被清成 0)。
func TestStockInHandler_UpdateRecomputesTotal(t *testing.T) {
db := testutil.SetupTestDB()
shop := testutil.CreateTestShop(db, "SI_UPD")
user := testutil.CreateTestUser(db, shop.ID, "admin", "pass", "admin")
warehouse := testutil.CreateTestWarehouse(db, shop.ID, "Warehouse")
token := getAuthToken(user.ID, shop.ID, "admin")
r := setupProtectedRouter(db)
// 建草稿:1 行 10×5 = 50
w := makeRequest(r, "POST", "/api/v1/stock-in/orders", token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_name": "茅台", "series": "飞天", "spec": "500ml", "quantity": 10.0, "unit_price": 5.0},
},
})
require.Equal(t, http.StatusCreated, w.Code)
orderID := extractID(w)
// 编辑:改成 2 行 12×3100 + 1×4500 = 41700
w = makeRequest(r, "PUT", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), token, map[string]interface{}{
"warehouse_id": warehouse.ID,
"order_date": time.Now().Format(time.RFC3339),
"items": []map[string]interface{}{
{"product_name": "茅台2009", "series": "大件", "spec": "500ml", "quantity": 12.0, "unit_price": 3100.0},
{"product_name": "茅台十五年", "series": "大件", "spec": "500ml", "quantity": 1.0, "unit_price": 4500.0},
},
})
require.Equal(t, http.StatusOK, w.Code)
// 取详情核对 total_amount 已重算(不是 0
w = makeRequest(r, "GET", fmt.Sprintf("/api/v1/stock-in/orders/%d", orderID), token, nil)
require.Equal(t, http.StatusOK, w.Code)
data := parseResponse(w)["data"].(map[string]interface{})
assert.Equal(t, float64(41700), data["total_amount"], "编辑后金额应按新明细重算")
}
func TestStockInHandler_NoAuth(t *testing.T) {
db := testutil.SetupTestDB()
r := setupProtectedRouter(db)
@@ -60,6 +60,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
stockIn.GET("/orders", stockInH.List)
stockIn.GET("/orders/:id", stockInH.Get)
stockIn.POST("/orders", stockInH.Create)
stockIn.PUT("/orders/:id", stockInH.Update)
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
@@ -71,6 +72,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
stockOut.GET("/orders", stockOutH.List)
stockOut.GET("/orders/:id", stockOutH.Get)
stockOut.POST("/orders", stockOutH.Create)
stockOut.PUT("/orders/:id", stockOutH.Update)
stockOut.PUT("/orders/:id/submit", stockOutH.Submit)
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
@@ -961,6 +961,27 @@ class _InventoryPickerDialogState
};
bool _loading = false;
Timer? _debounce;
// 服务端分页:默认第 1 页 20 条;_total 为当前筛选下的库存总数
static const int _pageSize = 20;
int _page = 1;
int _total = 0;
int get _totalPages => _total <= 0 ? 1 : ((_total + _pageSize - 1) ~/ _pageSize);
@override
void initState() {
super.initState();
// 打开即拉取第 1 页(默认前 20 条),不依赖调用方预加载,避免默认空白。
// 用首帧后回调,避开 initState 同步段调用 setState。
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) _fetch();
});
}
void _goPage(int p) {
if (p < 1 || p > _totalPages || _loading) return;
setState(() => _page = p);
_fetch();
}
@override
void dispose() {
@@ -970,7 +991,11 @@ class _InventoryPickerDialogState
}
void _onSearchChanged(String v) {
setState(() => _search = v);
// 改搜索词回到第 1 页
setState(() {
_search = v;
_page = 1;
});
_debounce?.cancel();
_debounce = Timer(const Duration(milliseconds: 300), _fetch);
}
@@ -982,13 +1007,19 @@ class _InventoryPickerDialogState
final result = await ref.read(inventoryRepositoryProvider).listInventory(
warehouseId: widget.warehouseId,
keyword: kw.isEmpty ? null : kw,
pageSize: AppConstants.stockOutPickerPageSize,
page: _page,
pageSize: _pageSize,
);
final items = _aggregatePickerItems(result.data);
for (final it in items) {
_known[it.productId] = it;
}
if (mounted) setState(() => _results = items);
if (mounted) {
setState(() {
_results = items;
_total = result.total;
});
}
} catch (_) {
// 忽略:保留上次结果
} finally {
@@ -1161,6 +1192,39 @@ class _InventoryPickerDialogState
}),
child: Text(allSelected ? '取消全选' : '全选当前'),
),
const SizedBox(width: 8),
// 库存总数汇总(无搜索=整仓总数,搜索时=匹配总数)
Text(
_loading ? '加载中…' : '$_total 项库存商品',
style: const TextStyle(
fontSize: 13, color: AppTheme.textSecondary),
),
const SizedBox(width: 12),
// 翻页
IconButton(
icon: const Icon(Icons.chevron_left, size: 20),
onPressed: (_page > 1 && !_loading)
? () => _goPage(_page - 1)
: null,
tooltip: '上一页',
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 32, minHeight: 32),
),
Text('$_page / $_totalPages',
style: const TextStyle(fontSize: 13)),
IconButton(
icon: const Icon(Icons.chevron_right, size: 20),
onPressed: (_page < _totalPages && !_loading)
? () => _goPage(_page + 1)
: null,
tooltip: '下一页',
visualDensity: VisualDensity.compact,
padding: EdgeInsets.zero,
constraints:
const BoxConstraints(minWidth: 32, minHeight: 32),
),
const Spacer(),
OutlinedButton(
onPressed: () => Navigator.pop(context),