liu
7 天以前 ee982b85c9ff09fbf6a6eb7c133b1e6f5fb160ba
Merge remote-tracking branch 'origin/dev_business' into dev_business
已修改32个文件
已添加7个文件
已删除1个文件
901 ■■■■ 文件已修改
.playwright-mcp/page-2026-08-14T05-18-36-135Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-14T05-18-50-100Z.yml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-14T05-19-08-995Z.yml 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/contract_generate_order_frontend.md 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/erp_purchase_invoice_remove_audit.md 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/projectSql/ruoyi-vue-pro.sql 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/purchase_invoice_payment_link.md 49 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/sql/erp_purchase_invoice_remove_audit.sql 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/srm_certificate_attachment_integration.md 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/srm_supplier_tender_integration.md 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java 39 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java 27 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java 25 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/SrmSupplierCertificateController.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateSaveReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/SrmSupplierController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderProjectController.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectPartialUpdateReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectSaveReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyServiceImpl.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/certificate/SrmSupplierCertificateServiceImpl.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.playwright-mcp/page-2026-08-14T05-18-36-135Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-14T05-18-50-100Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
- generic [active]: å·¥ä¸šAI智造平台
.playwright-mcp/page-2026-08-14T05-19-08-995Z.yml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,103 @@
- generic [ref=f1e7]:
  - complementary [ref=f1e8]:
    - generic [ref=f1e9]:
      - generic [ref=f1e10] [cursor=pointer]
      - link "工业AI智造平台 å·¥ä¸šAI智造平台" [ref=f1e15] [cursor=pointer]:
        - /url: javascript:void 0
        - img "工业AI智造平台" [ref=f1e18]
        - generic [ref=f1e19]: å·¥ä¸šAI智造平台
      - menu [ref=f1e22]:
        - menuitem "首页" [ref=f1e23] [cursor=pointer]
        - listitem [ref=f1e30]:
          - generic [ref=f1e31] [cursor=pointer]: åŸºç¡€æ•°æ®
        - listitem [ref=f1e39]:
          - generic [ref=f1e40] [cursor=pointer]: é‡‡è´­ç®¡ç†
        - listitem [ref=f1e46]:
          - generic [ref=f1e47] [cursor=pointer]: è¥é”€ç®¡ç†
        - listitem [ref=f1e53]:
          - generic [ref=f1e54] [cursor=pointer]: è®¾å¤‡ç®¡ç†
        - listitem [ref=f1e61]:
          - generic [ref=f1e62] [cursor=pointer]: ä»“储物流
        - listitem [ref=f1e70]:
          - generic [ref=f1e71] [cursor=pointer]: ç”Ÿäº§ç®¡æŽ§
        - listitem [ref=f1e78]:
          - generic [ref=f1e79] [cursor=pointer]: è´¨é‡ç®¡ç†
        - listitem [ref=f1e85]:
          - generic [ref=f1e86] [cursor=pointer]: å†³ç­–BI大屏
        - listitem [ref=f1e92]:
          - generic [ref=f1e93] [cursor=pointer]: ååŒåŠžå…¬
          - list [ref=f1e99]:
            - listitem [ref=f1e100]:
              - generic [ref=f1e101] [cursor=pointer]: æµç¨‹ç®¡ç†
              - list [ref=f1e107]:
                - menuitem "流程模型" [ref=f1e108] [cursor=pointer]
                - menuitem "流程表单" [ref=f1e113] [cursor=pointer]
                - menuitem "流程分类" [ref=f1e118] [cursor=pointer]
                - menuitem "用户分组" [ref=f1e123] [cursor=pointer]
                - menuitem "流程监听器" [ref=f1e128] [cursor=pointer]
                - menuitem "流程表达式" [ref=f1e133] [cursor=pointer]
                - menuitem "流程实例" [ref=f1e138] [cursor=pointer]
                - menuitem "流程任务" [ref=f1e143] [cursor=pointer]
            - listitem [ref=f1e148]:
              - generic [ref=f1e149] [cursor=pointer]: å®¡æ‰¹ä¸­å¿ƒ
            - menuitem "API密钥管理" [ref=f1e155] [cursor=pointer]
            - menuitem "模型管理" [ref=f1e161] [cursor=pointer]
            - menuitem "知识库管理" [ref=f1e166] [cursor=pointer]
            - menuitem "通知公告" [ref=f1e172] [cursor=pointer]
        - listitem [ref=f1e177]:
          - generic [ref=f1e178] [cursor=pointer]: äººåŠ›èµ„æº
        - listitem [ref=f1e184]:
          - generic [ref=f1e185] [cursor=pointer]: å”®åŽç®¡ç†
        - listitem [ref=f1e191]:
          - generic [ref=f1e192] [cursor=pointer]: åŸºç¡€è®¾æ–½
        - listitem [ref=f1e198]:
          - generic [ref=f1e199] [cursor=pointer]: ç³»ç»Ÿç®¡ç†
        - listitem [ref=f1e205]:
          - generic [ref=f1e206] [cursor=pointer]: IM å³æ—¶é€šè®¯
      - generic [ref=f1e214] [cursor=pointer]
  - generic [ref=f1e219]:
    - generic [ref=f1e220]:
      - banner [ref=f1e221]:
        - button [ref=f1e222] [cursor=pointer]
        - button [ref=f1e225] [cursor=pointer]
        - navigation "breadcrumb" [ref=f1e230]:
          - list [ref=f1e231]:
            - listitem [ref=f1e232]:
              - link "协同办公" [ref=f1e233] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f1e237]
            - listitem [ref=f1e240]:
              - link "流程管理" [ref=f1e241] [cursor=pointer]:
                - /url: javascript:void 0
              - listitem [ref=f1e245]
            - listitem [ref=f1e248]:
              - link "流程模型" [disabled] [ref=f1e249]
        - generic [ref=f1e254]:
          - generic [ref=f1e256] [cursor=pointer]:
            - generic [ref=f1e260]: æœç´¢
            - generic [ref=f1e261]: Ctrl K
          - button [ref=f1e263] [cursor=pointer]
          - button "dark" [ref=f1e268] [cursor=pointer]
          - button [ref=f1e277] [cursor=pointer]
          - button [ref=f1e283] [cursor=pointer]
          - button [ref=f1e286] [cursor=pointer]
          - button [ref=f1e292] [cursor=pointer]:
            - button [ref=f1e294]
          - button "工作台" [ref=f1e299] [cursor=pointer]
          - button [ref=f1e303] [cursor=pointer]:
            - img "超级管理员" [ref=f1e308]
      - generic [ref=f1e310]:
        - generic [ref=f1e315]:
          - generic [ref=f1e316] [cursor=pointer]: é¦–页
          - generic [ref=f1e334] [cursor=pointer]: æµç¨‹æ¨¡åž‹
        - generic [ref=f1e351]:
          - button [ref=f1e352] [cursor=pointer]
          - generic [ref=f1e359] [cursor=pointer]
          - generic [ref=f1e363] [cursor=pointer]
    - main [ref=f1e370]:
      - generic [ref=f1e376]:
        - generic [ref=f1e377]: æµç¨‹æ¨¡åž‹
        - generic [ref=f1e379]:
          - textbox "搜索流程" [ref=f1e381]
          - button "新建模型" [ref=f1e383] [cursor=pointer]
          - button [ref=f1e387] [cursor=pointer]
docs/contract_generate_order_frontend.md
@@ -228,6 +228,9 @@
ALTER TABLE `erp_sale_order`
ADD COLUMN `contract_id` bigint DEFAULT NULL COMMENT '关联 CRM åˆåŒç¼–号' AFTER `customer_id`,
ADD COLUMN `contract_no` varchar(32) DEFAULT NULL COMMENT 'CRM åˆåŒå•号' AFTER `contract_id`;
ALTER TABLE `erp_sale_order`
MODIFY COLUMN `deposit_price` decimal(24,6) NOT NULL DEFAULT 0 COMMENT '定金金额,单位:元';
```
---
docs/erp_purchase_invoice_remove_audit.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
# ERP é‡‡è´­æ¥ç¥¨åŽ»æŽ‰å®¡æ ¸ - å‰ç«¯è”调方案
## å˜æ›´èƒŒæ™¯
采购来票(`erp_purchase_invoice`)不再需要审核流程,来票创建后直接生效、可直接用于付款。后端已删除审核状态字段(`auditStatus`)与工作流残留字段(`processInstanceId`),并移除了审核相关接口;付款创建时不再校验"来票必须审核通过"。
## æ¶‰åŠé¡µé¢
- é‡‡è´­æ¥ç¥¨ç®¡ç†é¡µï¼ˆ`views/erp/purchase/invoice/`)
- ä»˜æ¬¾å•管理页(`views/erp/finance/payment/`,仅行为变化,无需改代码)
## æŽ¥å£å˜æ›´
### å·²åˆ é™¤æŽ¥å£
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| PUT | /erp/purchase-invoice/update-status | å®¡æ ¸/反审核,前端需删除对应按钮与 API å‡½æ•° |
| GET | /erp/purchase-invoice/audit-count | å¾…审核来票数量,前端需删除对应 API å‡½æ•° |
### å“åº”字段变化
| æŽ¥å£ | å˜åŒ– |
|------|------|
| /erp/purchase-invoice/page | å“åº”移除 `auditStatus`、`processInstanceId` å­—段 |
| /erp/purchase-invoice/get | å“åº”移除 `auditStatus`、`processInstanceId` å­—段 |
| /erp/purchase-invoice/simple-list | å“åº”移除 `auditStatus` å­—段;不再只返回"审核通过"来票,改为返回该供应商/订单下"已上传附件且剩余可付 > 0"的全部来票 |
| /erp/finance-payment/get、/page | åµŒå¥— `invoice` å¯¹è±¡ä¸­ä¸å†åŒ…含 `auditStatus` å­—段 |
### æ ¡éªŒè§„则变化(后端,前端无改动)
| åœºæ™¯ | å˜åŒ– |
|------|------|
| ä»˜æ¬¾åˆ›å»º/更新 | åŽ»æŽ‰"关联来票必须审核通过"校验,其余校验(来票存在、已传附件、供应商一致、金额不超剩余可付)保留 |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. API å®šä¹‰ï¼ˆ`src/api/erp/purchase/invoice/index.ts`)
- `PurchaseInvoice` ç±»åž‹ï¼šåˆ é™¤ `auditStatus`、`processInstanceId` å­—段
- `PurchaseInvoiceSimple` ç±»åž‹ï¼šåˆ é™¤ `auditStatus` å­—段
- åˆ é™¤å‡½æ•°ï¼š`updatePurchaseInvoiceStatus`、`getAuditPurchaseInvoiceCount`
- æ›´æ–° `getPurchaseInvoiceSimpleList` æ³¨é‡Šè¯´æ˜Ž
### 2. æ¥ç¥¨åˆ—表页(`src/views/erp/purchase/invoice/index.vue`)
- åˆ é™¤"审核""反审核"操作按钮及对应处理函数
- åˆ é™¤"审批状态"列的状态 Tag æ¨¡æ¿
- ç¼–辑按钮不再按审核状态控制显隐(任意状态可编辑)
- åˆ é™¤æŒ‰é’®ä¸å†æŒ‰å®¡æ ¸çŠ¶æ€æŽ§åˆ¶æ˜¾éšï¼ˆæ˜¯å¦å¯åˆ ç”±åŽç«¯æ ¡éªŒ"已有有效付款单"决定)
### 3. æ¥ç¥¨åˆ—表配置(`src/views/erp/purchase/invoice/data.ts`)
- æœç´¢è¡¨å•:删除"审核状态"筛选项
- åˆ—表列:删除"审批状态"列
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ¥ç¥¨åˆ›å»º | åˆ›å»ºå³ç”Ÿæ•ˆï¼Œæ— å®¡æ ¸çŽ¯èŠ‚ |
| æ¥ç¥¨ç¼–辑 | ä»»æ„çŠ¶æ€å¯ç¼–è¾‘ |
| æ¥ç¥¨åˆ é™¤ | å·²æœ‰æœ‰æ•ˆä»˜æ¬¾å•(付款金额 > 0)关联的来票不可删除,后端会返回错误提示 |
| ä»˜æ¬¾é€‰æ‹©æ¥ç¥¨ | ä¸‹æ‹‰åªå±•示"已上传附件且剩余可付 > 0"的来票,不再有"待审核"来票 |
## æ³¨æ„äº‹é¡¹
- ä»˜æ¬¾æ¨¡å—(`views/erp/finance/payment/`)未引用 `auditStatus`,无需修改;其"关联来票"下拉可选范围随后端 simple-list é€»è¾‘自动变化。
- åŽ†å²æ¥ç¥¨æ•°æ®ï¼šæ•°æ®åº“å·²åˆ é™¤ `audit_status`、`process_instance_id` ä¸¤åˆ—,历史来票全部视为有效,可直接用于付款。
- å‰ç«¯æŽ¥å£åˆ é™¤åŽï¼Œè¯·åŒæ­¥æ£€æŸ¥æ˜¯å¦æœ‰å…¶å®ƒé¡µé¢ï¼ˆå¦‚工作台/待办)调用已删除的接口,当前未发现。
docs/projectSql/ruoyi-vue-pro.sql
@@ -3449,7 +3449,7 @@
  `total_tax_price` decimal(24, 6) NOT NULL COMMENT '合计税额,单位:元',
  `discount_percent` decimal(24, 6) NOT NULL COMMENT '优惠率,百分比',
  `discount_price` decimal(24, 6) NOT NULL COMMENT '优惠金额,单位:元',
  `deposit_price` decimal(24, 6) NOT NULL COMMENT '定金金额,单位:元',
  `deposit_price` decimal(24, 6) NOT NULL DEFAULT 0 COMMENT '定金金额,单位:元',
  `file_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '附件地址',
  `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
  `process_instance_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'BPM æµç¨‹å®žä¾‹ç¼–号',
