Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 078fab30f0 | |||
| 08f9e20010 |
@@ -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/),
|
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.74] - 2026-06-22
|
||||||
|
|
||||||
|
### 改进
|
||||||
|
- 入库单 / 出库单详情页的商品明细表新增「生产日期」「批次号」两列,便于核对货品批次信息
|
||||||
|
|
||||||
## [1.0.73] - 2026-06-22
|
## [1.0.73] - 2026-06-22
|
||||||
|
|
||||||
### 改进
|
### 改进
|
||||||
|
|||||||
@@ -5,6 +5,11 @@
|
|||||||
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.76] - 2026-06-22
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- 修复「编辑入库单草稿后合计金额变成 0」的问题:此前每次编辑草稿会把单据合计金额错误清零(明细的单价/金额正常,仅单据头合计未重算),现已按明细正确重算。
|
||||||
|
|
||||||
## [1.0.75] - 2026-06-22
|
## [1.0.75] - 2026-06-22
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ func (h *StockInHandler) Update(c *gin.Context) {
|
|||||||
it.ProductID = prod.ID
|
it.ProductID = prod.ID
|
||||||
it.ProductCode = prod.Code
|
it.ProductCode = prod.Code
|
||||||
it.TotalPrice = it.Quantity * it.UnitPrice
|
it.TotalPrice = it.Quantity * it.UnitPrice
|
||||||
|
total += it.TotalPrice
|
||||||
}
|
}
|
||||||
updates := map[string]interface{}{
|
updates := map[string]interface{}{
|
||||||
"warehouse_id": req.WarehouseID,
|
"warehouse_id": req.WarehouseID,
|
||||||
|
|||||||
@@ -272,6 +272,44 @@ func TestStockInHandler_TotalAmount(t *testing.T) {
|
|||||||
assert.Equal(t, float64(110), data["total_amount"])
|
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) {
|
func TestStockInHandler_NoAuth(t *testing.T) {
|
||||||
db := testutil.SetupTestDB()
|
db := testutil.SetupTestDB()
|
||||||
r := setupProtectedRouter(db)
|
r := setupProtectedRouter(db)
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ func setupProtectedRouter(db *gorm.DB) *gin.Engine {
|
|||||||
stockIn.GET("/orders", stockInH.List)
|
stockIn.GET("/orders", stockInH.List)
|
||||||
stockIn.GET("/orders/:id", stockInH.Get)
|
stockIn.GET("/orders/:id", stockInH.Get)
|
||||||
stockIn.POST("/orders", stockInH.Create)
|
stockIn.POST("/orders", stockInH.Create)
|
||||||
|
stockIn.PUT("/orders/:id", stockInH.Update)
|
||||||
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
|
||||||
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
|
||||||
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
|
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", stockOutH.List)
|
||||||
stockOut.GET("/orders/:id", stockOutH.Get)
|
stockOut.GET("/orders/:id", stockOutH.Get)
|
||||||
stockOut.POST("/orders", stockOutH.Create)
|
stockOut.POST("/orders", stockOutH.Create)
|
||||||
|
stockOut.PUT("/orders/:id", stockOutH.Update)
|
||||||
stockOut.PUT("/orders/:id/submit", stockOutH.Submit)
|
stockOut.PUT("/orders/:id/submit", stockOutH.Submit)
|
||||||
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
|
||||||
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class StockOutItem {
|
|||||||
final String? productSeries;
|
final String? productSeries;
|
||||||
final String? productSpec;
|
final String? productSpec;
|
||||||
final String? productUnit;
|
final String? productUnit;
|
||||||
|
final String? batchNo;
|
||||||
|
final String? productionDate;
|
||||||
|
|
||||||
bool get isReturned => returnedQuantity >= quantity && quantity > 0;
|
bool get isReturned => returnedQuantity >= quantity && quantity > 0;
|
||||||
|
|
||||||
@@ -29,6 +31,8 @@ class StockOutItem {
|
|||||||
this.productSeries,
|
this.productSeries,
|
||||||
this.productSpec,
|
this.productSpec,
|
||||||
this.productUnit,
|
this.productUnit,
|
||||||
|
this.batchNo,
|
||||||
|
this.productionDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory StockOutItem.fromJson(Map<String, dynamic> json) {
|
factory StockOutItem.fromJson(Map<String, dynamic> json) {
|
||||||
@@ -57,6 +61,8 @@ class StockOutItem {
|
|||||||
productSeries: lineOrProduct('series', 'series'),
|
productSeries: lineOrProduct('series', 'series'),
|
||||||
productSpec: lineOrProduct('spec', 'spec'),
|
productSpec: lineOrProduct('spec', 'spec'),
|
||||||
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
|
productUnit: (json['product'] as Map<String, dynamic>?)?['unit'] as String?,
|
||||||
|
batchNo: json['batch_no'] as String?,
|
||||||
|
productionDate: json['production_date'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1064,16 +1064,18 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
|||||||
0: FixedColumnWidth(36),
|
0: FixedColumnWidth(36),
|
||||||
1: FlexColumnWidth(1.2),
|
1: FlexColumnWidth(1.2),
|
||||||
2: FlexColumnWidth(2.2),
|
2: FlexColumnWidth(2.2),
|
||||||
3: FlexColumnWidth(1.5),
|
3: FlexColumnWidth(1.3),
|
||||||
4: FlexColumnWidth(1.5),
|
4: FlexColumnWidth(1.3),
|
||||||
5: FlexColumnWidth(1.2),
|
5: FlexColumnWidth(1.3),
|
||||||
6: FlexColumnWidth(1.2),
|
6: FlexColumnWidth(1.3),
|
||||||
7: FlexColumnWidth(1.2),
|
7: FlexColumnWidth(1.0),
|
||||||
|
8: FlexColumnWidth(1.1),
|
||||||
|
9: FlexColumnWidth(1.1),
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
TableRow(
|
TableRow(
|
||||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||||
children: ['序号', '商品编码', '名称', '系列', '规格', '数量', '单价', '金额']
|
children: ['序号', '商品编码', '名称', '系列', '规格', '生产日期', '批次号', '数量', '单价', '金额']
|
||||||
.map((h) => Padding(
|
.map((h) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 8, vertical: 10),
|
horizontal: 8, vertical: 10),
|
||||||
@@ -1122,6 +1124,12 @@ class _StockInDetailDialogState extends ConsumerState<_StockInDetailDialog> {
|
|||||||
),
|
),
|
||||||
_TableCell(item.productSeries ?? '-'),
|
_TableCell(item.productSeries ?? '-'),
|
||||||
_TableCell(item.productSpec ?? '-'),
|
_TableCell(item.productSpec ?? '-'),
|
||||||
|
_TableCell((item.productionDate != null &&
|
||||||
|
item.productionDate!.length >= 10)
|
||||||
|
? item.productionDate!.substring(0, 10)
|
||||||
|
: (item.productionDate ?? '-')),
|
||||||
|
_TableCell(
|
||||||
|
(item.batchNo?.isNotEmpty ?? false) ? item.batchNo! : '-'),
|
||||||
_TableCell(item.quantity.toStringAsFixed(3)),
|
_TableCell(item.quantity.toStringAsFixed(3)),
|
||||||
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
|
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
|
||||||
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'),
|
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'),
|
||||||
|
|||||||
@@ -1003,16 +1003,18 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
|
|||||||
0: FixedColumnWidth(36),
|
0: FixedColumnWidth(36),
|
||||||
1: FlexColumnWidth(1.2),
|
1: FlexColumnWidth(1.2),
|
||||||
2: FlexColumnWidth(2.2),
|
2: FlexColumnWidth(2.2),
|
||||||
3: FlexColumnWidth(1.5),
|
3: FlexColumnWidth(1.3),
|
||||||
4: FlexColumnWidth(1.5),
|
4: FlexColumnWidth(1.3),
|
||||||
5: FlexColumnWidth(1.2),
|
5: FlexColumnWidth(1.3),
|
||||||
6: FlexColumnWidth(1.2),
|
6: FlexColumnWidth(1.3),
|
||||||
7: FlexColumnWidth(1.2),
|
7: FlexColumnWidth(1.0),
|
||||||
|
8: FlexColumnWidth(1.1),
|
||||||
|
9: FlexColumnWidth(1.1),
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
TableRow(
|
TableRow(
|
||||||
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
decoration: const BoxDecoration(color: Color(0xFFF0F4FF)),
|
||||||
children: ['序号', '商品编码', '名称', '系列', '规格', '数量', '单价', '金额']
|
children: ['序号', '商品编码', '名称', '系列', '规格', '生产日期', '批次号', '数量', '单价', '金额']
|
||||||
.map((h) => Padding(
|
.map((h) => Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
|
||||||
child: Text(h,
|
child: Text(h,
|
||||||
@@ -1035,6 +1037,12 @@ class _StockOutDetailDialogState extends ConsumerState<_StockOutDetailDialog> {
|
|||||||
_TableCell(item.productName ?? '-'),
|
_TableCell(item.productName ?? '-'),
|
||||||
_TableCell(item.productSeries ?? '-'),
|
_TableCell(item.productSeries ?? '-'),
|
||||||
_TableCell(item.productSpec ?? '-'),
|
_TableCell(item.productSpec ?? '-'),
|
||||||
|
_TableCell((item.productionDate != null &&
|
||||||
|
item.productionDate!.length >= 10)
|
||||||
|
? item.productionDate!.substring(0, 10)
|
||||||
|
: (item.productionDate ?? '-')),
|
||||||
|
_TableCell(
|
||||||
|
(item.batchNo?.isNotEmpty ?? false) ? item.batchNo! : '-'),
|
||||||
_TableCell(item.quantity.toStringAsFixed(3)),
|
_TableCell(item.quantity.toStringAsFixed(3)),
|
||||||
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
|
_TableCell('¥${item.unitPrice.toStringAsFixed(2)}'),
|
||||||
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'),
|
_TableCell('¥${item.totalPrice.toStringAsFixed(2)}'),
|
||||||
|
|||||||
Reference in New Issue
Block a user