2026-05-11 99e26030611fdc06ee3dd523072fe5516b78dc5b
feat(sales): 添加销售订单和产品相关字段

- 在 SalesLedger 实体中添加订单类型、订单行和需求日期字段
- 在 SalesLedgerDto 和 SalesLedgerImportDto 中添加相应字段支持
- 在 SalesLedgerProduct 实体中添加交货数量、剩余数量、子库存、货位和是否喷砂字段
- 将物料号字段从 material 重命名为 materialNo 并添加到 DTO 中
- 更新 MyBatis 映射文件中的查询语句以包含新字段
- 在服务层修复产品结构复制时 ID 重复问题
- 优化销售 ledger 服务中的税务计算逻辑,增加空值检查
- 更新销售产品待开票金额计算逻辑
- 添加数据库字段变更脚本和前端联调文档
已添加2个文件
已修改9个文件
350 ■■■■■ 文件已修改
doc/20260511_sales_ledger_add_fields.sql 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/销售订单模块-前端联调文档-20260511.md 108 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java 26 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/sales/SalesLedgerMapper.xml 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/销售台账导入模板.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260511_sales_ledger_add_fields.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,128 @@
-- é”€å”®è®¢å•模块新增字段(2026-05-11)
-- è¯´æ˜Žï¼š
-- 1) è„šæœ¬æŒ‰â€œå­˜åœ¨åˆ™è·³è¿‡ï¼Œä¸å­˜åœ¨åˆ™æ–°å¢žâ€æ‰§è¡Œï¼Œå¯é‡å¤æ‰§è¡Œã€‚
-- 2) ç‰©æ–™å·ç»Ÿä¸€å­—段为 material_no;若历史存在 material,会回填到 material_no。
SET @db_name = DATABASE();
-- ----------------------------
-- sales_ledger æ–°å¢žå­—段
-- ----------------------------
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger'
  AND COLUMN_NAME = 'order_type';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger ADD COLUMN order_type VARCHAR(64) NULL COMMENT ''订单类型''',
              'SELECT ''skip: sales_ledger.order_type exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger'
  AND COLUMN_NAME = 'order_line';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger ADD COLUMN order_line VARCHAR(64) NULL COMMENT ''订单行''',
              'SELECT ''skip: sales_ledger.order_line exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger'
  AND COLUMN_NAME = 'demand_date';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger ADD COLUMN demand_date DATE NULL COMMENT ''需求日期''',
              'SELECT ''skip: sales_ledger.demand_date exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ----------------------------
-- sales_ledger_product æ–°å¢žå­—段
-- ----------------------------
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'delivery_quantity';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger_product ADD COLUMN delivery_quantity DECIMAL(18,6) NULL COMMENT ''交货数量''',
              'SELECT ''skip: sales_ledger_product.delivery_quantity exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'remaining_quantity';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger_product ADD COLUMN remaining_quantity DECIMAL(18,6) NULL COMMENT ''剩余数量''',
              'SELECT ''skip: sales_ledger_product.remaining_quantity exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'sub_inventory';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger_product ADD COLUMN sub_inventory VARCHAR(128) NULL COMMENT ''子库存''',
              'SELECT ''skip: sales_ledger_product.sub_inventory exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'location';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger_product ADD COLUMN location VARCHAR(128) NULL COMMENT ''货位''',
              'SELECT ''skip: sales_ledger_product.location exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'is_spray';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger_product ADD COLUMN is_spray TINYINT(1) NULL COMMENT ''是否喷砂''',
              'SELECT ''skip: sales_ledger_product.is_spray exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SELECT COUNT(*) INTO @cnt
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'material_no';
SET @sql = IF(@cnt = 0,
              'ALTER TABLE sales_ledger_product ADD COLUMN material_no VARCHAR(128) NULL COMMENT ''物料号''',
              'SELECT ''skip: sales_ledger_product.material_no exists''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- ----------------------------
-- åŽ†å²æ•°æ®å…¼å®¹ï¼šmaterial -> material_no
-- ----------------------------
SELECT COUNT(*) INTO @has_old_material
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'material';
SELECT COUNT(*) INTO @has_new_material_no
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @db_name
  AND TABLE_NAME = 'sales_ledger_product'
  AND COLUMN_NAME = 'material_no';
SET @sql = IF(@has_old_material = 1 AND @has_new_material_no = 1,
              'UPDATE sales_ledger_product
               SET material_no = material
               WHERE (material_no IS NULL OR material_no = '''')
                 AND material IS NOT NULL',
              'SELECT ''skip: material -> material_no backfill''');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- å¯é€‰ï¼šç¡®è®¤å‰ç«¯/后端都已切换后,再评估是否删除旧列 material
-- ALTER TABLE sales_ledger_product DROP COLUMN material;
doc/ÏúÊÛ¶©µ¥Ä£¿é-ǰ¶ËÁªµ÷Îĵµ-20260511.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,108 @@
# é”€å”®è®¢å•模块前端联调文档(2026-05-11)
## 1. æœ¬æ¬¡å˜æ›´èŒƒå›´
- æ¨¡å—:`销售订单 / é”€å”®äº§å“`
- ç›¸å…³æŽ¥å£å‰ç¼€ï¼š`/sales/ledger`、`/sales/product`
- æœ¬æ¬¡æ ¸å¿ƒå£å¾„:`单价 = å«ç¨Žå•ä»·`,`总价 = å«ç¨Žæ€»ä»·`
## 2. æ–°å¢ž/调整字段
### 2.1 é”€å”®ä¸»è¡¨ï¼ˆsales_ledger)
1. `orderType`:订单类型(`VARCHAR`)
2. `orderLine`:订单行(`VARCHAR`)
3. `demandDate`:需求日期(`yyyy-MM-dd`)
### 2.2 é”€å”®äº§å“è¡¨ï¼ˆsales_ledger_product)
1. `deliveryQuantity`:交货数量(`DECIMAL`)
2. `remainingQuantity`:剩余数量(`DECIMAL`)
3. `subInventory`:子库存(`VARCHAR`)
4. `location`:货位(`VARCHAR`)
5. `isSpray`:是否喷砂(`Boolean`)
6. `materialNo`:物料号(`VARCHAR`)
## 3. æŽ¥å£è”调清单
### 3.1 æ–°å¢ž/编辑销售订单
- `POST /sales/ledger/addOrUpdateSalesLedger`
- `Content-Type: application/json`
请求示例:
```json
{
  "id": null,
  "salesContractNo": "XS20260511001",
  "customerId": 10001,
  "customerName": "南通某客户",
  "projectName": "XX项目",
  "entryDate": "2026-05-11",
  "salesman": "张三",
  "paymentMethod": "月结30天",
  "orderType": "常规订单",
  "orderLine": "10",
  "demandDate": "2026-05-25",
  "deliveryDate": "2026-05-30",
  "type": 1,
  "productData": [
    {
      "productCategory": "成品A",
      "specificationModel": "A-001",
      "unit": "ä»¶",
      "quantity": 100,
      "taxRate": 13,
      "taxInclusiveUnitPrice": 11.30,
      "taxInclusiveTotalPrice": 1130.00,
      "invoiceType": "增值税专票",
      "materialNo": "MAT-001",
      "deliveryQuantity": 60,
      "remainingQuantity": 40,
      "subInventory": "FG",
      "location": "A-01-01",
      "isSpray": false
    }
  ]
}
```
### 3.2 é”€å”®è®¢å•详情(主子)
- `GET /sales/ledger/getSalesLedgerWithProducts?id={id}`
- è¿”回中会包含主表新增字段:`orderType/orderLine/demandDate`
- `productData` ä¸­ä¼šåŒ…含产品新增字段:`materialNo/deliveryQuantity/remainingQuantity/subInventory/location/isSpray`
### 3.3 é”€å”®è®¢å•分页列表
- `GET /sales/ledger/listPage`
- å•条记录返回新增字段:`orderType/orderLine/demandDate`
### 3.4 é”€å”®äº§å“åˆ—表
- `GET /sales/product/list?salesLedgerId={id}&type=1`
- å•条记录返回新增字段:`materialNo/deliveryQuantity/remainingQuantity/subInventory/location/isSpray`
### 3.5 Excel å¯¼å…¥
- `POST /sales/ledger/import`(`form-data`,参数名:`file`)
- å¯¼å…¥æ¨¡æ¿ä¸­é”€å”®äº§å“é¡µå­—段口径:
  1. `单价` -> `taxInclusiveUnitPrice`
  2. `总价` -> `taxInclusiveTotalPrice`
后端导入逻辑口径已统一为含税:
1. `noInvoiceAmount = taxInclusiveTotalPrice`
2. `pendingInvoiceTotal = taxInclusiveTotalPrice`
## 4. å‰ç«¯è”调注意点
1. æ—¥æœŸå­—段统一传 `yyyy-MM-dd`(`demandDate`、`deliveryDate`)。
2. `isSpray` æŒ‰å¸ƒå°”值传递(`true/false`)。
3. `deliveryQuantity`、`remainingQuantity`、`quantity`、`taxInclusiveUnitPrice`、`taxInclusiveTotalPrice` å»ºè®®ç»Ÿä¸€ `Number` ç²¾åº¦å¤„理。
4. é¡µé¢å±•示“单价/总价”时,直接使用 `taxInclusiveUnitPrice/taxInclusiveTotalPrice`。
## 5. æœ¬æ¬¡å¯¹åº” SQL
- è§æ–‡ä»¶ï¼š`doc/20260511_sales_ledger_add_fields.sql`
src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java
@@ -56,6 +56,16 @@
    @ApiModelProperty(value = "付款方式")
    private String paymentMethod;
    @ApiModelProperty(value = "订单类型")
    private String orderType;
    @ApiModelProperty(value = "订单行")
    private String orderLine;
    @ApiModelProperty(value = "需求日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date demandDate;
    @ApiModelProperty(value = "交货日期")
    private LocalDate deliveryDate;
}
src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java
@@ -47,5 +47,19 @@
    @Excel(name = "付款方式")
    private String paymentMethod;
    @Excel(name = "订单类型")
    private String orderType;
    @Excel(name = "订单行")
    private String orderLine;
    @Excel(name = "需求日期", width = 30, dateFormat = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date demandDate;
    @Excel(name = "交货日期", width = 30, dateFormat = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private Date deliveryDate;
}
src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java
@@ -22,13 +22,13 @@
    /**
     * äº§å“å¤§ç±»
     */
    @Excel(name = "产品大类")
    @Excel(name = "产品名称")
    private String productCategory;
    /**
     * è§„格型号
     */
    @Excel(name = "规格型号")
    @Excel(name = "图纸编号")
    private String specificationModel;
    /**
@@ -52,13 +52,13 @@
    /**
     * å«ç¨Žå•ä»·
     */
    @Excel(name = "含税单价")
    @Excel(name = "单价")
    private BigDecimal taxInclusiveUnitPrice;
    /**
     * å«ç¨Žæ€»ä»·
     */
    @Excel(name = "含税总价")
    @Excel(name = "总价")
    private BigDecimal taxInclusiveTotalPrice;
    /**
@@ -73,6 +73,24 @@
    @Excel(name = "是否质检", readConverterExp = "0=否,1=是")
    private Boolean isChecked;
    @Excel(name = "物料号")
    private String materialNo;
    @Excel(name = "交货数量")
    private BigDecimal deliveryQuantity;
    @Excel(name = "剩余数量")
    private BigDecimal remainingQuantity;
    @Excel(name = "子库存")
    private String subInventory;
    @Excel(name = "货位")
    private String location;
    @Excel(name = "是否喷砂")
    private Boolean isSpray;
}
src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
@@ -127,6 +127,21 @@
    @ApiModelProperty(value = "付款方式")
    private String paymentMethod;
    @ApiModelProperty(value = "订单类型")
    @Excel(name = "订单类型")
    private String orderType;
    @ApiModelProperty(value = "订单行")
    @Excel(name = "订单行")
    private String orderLine;
    @ApiModelProperty(value = "需求日期")
    @Excel(name = "需求日期", width = 30, dateFormat = "yyyy-MM-dd")
    @JsonFormat(pattern = "yyyy-MM-dd")
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @TableField(value = "demand_date")
    private Date demandDate;
    @TableField(exist = false)
    @ApiModelProperty(value = "生产状态")
    private String productionStatus = "未开始";
src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
@@ -96,6 +96,26 @@
    @Excel(name = "含税总价")
    private BigDecimal taxInclusiveTotalPrice;
    @ApiModelProperty(value = "交货数量")
    @Excel(name = "交货数量")
    private BigDecimal deliveryQuantity;
    @ApiModelProperty(value = "剩余数量")
    @Excel(name = "剩余数量")
    private BigDecimal remainingQuantity;
    @ApiModelProperty(value = "子库存")
    @Excel(name = "子库存")
    private String subInventory;
    @ApiModelProperty(value = "货位")
    @Excel(name = "货位")
    private String location;
    @ApiModelProperty(value = "是否喷砂")
    @Excel(name = "是否喷砂")
    private Boolean isSpray;
    /**
     * ä¸å«ç¨Žæ€»ä»·
     */
@@ -250,5 +270,6 @@
    private BigDecimal returnNum;
    @ApiModelProperty(value = "物料号")
    private String material;
    @Excel(name = "物料号")
    private String materialNo;
}
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -317,6 +317,7 @@
                        productStructures.forEach(item ->{
                            ProductStructureRecord item1 = new ProductStructureRecord();
                            BeanUtils.copyProperties(item, item1);
                            item1.setId( null);
                            item1.setProductOrderId(productOrder.getId());
                            item1.setDemandedQuantity(item.getUnitQuantity().multiply(productOrder.getQuantity()));
                            item1.setBomId(Long.valueOf(productBom.getId()));
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -412,9 +412,19 @@
                    salesLedgerProduct.setSalesLedgerId(salesLedger.getId());
                    salesLedgerProduct.setType(1);
                    // è®¡ç®—不含税总价
                    salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice().divide(new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))), 2, RoundingMode.HALF_UP));
                    if (salesLedgerProduct.getTaxInclusiveTotalPrice() != null && salesLedgerProduct.getTaxRate() != null) {
                        salesLedgerProduct.setTaxExclusiveTotalPrice(
                                salesLedgerProduct.getTaxInclusiveTotalPrice().divide(
                                        new BigDecimal(1).add(salesLedgerProduct.getTaxRate().divide(new BigDecimal(100))),
                                        2,
                                        RoundingMode.HALF_UP
                                )
                        );
                    } else {
                        salesLedgerProduct.setTaxExclusiveTotalPrice(salesLedgerProduct.getTaxInclusiveTotalPrice());
                    }
                    salesLedgerProduct.setNoInvoiceNum(salesLedgerProduct.getQuantity());
                    salesLedgerProduct.setNoInvoiceAmount(salesLedgerProduct.getTaxExclusiveTotalPrice());
                    salesLedgerProduct.setNoInvoiceAmount(salesLedgerProduct.getTaxInclusiveTotalPrice());
                    list.stream()
                            .filter(map -> map.get("productName").equals(salesLedgerProduct.getProductCategory()) && map.get("model").equals(salesLedgerProduct.getSpecificationModel()))
                            .findFirst()