docs/purchase_invoice_payment_link.md
@@ -2,7 +2,7 @@
## æ¶‰åŠé¡µé¢
- é‡‡è´­æ¥ç¥¨ç®¡ç†é¡µï¼ˆ**新建页面**,参考采购申请/采购订单页面结构)
- é‡‡è´­æ¥ç¥¨ç®¡ç†é¡µï¼ˆæ¥ç¥¨åˆ—表、新增/编辑/详情弹窗)
- é‡‡è´­è®¢å•详情页(展示该订单下已关联的来票信息)
- ä»˜æ¬¾å•管理页(新建/编辑弹窗、列表、详情)
- ä»˜æ¬¾å•详情页(展示关联的来票信息)
@@ -10,15 +10,14 @@
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. é‡‡è´­è®¢å•(审核通过)-> åˆ›å»ºæ¥ç¥¨ï¼š`purchaseOrderId` å¿…选,供应商自动带出(以采购订单的供应商为准,前端无需传 `supplierId`,由后端按采购订单查询设置)。
2. æ¥ç¥¨åˆ›å»ºåŽ -> ä¸Šä¼ å‘票附件(走 system æ¨¡å— Storage API)。
3. æ¥ç¥¨æäº¤å®¡æ‰¹ -> å®¡æ‰¹é€šè¿‡ -> æ‰å¯ä½œä¸ºä»˜æ¬¾å…³è”对象。
4. æ–°å»ºä»˜æ¬¾å• -> é€‰æ‹©å…³è”来票(`invoiceId` å¿…填):从 `GET /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId=` é€‰æ‹©ï¼Œä»…显示"审批通过 + å·²ä¸Šä¼ é™„ä»¶ + å‰©ä½™å¯ä»˜é‡‘额 > 0"的来票;选择后 `remainingInvoicePrice` å¯ç”¨äºŽé¢„填付款金额上限。
2. æ¥ç¥¨åˆ›å»ºåŽ -> ä¸Šä¼ å‘票附件(走 system æ¨¡å— Storage API)。**来票创建即生效,无审核环节**。
3. æ–°å»ºä»˜æ¬¾å• -> é€‰æ‹©å…³è”来票(`invoiceId` å¿…填):从 `GET /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId=` é€‰æ‹©ï¼Œä»…显示"已上传附件 + å‰©ä½™å¯ä»˜é‡‘额 > 0"的来票;选择后 `remainingInvoicePrice` å¯ç”¨äºŽé¢„填付款金额上限。
对应关系:**采购订单(1) -> æ¥ç¥¨(N) -> ä»˜æ¬¾(M)**。
## API
### 1. æ¥ç¥¨ç®¡ç†ï¼ˆæ–°ï¼‰
### 1. æ¥ç¥¨ç®¡ç†
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
@@ -28,9 +27,9 @@
| GET | /erp/purchase-invoice/get?id= | èŽ·å¾—æ¥ç¥¨è¯¦æƒ… |
| GET | /erp/purchase-invoice/page | æ¥ç¥¨åˆ†é¡µ |
| GET | /erp/purchase-invoice/export-excel | å¯¼å‡ºæ¥ç¥¨ Excel |
| PUT | /erp/purchase-invoice/submit?id= | æäº¤æ¥ç¥¨å®¡æ‰¹ |
| GET | /erp/purchase-invoice/audit-count | å¾…审核来票数量 |
| GET | /erp/purchase-invoice/simple-list?supplierId=&purchaseOrderId= | ç²¾ç®€åˆ—表(付款表单选来票) |
> å·²ç§»é™¤æŽ¥å£ï¼š`/erp/purchase-invoice/update-status`(审核/反审核)、`/erp/purchase-invoice/audit-count`(待审核数量)。前端请同步删除对应按钮与接口调用。
**创建/更新来票请求参数:**
@@ -46,17 +45,18 @@
| remark | String | å¦ | å¤‡æ³¨ |
| blobIds | List\<Long\> | å¦ | å‘票附件 blobId åˆ—表 |
**来票分页/详情响应新增字段:**
**来票分页/详情响应字段:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| no | String | æ¥ç¥¨ç¼–号(后端生成,前缀 FPD) |
| supplierName | String | ä¾›åº”商名称 |
| order | Object | é‡‡è´­è®¢å•信息(嵌套) |
| auditStatus | Integer | å®¡æ‰¹çŠ¶æ€ï¼ˆ0未提交/10审批中/20审核通过/30审核不通过/40已取消) |
| attachmentList | List | å‘票附件列表 |
| hasAttachment | Boolean | æ˜¯å¦å·²ä¸Šä¼ å‘票附件 |
| remainingInvoicePrice | BigDecimal | å‰©ä½™å¯ä»˜é‡‘额 = æ¥ç¥¨é‡‘额 - è¯¥æ¥ç¥¨å·²ä»˜æ¬¾é‡‘额 |
> å·²ç§»é™¤å“åº”字段:`auditStatus`(审批状态)、`processInstanceId`(工作流编号)。
**simple-list å“åº”:**
@@ -69,18 +69,17 @@
| price | BigDecimal | æ¥ç¥¨é‡‘额 |
| supplierId | Long | ä¾›åº”商编号 |
| purchaseOrderId | Long | é‡‡è´­è®¢å•编号 |
| auditStatus | Integer | å®¡æ‰¹çŠ¶æ€ |
| hasAttachment | Boolean | æ˜¯å¦å·²ä¸Šä¼ é™„件(恒为 true) |
| remainingInvoicePrice | BigDecimal | å‰©ä½™å¯ä»˜é‡‘额 |
### 2. ä»˜æ¬¾å•(接口变更)
### 2. ä»˜æ¬¾å•
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /erp/finance-payment/create | åˆ›å»ºä»˜æ¬¾å•,请求体新增 `invoiceId`(必填) |
| PUT | /erp/finance-payment/update | æ›´æ–°ä»˜æ¬¾å•,请求体新增 `invoiceId`(必填) |
| GET | /erp/finance-payment/get | å“åº”新增 `invoiceId`、`invoice` åµŒå¥—对象 |
| GET | /erp/finance-payment/page | å“åº”新增 `invoiceId`、`invoice` åµŒå¥—对象 |
| POST | /erp/finance-payment/create | åˆ›å»ºä»˜æ¬¾å•,请求体含 `invoiceId`(必填) |
| PUT | /erp/finance-payment/update | æ›´æ–°ä»˜æ¬¾å•,请求体含 `invoiceId`(必填) |
| GET | /erp/finance-payment/get | å“åº”含 `invoiceId`、`invoice` åµŒå¥—对象 |
| GET | /erp/finance-payment/page | å“åº”含 `invoiceId`、`invoice` åµŒå¥—对象 |
**请求参数新增:**
@@ -93,7 +92,7 @@
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| invoiceId | Long | å…³è”的来票编号 |
| invoice | Object | å…³è”的来票信息(含 no、invoiceNo、invoiceTitle、price、auditStatus ç­‰ï¼‰ |
| invoice | Object | å…³è”的来票信息(含 no、invoiceNo、invoiceTitle、price ç­‰ï¼‰ |
**响应示例:**
@@ -109,8 +108,7 @@
      "no": "FPD20260810000001",
      "invoiceNo": "02564178",
      "invoiceTitle": "XX科技有限公司",
      "price": 9000.00,
      "auditStatus": 20
      "price": 9000.00
    },
    "paymentPrice": 5000.00
  }
@@ -127,20 +125,20 @@
| å‘票号码/抬头 | æ¥ç¥¨åˆ—表、详情 | å½•入字段 |
| æ¥ç¥¨é‡‘额 | æ¥ç¥¨åˆ—表、详情、付款表单 | å…ƒ |
| æ¥ç¥¨æ—¥æœŸ | æ¥ç¥¨åˆ—表、详情 | |
| å®¡æ‰¹çŠ¶æ€ | æ¥ç¥¨åˆ—表、详情 | å­—典展示 |
| å‘票附件 | æ¥ç¥¨è¯¦æƒ…、来票列表 | ä¸Šä¼ /预览/下载 |
| å‘票附件 | æ¥ç¥¨è¯¦æƒ…、来票列表 | ä¸Šä¼ /预览/下载,有附件显示"已上传"Tag |
| å‰©ä½™å¯ä»˜é‡‘额 | æ¥ç¥¨åˆ—表、付款表单 | æ¥ç¥¨é‡‘额 - å·²ä»˜æ¬¾é‡‘额 |
| å…³è”来票 | ä»˜æ¬¾åˆ—表、付款详情 | å±•示 invoice.no / invoice.invoiceNo / invoice.price |
> å·²ç§»é™¤å±•示字段:来票列表的"审批状态"列、搜索条件"审核状态"、"审核/反审核"操作按钮。
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ¥ç¥¨åˆ›å»º | é‡‡è´­è®¢å•必须审核通过;来票供应商与采购订单一致;来票金额不超过订单剩余可开票金额 |
| æ¥ç¥¨ç¼–辑 | ä»…"未提交"状态可编辑 |
| æ¥ç¥¨åˆ é™¤ | å®¡æ‰¹é€šè¿‡åŽä¸å¯åˆ é™¤ |
| æ¥ç¥¨æäº¤å®¡æ‰¹ | ä»…"未提交"状态可提交,提交后进入审批中 |
| ä»˜æ¬¾é€‰æ‹©æ¥ç¥¨ | ä»…审批通过 + å·²ä¸Šä¼ å‘票附件的来票可选 |
| æ¥ç¥¨åˆ›å»º | é‡‡è´­è®¢å•必须审核通过;来票供应商与采购订单一致;来票金额不超过订单剩余可开票金额;**创建即生效,无需审核** |
| æ¥ç¥¨ç¼–辑 | ä»»æ„çŠ¶æ€å¯ç¼–è¾‘ï¼Œæ— å®¡æ ¸çŠ¶æ€é™åˆ¶ |
| æ¥ç¥¨åˆ é™¤ | å·²æœ‰æœ‰æ•ˆä»˜æ¬¾å•(未作废且付款金额 > 0)关联的来票不可删除,其余可删除 |
| ä»˜æ¬¾é€‰æ‹©æ¥ç¥¨ | å·²ä¸Šä¼ å‘票附件且剩余可付金额 > 0 çš„æ¥ç¥¨å¯é€‰ï¼ˆ**不再要求审核通过**) |
| ä»˜æ¬¾é‡‘额上限 | ä»˜æ¬¾é‡‘额 <= è¯¥æ¥ç¥¨é‡‘额 - è¯¥æ¥ç¥¨å·²ä»˜æ¬¾é‡‘额(未审核+审批中+已审核付款,排除作废) |
| ä¾›åº”商一致性 | ä»˜æ¬¾çš„供应商必须与关联来票的供应商一致 |
| å­˜é‡ä»˜æ¬¾å• | invoiceId ä¸ºç©ºçš„存量付款单,编辑时需重新选择合格来票 |
@@ -151,5 +149,4 @@
- ä»˜æ¬¾è¡¨å•选择来票时,先按供应商过滤(`supplierId`),可选订单时传 `purchaseOrderId` è¿›ä¸€æ­¥è¿‡æ»¤ã€‚
- `remainingInvoicePrice` ä¸ºè´Ÿæ•°æˆ– 0 çš„æ¥ç¥¨ä¸ä¼šå‡ºçŽ°åœ¨ simple-list ä¸­ï¼Œå‰ç«¯å¯ç›´æŽ¥æŒ‰è¿”回列表渲染。
- ä»˜æ¬¾é‡‘额校验在后端,前端可将来票的 `remainingInvoicePrice` ä½œä¸ºä»˜æ¬¾é‡‘额输入上限提示。
- æ¥ç¥¨å®¡æ‰¹çŠ¶æ€å˜åŒ–é€šè¿‡ BPM å›žè°ƒæ›´æ–°ï¼Œå‰ç«¯æ— éœ€å¤„理,分页接口直接返回最新状态。
- æ¥ç¥¨å‘票附件上传使用 system æ¨¡å— Storage API(upload -> bind),不要使用旧版 /infra/file æŽ¥å£ã€‚
docs/sql/erp_purchase_invoice_remove_audit.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
-- ERP é‡‡è´­æ¥ç¥¨åŽ»æŽ‰å®¡æ ¸ï¼šåˆ é™¤å®¡æ ¸çŠ¶æ€å­—æ®µä¸Žå·¥ä½œæµæ®‹ç•™å­—æ®µ
-- èƒŒæ™¯ï¼šæ¥ç¥¨åˆ›å»ºå³ç”Ÿæ•ˆï¼Œæ— éœ€å®¡æ ¸ï¼Œä»˜æ¬¾ä¸å†æ ¡éªŒ"来票审核通过"
-- æ‰§è¡Œæ—¶æœºï¼šæ–°åŽç«¯éƒ¨ç½²åŽçš„停服窗口执行(旧代码读写这两列,先删列会导致运行时报错)
ALTER TABLE erp_purchase_invoice DROP COLUMN audit_status;
ALTER TABLE erp_purchase_invoice DROP COLUMN process_instance_id;
-- æ¸…理孤儿菜单权限(对应已删除的 /update-status å®¡æ ¸æŽ¥å£ï¼‰
DELETE FROM system_menu WHERE permission = 'erp:purchase-invoice:update-status';
docs/srm_certificate_attachment_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
# SRM èµ„质附件上传 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- é‡‡è´­ç®¡ç†-SRM-供应商管理-资质证书列表页
- é‡‡è´­ç®¡ç†-SRM-供应商管理-资质证书新增/编辑弹窗
- é‡‡è´­ç®¡ç†-SRM-招标项目-新增/编辑弹窗(含发布中/投标中的部分更新)
- é‡‡è´­ç®¡ç†-SRM-招标项目-详情页
## ä¸šåŠ¡æµç¨‹ä¸Žæ•°æ®å¸¦å…¥
1. ç”¨æˆ·åœ¨è¡¨å•中通过上传组件选择文件
2. ä¸Šä¼ ç»„件调用 `/system/storage-blob/upload` èŽ·å¾— `blobId`
3. ä¿å­˜ä¸šåŠ¡è®°å½•ï¼ˆèµ„è´¨è¯ä¹¦ / æ‹›æ ‡é¡¹ç›®ï¼‰æ—¶ï¼Œå°†æ–‡ä»¶ ID åˆ—表 `blobIds` éšä¸šåŠ¡æ•°æ®ä¸€å¹¶æäº¤
4. åŽç«¯åœ¨ä¸šåŠ¡è®°å½•ä¿å­˜åŽï¼Œé€šè¿‡ `recordType + recordId` å°†æ–‡ä»¶ç»‘定到 `system_storage_attachment` ä¸­é—´è¡¨
5. æŸ¥è¯¢ä¸šåŠ¡è®°å½•ï¼ˆè¯¦æƒ…ã€åˆ†é¡µï¼‰æ—¶ï¼ŒåŽç«¯è¿”å›ž `attachmentList`(含文件 URL、名称),前端展示下载链接
## API
### 1. èµ„质证书附件
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /system/storage-blob/upload | ä¸Šä¼ æ–‡ä»¶ï¼Œå…¥å‚ multipart çš„ `files`,返回 Blob åˆ—表(含 `id`) |
| POST | /srm/supplier-certificate/create | åˆ›å»ºèµ„质证书,新增入参 `blobIds` |
| PUT | /srm/supplier-certificate/update | æ›´æ–°èµ„质证书,新增入参 `blobIds`(增量更新附件) |
| DELETE | /srm/supplier-certificate/delete | åˆ é™¤èµ„质证书,同步删除附件 |
| GET | /srm/supplier-certificate/page | åˆ†é¡µæŸ¥è¯¢ï¼Œå“åº”新增 `attachmentList` |
| GET | /srm/supplier-certificate/get | è¯¦æƒ…查询,响应新增 `attachmentList` |
| GET | /srm/supplier-certificate/list-by-supplier | æŒ‰ä¾›åº”商查询,响应新增 `attachmentList` |
**请求参数(create/update æ–°å¢žå­—段):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| blobIds | Array\<Long\> | å¦ | èµ„质证书附件文件 ID åˆ—表 |
**响应字段新增(attachmentList æ•°ç»„元素):**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | Long | æ–‡ä»¶ ID |
| name | String | åŽŸæ–‡ä»¶å |
| url | String | æ–‡ä»¶è®¿é—®åœ°å€ |
| previewURL | String | é¢„览地址 |
| downloadURL | String | ä¸‹è½½åœ°å€ |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "id": 1,
    "supplierId": 10,
    "certificateName": "ISO9001质量管理体系",
    "attachmentList": [
      {
        "id": 1001,
        "name": "iso9001_cert.pdf",
        "url": "https://example.com/files/xxx.pdf"
      }
    ]
  }
}
```
### 2. æ‹›æ ‡é¡¹ç›®èµ„质要求附件
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | /system/storage-blob/upload | ä¸Šä¼ æ–‡ä»¶ï¼Œå…¥å‚ multipart çš„ `files`,返回 Blob åˆ—表(含 `id`) |
| POST | /srm/tender-project/create | åˆ›å»ºæ‹›æ ‡é¡¹ç›®ï¼Œæ–°å¢žå…¥å‚ `blobIds` |
| PUT | /srm/tender-project/update | æ›´æ–°æ‹›æ ‡é¡¹ç›®ï¼Œæ–°å¢žå…¥å‚ `blobIds`(草稿/发布中/投标中均增量更新附件) |
| PUT | /srm/tender-project/update-info | éƒ¨åˆ†æ›´æ–°ï¼ˆå‘布中/投标中),新增入参 `blobIds` |
| DELETE | /srm/tender-project/delete | åˆ é™¤æ‹›æ ‡é¡¹ç›®ï¼ŒåŒæ­¥åˆ é™¤é™„ä»¶ |
| GET | /srm/tender-project/get | è¯¦æƒ…查询,响应新增 `attachmentList` |
| GET | /srm/tender-project/page | åˆ†é¡µæŸ¥è¯¢ï¼Œå“åº”新增 `attachmentList` |
**请求参数(create/update/update-info æ–°å¢žå­—段):**
| å‚æ•° | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
|------|------|------|------|
| blobIds | Array\<Long\> | å¦ | èµ„质要求附件文件 ID åˆ—表 |
**响应字段新增:** åŒèµ„质证书,`attachmentList` æ•°ç»„元素含 `id`、`name`、`url`、`previewURL`、`downloadURL`。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| è¯ä¹¦é™„ä»¶ | èµ„质证书列表 | æ–°å¢žã€Œé™„件」列,显示附件名称链接,点击新窗口打开;无附件显示 `--` |
| è¯ä¹¦é™„ä»¶ | èµ„质证书新增/编辑弹窗 | ã€Œè¯ä¹¦é™„件」上传项,支持多文件,编辑时回显已上传文件 |
| èµ„质要求附件 | æ‹›æ ‡é¡¹ç›®æ–°å¢ž/编辑弹窗 | ã€Œèµ„质要求附件」上传项,支持多文件,编辑时回显已上传文件 |
| èµ„质要求附件 | æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页 | ã€Œèµ„质要求」折叠面板内展示附件名称链接,点击新窗口打开 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| ç¼–辑回显 | ç¼–辑时后端返回 `attachmentList`,前端将其映射为上传组件文件对象(`uid`、`name`、`url`、`status='done'`、`id`)并赋给 `blobIds`,提交时按 `valueKey='id'` æå–文件 ID æäº¤ |
| é™„ä»¶æ›´æ–° | åŽç«¯æŒ‰ã€Œå¢žé‡æ›´æ–°ã€å¤„理:以本次提交的 `blobIds` ä¸ºå‡†ï¼Œè‡ªåŠ¨åˆ é™¤å·²ç§»é™¤çš„æ–‡ä»¶ç»‘å®šã€æ–°å¢žæ–°ä¸Šä¼ çš„æ–‡ä»¶ç»‘å®š |
| åˆ é™¤ä¸šåŠ¡è®°å½• | åˆ é™¤èµ„质证书/招标项目时,后端同步删除其附件 |
| å‘布中/投标中编辑 | æ‹›æ ‡é¡¹ç›®åœ¨å‘布中/投标中阶段通过 `update-info` æŽ¥å£ä»…能修改招标说明、资质要求及资质要求附件 |
| ä¸Šä¼ æ ¼å¼ | æ”¯æŒ jpg、jpeg、png、pdf、doc、docx,单个文件不超过 2MB |
## æ³¨æ„äº‹é¡¹
- ä¸Šä¼ å¿…须走 `/system/storage-blob/upload`,禁止使用旧版 `/infra/file/*` æŽ¥å£ã€‚
- èµ„质证书与招标项目使用不同的 `recordType`:资质证书为 `srm_supplier_certificate`,招标项目为 `srm_tender_project`,前端无需关心,由后端处理。
- ç¼–辑回显时若用户未改动附件,提交的 `blobIds` ä»åº”包含原有文件 ID(由上传组件回显对象携带),避免附件被误删。
- æ‹›æ ‡é¡¹ç›®éƒ¨åˆ†æ›´æ–°ï¼ˆ`update-info`)请求体同样需携带 `blobIds`。
- åˆ—表页附件链接建议 `target="_blank"` æ–°çª—口打开,便于预览 PDF/图片。
docs/srm_supplier_tender_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
# SRM ä¾›åº”商投标选人 & ä¸­æ ‡è‡ªåЍ准入 - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页(投标管理 Tab、评标 Tab çš„供应商选择)
- SRM ä¾›åº”商管理页面(仅新增 `getSupplierList` API,页面本身无改动)
## èƒŒæ™¯ä¸Žéœ€æ±‚
1. æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页中「新增投标」「评标」时选择供应商,**不再过滤准入状态**,直接查询供应商档案列表。
2. ä¸­æ ‡å¹¶ç”Ÿæˆé‡‡è´­è®¢å•时,如果供应商尚未准入,后端会自动创建准入申请并置为已通过,前端无需额外处理。
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| GET | /srm/supplier/list | èŽ·å–ä¾›åº”å•†æ¡£æ¡ˆåˆ—è¡¨ï¼ˆä¸è¿‡æ»¤å‡†å…¥çŠ¶æ€ï¼‰ |
**请求参数:** æ— 
**响应:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| id | number | ä¾›åº”商 ID |
| name | string | ä¾›åº”商名称 |
| code | string | ä¾›åº”商编码 |
| status | number | çŠ¶æ€ï¼ˆ0 å¯ç”¨ï¼Œ1 ç¦ç”¨ï¼‰ |
**响应示例:**
```json
{
  "code": 0,
  "data": [
    { "id": 1, "code": "SUP001", "name": "某某公司", "status": 0 }
  ]
}
```
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. æ–°å¢ž API å‡½æ•°
在 `src/api/srm/supplier/index.ts` ä¸­æ–°å¢ž `getSupplierList()`,请求 `/srm/supplier/list`。
> åŽŸæœ‰çš„ `getSupplierSimpleList()`(请求 `/srm/supplier/simple-list`,仅返回已准入供应商)**保留不动**,其它页面(采购订单、采购入库、IQC ç­‰ï¼‰ä»ä½¿ç”¨å®ƒã€‚
### 2. æ‹›æ ‡é¡¹ç›®è¯¦æƒ…页改用档案列表
`src/views/srm/tender/detail.vue` ä¸­ï¼š
- å°† `getSupplierSimpleList` çš„ import æ›¿æ¢ä¸º `getSupplierList`
- å°† `onMounted` ä¸­åŠ è½½ä¾›åº”å•†æ•°æ®çš„è°ƒç”¨æ”¹ä¸º `getSupplierList`
该页面的「投标管理」供应商选择下拉、「评标」供应商选择下拉,均使用同一个 `supplierList`,替换后两个下拉都会展示全部供应商档案(不过滤准入)。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| id | ä¸‹æ‹‰ value | ä¾›åº”商 ID,提交给后端 |
| name | ä¸‹æ‹‰ label | ä¾›åº”商名称 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æŠ•标时选择供应商 | å±•示全部供应商档案,不再过滤准入状态 |
| è¯„标时选择供应商 | åŒæŠ•标,展示全部供应商档案 |
| ä¸­æ ‡ç”Ÿæˆé‡‡è´­è®¢å• | è‹¥ä¾›åº”商未准入,后端自动创建准入申请并直接置为已通过,再生成采购订单;前端无需调用准入相关接口 |
| æœªå‡†å…¥ä¾›åº”商能否投标 | å¯ä»¥ï¼ŒæŠ•标不受准入状态限制 |
## æ³¨æ„äº‹é¡¹
- `getSupplierList` è¿”回的数组元素含 `id`、`name`,可直接用于下拉选项(`{ id, name }`)。
- ä¾›åº”商被禁用(status = 1)时是否允许投标:本接口不过滤准入状态,`status` å­—段由前端决定是否过滤展示;目前招标详情页下拉不过滤。
- ä¸­æ ‡è‡ªåŠ¨å‡†å…¥æ˜¯åŽç«¯è¡Œä¸ºï¼Œå‰ç«¯ã€Œç¡®è®¤ä¸­æ ‡ã€ã€Œç”Ÿæˆé‡‡è´­è®¢å•ã€æŒ‰é’®çš„è°ƒç”¨æ–¹å¼ä¸å˜ã€‚
yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmModelConvert.java
@@ -58,8 +58,9 @@
            List<DeptRespDTO> startDepts = metaInfo != null ? convertList(metaInfo.getStartDeptIds(), deptMap::get) : null;
            return buildModel0(model, metaInfo, form, category, deployment, processDefinition, startUsers, startDepts);
        });
        // æŽ’序
        result.sort(Comparator.comparing(BpmModelMetaInfoVO::getSort));
        // æŽ’序:手工部署(DB ç›´æ’)的模型可能没有 metaInfo/sort,兜底排到最后
        result.sort(Comparator.comparing(BpmModelMetaInfoVO::getSort,
                Comparator.nullsLast(Comparator.naturalOrder())));
        return result;
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java
@@ -161,9 +161,14 @@
        // 1. æž„建物料明细
        if (CollUtil.isNotEmpty(productList)) {
            Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(convertSet(productList, CrmSaleQuotationProductDO::getItemId));
            java.util.Set<Long> unitIds = new java.util.HashSet<>(convertSet(productList, CrmSaleQuotationProductDO::getItemUnitId));
            itemMap.values().forEach(item -> {
                if (item.getUnitMeasureId2() != null) {
                    unitIds.add(item.getUnitMeasureId2());
                }
            });
            Map<Long, MdmUnitMeasureRespDTO> unitMap = convertMap(
                    mdmUnitMeasureApi.getUnitMeasureList(
                            convertSet(productList, CrmSaleQuotationProductDO::getItemUnitId)).getCheckedData(),
                    mdmUnitMeasureApi.getUnitMeasureList(unitIds).getCheckedData(),
                    MdmUnitMeasureRespDTO::getId);
            respVO.setItems(BeanUtils.toBean(productList, CrmSaleQuotationRespVO.Item.class, item -> {
                MdmItemRespDTO mdmItem = itemMap.get(item.getItemId());
@@ -171,8 +176,13 @@
                    item.setItemCode(mdmItem.getCode());
                    item.setItemName(mdmItem.getName());
                    item.setItemBarCode(mdmItem.getBarCode());
                    item.setItemSpecification(mdmItem.getSpecification());
                    item.setItemUnitId2(mdmItem.getUnitMeasureId2());
                    item.setItemUnitId3(mdmItem.getUnitMeasureId3());
                }
                MapUtils.findAndThen(unitMap, item.getItemUnitId(), unit -> item.setItemUnitName(unit.getName()));
                MapUtils.findAndThen(unitMap, item.getItemUnitId2(), unit -> item.setItemUnitName2(unit.getName()));
                MapUtils.findAndThen(unitMap, item.getItemUnitId3(), unit -> item.setItemUnitName3(unit.getName()));
            }));
        }
        // 2. å¡«å……客户名称
@@ -206,6 +216,15 @@
        Map<Long, List<CrmSaleQuotationProductDO>> productMap = convertMultiMap(productList, CrmSaleQuotationProductDO::getQuotationId);
        // 1.2 ç‰©æ–™ä¿¡æ¯
        Map<Long, MdmItemRespDTO> itemMap = mdmItemApi.getItemMap(convertSet(productList, CrmSaleQuotationProductDO::getItemId));
        java.util.Set<Long> unitIds = new java.util.HashSet<>(convertSet(productList, CrmSaleQuotationProductDO::getItemUnitId));
        itemMap.values().forEach(item -> {
            if (item.getUnitMeasureId2() != null) {
                unitIds.add(item.getUnitMeasureId2());
            }
        });
        Map<Long, MdmUnitMeasureRespDTO> unitMap = convertMap(
                mdmUnitMeasureApi.getUnitMeasureList(unitIds).getCheckedData(),
                MdmUnitMeasureRespDTO::getId);
        // 1.3 å®¢æˆ·ä¿¡æ¯
        Map<Long, CrmCustomerDO> customerMap = customerService.getCustomerMap(
                convertSet(pageResult.getList(), CrmSaleQuotationDO::getCustomerId));
@@ -222,8 +241,18 @@
        // 2. å¼€å§‹æ‹¼æŽ¥
        return BeanUtils.toBean(pageResult, CrmSaleQuotationRespVO.class, vo -> {
            vo.setItems(BeanUtils.toBean(productMap.get(vo.getId()), CrmSaleQuotationRespVO.Item.class,
                    item -> MapUtils.findAndThen(itemMap, item.getItemId(), mdmItem -> item.setItemCode(mdmItem.getCode())
                            .setItemName(mdmItem.getName()).setItemBarCode(mdmItem.getBarCode()))));
                    item -> {
                        MapUtils.findAndThen(itemMap, item.getItemId(), mdmItem -> item
                                .setItemCode(mdmItem.getCode())
                                .setItemName(mdmItem.getName())
                                .setItemBarCode(mdmItem.getBarCode())
                                .setItemSpecification(mdmItem.getSpecification())
                                .setItemUnitId2(mdmItem.getUnitMeasureId2())
                                .setItemUnitId3(mdmItem.getUnitMeasureId3()));
                        MapUtils.findAndThen(unitMap, item.getItemUnitId(), unit -> item.setItemUnitName(unit.getName()));
                        MapUtils.findAndThen(unitMap, item.getItemUnitId2(), unit -> item.setItemUnitName2(unit.getName()));
                        MapUtils.findAndThen(unitMap, item.getItemUnitId3(), unit -> item.setItemUnitName3(unit.getName()));
                    }));
            MapUtils.findAndThen(customerMap, vo.getCustomerId(), customer -> vo.setCustomerName(customer.getName()));
            MapUtils.findAndThen(userMap, vo.getOwnerUserId(), user -> vo.setOwnerUserName(user.getNickname()));
            MapUtils.findAndThen(deptMap, vo.getOwnerUserId(), dept -> vo.setOwnerUserDeptName(dept.getName()));
@@ -258,4 +287,4 @@
        }
    }
}
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java
@@ -141,10 +141,20 @@
        private String itemName;
        @Schema(description = "物料条码", example = "BAR001")
        private String itemBarCode;
        @Schema(description = "规格型号", example = "M8*20")
        private String itemSpecification;
        @Schema(description = "计量单位编号", example = "1")
        private Long itemUnitId;
        @Schema(description = "计量单位名称", example = "个")
        private String itemUnitName;
        @Schema(description = "辅单位1编号", example = "2")
        private Long itemUnitId2;
        @Schema(description = "辅单位1名称", example = "ç®±")
        private String itemUnitName2;
        @Schema(description = "辅单位2编号", example = "3")
        private Long itemUnitId3;
        @Schema(description = "辅单位2名称", example = "托")
        private String itemUnitName3;
        @Schema(description = "物料原价", requiredMode = Schema.RequiredMode.REQUIRED, example = "100.00")
        private BigDecimal itemPrice;
@@ -169,4 +179,4 @@
    }
}
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ErpPurchaseInvoiceController.java
@@ -15,7 +15,6 @@
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import cn.iocoder.yudao.module.erp.service.finance.ErpFinancePaymentService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
@@ -169,24 +168,8 @@
        });
    }
    @PutMapping("/update-status")
    @Operation(summary = "更新来票的状态(审核/反审核)")
    @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:update-status')")
    public CommonResult<Boolean> updatePurchaseInvoiceStatus(@RequestParam("id") Long id,
                                                             @RequestParam("status") Integer status) {
        invoiceService.updatePurchaseInvoiceStatus(id, status);
        return success(true);
    }
    @GetMapping("/audit-count")
    @Operation(summary = "获得待审核来票数量")
    @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
    public CommonResult<Long> getAuditInvoiceCount() {
        return success(invoiceService.getAuditInvoiceCount());
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得来票精简列表", description = "只包含审核通过且已上传发票附件的来票,主要用于付款创建时选择关联来票")
    @Operation(summary = "获得来票精简列表", description = "只包含已上传发票附件的来票,主要用于付款创建时选择关联来票")
    @Parameter(name = "supplierId", description = "供应商编号", required = true)
    @Parameter(name = "purchaseOrderId", description = "采购订单编号(可选,用于按订单过滤)")
    @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:query')")
@@ -195,11 +178,9 @@
            @RequestParam(value = "purchaseOrderId", required = false) Long purchaseOrderId) {
        List<ErpPurchaseInvoiceDO> invoices;
        if (purchaseOrderId != null) {
            invoices = invoiceService.getPurchaseInvoiceListByOrderIdAndStatus(purchaseOrderId,
                    List.of(ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
            invoices = invoiceService.getPurchaseInvoiceListByOrderId(purchaseOrderId);
        } else {
            invoices = invoiceService.getPurchaseInvoiceListBySupplierIdAndStatus(supplierId,
                    List.of(ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
            invoices = invoiceService.getPurchaseInvoiceListBySupplierId(supplierId);
        }
        if (CollUtil.isEmpty(invoices)) {
            return success(Collections.emptyList());
@@ -225,7 +206,7 @@
                    .setId(invoice.getId()).setNo(invoice.getNo()).setInvoiceNo(invoice.getInvoiceNo())
                    .setInvoiceTitle(invoice.getInvoiceTitle()).setPrice(invoice.getPrice())
                    .setSupplierId(invoice.getSupplierId()).setPurchaseOrderId(invoice.getPurchaseOrderId())
                    .setAuditStatus(invoice.getAuditStatus()).setHasAttachment(true)
                    .setHasAttachment(true)
                    .setRemainingInvoicePrice(remainingPrice));
        }
        return success(result);
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoicePageReqVO.java
@@ -1,8 +1,6 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -22,9 +20,5 @@
    @Schema(description = "采购订单编号", example = "17386")
    private Long purchaseOrderId;
    @Schema(description = "审批状态", example = "20")
    @InEnum(ErpPurchaseInvoiceAuditStatusEnum.class)
    private Integer auditStatus;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/invoice/ErpPurchaseInvoiceRespVO.java
@@ -1,9 +1,6 @@
package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order.ErpPurchaseOrderRespVO;
import cn.iocoder.yudao.module.erp.enums.DictTypeConstants;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
@@ -54,15 +51,6 @@
    @Schema(description = "来票日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2024-02-02")
    @ExcelProperty("来票日期")
    private LocalDateTime invoiceTime;
    @Schema(description = "审批状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
    @ExcelProperty(value = "审批状态", converter = DictConvert.class)
    @DictFormat(DictTypeConstants.AUDIT_STATUS)
    private Integer auditStatus;
    @Schema(description = "工作流编号", example = "1043")
    @ExcelProperty("工作流编号")
    private String processInstanceId;
    @Schema(description = "备注", example = "备注")
    @ExcelProperty("备注")
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseInvoiceDO.java
@@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.erp.dal.dataobject.purchase;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
@@ -66,17 +65,5 @@
     * å¤‡æ³¨
     */
    private String remark;
    /**
     * å·¥ä½œæµç¼–号
     *
     * å…³è” ProcessInstance çš„ id å±žæ€§
     */
    private String processInstanceId;
    /**
     * å®¡æ‰¹çŠ¶æ€
     *
     * æžšä¸¾ {@link ErpPurchaseInvoiceAuditStatusEnum}
     */
    private Integer auditStatus;
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/mysql/purchase/ErpPurchaseInvoiceMapper.java
@@ -5,11 +5,8 @@
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoicePageReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/**
@@ -24,34 +21,22 @@
        return selectOne(ErpPurchaseInvoiceDO::getNo, no);
    }
    default int updateByIdAndStatus(Long id, Integer status, ErpPurchaseInvoiceDO updateObj) {
        return update(updateObj, new LambdaUpdateWrapper<ErpPurchaseInvoiceDO>()
                .eq(ErpPurchaseInvoiceDO::getId, id).eq(ErpPurchaseInvoiceDO::getAuditStatus, status));
    }
    default PageResult<ErpPurchaseInvoiceDO> selectPage(ErpPurchaseInvoicePageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
                .likeIfPresent(ErpPurchaseInvoiceDO::getNo, reqVO.getNo())
                .eqIfPresent(ErpPurchaseInvoiceDO::getSupplierId, reqVO.getSupplierId())
                .eqIfPresent(ErpPurchaseInvoiceDO::getPurchaseOrderId, reqVO.getPurchaseOrderId())
                .eqIfPresent(ErpPurchaseInvoiceDO::getAuditStatus, reqVO.getAuditStatus())
                .orderByDesc(ErpPurchaseInvoiceDO::getId));
    }
    default List<ErpPurchaseInvoiceDO> selectListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses) {
    default List<ErpPurchaseInvoiceDO> selectListByOrderId(Long orderId) {
        return selectList(new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
                .eq(ErpPurchaseInvoiceDO::getPurchaseOrderId, orderId)
                .in(ErpPurchaseInvoiceDO::getAuditStatus, auditStatuses));
                .eq(ErpPurchaseInvoiceDO::getPurchaseOrderId, orderId));
    }
    default List<ErpPurchaseInvoiceDO> selectListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses) {
    default List<ErpPurchaseInvoiceDO> selectListBySupplierId(Long supplierId) {
        return selectList(new LambdaQueryWrapperX<ErpPurchaseInvoiceDO>()
                .eq(ErpPurchaseInvoiceDO::getSupplierId, supplierId)
                .in(ErpPurchaseInvoiceDO::getAuditStatus, auditStatuses));
    }
    default Long selectCountByAudit() {
        return selectCount(ErpPurchaseInvoiceDO::getAuditStatus, ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus());
                .eq(ErpPurchaseInvoiceDO::getSupplierId, supplierId));
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErpPurchaseInvoiceAuditStatusEnum.java
ÎļþÒÑɾ³ý
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/enums/ErrorCodeConstants.java
@@ -57,14 +57,10 @@
    // ========== ERP é‡‡è´­æ¥ç¥¨ï¼ˆ1-030-105-000) ==========
    ErrorCode PURCHASE_INVOICE_NOT_EXISTS = new ErrorCode(1_030_105_000, "来票不存在");
    ErrorCode PURCHASE_INVOICE_NO_EXISTS = new ErrorCode(1_030_105_001, "生成来票编号失败,请重新提交");
    ErrorCode PURCHASE_INVOICE_UPDATE_FAIL_EDITING_PROHIBITED = new ErrorCode(1_030_105_002, "来票({})不是未审核状态,无法修改");
    ErrorCode PURCHASE_INVOICE_DELETE_FAIL_IS_APPROVE = new ErrorCode(1_030_105_003, "删除来票失败,原因:来票审批已通过");
    ErrorCode PURCHASE_INVOICE_UPDATE_AUDIT_STATUS_FAIL_NOT_PROCESS = new ErrorCode(1_030_105_005, "更新来票审核状态失败,原因:来票不是审核中状态");
    ErrorCode PURCHASE_INVOICE_DELETE_FAIL_HAS_PAYMENT = new ErrorCode(1_030_105_004, "删除来票失败,原因:来票已存在有效付款单");
    ErrorCode PURCHASE_INVOICE_CREATE_FAIL_ORDER_NOT_APPROVE = new ErrorCode(1_030_105_006, "创建来票失败,原因:采购订单不是审核通过状态");
    ErrorCode PURCHASE_INVOICE_CREATE_FAIL_SUPPLIER_MISMATCH = new ErrorCode(1_030_105_007, "创建来票失败,原因:来票供应商与采购订单不一致");
    ErrorCode PURCHASE_INVOICE_CREATE_FAIL_PRICE_EXCEEDS_LIMIT = new ErrorCode(1_030_105_008, "创建来票失败,原因:来票金额超出采购订单金额,目前可开票:{} å…ƒ");
    ErrorCode PURCHASE_INVOICE_APPROVE_FAIL = new ErrorCode(1_030_105_009, "审核失败,只有未审核的来票才能审核");
    ErrorCode PURCHASE_INVOICE_PROCESS_FAIL = new ErrorCode(1_030_105_010, "反审核失败,只有已审核的来票才能反审核");
    // ========== ERP é‡‡è´­é€€è´§ï¼ˆ1-030-103-000) ==========
    ErrorCode PURCHASE_RETURN_NOT_EXISTS = new ErrorCode(1_030_103_000, "采购退货单不存在");
@@ -193,7 +189,6 @@
    ErrorCode FINANCE_PAYMENT_NO_EXISTS = new ErrorCode(1_030_601_004, "生成付款单号失败,请重新提交");
    ErrorCode FINANCE_PAYMENT_UPDATE_FAIL_APPROVE = new ErrorCode(1_030_601_005, "付款单({})已审核,无法修改");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_EXISTS = new ErrorCode(1_030_601_006, "创建付款单失败,原因:关联的来票不存在");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_APPROVE = new ErrorCode(1_030_601_007, "创建付款单失败,原因:关联的来票不是审批通过状态");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_ATTACHMENT = new ErrorCode(1_030_601_008, "创建付款单失败,原因:关联的来票未上传发票附件");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_SUPPLIER_MISMATCH = new ErrorCode(1_030_601_009, "创建付款单失败,原因:关联的来票与所选供应商不匹配");
    ErrorCode FINANCE_PAYMENT_CREATE_FAIL_INVOICE_PRICE_EXCEEDS = new ErrorCode(1_030_601_010, "创建付款单失败,原因:付款金额超出该来票剩余可付金额,目前可付:{} å…ƒ");
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/finance/ErpFinancePaymentServiceImpl.java
@@ -20,7 +20,6 @@
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpAuditStatus;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceService;
import cn.iocoder.yudao.module.erp.service.purchase.ErpPurchaseInvoiceServiceImpl;
import cn.iocoder.yudao.module.erp.service.purchase.ErpSupplierService;
@@ -354,7 +353,7 @@
    }
    /**
     * æ ¡éªŒä»˜æ¬¾å•关联的来票:存在、审批通过、已上传发票附件、供应商与来票一致
     * æ ¡éªŒä»˜æ¬¾å•关联的来票:存在、已上传发票附件、供应商与来票一致
     *
     * @param invoiceId æ¥ç¥¨ç¼–号
     * @param supplierId ä¾›åº”商编号
@@ -365,16 +364,12 @@
        if (invoice == null) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_EXISTS);
        }
        // 2. æ ¡éªŒæ¥ç¥¨å®¡æ‰¹é€šè¿‡
        if (ObjectUtil.notEqual(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_APPROVE);
        }
        // 3. æ ¡éªŒå·²ä¸Šä¼ å‘票附件
        // 2. æ ¡éªŒå·²ä¸Šä¼ å‘票附件
        if (CollUtil.isEmpty(storageAttachmentApi.listAttachments(
                ErpPurchaseInvoiceServiceImpl.PURCHASE_INVOICE_RECORD_TYPE, invoiceId))) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_NOT_ATTACHMENT);
        }
        // 4. æ ¡éªŒä¾›åº”商与来票一致(防脏数据)
        // 3. æ ¡éªŒä¾›åº”商与来票一致(防脏数据)
        if (ObjectUtil.notEqual(invoice.getSupplierId(), supplierId)) {
            throw exception(FINANCE_PAYMENT_CREATE_FAIL_INVOICE_SUPPLIER_MISMATCH);
        }
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceService.java
@@ -42,14 +42,6 @@
    void deletePurchaseInvoice(Long id);
    /**
     * æ›´æ–°æ¥ç¥¨çš„状态(审核/反审核)
     *
     * @param id ç¼–号
     * @param status çŠ¶æ€
     */
    void updatePurchaseInvoiceStatus(Long id, Integer status);
    /**
     * èŽ·å¾—æ¥ç¥¨
     *
     * @param id ç¼–号
@@ -84,28 +76,19 @@
    PageResult<ErpPurchaseInvoiceDO> getPurchaseInvoicePage(ErpPurchaseInvoicePageReqVO pageReqVO);
    /**
     * æ ¹æ®é‡‡è´­è®¢å•编号和审批状态获得来票列表
     * æ ¹æ®é‡‡è´­è®¢å•编号获得来票列表
     *
     * @param orderId é‡‡è´­è®¢å•编号
     * @param auditStatuses å®¡æ‰¹çŠ¶æ€é›†åˆ
     * @return æ¥ç¥¨åˆ—表
     */
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses);
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderId(Long orderId);
    /**
     * æ ¹æ®ä¾›åº”商编号和审批状态获得来票列表
     * æ ¹æ®ä¾›åº”商编号获得来票列表
     *
     * @param supplierId ä¾›åº”商编号
     * @param auditStatuses å®¡æ‰¹çŠ¶æ€é›†åˆ
     * @return æ¥ç¥¨åˆ—表
     */
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses);
    /**
     * èŽ·å¾—å¾…å®¡æ ¸æ¥ç¥¨æ•°é‡
     *
     * @return æ•°é‡
     */
    Long getAuditInvoiceCount();
    List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierId(Long supplierId);
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ErpPurchaseInvoiceServiceImpl.java
@@ -9,9 +9,9 @@
import cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.invoice.ErpPurchaseInvoiceSaveReqVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseInvoiceDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.purchase.ErpPurchaseOrderDO;
import cn.iocoder.yudao.module.erp.dal.mysql.finance.ErpFinancePaymentMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.purchase.ErpPurchaseInvoiceMapper;
import cn.iocoder.yudao.module.erp.dal.redis.no.ErpNoRedisDAO;
import cn.iocoder.yudao.module.erp.enums.ErpPurchaseInvoiceAuditStatusEnum;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -21,6 +21,7 @@
import java.math.BigDecimal;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.getSumValue;
@@ -44,6 +45,9 @@
    private ErpPurchaseInvoiceMapper invoiceMapper;
    @Resource
    private ErpFinancePaymentMapper financePaymentMapper;
    @Resource
    private ErpNoRedisDAO noRedisDAO;
    @Resource