@@ -435,7 +445,7 @@
                    salesLedgerProduct.setRegister(loginUser.getNickName());
                    salesLedgerProduct.setRegisterDate(LocalDateTime.now());
                    salesLedgerProduct.setApproveStatus(0);
                    salesLedgerProduct.setPendingInvoiceTotal(salesLedgerProductImportDto.getTaxInclusiveTotalPrice());
                    salesLedgerProduct.setPendingInvoiceTotal(salesLedgerProduct.getTaxInclusiveTotalPrice());
                    salesLedgerProductMapper.insert(salesLedgerProduct);
                    // æ·»åŠ ç”Ÿäº§æ•°æ®
                    salesLedgerProductServiceImpl.addProductionData(salesLedgerProduct);
src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -31,6 +31,10 @@
        T1.execution_date,
        T2.nick_name AS entry_person_name,
        T1.payment_method,
        T1.order_type,
        T1.order_line,
        T1.demand_date,
        T1.delivery_date,
        DATEDIFF(T1.delivery_date, CURDATE()) AS delivery_days_diff
        FROM
        sales_ledger T1
@@ -60,6 +64,9 @@
        T1.execution_date,
        T2.nick_name                          AS entry_person_name,
        T1.payment_method,
        T1.order_type,
        T1.order_line,
        T1.demand_date,
        T1.delivery_date,
        DATEDIFF(T1.delivery_date, CURDATE()) AS delivery_days_diff,
        CASE
@@ -111,4 +118,4 @@
        FROM sales_ledger
        GROUP BY customer_name
    </select>
</mapper>
</mapper>
src/main/resources/static/ÏúÊŲ̂Õ˵¼ÈëÄ£°å.xlsx
Binary files differ