@@ -65,9 +69,8 @@
            throw exception(PURCHASE_INVOICE_NO_EXISTS);
        }
        // 2.1 æ’入来票(创建后即为未审核状态)
        ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class)
                .setNo(no).setAuditStatus(ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus());
        // 2.1 æ’入来票(创建后即为有效状态,无需审核)
        ErpPurchaseInvoiceDO invoice = BeanUtils.toBean(createReqVO, ErpPurchaseInvoiceDO.class).setNo(no);
        invoiceMapper.insert(invoice);
        // 2.2 ç»‘定发票附件
@@ -88,12 +91,9 @@
    }
    private void validatePurchaseInvoicePriceExceedsLimit(ErpPurchaseInvoiceSaveReqVO reqVO) {
        // 1. è®¡ç®—剩余可开票金额,不包括 reqVO è‡ªèº«ï¼ˆå®¡æ‰¹ä¸­ä¹Ÿå ç”¨é¢åº¦ï¼Œé˜²æ­¢ç´¯è®¡æ¥ç¥¨è¶…过采购订单金额)
        // 1. è®¡ç®—剩余可开票金额,不包括 reqVO è‡ªèº«ï¼ˆæ‰€æœ‰æœ‰æ•ˆæ¥ç¥¨éƒ½å ç”¨é¢åº¦ï¼Œé˜²æ­¢ç´¯è®¡æ¥ç¥¨è¶…过采购订单金额)
        ErpPurchaseOrderDO purchaseOrder = purchaseOrderService.getPurchaseOrder(reqVO.getPurchaseOrderId());
        List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderIdAndStatus(reqVO.getPurchaseOrderId(),
                List.of(ErpPurchaseInvoiceAuditStatusEnum.DRAFT.getStatus(),
                        ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus(),
                        ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus()));
        List<ErpPurchaseInvoiceDO> invoices = invoiceMapper.selectListByOrderId(reqVO.getPurchaseOrderId());
        if (reqVO.getId() != null) {
            invoices.removeIf(invoice -> ObjectUtil.equal(invoice.getId(), reqVO.getId()));
        }
@@ -110,12 +110,8 @@
    public void updatePurchaseInvoice(ErpPurchaseInvoiceSaveReqVO updateReqVO) {
        Assert.notNull(updateReqVO.getId(), "来票编号不能为空");
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseInvoiceDO oldInvoice = validatePurchaseInvoiceExists(updateReqVO.getId());
        // 1.2 åªæœ‰æœªå®¡æ ¸å¯ä»¥ç¼–辑
        if (ObjectUtil.notEqual(oldInvoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.PROCESS.getStatus())) {
            throw exception(PURCHASE_INVOICE_UPDATE_FAIL_EDITING_PROHIBITED, oldInvoice.getNo());
        }
        // 1.3 æ ¡éªŒå…³è”数据 + å¯å¼€ç¥¨é‡‘额
        validatePurchaseInvoiceExists(updateReqVO.getId());
        // 1.2 æ ¡éªŒå…³è”数据 + å¯å¼€ç¥¨é‡‘额
        validateRelationDataExists(updateReqVO);
        validatePurchaseInvoicePriceExceedsLimit(updateReqVO);
@@ -131,35 +127,17 @@
    @Transactional(rollbackFor = Exception.class)
    public void deletePurchaseInvoice(Long id) {
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseInvoiceDO invoice = validatePurchaseInvoiceExists(id);
        // 1.2 å®¡æ‰¹é€šè¿‡æ—¶ï¼Œä¸å…è®¸åˆ é™¤
        if (ObjectUtil.equal(invoice.getAuditStatus(), ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus())) {
            throw exception(PURCHASE_INVOICE_DELETE_FAIL_IS_APPROVE);
        validatePurchaseInvoiceExists(id);
        // 1.2 å·²æœ‰æœ‰æ•ˆä»˜æ¬¾å•关联的来票,不允许删除(保持来票与付款的关联完整性)
        Map<Long, BigDecimal> paidMap = financePaymentMapper.selectPaymentPriceMapByInvoiceId(List.of(id));
        if (paidMap.getOrDefault(id, BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0) {
            throw exception(PURCHASE_INVOICE_DELETE_FAIL_HAS_PAYMENT);
        }
        // 2.1 åˆ é™¤å‘票附件
        storageAttachmentApi.deleteAttachmentsByRecord(PURCHASE_INVOICE_RECORD_TYPE, id);
        // 2.2 åˆ é™¤æ¥ç¥¨
        invoiceMapper.deleteById(id);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updatePurchaseInvoiceStatus(Long id, Integer status) {
        boolean approve = ErpPurchaseInvoiceAuditStatusEnum.APPROVE.getStatus().equals(status);
        // 1.1 æ ¡éªŒå­˜åœ¨
        ErpPurchaseInvoiceDO invoice = validatePurchaseInvoiceExists(id);
        // 1.2 æ ¡éªŒçŠ¶æ€
        if (invoice.getAuditStatus().equals(status)) {
            throw exception(approve ? PURCHASE_INVOICE_APPROVE_FAIL : PURCHASE_INVOICE_PROCESS_FAIL);
        }
        // 2. æ›´æ–°çŠ¶æ€
        int updateCount = invoiceMapper.updateByIdAndStatus(id, invoice.getAuditStatus(),
                new ErpPurchaseInvoiceDO().setAuditStatus(status));
        if (updateCount == 0) {
            throw exception(approve ? PURCHASE_INVOICE_APPROVE_FAIL : PURCHASE_INVOICE_PROCESS_FAIL);
        }
    }
    private ErpPurchaseInvoiceDO validatePurchaseInvoiceExists(Long id) {
@@ -189,18 +167,13 @@
    }
    @Override
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderIdAndStatus(Long orderId, Collection<Integer> auditStatuses) {
        return invoiceMapper.selectListByOrderIdAndStatus(orderId, auditStatuses);
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListByOrderId(Long orderId) {
        return invoiceMapper.selectListByOrderId(orderId);
    }
    @Override
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierIdAndStatus(Long supplierId, Collection<Integer> auditStatuses) {
        return invoiceMapper.selectListBySupplierIdAndStatus(supplierId, auditStatuses);
    }
    @Override
    public Long getAuditInvoiceCount() {
        return invoiceMapper.selectCountByAudit();
    public List<ErpPurchaseInvoiceDO> getPurchaseInvoiceListBySupplierId(Long supplierId) {
        return invoiceMapper.selectListBySupplierId(supplierId);
    }
}
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/sale/ErpSaleOrderServiceImpl.java
@@ -101,6 +101,7 @@
        // 2.1 æ’入订单
        ErpSaleOrderDO saleOrder = BeanUtils.toBean(createReqVO, ErpSaleOrderDO.class, in -> in
                .setNo(no).setStatus(ErpAuditStatus.PROCESS.getStatus())
                .setDepositPrice(ObjectUtil.defaultIfNull(createReqVO.getDepositPrice(), BigDecimal.ZERO))
                .setOutCount(BigDecimal.ZERO).setOutStatus(ErpSaleOrderOutStatusEnum.NOT_OUT.getStatus()));
        calculateTotalPrice(saleOrder, saleOrderItems);
        saleOrderMapper.insert(saleOrder);
@@ -136,6 +137,7 @@
        // 2.1 æ›´æ–°è®¢å•
        ErpSaleOrderDO updateObj = BeanUtils.toBean(updateReqVO, ErpSaleOrderDO.class);
        updateObj.setDepositPrice(ObjectUtil.defaultIfNull(updateReqVO.getDepositPrice(), BigDecimal.ZERO));
        calculateTotalPrice(updateObj, saleOrderItems);
        saleOrderMapper.updateById(updateObj);
        // 2.2 æ›´æ–°è®¢å•项
@@ -478,4 +480,4 @@
        }
    }
}
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/SrmSupplierCertificateController.java
@@ -6,7 +6,9 @@
import cn.iocoder.yudao.module.srm.controller.admin.certificate.vo.*;
import cn.iocoder.yudao.module.srm.dal.dataobject.SrmSupplierDO;
import cn.iocoder.yudao.module.srm.service.certificate.SrmSupplierCertificateService;
import cn.iocoder.yudao.module.srm.service.certificate.SrmSupplierCertificateServiceImpl;
import cn.iocoder.yudao.module.srm.service.supplier.SrmSupplierService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -34,6 +36,9 @@
    @Resource
    private SrmSupplierService supplierService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建资质证书")
@@ -66,6 +71,7 @@
        var pageResult = certificateService.getCertificatePage(pageReqVO);
        var respList = BeanUtils.toBean(pageResult.getList(), SrmSupplierCertificateRespVO.class);
        enrichSupplierNames(respList);
        enrichAttachments(respList);
        return success(new PageResult<>(respList, pageResult.getTotal()));
    }
@@ -75,6 +81,7 @@
    public CommonResult<List<SrmSupplierCertificateRespVO>> getListBySupplier(@RequestParam("supplierId") Long supplierId) {
        var list = BeanUtils.toBean(certificateService.getCertificateListBySupplierId(supplierId), SrmSupplierCertificateRespVO.class);
        enrichSupplierNames(list);
        enrichAttachments(list);
        return success(list);
    }
@@ -93,6 +100,7 @@
    public CommonResult<SrmSupplierCertificateRespVO> getCertificate(@RequestParam("id") Long id) {
        var resp = BeanUtils.toBean(certificateService.getCertificate(id), SrmSupplierCertificateRespVO.class);
        enrichSupplierName(resp);
        enrichAttachments(List.of(resp));
        return success(resp);
    }
@@ -118,4 +126,10 @@
        }
    }
    private void enrichAttachments(List<SrmSupplierCertificateRespVO> list) {
        if (list == null || list.isEmpty()) return;
        list.forEach(vo -> vo.setAttachmentList(storageAttachmentApi.listAttachments(
                SrmSupplierCertificateServiceImpl.CERTIFICATE_RECORD_TYPE, vo.getId())));
    }
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateRespVO.java
@@ -1,10 +1,12 @@
package cn.iocoder.yudao.module.srm.controller.admin.certificate.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - SRM ä¾›åº”商资质证书 Response VO")
@Data
@@ -52,4 +54,7 @@
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    @Schema(description = "证书附件列表")
    private List<StorageBlobRespDTO> attachmentList;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/certificate/vo/SrmSupplierCertificateSaveReqVO.java
@@ -6,6 +6,7 @@
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - SRM ä¾›åº”商资质证书创建/更新 Request VO")
@Data
@@ -47,4 +48,7 @@
    @Schema(description = "备注", example = "年度资质审核")
    private String remark;
    @Schema(description = "证书附件文件ID列表")
    private List<Long> blobIds;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/supplier/SrmSupplierController.java
@@ -80,6 +80,13 @@
        return success(BeanUtils.toBean(list, SrmSupplierRespVO.class));
    }
    @GetMapping("/list")
    @Operation(summary = "获取供应商档案列表(不过滤准入状态)")
    public CommonResult<List<SrmSupplierRespVO>> getSupplierAllList() {
        List<SrmSupplierDO> list = supplierService.getSupplierAllList();
        return success(BeanUtils.toBean(list, SrmSupplierRespVO.class));
    }
    @GetMapping("/export-excel")
    @Operation(summary = "导出供应商 Excel")
    @PreAuthorize("@ss.hasPermission('srm:supplier:export')")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/SrmTenderProjectController.java
@@ -5,6 +5,8 @@
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.*;
import cn.iocoder.yudao.module.srm.service.tender.SrmTenderProjectService;
import cn.iocoder.yudao.module.srm.service.tender.SrmTenderProjectServiceImpl;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -26,6 +28,9 @@
    @Resource
    private SrmTenderProjectService tenderProjectService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建招标项目")
@@ -54,14 +59,27 @@
    @Operation(summary = "获取招标项目详情")
    @PreAuthorize("@ss.hasPermission('srm:tender-project:query')")
    public CommonResult<SrmTenderProjectRespVO> getTenderProject(@RequestParam("id") Long id) {
        return success(BeanUtils.toBean(tenderProjectService.getTenderProject(id), SrmTenderProjectRespVO.class));
        var resp = BeanUtils.toBean(tenderProjectService.getTenderProject(id), SrmTenderProjectRespVO.class);
        enrichAttachments(List.of(resp));
        return success(resp);
    }
    @GetMapping("/page")
    @Operation(summary = "获取招标项目分页")
    @PreAuthorize("@ss.hasPermission('srm:tender-project:query')")
    public CommonResult<PageResult<SrmTenderProjectRespVO>> getTenderProjectPage(@Valid SrmTenderProjectPageReqVO pageReqVO) {
        return success(BeanUtils.toBean(tenderProjectService.getTenderProjectPage(pageReqVO), SrmTenderProjectRespVO.class));
        var pageResult = tenderProjectService.getTenderProjectPage(pageReqVO);
        var respList = BeanUtils.toBean(pageResult.getList(), SrmTenderProjectRespVO.class);
        enrichAttachments(respList);
        return success(new PageResult<>(respList, pageResult.getTotal()));
    }
    // ========== å…³è”数据填充 ==========
    private void enrichAttachments(List<SrmTenderProjectRespVO> list) {
        if (list == null || list.isEmpty()) return;
        list.forEach(vo -> vo.setAttachmentList(storageAttachmentApi.listAttachments(
                SrmTenderProjectServiceImpl.TENDER_PROJECT_RECORD_TYPE, vo.getId())));
    }
    @PutMapping("/publish")
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectPartialUpdateReqVO.java
@@ -4,6 +4,8 @@
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - SRM æ‹›æ ‡é¡¹ç›®éƒ¨åˆ†æ›´æ–° Request VO(发布中/投标中阶段)")
@Data
public class SrmTenderProjectPartialUpdateReqVO {
@@ -18,4 +20,7 @@
    @Schema(description = "资质要求", example = "需具备ISO9001认证")
    private String qualificationRequirements;
    @Schema(description = "资质要求附件文件ID列表")
    private List<Long> blobIds;
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectRespVO.java
@@ -1,10 +1,12 @@
package cn.iocoder.yudao.module.srm.controller.admin.tender.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - SRM æ‹›æ ‡é¡¹ç›® Response VO")
@Data
@@ -55,6 +57,9 @@
    @Schema(description = "资质要求", example = "需具备ISO9001认证")
    private String qualificationRequirements;
    @Schema(description = "资质要求附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "采购申请ID", example = "1")
    private Long purchaseRequestId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/controller/admin/tender/vo/SrmTenderProjectSaveReqVO.java
@@ -7,6 +7,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - SRM æ‹›æ ‡é¡¹ç›®åˆ›å»º/更新 Request VO")
@Data
@@ -60,6 +61,9 @@
    @Schema(description = "资质要求", example = "需具备ISO9001认证")
    private String qualificationRequirements;
    @Schema(description = "资质要求附件文件ID列表")
    private List<Long> blobIds;
    @Schema(description = "采购申请ID", example = "1")
    private Long purchaseRequestId;
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyService.java
@@ -61,4 +61,11 @@
     */
    void approve(Long id);
    /**
     * ç¡®ä¿ä¾›åº”商已准入:未准入则自动创建准入申请并置为已通过
     *
     * @param supplierId ä¾›åº”商编号
     */
    void ensureSupplierApproved(Long supplierId);
}
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/apply/SrmSupplierApplyServiceImpl.java
@@ -145,6 +145,43 @@
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void ensureSupplierApproved(Long supplierId) {
        if (supplierId == null) {
            return;
        }
        // æ ¡éªŒä¾›åº”商存在
        SrmSupplierDO supplier = supplierMapper.selectById(supplierId);
        if (supplier == null) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.SUPPLIER_NOT_EXISTS);
        }
        // å·²å‡†å…¥åˆ™æ— éœ€å¤„理
        if (supplierApplyMapper.existsApprovedBySupplierId(supplierId)) {
            return;
        }
        // è‡ªåŠ¨åˆ›å»ºå‡†å…¥ç”³è¯·å¹¶ç›´æŽ¥ç½®ä¸ºå·²é€šè¿‡
        SrmSupplierApplyDO apply = new SrmSupplierApplyDO();
        apply.setApplyNo(generateApplyNo());
        apply.setSupplierId(supplierId);
        apply.setSupplierName(supplier.getName());
        apply.setSupplierType(supplier.getType());
        apply.setCompanyNature(supplier.getCompanyNature());
        apply.setRegisteredCapital(supplier.getRegisteredCapital());
        apply.setBusinessLicenseNo(supplier.getBusinessLicenseNo());
        apply.setTaxNo(supplier.getTaxNo());
        apply.setAddress(supplier.getAddress());
        apply.setBankName(supplier.getBankName());
        apply.setBankAccount(supplier.getBankAccount());
        apply.setContactName(supplier.getContactName());
        apply.setContactMobile(supplier.getContactMobile());
        apply.setContactEmail(supplier.getContactEmail());
        apply.setRemark("中标生成采购订单时自动准入并通过");
        apply.setApplyStatus(SupplierApplyStatusEnum.APPROVED.getCode());
        apply.setApplyTime(LocalDateTime.now());
        supplierApplyMapper.insert(apply);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void approve(Long id) {
        SrmSupplierApplyDO apply = validateApplyExists(id);
        if (!SupplierApplyStatusEnum.APPROVED.getCode().equals(apply.getApplyStatus())) {
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/certificate/SrmSupplierCertificateServiceImpl.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.srm.service.certificate;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.srm.controller.admin.certificate.vo.SrmSupplierCertificatePageReqVO;
@@ -8,8 +9,10 @@
import cn.iocoder.yudao.module.srm.dal.mysql.SrmSupplierCertificateMapper;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDate;
@@ -19,28 +22,47 @@
@Validated
public class SrmSupplierCertificateServiceImpl implements SrmSupplierCertificateService {
    /**
     * èµ„质证书附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#SRM_SUPPLIER_CERTIFICATE}
     */
    public static final String CERTIFICATE_RECORD_TYPE = "srm_supplier_certificate";
    @Resource
    private SrmSupplierCertificateMapper certificateMapper;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createCertificate(SrmSupplierCertificateSaveReqVO createReqVO) {
        SrmSupplierCertificateDO certificate = BeanUtils.toBean(createReqVO, SrmSupplierCertificateDO.class);
        updateStatus(certificate);
        certificateMapper.insert(certificate);
        // ç»‘定证书附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", CERTIFICATE_RECORD_TYPE, certificate.getId(), createReqVO.getBlobIds());
        }
        return certificate.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateCertificate(SrmSupplierCertificateSaveReqVO updateReqVO) {
        validateCertificateExists(updateReqVO.getId());
        SrmSupplierCertificateDO updateObj = BeanUtils.toBean(updateReqVO, SrmSupplierCertificateDO.class);
        updateStatus(updateObj);
        certificateMapper.updateById(updateObj);
        // å¢žé‡æ›´æ–°è¯ä¹¦é™„ä»¶
        storageAttachmentApi.updateAttachments("file", CERTIFICATE_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteCertificate(Long id) {
        validateCertificateExists(id);
        // åˆ é™¤è¯ä¹¦é™„ä»¶
        storageAttachmentApi.deleteAttachmentsByRecord(CERTIFICATE_RECORD_TYPE, id);
        certificateMapper.deleteById(id);
    }
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierService.java
@@ -46,6 +46,11 @@
    List<SrmSupplierDO> getSupplierSimpleList();
    /**
     * èŽ·å–å…¨éƒ¨ä¾›åº”å•†æ¡£æ¡ˆåˆ—è¡¨ï¼ˆä¸è¿‡æ»¤å‡†å…¥çŠ¶æ€ï¼Œç”¨äºŽæŠ•æ ‡é€‰æ‹©ä¾›åº”å•†ï¼‰
     */
    List<SrmSupplierDO> getSupplierAllList();
    /**
     * èŽ·å–ä¾›åº”å•†åˆ—è¡¨
     */
    List<SrmSupplierDO> getSupplierList(Collection<Long> ids);
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/supplier/SrmSupplierServiceImpl.java
@@ -3,6 +3,7 @@
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.srm.controller.admin.supplier.vo.SrmSupplierPageReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.supplier.vo.SrmSupplierSaveReqVO;
import cn.iocoder.yudao.module.srm.dal.dataobject.*;
@@ -107,6 +108,12 @@
    }
    @Override
    public List<SrmSupplierDO> getSupplierAllList() {
        return supplierMapper.selectList(new LambdaQueryWrapperX<SrmSupplierDO>()
                .orderByAsc(SrmSupplierDO::getId));
    }
    @Override
    public List<SrmSupplierDO> getSupplierList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmBidAwardServiceImpl.java
@@ -14,6 +14,7 @@
import cn.iocoder.yudao.module.srm.enums.BidStatusEnum;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import cn.iocoder.yudao.module.srm.service.apply.SrmSupplierApplyService;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -61,6 +62,9 @@
    @Resource
    private ErpPurchaseOrderApi erpPurchaseOrderApi;
    @Resource
    private SrmSupplierApplyService supplierApplyService;
    @Override
    @Transactional(rollbackFor = Exception.class)
@@ -162,6 +166,9 @@
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_SUPPLIER_NOT_SYNCED);
        }
        // ä¾›åº”商未准入则自动准入并通过,保证采购订单流程可用
        supplierApplyService.ensureSupplierApproved(srmSupplier.getId());
        // 3. æŸ¥æ‹›æ ‡ç‰©æ–™åˆ—表
        List<SrmTenderMaterialDO> materials = tenderMaterialMapper
                .selectListByTenderProjectId(award.getTenderProjectId());
yudao-module-srm/src/main/java/cn/iocoder/yudao/module/srm/service/tender/SrmTenderProjectServiceImpl.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.erp.api.purchase.dto.ErpPurchaseOrderCreateReqDTO;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderMaterialSaveReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPageReqVO;
import cn.iocoder.yudao.module.srm.controller.admin.tender.vo.SrmTenderProjectPartialUpdateReqVO;
@@ -18,6 +19,7 @@
import cn.iocoder.yudao.module.srm.enums.BidStatusEnum;
import cn.iocoder.yudao.module.srm.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.srm.enums.TenderStatusEnum;
import cn.iocoder.yudao.module.srm.service.apply.SrmSupplierApplyService;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import com.mzt.logapi.context.LogRecordContext;
import com.mzt.logapi.starter.annotation.LogRecord;
@@ -41,6 +43,11 @@
@Slf4j
public class SrmTenderProjectServiceImpl implements SrmTenderProjectService {
    /**
     * æ‹›æ ‡é¡¹ç›®èµ„质要求附件的业务记录类型,对应 {@link cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum#SRM_TENDER_PROJECT}
     */
    public static final String TENDER_PROJECT_RECORD_TYPE = "srm_tender_project";
    @Resource
    private SrmTenderProjectMapper tenderProjectMapper;
    @Resource
@@ -60,8 +67,13 @@
    private MdmItemApi mdmItemApi;
    @Resource
    private ErpPurchaseOrderApi erpPurchaseOrderApi;
    @Resource
    private SrmSupplierApplyService supplierApplyService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createTenderProject(SrmTenderProjectSaveReqVO createReqVO) {
        SrmTenderProjectDO project = BeanUtils.toBean(createReqVO, SrmTenderProjectDO.class);
        project.setTenderStatus(TenderStatusEnum.DRAFT.getCode());
@@ -69,10 +81,15 @@
            project.setBiddingMode(tenderConfigService.getConfig().getDefaultBiddingMode());
        }
        tenderProjectMapper.insert(project);
        // ç»‘定资质要求附件
        if (CollUtil.isNotEmpty(createReqVO.getBlobIds())) {
            storageAttachmentApi.bindAttachments("file", TENDER_PROJECT_RECORD_TYPE, project.getId(), createReqVO.getBlobIds());
        }
        return project.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE,
            bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS)
    public void updateTenderProject(SrmTenderProjectSaveReqVO updateReqVO) {
@@ -84,6 +101,8 @@
            SrmTenderProjectDO updateObj = BeanUtils.toBean(updateReqVO, SrmTenderProjectDO.class);
            updateObj.setTenderStatus(status); // çŠ¶æ€ä¸å…è®¸å‰ç«¯ä¼ å…¥è¦†ç›–
            tenderProjectMapper.updateById(updateObj);
            // å¢žé‡æ›´æ–°èµ„质要求附件
            storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
            LogRecordContext.putVariable("tenderName", project.getTenderName());
            LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
            LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
@@ -99,6 +118,8 @@
            updateObj.setTenderDesc(updateReqVO.getTenderDesc());
            updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements());
            tenderProjectMapper.updateById(updateObj);
            // å¢žé‡æ›´æ–°èµ„质要求附件
            storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
            LogRecordContext.putVariable("tenderName", project.getTenderName());
            LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
            LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
@@ -112,6 +133,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteTenderProject(Long id) {
        SrmTenderProjectDO project = validateTenderExists(id);
        if (!TenderStatusEnum.DRAFT.getCode().equals(project.getTenderStatus())) {
@@ -121,6 +143,8 @@
        if (tenderBidMapper.countByTenderProjectId(id) > 0) {
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.TENDER_HAS_BIDS_CANNOT_DELETE);
        }
        // åˆ é™¤èµ„质要求附件
        storageAttachmentApi.deleteAttachmentsByRecord(TENDER_PROJECT_RECORD_TYPE, id);
        tenderMaterialMapper.deleteByTenderProjectId(id);
        tenderProjectMapper.deleteById(id);
    }
@@ -284,6 +308,9 @@
            throw ServiceExceptionUtil.exception(ErrorCodeConstants.AWARD_SUPPLIER_NOT_SYNCED);
        }
        // ä¾›åº”商未准入则自动准入并通过,保证采购订单流程可用
        supplierApplyService.ensureSupplierApproved(supplier.getId());
        // æŸ¥æ‹›æ ‡ç‰©æ–™
        List<SrmTenderMaterialDO> materials = tenderMaterialMapper
                .selectListByTenderProjectId(award.getTenderProjectId());
@@ -368,6 +395,7 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = LOG_TENDER_TYPE, subType = LOG_TENDER_UPDATE_SUB_TYPE,
            bizNo = "{{#updateReqVO.id}}", success = LOG_TENDER_UPDATE_SUCCESS)
    public void updateTenderProjectPartial(SrmTenderProjectPartialUpdateReqVO updateReqVO) {
@@ -380,6 +408,8 @@
        updateObj.setTenderDesc(updateReqVO.getTenderDesc());
        updateObj.setQualificationRequirements(updateReqVO.getQualificationRequirements());
        tenderProjectMapper.updateById(updateObj);
        // å¢žé‡æ›´æ–°èµ„质要求附件
        storageAttachmentApi.updateAttachments("file", TENDER_PROJECT_RECORD_TYPE, updateReqVO.getId(), updateReqVO.getBlobIds());
        LogRecordContext.putVariable("tenderName", project.getTenderName());
        LogRecordContext.putVariable("oldTenderDesc", project.getTenderDesc());
        LogRecordContext.putVariable("newTenderDesc", updateReqVO.getTenderDesc());
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
@@ -60,7 +60,11 @@
    AFTER_SALE_RETURN("aftersales_after_sale_return", "售后退货申请"),
    // ===== MES =====
    MES_PD_DOCUMENT("mes_pd_document", "MES è®¾è®¡èµ„æ–™");
    MES_PD_DOCUMENT("mes_pd_document", "MES è®¾è®¡èµ„æ–™"),
    // ===== SRM =====
    SRM_SUPPLIER_CERTIFICATE("srm_supplier_certificate", "SRM ä¾›åº”商资质证书"),
    SRM_TENDER_PROJECT("srm_tender_project", "SRM æ‹›æ ‡é¡¹ç›®");
    private final String type;
    private final String description;