zhuangyunsheng 5 ore fa
parent
commit
4d429d1192
96 ha cambiato i file con 6712 aggiunte e 356 eliminazioni
  1. 0 2
      .env
  2. 4 2
      .env.development
  3. 1 3
      .env.production
  4. 10 0
      README.md
  5. 3 3
      src/api/model/auth.js
  6. 4 4
      src/api/model/basic.js
  7. 2 2
      src/api/model/common.js
  8. 1 1
      src/api/model/equip.js
  9. 49 0
      src/api/model/outsourcing.js
  10. 2 2
      src/api/model/process.js
  11. 45 4
      src/api/model/production.js
  12. 49 0
      src/api/model/purchase.js
  13. 3 3
      src/api/model/sales.js
  14. 5 5
      src/api/model/system.js
  15. 52 0
      src/api/model/warehouse.js
  16. 2 0
      src/components/scFormTable/detail.vue
  17. 8 3
      src/components/scFormTable/index.vue
  18. 13 5
      src/components/scTable/renderer/cell-tag.vue
  19. 3 0
      src/config/index.js
  20. 1 1
      src/config/table.js
  21. 1 1
      src/layout/index.vue
  22. 12 30
      src/store/modules/viewTags.js
  23. 2 1
      src/style/fix.scss
  24. 177 2
      src/utils/basicDic.js
  25. 3 3
      src/views/basic/qualityPlan/desc.vue
  26. 104 0
      src/views/outsourcing/order/desc.vue
  27. 240 0
      src/views/outsourcing/order/detail.vue
  28. 133 0
      src/views/outsourcing/order/index.vue
  29. 82 0
      src/views/outsourcing/order/main.js
  30. 94 0
      src/views/outsourcing/plan/desc.vue
  31. 176 0
      src/views/outsourcing/plan/detail.vue
  32. 161 0
      src/views/outsourcing/plan/index.vue
  33. 54 0
      src/views/outsourcing/plan/main.js
  34. 53 8
      src/views/process/line/desc.vue
  35. 4 9
      src/views/process/line/main.js
  36. 3 3
      src/views/production/bom/desc.vue
  37. 5 2
      src/views/production/bom/detail.vue
  38. 4 2
      src/views/production/bom/index.vue
  39. 4 4
      src/views/production/bom/main.js
  40. 134 0
      src/views/production/dispatch/desc.vue
  41. 175 0
      src/views/production/dispatch/detail.vue
  42. 158 0
      src/views/production/dispatch/index.vue
  43. 29 0
      src/views/production/dispatch/main.js
  44. 134 0
      src/views/production/dispatch/report.vue
  45. 119 0
      src/views/production/inspection/index.vue
  46. 89 0
      src/views/production/order/desc.vue
  47. 211 0
      src/views/production/order/detail.vue
  48. 162 0
      src/views/production/order/dispatch.vue
  49. 127 30
      src/views/production/order/index.vue
  50. 64 0
      src/views/production/order/main.js
  51. 18 40
      src/views/production/plan/desc.vue
  52. 50 69
      src/views/production/plan/detail.vue
  53. 47 20
      src/views/production/plan/index.vue
  54. 0 34
      src/views/production/plan/main copy.js
  55. 8 10
      src/views/production/plan/main.js
  56. 69 12
      src/views/production/prePlan/detail.vue
  57. 2 2
      src/views/production/prePlan/index.vue
  58. 5 5
      src/views/production/prePlan/main.js
  59. 124 0
      src/views/production/report/detail.vue
  60. 143 0
      src/views/production/report/index.vue
  61. 32 0
      src/views/production/report/main.js
  62. 118 0
      src/views/production/rework/index.vue
  63. 151 0
      src/views/purchase/inspection/entry.vue
  64. 107 0
      src/views/purchase/inspection/index.vue
  65. 44 0
      src/views/purchase/inspection/main.js
  66. 108 0
      src/views/purchase/order/desc.vue
  67. 265 0
      src/views/purchase/order/detail.vue
  68. 166 0
      src/views/purchase/order/index.vue
  69. 53 0
      src/views/purchase/order/main.js
  70. 82 0
      src/views/purchase/plan/desc.vue
  71. 156 0
      src/views/purchase/plan/detail.vue
  72. 158 0
      src/views/purchase/plan/index.vue
  73. 27 0
      src/views/purchase/plan/main.js
  74. 4 4
      src/views/sales/order/desc.vue
  75. 10 8
      src/views/sales/order/index.vue
  76. 8 5
      src/views/sales/order/main.js
  77. 1 1
      src/views/sales/performance/components/line.vue
  78. 1 1
      src/views/sales/plan/detail.vue
  79. 6 6
      src/views/sales/plan/index.vue
  80. 167 0
      src/views/warehouse/inbound/confirm.vue
  81. 106 0
      src/views/warehouse/inbound/detail.vue
  82. 134 0
      src/views/warehouse/inbound/index.vue
  83. 53 0
      src/views/warehouse/inbound/main.js
  84. 129 0
      src/views/warehouse/inventory/detail.vue
  85. 82 0
      src/views/warehouse/inventory/index.vue
  86. 14 0
      src/views/warehouse/inventory/main.js
  87. 149 0
      src/views/warehouse/outbound/detail.vue
  88. 151 0
      src/views/warehouse/outbound/index.vue
  89. 106 0
      src/views/warehouse/outbound/list.vue
  90. 23 0
      src/views/warehouse/outbound/main.js
  91. 177 0
      src/views/warehouse/outbound/review.vue
  92. 108 0
      src/views/warehouse/production/requisition/desc.vue
  93. 198 0
      src/views/warehouse/production/requisition/detail.vue
  94. 135 0
      src/views/warehouse/production/requisition/index.vue
  95. 42 0
      src/views/warehouse/production/requisition/main.js
  96. 4 4
      vue.config.js

+ 0 - 2
.env

@@ -1,4 +1,2 @@
-# 所有环境都会加载
-
 # 标题
 VUE_APP_TITLE = EasyDo智能生产运营平台

+ 4 - 2
.env.development

@@ -5,8 +5,10 @@ NODE_ENV = development
 VUE_APP_ICONIFY_BASEURL = https://api.iconify.design
 VUE_APP_ZEROAPI_BASEURL = http://www.qdeasydo.com
 # VUE_APP_MES_BASEURL = http://www.qdeasydo.com/mes
-VUE_APP_MES_BASEURL = http://192.168.101.230:10160
-# VUE_APP_MES_BASEURL = http://192.168.101.135:10160
+
+VUE_APP_MES_BASEURL = http://192.168.101.251:10160
+# VUE_APP_MES_BASEURL = http://192.168.101.230:10160 #宏宏
+# VUE_APP_MES_BASEURL = http://192.168.101.135:10160 #win7
 
 # 本地端口
 VUE_APP_PORT = 4400

+ 1 - 3
.env.production

@@ -2,6 +2,4 @@
 NODE_ENV = production
 
 # 接口地址
-VUE_APP_MES_BASEURL =
-VUE_APP_ICONIFY_BASEURL = 
-VUE_APP_ZEROAPI_BASEURL =
+VUE_APP_API_BASEURL = /mesWeb

+ 10 - 0
README.md

@@ -0,0 +1,10 @@
+### 搬运批量
+0:不限制或按加工批量同步搬运
+10(其他数):每完成10件,就将这10件搬运到下一道工序
+
+### 加工天数
+### 按8小时工作制换算加工天数
+
+1. 统一单位
+2. 总工时 = 加工数量 × (加工工时 + 搬运工时) + ceil(总数量 / 加工批量) × 准备时间
+3. 加工天数 = 总工时 / 8或24

+ 3 - 3
src/api/model/auth.js

@@ -12,17 +12,17 @@ export default {
             code: data.code,
             uuid: data.uuid
         }
-        return await http.post("/mes/auth/login", query)
+        return await http.post(`${config.API_URL}/mes/auth/login`, query)
 	},
 
     // 获取登录验证码
 	codeImg: async function () {
-        return await http.get("/mes/auth/code")
+        return await http.get(`${config.API_URL}/mes/auth/code`)
 	},
 
     user: {
 		name: "用户管理",
-        url: "/mes/sysUser",
+        url: `${config.API_URL}/mes/sysUser`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId

+ 4 - 4
src/api/model/basic.js

@@ -5,7 +5,7 @@ import http from "@/utils/request"
 export default {
     material: {
         name: "物料管理",
-        url: "/mes/processMaterial",
+        url: `${config.API_URL}/mes/processMaterial`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -27,7 +27,7 @@ export default {
 
     warehouse: {
         name: "仓库管理",
-        url: "/mes/warehouse",
+        url: `${config.API_URL}/mes/warehouse`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -54,7 +54,7 @@ export default {
 
     qualityPlan: {
         name: "质检方案",
-        url: "/mes/qualityInspectProgram",
+        url: `${config.API_URL}/mes/qualityInspectProgram`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -81,7 +81,7 @@ export default {
 
     customer: {
         name: "客户/供应商管理",
-        url: "/mes/customer",
+        url: `${config.API_URL}/mes/customer`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId

+ 2 - 2
src/api/model/common.js

@@ -5,13 +5,13 @@ export default {
     iconify: {
         name: "图标查询",
 		get: async function (params = {}) {
-			return await http.get("/iconify-api/search", params, config)
+			return await http.get(`${config.API_URL}/iconify-api/search`, params, config)
 		}
     },
 
     minio: {
 		name: "文件上传",
-		url: `/mes/file`,
+		url: `${config.API_URL}/mes/file`,
 
 		up: async function (data, config = {}) {
 			return await http.post(`${this.url}/upload`, data, config)

+ 1 - 1
src/api/model/equip.js

@@ -5,7 +5,7 @@ import http from "@/utils/request"
 export default {
     device: {
         name: "设备列表",
-        url: "/mes/device",
+        url: `${config.API_URL}/mes/device`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId

+ 49 - 0
src/api/model/outsourcing.js

@@ -0,0 +1,49 @@
+import store from "@/store"
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    plan: {
+        name: "委外计划",
+        url: `${config.API_URL}/mes/outsourcingPlan`,
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    },
+    
+    order: {
+        name: "委外订单",
+        url: `${config.API_URL}/mes/outsourcingOrder`,
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    }
+}

+ 2 - 2
src/api/model/process.js

@@ -5,7 +5,7 @@ import http from "@/utils/request"
 export default {
     stage: {
 		name: "工序管理",
-        url: "/mes/processStage",
+        url: `${config.API_URL}/mes/processStage`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -27,7 +27,7 @@ export default {
 
     line: {
 		name: "工艺路线",
-        url: "/mes/processRoute",
+        url: `${config.API_URL}/mes/processRoute`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId

+ 45 - 4
src/api/model/production.js

@@ -5,7 +5,7 @@ import http from "@/utils/request"
 export default {
     bom: {
         name: "BOM管理",
-        url: "/mes/productBom",
+        url: `${config.API_URL}/mes/productBom`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -32,7 +32,7 @@ export default {
 
     prePlan: {
         name: "预生产计划",
-        url: "/mes/productPrePlan",
+        url: `${config.API_URL}/mes/productPrePlan`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -46,7 +46,7 @@ export default {
 
     plan: {
         name: "生产计划",
-        url: "/mes/productPlan",
+        url: `${config.API_URL}/mes/productPlan`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -64,5 +64,46 @@ export default {
         del: async function (data = {}) {
             return await http.post(`${this.url}/remove`, data)
         }
-    }
+    },
+
+    order: {
+        name: "生产订单",
+        url: `${config.API_URL}/mes/productOrder`,
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    },
+
+    dispatch: {
+        name: "生产派工",
+        url: `${config.API_URL}/mes/productOrderDispatch`,
+
+        getSummary: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getDispatchSummary`, data)
+        },
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        handler: async function (data = {}) {
+            return await http.post(`${this.url}/dispatch`, data)
+        }
+    },
 }

+ 49 - 0
src/api/model/purchase.js

@@ -0,0 +1,49 @@
+import store from "@/store"
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    plan: {
+        name: "采购计划",
+        url: `${config.API_URL}/mes/purchasePlan`,
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    },
+
+    order: {
+        name: "采购订单",
+        url: `${config.API_URL}/mes/purchaseOrder`,
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    }
+}

+ 3 - 3
src/api/model/sales.js

@@ -5,7 +5,7 @@ import http from "@/utils/request"
 export default {
     plan: {
         name: "销售计划",
-        url: "/mes/salePlan",
+        url: `${config.API_URL}/mes/salePlan`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -27,7 +27,7 @@ export default {
 
     order: {
         name: "销售订单",
-        url: "/mes/saleOrder",
+        url: `${config.API_URL}/mes/saleOrder`,
         
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -49,7 +49,7 @@ export default {
 
     performance: {
         name: "销售业绩",
-        url: "/mes/salePerformance",
+        url: `${config.API_URL}/mes/salePerformance`,
         
         census: async function (data = {}) {
             data.tenantId = store.state.tenant.tenantId

+ 5 - 5
src/api/model/system.js

@@ -5,7 +5,7 @@ import http from "@/utils/request"
 export default {
     menu: {
         name: "菜单管理",
-		url: "/mes/sysMenu",
+		url: `${config.API_URL}/mes/sysMenu`,
 
         build: async function () {
 			return await http.post(`${this.url}/build`)
@@ -30,7 +30,7 @@ export default {
 
     role: {
         name: "角色管理",
-		url: "/mes/sysRole",
+		url: `${config.API_URL}/mes/sysRole`,
 
 		get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -57,7 +57,7 @@ export default {
 
     roleMenu: {
         name: "角色菜单管理",
-		url: "/mes/sysRolesMenus",
+		url: `${config.API_URL}/mes/sysRolesMenus`,
         
         get: async function (data = {}) {
 			return await http.post(`${this.url}/getList`, data)
@@ -70,7 +70,7 @@ export default {
 
     dept: {
         name: "部门管理",
-		url: "/mes/sysDept",
+		url: `${config.API_URL}/mes/sysDept`,
 
         get: async function (data = {}) {
             if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
@@ -92,7 +92,7 @@ export default {
 
     tenant: {
         name: "租户管理",
-		url: "/mes/tenant",
+		url: `${config.API_URL}/mes/tenant`,
 
 		get: async function (data = {}) {
 			return await http.post(`${this.url}/getPage`, data)

+ 52 - 0
src/api/model/warehouse.js

@@ -0,0 +1,52 @@
+import store from "@/store"
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    requisition: {
+        name: "领料单",
+        url: `${config.API_URL}/mes/materialRequisition`,
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        getStock: async function (data = {}) {
+            return await http.post(`${this.url}/getMaterialStock`, data)
+        },
+
+        generate: async function (data = {}) { // 生成出库单
+            return await http.post(`${this.url}/generateOutbound`, data)
+        },
+    },
+
+    outbound: {
+        name: "出库单",
+        url: `${config.API_URL}/mes/warehouseOutbound`,
+
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        detail: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getItemList`, data)
+        },
+    },
+
+    inventory: {
+        name: "库存管理",
+        url: `${config.API_URL}/mes/warehouseMaterial`,
+
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+    }
+}

+ 2 - 0
src/components/scFormTable/detail.vue

@@ -15,6 +15,7 @@ import stageTable from "@/views/process/stage/index";
 import materialTable from "@/views/basic/material/index";
 import bomTable from "@/views/production/bom/index";
 import customerTable from "@/views/basic/customer/index";
+import supplierTable from "@/views/basic/supplier/index";
 
 const $emit = defineEmits(["success", "closed"]);
 const props = defineProps({
@@ -29,6 +30,7 @@ const compDic = reactive({
     material: { title: "产品选择", compName: materialTable },
     bom: { title: "产品选择", compName: bomTable },
     customer: { title: "客户选择", compName: customerTable },
+    supplier: { title: "供应商选择", compName: supplierTable },
 });
 
 const tableOptions = reactive({

+ 8 - 3
src/components/scFormTable/index.vue

@@ -61,7 +61,9 @@ const props = defineProps({
     layouts: { type: Array, default: () => [["Top", "Form"], ["Toolbar", "Table", "Bottom", "Pager"]] },
     rowKey: { type: String, default: "id" },
     columns: { type: Array, default: () => [] },
+    mergeCells: { type: Array, default: () => [] },
     treeConfig: { type: Object, default: null },
+    expandConfig: { type: Object, default: null },
     editRules: { type: Object, default: () => ({}) },
     editDiasbled: { type: Object, default: () => ({}) }, // 每个field是否禁用方法
     footerField: { type: Array, default: () => [] },
@@ -95,6 +97,8 @@ const gridOptions = reactive({
     toolbarConfig: { enabled: false },
     formConfig: { enabled: false },
     pagerConfig: { enabled: false },
+    mergeCells: props.mergeCells,
+    expandConfig: props.expandConfig,
     treeConfig: props.treeConfig,
     rowConfig: { keyField: props.rowKey, drag: !props.treeConfig && !props.disabled, resizable: true, useKey: true, isHover: true },
     editRules: props.editRules,
@@ -103,7 +107,6 @@ const gridOptions = reactive({
     resizableConfig: { isAllRowDrag: !props.disabled },
     tooltipConfig: { enterable: true },
     rowDragConfig: { trigger: "cell", showGuidesStatus: true },
-
     showFooter: computed(() => props.footerField.length > 0 && props.modelValue.length > 0),
     footerRowClassName: "vxe-table-footer-cell-required",
     footerSpanMethod: ({ rowIndex, itemIndex, column }) => { // ?评估阶段
@@ -148,7 +151,7 @@ const selectTableOptions = reactive({
 });
 
 watch(() => gridOptions.data, val => $emit("update:modelValue", val), { deep: true });
-watch(inject("tenantId"), (newVal, oldVal) => newVal !== oldVal && (gridOptions.data = []), { immediate: false });
+watch(inject("tenantId"), (newVal, oldVal) => newVal !== oldVal && table_clear(), { immediate: false });
 
 const xGrid = ref();
 const rowAdd = async () => {
@@ -159,6 +162,7 @@ const rowDel = row => gridOptions.data = XEUtils.filter(gridOptions.data, item =
 
 const selectTableRef = ref();
 const dialog = ref(false);
+const table_clear = () => gridOptions.data = [];
 const table_add = () => {
     dialog.value = true;
     nextTick(() => selectTableRef.value?.setData(gridOptions.data));
@@ -173,7 +177,7 @@ const selectChange = async array => {
 }
 
 const validateFormTable = async () => {
-    const errMap = await xGrid.value.validate(true)
+    const errMap = await xGrid.value.validate(true);
     if (errMap) {
         ElMessage.warning(`请维护${XEUtils.last(XEUtils.objectMap(errMap, item => XEUtils.first(item).column.title))}`);
         return false;
@@ -183,6 +187,7 @@ const validateFormTable = async () => {
 }
 
 defineExpose({
+    table_clear,
     validateFormTable
 });
 </script>

+ 13 - 5
src/components/scTable/renderer/cell-tag.vue

@@ -6,15 +6,23 @@
 import XEUtils from "xe-utils";
 
 const colorDic = {
-    enable: "success",
+    enable: "green",
     disable: "danger",
 
-    pending: "processing",
-    approved: "success",
+    pending: "default",
+    approved: "green",
     rejected: "red",
     
-    executing: "warning",
-    finished: "default",
+    processing: "warning",
+    partially: "processing",
+    finished: "green",
+    complete: "green",
+    received: "success",
+    shipped: "success",
+    noNeed: "default",
+    
+    overduePending: "pink",
+    overdueProcessing: "pink"
 }
 
 const props = defineProps({

+ 3 - 0
src/config/index.js

@@ -5,6 +5,9 @@ const DEFAULT_CONFIG = {
 	//首页地址
 	DASHBOARD_URL: "/home",
 
+	//接口地址
+	API_URL: process.env.NODE_ENV === "development" ? "" : process.env.VUE_APP_API_BASEURL,
+    
 	//请求超时
 	TIMEOUT: 30000,
 

+ 1 - 1
src/config/table.js

@@ -35,5 +35,5 @@ export default {
 }
 
 function valueIsNull(obj, key) {
-    return XEUtils.get(obj, key) === "" || XEUtils.isNull(XEUtils.get(obj, key)) || XEUtils.isUndefined(XEUtils.get(obj, key)) || XEUtils.isEmpty(XEUtils.get(obj, key))
+    return XEUtils.get(obj, key) === "" || XEUtils.isNull(XEUtils.get(obj, key)) || XEUtils.isUndefined(XEUtils.get(obj, key)) || (XEUtils.isObject(XEUtils.get(obj, key)) && XEUtils.isEmpty(XEUtils.get(obj, key)))
 }

+ 1 - 1
src/layout/index.vue

@@ -197,7 +197,7 @@ export default {
 
         scrollToActiveTab() {
             this.tabsName = XEUtils.get(this.currentMenu, "path", "");
-            this.$refs.menuTabs.tabNavRef.scrollToActiveTab();
+            this.$refs?.menuTabs?.tabNavRef.scrollToActiveTab();
         },
 
         // 点击显示

+ 12 - 30
src/store/modules/viewTags.js

@@ -1,45 +1,27 @@
-import router from '@/router'
+import router from "@/router"
 
 export default {
 	state: {
 		viewTags: []
 	},
 	mutations: {
-		pushViewTags(state, route){
+		pushViewTags(state, route) {
 			let backPathIndex = state.viewTags.findIndex(item => item.fullPath == router.options.history.state.back)
-			let target = state.viewTags.find((item) => item.fullPath === route.fullPath)
-			let isName = route.name
-			if(!target && isName){
-				if(backPathIndex == -1){
-					state.viewTags.push(route)
-				}else{
-					state.viewTags.splice(backPathIndex+1, 0, route)
-				}
+			if (!state.viewTags.find(item => item.fullPath === route.fullPath) && route.name) {
+				if (backPathIndex == -1) state.viewTags.push(route)
+				else state.viewTags.splice(backPathIndex + 1, 0, route)
 			}
 		},
-		removeViewTags(state, route){
-			state.viewTags.forEach((item, index) => {
-				if (item.fullPath === route.fullPath){
-					state.viewTags.splice(index, 1)
-				}
-			})
+		removeViewTags(state, route) {
+			state.viewTags.forEach((item, index) => item.fullPath === route.fullPath && state.viewTags.splice(index, 1))
 		},
-		updateViewTags(state, route){
-			state.viewTags.forEach((item) => {
-				if (item.fullPath == route.fullPath){
-					item = Object.assign(item, route)
-				}
-			})
+		updateViewTags(state, route) {
+			state.viewTags.forEach(item => item.fullPath == route.fullPath && (item = Object.assign(item, route)))
 		},
-		updateViewTagsTitle(state, title=''){
-			const nowFullPath = location.hash.substring(1)
-			state.viewTags.forEach((item) => {
-				if (item.fullPath == nowFullPath){
-					item.meta.title = title
-				}
-			})
+		updateViewTagsTitle(state, title = "") {
+			state.viewTags.forEach(item => item.fullPath == location.hash.substring(1) && (item.meta.title = title))
 		},
-		clearViewTags(state){
+		clearViewTags(state) {
 			state.viewTags = []
 		}
 	}

+ 2 - 1
src/style/fix.scss

@@ -87,4 +87,5 @@
 .el-tag--plain.el-tag--green {--el-tag-text-color: #389e0d;--el-tag-bg-color: #f6ffed;--el-tag-border-color: #b7eb8f;}
 .el-tag--plain.el-tag--blue {--el-tag-text-color: #0958d9;--el-tag-bg-color: #e6f4ff;--el-tag-border-color: #91caff;}
 .el-tag--plain.el-tag--red {--el-tag-text-color: #cf1322;--el-tag-bg-color: #fff1f0;--el-tag-border-color: #ffa39e;}
-.el-tag--plain.el-tag--orange {--el-tag-text-color: #d46b08;--el-tag-bg-color: #fff7e6;--el-tag-border-color: #ffd591;}
+.el-tag--plain.el-tag--orange {--el-tag-text-color: #d46b08;--el-tag-bg-color: #fff7e6;--el-tag-border-color: #ffd591;}
+.el-tag--plain.el-tag--pink {--el-tag-text-color: #c41d7f;--el-tag-bg-color: #fff0f6;--el-tag-border-color: #ffadd2;}

+ 177 - 2
src/utils/basicDic.js

@@ -135,7 +135,7 @@ export const supplierDic = {
         component: "零部件供应商", // 可装配的成品组件
         outsourcing: "委外供应商",
         mro: "MRO/耗材供应商", // 如工具、劳保用品、备件。(维护维修)
-        service: "服务供应商",
+        service: "服务供应商", // 如电脑、打印机、设备、车辆、厂房、仪器等长期使用的固定资产
         capital: "资产供应商"
     },
 
@@ -162,7 +162,7 @@ export const salesDic = {
 
     planStatus: {
         pending: "未开始",
-        executing: "进行中",
+        processing: "进行中",
         finished: "已结束"
     },
     
@@ -173,4 +173,179 @@ export const salesDic = {
         shipped: "已发货",
         complete: "已完成"
     }
+}
+
+export const productionDic = {
+    priority: {
+        low: "低",
+        medium: "中",
+        high: "高",
+        urgent: "紧急"
+    },
+
+    planStatus: {
+        pending: "未开始",
+        processing: "进行中",
+        complete: "已完成"
+    },
+
+    orderStatus: {
+        pending: "未开始",
+        processing: "生产中",
+        partially: "部分完成",
+        received: "已完工",
+        complete: "已完成"
+    },
+
+    dispatchStatus: {
+        pending: "未开始",
+        overduePending: "超期未开工", // 未开工 && 当前时间 > 计划开工时间
+        processing: "生产中",
+        overdueProcessing: "超期生产中", // 生产中 && 当前时间 > 计划完工时间 || 已完工 && 未质检
+        complete: "已完工"
+    },
+
+    reportStatus: {
+        pending: "未开始",
+        partially: "部分报工",
+        complete: "已完工"
+    },
+
+    // 派工-> (整单?/工序) -> 领料单 -> 出库 -> 按单汇报 (-> 质检 -> 返工)-> 入库申请
+    scrapType: { // 报废类型
+        material: "物料",
+        intermediate: "中间产出物"
+    },
+
+    reworkMethod: { // 返工类型
+        whole: "全工序返工", // 退回第一道工序
+        stage: "工序段返工", // 退回该工序段起点
+        single: "单工序返工",
+        repair: "返修式返工", // 在成品 / 半成品工位直接维修(如:更换零件、补焊、打磨)
+        outsource: "委外返工",
+        material: "物料返工" // 退回物料处理环节(如:更换不合格原材料后重加工)
+    }
+}
+
+export const purchaseDic = {
+    category: {
+        regular: "常规采购",
+        urgent: "特急采购",
+        sporadic: "零星采购",
+        bulk: "大量采购",
+        internal: "集团内部采购"
+    },
+
+    billType: {
+        vat: "增值税", // 增值税发票(含专票/普票,财务通用简称)
+        normalNationalTax: "普通国税",
+        normalLocalTax: "普通地税",
+        receipt: "收款收据", // 非发票类收款凭证
+        specialInvoice: "特殊类票据", // 特殊类票据(如机动车发票、海关缴款书等)
+        other: "其他",
+        groupInvoice: "集团发票类型",
+        noInvoice: "不开票"
+    },
+
+    orderStatus: {
+        pending: "未开始",
+        processing: "备货中",
+        partially: "部分到货",
+        received: "已到货",
+        complete: "已完成"
+    }
+}
+
+export const outsourcingDic = {
+    type: {
+        process: "工序委外",
+        whole: "整单委外"
+    },
+
+    orderStatus: {
+        pending: "未开始",
+        processing: "加工中",
+        partially: "部分到货",
+        received: "已到货",
+        complete: "已完成"
+    },
+}
+
+export const qualityInspectionDic = {
+    result: { // 处理结果
+        // "全部入库"
+        // "紧急接收"
+        // "让步接收"
+        // "全部数量补货"
+        // "不合格数量补货"
+        // "全部数量退货"
+        // "不合格数量退货"
+        // "不合格数量报废"
+        // "按质检结果执行"
+    },
+
+    status: {
+        noNeed: "无需检验",
+        pending: "待检验",
+        approved: "检验合格",
+        rejected: "检验不合格"
+    }
+}
+
+export const warehouseDic = {
+    requisition: { // 领料单
+        type: {
+            auto: "按单领料",
+            manual: "按单补料"
+        },
+
+        status: {
+            noNeed: "无需领料",
+            pending: "未领料",
+            partially: "部分领料",
+            complete: "全部领料"
+        }
+    },
+
+    inbound: { // 入库单
+        type: {
+            direct: "直接入库",
+            purchase: "采购入库",
+            return: "退货入库",
+            production_return: "生产退料入库",
+            production_waste: "生产废料入库",
+            finished_product: "成品入库",
+            outsourcing: "委外入库",
+            transfer: "调拨入库",
+            inventory_diff: "盘点差异入库"
+        },
+        status: {
+            pending: "未入库",
+            partially: "部分入库",
+            complete: "全部入库"
+        },
+    },
+
+    outbound: { // 出库单
+        type: {
+            direct: "直接出库",
+            requisition: "领料出库",
+            return: "退货出库",
+            scrap: "报废出库",
+            sales: "销售出库",
+            outsourcing: "委外出库",
+            transfer: "调拨出库",
+            inventory_diff: "盘点差异出库"
+        },
+        status: {
+            pending: "未出库",
+            partially: "部分出库",
+            complete: "全部出库"
+        },
+    },
+
+    status: {
+        // "已入库"
+        // "待入库"
+    }
 }

+ 3 - 3
src/views/basic/qualityPlan/desc.vue

@@ -7,9 +7,9 @@
                         <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="方案名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="方案编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="方案名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="方案编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
                         <el-descriptions-item v-if="!descData.reviewUserName" label="审批状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(reviewStatusDic, descData.reviewStatus, descData.reviewStatus) }}</el-descriptions-item>
                         <el-descriptions-item v-if="descData.reviewUserName && mode == 'approval'" label="审批人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.reviewUserName }}</el-descriptions-item>
                         <el-descriptions-item label="质检人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectUserName }}</el-descriptions-item>

+ 104 - 0
src/views/outsourcing/order/desc.vue

@@ -0,0 +1,104 @@
+<template>
+    <el-dialog v-model="visible" title="销售订单详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item>
+                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(salesDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="客户名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.customerName }}</el-descriptions-item>
+                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="预计交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.planReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="实际交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.deliveryDate }}</el-descriptions-item>
+                        <el-descriptions-item label="业务员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.managerName }}</el-descriptions-item>
+                        <el-descriptions-item label="收货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.actualReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="客户收货地址" label-align="right" :span="ismobile ? 3 : 1">{{ descData.deliveryAddress }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label="附件" label-align="right">
+                            <sc-upload-file v-model="descData.fileList" hideAdd disabled></sc-upload-file>
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table v-model="descData.childrenList" v-bind="tableOptions" disabled></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="金额信息" name="amount">
+                    <el-descriptions :column="2" label-width="140" border>
+                        <el-descriptions-item label="整单折扣额" label-align="right">{{ descData.freePrice }}</el-descriptions-item>
+                        <el-descriptions-item label="成交金额" label-align="right">{{ descData.actualPrice }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { salesDic } from "@/utils/basicDic";
+import { tableOptions } from "../plan/main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+import store from "@/store";
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const activeNames = ref(["basic", "material", "amount"]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    code: null,
+    orderDate: null,
+    customerName: null,
+    contractNo: null,
+    planReceiveDate: null,
+    actualReceiveDate: null,
+    deliveryDate: null,
+    managerName: null,
+    deliveryAddress: null,
+    childrenList: [],
+    freePrice: null,
+    actualPrice: null,
+    remark: null,
+    fileList: [],
+    status: "pending",
+    createTime: null
+});
+
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "fileList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+        else if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, quantity: item.materialQuantity, price: item.materialPrice })));
+        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 240 - 0
src/views/outsourcing/order/detail.vue

@@ -0,0 +1,240 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed', isDel)">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id && !form.planId" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入单据主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据日期" prop="orderDate">
+                                <el-date-picker v-model="form.orderDate" :clearable="false" value-format="YYYY-MM-DD" placeholder="请选择单据日期"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="加工厂商" prop="supplier.id">
+                                <sc-table-input v-model="form.supplier" placeholder="选择加工厂商" v-bind="selectOptions"></sc-table-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="合同编号">
+                                <el-input v-model="form.contractNo" maxlength="50" show-word-limit placeholder="请输入合同编号"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- <el-col :md="8" :xs="24">
+                            <el-form-item label="是否带料">
+                                <el-radio-group v-model="form.includeMaterial">
+                                    <el-radio label="是" :value="true"></el-radio>
+                                    <el-radio label="否" :value="false"></el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </el-col> -->
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="负责人" prop="managerId">
+                                <el-select v-model="form.managerId" placeholder="请选择负责人">
+                                    <el-option v-for="item in users.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.nickName" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="预计交期">
+                                <el-date-picker v-model="form.planReceiveDate" value-format="YYYY-MM-DD" placeholder="请选择预计交期"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="16" :xs="24">
+                            <el-form-item label="交货地址">
+                                <el-input v-model="form.deliveryAddress" type="textarea" maxlength="200" :rows="1" placeholder="请输入交货地址"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item v-if="form.type == 'process'" title="委外工序" name="process">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList[form.type]" :disabled="!!form.planId" v-bind="tableOptions[form.type]"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item v-if="form.type == 'whole'" title="产品信息" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList[form.type]" v-bind="tableOptions[form.type]">
+                        <template v-if="!!form.planId" #top></template>
+                    </sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row> 
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :xs="24">
+                            <el-form-item label="附件" label-width="100">
+                                <sc-upload-file v-model="form.fileList" @removeSuccess="removeSuccess">
+                                    <vxe-button status="primary" size="mini" content="上传附件"></vxe-button>
+                                </sc-upload-file>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { outsourcingDic } from "@/utils/basicDic";
+import { tableOptions, selectOptions } from "./main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+const isDel = ref(false);
+
+const activeNames = ref(["basic", "material", "process", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增委外订单",
+    edit: "修改委外订单"
+});
+
+const users = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    planId: null,
+    saleOrderId: null,
+    name: null,
+    code: null,
+    orderDate: moment().format("YYYY-MM-DD"),
+    supplier: { id: null, name: null },
+    contractNo: null,
+    type: "whole",
+    managerId: null,
+    // includeMaterial: false,
+    planReceiveDate: null,
+    deliveryAddress: null,
+    childrenList: {
+        process: [],
+        whole: []
+    },
+    remark: null,
+    fileList: []
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入单据主题" }],
+    orderDate: [{ required: true, message: "请选择单据日期" }],
+    "supplier.id": [{ required: true, message: "请选择加工厂商" }],
+    managerId: [{ required: true, message: "请选择负责人" }]
+});
+
+const setData = (data = {}, model = "add") => {
+    visible.value = true;
+    mode.value = model;
+
+    /**
+     * 从计划生成
+     * @childrenList planId 判断整体除 price 外禁用
+    */ 
+    if (model === "add") {
+        if (!XEUtils.isEmpty(data)) {
+            const planData = {
+                tenantId: data.tenantId,
+                planId: data.id,
+                saleOrderId: data.saleOrderId,
+                planReceiveDate: data.endDate,
+            }
+    
+            XEUtils.objectEach(form.value, (_, key) => XEUtils.has(planData, key) && XEUtils.set(form.value, key, XEUtils.get(planData, key)));
+            XEUtils.set(form.value.childrenList, "whole", XEUtils.map(data.childrenList, item => ({ ...item.material, planId: data.id, number: item.number, isInspection: true })));
+        }
+    } else {
+        XEUtils.objectEach(form.value, (_, key) => {
+            if (key == "supplier") XEUtils.set(form.value, key, { id: XEUtils.get(data, "customerId"), name: XEUtils.get(data, "customerName") });
+            else if (key == "childrenList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, planId: data.planId, number: item.materialQuantity, price: item.materialPrice, isInspection: item.isInspection })));
+            else if (key == "fileList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+            else XEUtils.set(form.value, key, XEUtils.get(data, key));
+        });
+    }
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            if (!form.value.childrenList[form.value.type].length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "supplier", "childrenList", "fileList");
+                const childrenList = XEUtils.map(form.value.childrenList[form.value.type], item => ({ materialCode: item.code, number: item.number, price: item.price, isInspection: item.isInspection }));
+                const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "outsourcingOrderAttach" }));
+                XEUtils.set(data, "supplierId", form.value.supplier.id);
+                XEUtils.set(data, "childrenList", childrenList);
+                fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+console.log(data)
+                // isSaving.value = true;
+                // API.sales.order[mode.value](data).then(res => {
+                //     ElMessage.success("操作成功");
+                //     isSaving.value = false;
+                //     isDel.value = false;
+                //     visible.value = false;
+                //     $emit("success", mode.value);
+                // }).catch(() => isSaving.value = false);
+            }
+        } else {
+            return false;
+        }
+    });
+}
+
+const removeSuccess = () => form.value.id && (isDel.value = true);
+
+const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 133 - 0
src/views/outsourcing/order/index.vue

@@ -0,0 +1,133 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+        
+        <scTable ref="xGridTable" :apiObj="$API.outsourcing.order" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <el-button v-if="row.status == 'processing'" type="primary" link @click="table_receipt(row)">
+                    <template #icon><sc-iconify icon="tabler:package-import"></sc-iconify></template>收货
+                </el-button>
+                <template v-if="row.status == 'pending'">
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                    <el-button type="primary" link @click="table_del(row)">
+                        <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                    </el-button>
+                </template>
+            </template>
+        </scTable>
+	</el-container>
+
+    <order-detail v-if="dialog.detail" ref="orderRef" @success="refreshTable" @closed="dialogClose"></order-detail>
+    <order-desc v-if="dialog.desc" ref="orderDescRef" @closed="dialog.desc = false"></order-desc>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { outsourcingDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import orderDetail from "./detail";
+import orderDesc from "./desc";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "计划主题"),
+        mapFormItemInput("codeLike", "计划编号"),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "委外主题", fixed: "left", minWidth: 150, sortable: true },
+    { field: "code", title: "委外编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    { field: "status", title: "委外状态", minWidth: 120, editRender: { name: "$cell-tag", options: outsourcingDic.orderStatus } },
+    { type: "html", field: "supplierName", title: "加工厂商", fixed: "left", minWidth: 150, sortable: true },
+    { type: "html", field: "orderDate", title: "委外日期", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { title: "操作", fixed: "right", width: 140, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const orderRef = ref();
+const orderDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    receipt: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData(row, "edit"));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => orderDescRef.value?.setData(row));
+}
+
+const table_receipt = row => {
+    // 收货日期、是否需要质检
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该委外订单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.outsourcing.order.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+const dialogClose = isDel => {
+    dialog.detail = false;
+    isDel && refreshTable();
+}
+</script>

+ 82 - 0
src/views/outsourcing/order/main.js

@@ -0,0 +1,82 @@
+import XEUtils from "xe-utils"
+
+const defaultOptions = {
+    tableKey: "material",
+    editRules: {
+        number: [{ required: true, message: "必须填写" }],
+        price: [{ required: true, message: "必须填写" }]
+    }
+}
+
+export const tableOptions = reactive({
+    process: {
+        ...defaultOptions,
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "specification", title: "规格型号", minWidth: 150 },
+            { field: "unit", title: "单位", minWidth: 120 },
+            { field: "stage.name", title: "工序名称", minWidth: 150 },
+            { field: "number", title: "委外数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+            { field: "isInspection", title: "是否质检", minWidth: 100, formatter: ({ cellValue }) => "是" },
+            { field: "number", title: "完工期", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } }
+            // 开工日期 + 加工天数
+        ],
+        selectOptions: {
+            paramsColums: [
+                { column: "status", defaultValue: "enable" },
+                { column: "needType", defaultValue: "outsourcing" }
+            ]
+        },
+
+        add_success: (oldValue, newValue) => XEUtils.map(newValue, (material, index) => ({ material, isInspection: true }))
+    },
+
+    whole: {
+        ...defaultOptions,
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => !!row.planId }, slots: { default: "seq_del" } },
+            { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "specification", title: "规格型号", minWidth: 150 },
+            { field: "unit", title: "单位", minWidth: 120 },
+            { field: "number", title: "委外数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+            { field: "price", title: "委外单价", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } },
+            { field: "isInspection", title: "是否质检", minWidth: 100, cellRender: { name: "VxeCheckbox" } }
+        ],
+        editDiasbled: { number: row => !!row.planId },
+        footerField: [["number", "price"], ["number"]],
+        footerTitle: ["合计:", "订单金额:"],
+        footerMethod: [
+            (data, field) => XEUtils.sum(data, field),
+            (data, field) => XEUtils.sum(XEUtils.map(data, item => XEUtils.multiply(XEUtils.get(item, field), XEUtils.get(item, "price"))))
+        ],
+        mergeFooterItems: [
+            { row: 0, col: 0, rowspan: 1, colspan: 5 },
+            { row: 1, col: 0, rowspan: 1, colspan: 5 },
+            { row: 1, col: 5, rowspan: 1, colspan: 2 },
+            { row: 0, col: 7, rowspan: 1, colspan: 1 },
+            { row: 1, col: 7, rowspan: 1, colspan: 1 }
+        ],
+
+        selectOptions: {
+            paramsColums: [
+                { column: "status", defaultValue: "enable" },
+                { column: "needType", defaultValue: "outsourcing" }
+            ]
+        },
+
+        add_success: (oldValue, newValue) => XEUtils.map(newValue, item => ({ ...XEUtils.pick(item, "id", "code", "name", "unit", "specification"), isInspection: true }))
+    }
+})
+
+export const selectOptions = reactive({
+    tableKey: "supplier",
+    valueKey: "name",
+    paramsColums: [
+        { column: "status", defaultValue: "enable" },
+        { column: "customerType", defaultValue: "supplier" },
+        { column: "type", defaultValue: "outsourcing" }
+    ]
+})

+ 94 - 0
src/views/outsourcing/plan/desc.vue

@@ -0,0 +1,94 @@
+<template>
+    <el-dialog v-model="visible" title="委外计划详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="计划主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="计划编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="计划状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(productionDic.planStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="计划开工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划完工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.endDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划类型" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(outsourcingDic.type, descData.type, descData.type) }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item v-if="descData.saleOrderId" label="来源单据" label-align="right">{{ descData.saleOrder.code }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item v-if="descData.type == 'process'" title="委外工序" name="process">
+                    <sc-form-table v-model="descData.childrenList[descData.type]" v-bind="tableOptions[descData.type]" disabled></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item v-if="descData.type == 'whole'" title="产品信息" name="material">
+                    <sc-form-table v-model="descData.childrenList[descData.type]" v-bind="tableOptions[descData.type]" disabled></sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { productionDic, outsourcingDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+import store from "@/store";
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const activeNames = ref(["basic", "material", "process"]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    saleOrderId: null,
+    saleOrder: null,
+    name: null,
+    code: null,
+    type: "whole",
+    beginDate: null,
+    endDate: null,
+    childrenList: {
+        process: [],
+        whole: []
+    },
+    remark: null,
+    status: "pending",
+    createTime: null
+});
+
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "priority") XEUtils.set(descData.value, key, XEUtils.get(data, key) || "medium");
+        else if (key == "childrenList") {
+            data.type == "whole" && XEUtils.set(descData.value.childrenList, data.type, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, number: item.number })));
+            if (data.type == "process") {}
+        } else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 176 - 0
src/views/outsourcing/plan/detail.vue

@@ -0,0 +1,176 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入计划主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划开工日期" prop="beginDate">
+                                <vxe-date-picker v-model="form.beginDate" :end-date="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划开工日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划完工日期" prop="endDate">
+                                <vxe-date-picker v-model="form.endDate" :start-date="form.beginDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划完工日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划优先级">
+                                <el-select v-model="form.priority" placeholder="请选择计划优先级">
+                                    <el-option v-for="(label, key) in productionDic.priority" :key="key" :label="label" :value="key"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item v-if="form.type == 'process'" title="委外工序" name="process">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList[form.type]" :disabled="!!form.saleOrderId" v-bind="tableOptions[form.type]"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item v-if="form.type == 'whole'" title="产品信息" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList[form.type]" :disabled="!!form.saleOrderId" v-bind="tableOptions[form.type]"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { productionDic, outsourcingDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "material", "process", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增委外计划",
+    edit: "修改委外计划"
+});
+
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    saleOrderId: null,
+    name: null,
+    code: null,
+    type: "whole",
+    beginDate: null,
+    endDate: null,
+    priority: "medium",
+    childrenList: {
+        process: [],
+        whole: []
+    },
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入计划主题" }],
+    beginDate: [{ required: true, message: "请选择计划开工日期" }],
+    endDate: [{ required: true, message: "请选择计划完工日期" }]
+});
+
+const open = () => visible.value = true;
+const setData = data => {//工序委外新增
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "priority") XEUtils.set(form.value, key, XEUtils.get(data, key) || "medium");
+        else if (key == "childrenList") {
+            data.type == "whole" && XEUtils.set(form.value.childrenList, data.type, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, number: item.number })));
+            if (data.type == "process") {}
+        } else XEUtils.set(form.value, key, XEUtils.get(data, key))
+    });
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            if (!form.value.childrenList[form.value.type].length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "childrenList");
+                const childrenList = XEUtils.map(form.value.childrenList[form.value.type], item => ({ materialCode: item.code, number: item.number }));
+                XEUtils.set(data, "childrenList", childrenList);
+                
+                isSaving.value = true;
+                API.outsourcing.plan[mode.value](data).then(res => {
+                    ElMessage.success("操作成功");
+                    isSaving.value = false;
+                    visible.value = false;
+                    $emit("success", mode.value);
+                }).catch(() => isSaving.value = false);
+            }
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--inner::placeholder) {color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 161 - 0
src/views/outsourcing/plan/index.vue

@@ -0,0 +1,161 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.outsourcing.plan" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+            <template #order_link="{ row }">
+                <vxe-text v-if="row.saleOrderId" status="primary" @click="table_order(row)">{{ row.saleOrder.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <template v-if="row.status === 'pending'">
+                    <el-button type="primary" link @click="table_outsourcing(row)">
+                        <template #icon><sc-iconify icon="material-symbols:inactive-order-outline"></sc-iconify></template>生成委外单
+                    </el-button>
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                    <el-button type="primary" link @click="table_del(row)">
+                        <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                    </el-button>
+                </template>
+            </template>
+        </scTable>
+	</el-container>
+
+    <plan-detail v-if="dialog.detail" ref="planRef" @success="refreshTable" @closed="dialog.detail = false"></plan-detail>
+    <plan-desc v-if="dialog.desc" ref="planDescRef" @closed="dialog.desc = false"></plan-desc>
+    <order-desc v-if="dialog.orderDesc" ref="orderDescRef" @closed="dialog.orderDesc = false"></order-desc>
+    <outsourcing-detail v-if="dialog.outsourcing" ref="outsourcingRef" @success="refreshTable" @closed="dialog.outsourcing = false"></outsourcing-detail>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { productionDic, outsourcingDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+
+import orderDesc from "@/views/sales/order/desc";
+import outsourcingDetail from "@/views/outsourcing/order/detail";
+import planDetail from "./detail";
+import planDesc from "./desc";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const selectConfig = reactive({
+    options: productionDic.planStatus,
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        valueFormat: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "计划主题"),
+        mapFormItemInput("codeLike", "计划编号"),
+        mapFormItemSelect("status", "计划状态", selectConfig),
+        mapFormItemSelect("type", "计划类型", { ...selectConfig, options: outsourcingDic.type }),
+        mapFormItemDatePicker("beginDate", "计划开工日期", daterangeConfig),
+        mapFormItemDatePicker("endDate", "计划完工日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "type" },
+    { column: "status" },
+    { column: "beginDateBegin", field: "beginDate[0]" },
+    { column: "beginDateEnd", field: "beginDate[1]" },
+    { column: "endDateBegin", field: "endDate[0]" },
+    { column: "endDateEnd", field: "endDate[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "计划主题", fixed: "left", minWidth: 150, sortable: true },
+    { field: "code", title: "计划编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    { field: "orderCode", title: "来源单据", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "order_link" } },
+    { field: "type", title: "计划类型", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(outsourcingDic.type, cellValue, cellValue) },
+    { field: "status", title: "计划状态", minWidth: 120, editRender: { name: "$cell-tag", options: productionDic.planStatus } },
+    { type: "html", field: "beginDate", title: "计划开工日期", minWidth: 150, sortable: true },
+    { type: "html", field: "endDate", title: "计划完工日期", minWidth: 150, sortable: true },
+    { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { title: "操作", fixed: "right", width: 240, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const planRef = ref();
+const planDescRef = ref();
+const orderDescRef = ref();
+const outsourcingRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    orderDesc: false,
+    outsourcing: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => planRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => planRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => planDescRef.value?.setData(row));
+}
+
+const table_order = row => {
+    dialog.orderDesc = true;
+    nextTick(() => orderDescRef.value?.setData(row.saleOrder));
+}
+
+const table_outsourcing = row => {
+    dialog.outsourcing = true;
+    nextTick(() => outsourcingRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该委外计划?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.outsourcing.plan.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 54 - 0
src/views/outsourcing/plan/main.js

@@ -0,0 +1,54 @@
+import XEUtils from "xe-utils"
+
+const defaultOptions = {
+    tableKey: "material",
+    editRules: {
+        number: [{ required: true, message: "必须填写" }]
+    }
+}
+
+export const tableOptions = reactive({
+    process: {
+        ...defaultOptions,
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "specification", title: "规格型号", minWidth: 150 },
+            { field: "unit", title: "单位", minWidth: 120 },
+            { field: "stage.name", title: "工序名称", minWidth: 150 },
+            { field: "number", title: "委外数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+            { field: "isInspection", title: "是否质检", minWidth: 100, formatter: ({ cellValue }) => "是" },
+            { field: "number", title: "完工期", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } }
+            // 开工日期 + 加工天数
+        ],
+        selectOptions: {
+            paramsColums: [
+                { column: "status", defaultValue: "enable" },
+                { column: "needType", defaultValue: "outsourcing" }
+            ]
+        },
+
+        add_success: (oldValue, newValue) => XEUtils.map(newValue, (material, index) => ({ material, isInspection: true }))
+    },
+
+    whole: {
+        ...defaultOptions,
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "specification", title: "规格型号", minWidth: 150 },
+            { field: "unit", title: "单位", minWidth: 120 },
+            { field: "number", title: "委外数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } }
+        ],
+        selectOptions: {
+            paramsColums: [
+                { column: "status", defaultValue: "enable" },
+                { column: "needType", defaultValue: "outsourcing" }
+            ]
+        },
+
+        add_success: (oldValue, newValue) => XEUtils.map(newValue, item => XEUtils.pick(item, "id", "code", "name", "unit", "specification"))
+    }
+})

+ 53 - 8
src/views/process/line/desc.vue

@@ -7,9 +7,9 @@
                         <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="工艺路线名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="工艺路线编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="工艺路线名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="工艺路线编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
                         <el-descriptions-item label="工艺路线状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(statusDic, descData.status, descData.status) }}</el-descriptions-item>
                         <el-descriptions-item label="时间单位" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(processDic.timeUnit, descData.timeUnit, descData.timeUnit) }}</el-descriptions-item>
                         <el-descriptions-item label="版本号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.version }}</el-descriptions-item>
@@ -27,6 +27,29 @@
                 </el-collapse-item>
 
                 <el-collapse-item title="质检方案" name="plan">
+                    <template v-if="descData.inspectProgram.name">
+                        <el-descriptions :column="3" label-width="140" border>
+                            <el-descriptions-item label="方案名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectProgram.name }}</el-descriptions-item>
+                            <el-descriptions-item label="方案编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectProgram.code }}</el-descriptions-item>
+                            <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectProgram.createTime }}</el-descriptions-item>
+                            <template v-if="descData.inspectProgram.reviewUserName">
+                                <el-descriptions-item label="审批状态" :span="ismobile ? 3 : 1" label-align="right">审批通过</el-descriptions-item>
+                                <el-descriptions-item label="审批人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectProgram.reviewUserName }}</el-descriptions-item>
+                            </template>
+                            <el-descriptions-item label="质检人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectProgram.inspectUserName }}</el-descriptions-item>
+                            <el-descriptions-item label="方案类型" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(qualityPlanTypeDic, descData.inspectProgram.type, descData.inspectProgram.type) }}</el-descriptions-item>
+                            <template v-if="descData.inspectProgram.type == 'sampling'">
+                                <el-descriptions-item label="抽检比例" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectProgram.sampleRate }}%</el-descriptions-item>
+                                <el-descriptions-item label="合格率" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectProgram.passedRate }}%</el-descriptions-item>
+                            </template>
+                        </el-descriptions>
+                        <el-descriptions :column="1" label-width="140" border>
+                            <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                            <el-descriptions-item label="附件" label-align="right">
+                                <sc-upload-file v-model="descData.inspectProgram.fileList" hideAdd disabled></sc-upload-file>
+                            </el-descriptions-item>
+                        </el-descriptions>
+                    </template>
                 </el-collapse-item>
             </el-collapse>
         </el-main>
@@ -35,7 +58,7 @@
 
 <script setup>
 import XEUtils from "xe-utils";
-import { statusDic, processDic } from "@/utils/basicDic";
+import { statusDic, processDic, qualityPlanTypeDic } from "@/utils/basicDic";
 import { tableOptions } from "./main";
 import scUploadFile from "@/components/scUpload/file";
 
@@ -50,7 +73,7 @@ const options = reactive({
     disabled: true,
     ...tableOptions,
     columns: tableOptions.columns.slice(1),
-    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 4 }]
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 5 }]
 });
 
 const activeNames = ref(["basic", "line", "plan"]);
@@ -66,16 +89,38 @@ const descData = ref({
     remark: null,
     fileList: [],
     status: "enable",
-    createTime: null
+    createTime: null,
+    inspectProgram: {
+        name: null,
+        code: null,
+        inspectUserName: null,
+        type: "full",
+        sampleRate: null,
+        passedRate: null,
+        remark: null,
+        fileList: [],
+        createTime: null,
+        reviewUserName: null,
+        reviewTime: null,
+        reviewReason: null
+    }
 });
 
 const setData = data => {
     visible.value = true;
     XEUtils.objectEach(descData.value, (_, key) => {
         if (key == "fileList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
-        else if (key == "detailList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage") })));
-        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+        else if (key == "detailList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage"), deviceList: item.deviceList ? XEUtils.toStringJSON(item.deviceList) : null })));
+        else if (key == "inspectProgram") {
+            data[key] && XEUtils.objectEach(descData.value[key], (_, plan_key) => {
+                if (plan_key == "fileList") XEUtils.set(descData.value[key], plan_key, XEUtils.map(XEUtils.get(data[key], plan_key), item => ({ ...item, name: item.fileName })));
+                else XEUtils.set(descData.value[key], plan_key, XEUtils.get(data[key], plan_key));
+            });
+        } else XEUtils.set(descData.value, key, XEUtils.get(data, key));
     });
+
+    if (data.inspectProgramId) XEUtils.set(descData.value.inspectProgram.fileList, key, XEUtils.map(XEUtils.get(data.inspectProgram, key), item => ({ ...item, name: item.fileName })));
+
 }
 
 defineExpose({

File diff suppressed because it is too large
+ 4 - 9
src/views/process/line/main.js


+ 3 - 3
src/views/production/bom/desc.vue

@@ -7,9 +7,9 @@
                         <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="BOM单编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.bomCode }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="BOM单状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(statusDic, descData.status, descData.status) }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="BOM单编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.bomCode }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="BOM单状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(statusDic, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
                         <el-descriptions-item label="概要" :span="3" label-align="right">{{ descData.remark }}</el-descriptions-item>
                     </el-descriptions>
                 </el-collapse-item>

+ 5 - 2
src/views/production/bom/detail.vue

@@ -155,8 +155,11 @@ const submit = () => {
                 XEUtils.set(data, "materialCode", form.value.material.code);
                 XEUtils.set(data, "childrenList", childrenList);
                 form.value.parentId === "0" && mode.value == "add" && XEUtils.set(data, "quantity", 1);
-                form.value.parentId !== "0" && mode.value == "add" && (data.remark === "" || XEUtils.isNull(data.remark) || XEUtils.isUndefined(data.remark)) && XEUtils.set(data, "emptyField", ["remark"]);
-                
+                if (form.value.parentId !== "0" && mode.value == "add") {
+                    const emptyField = XEUtils.filter(["routeId", "remark"], field => data[field] === "" || XEUtils.isNull(data[field]) || XEUtils.isUndefined(data[field]))
+                    emptyField.length && XEUtils.set(data, "emptyField", emptyField);
+                }
+
                 isSaving.value = true;
                 API.production.bom[mode.value](data).then(res => {
                     ElMessage.success("操作成功");

+ 4 - 2
src/views/production/bom/index.vue

@@ -121,9 +121,11 @@ const paramsColums = reactive([
 
 const columns = computed(() => props.selectable ? [
     { type: props.multiple && "checkbox" || "radio", fixed: "left", width: 40 },
-    { type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, sortable: true },
+    // { visible: computed(() => !XEUtils.isNull(XEUtils.get(props.options, "treeConfig"))), type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left", sortable: true },
+    // { visible: computed(() => XEUtils.isNull(XEUtils.get(props.options, "treeConfig"))), type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 200, sortable: true },
+    { type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 200, sortable: true },
     { type: "html", field: "materialName", title: "产品名称", fixed: "left", minWidth: 150, sortable: true },
-    { field: "bomCode", title: "BOM单编号", minWidth: 150, sortable: true },
+    { type: "html", field: "material.specification", title: "规格型号", minWidth: 120, sortable: true },
     { type: "html", field: "quantity", title: "标准用量", minWidth: 100, sortable: true },
     { type: "html", field: "material.unit", title: "单位", minWidth: 100, sortable: true }
 ] : [

+ 4 - 4
src/views/production/bom/main.js

@@ -6,13 +6,13 @@ export const tableOptions = reactive({
     tableKey: "material",
 
     columns: [
-        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => XEUtils.get(row, "isHaveChildren", false) }, slots: { default: "seq_del" } },
-        { field: "code", title: "物料编码", fixed: "left", minWidth: 150 },
-        { field: "name", title: "物料名称", fixed: "left", minWidth: 150 },
+        { type: "seq", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => XEUtils.get(row, "isHaveChildren", false) }, slots: { default: "seq_del" } },
+        { field: "code", title: "物料编码", minWidth: 150 },
+        { field: "name", title: "物料名称", minWidth: 150 },
         { field: "specification", title: "规格型号", minWidth: 150 },
         { field: "unit", title: "单位", minWidth: 150 },
         { field: "quantity", title: "标准用量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
-        { field: "routeId", title: "质检方案", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, clearable: true }, optionProps: { label: "name", value: "id" } }, formatter: ({ cellValue, row, column }) => cellValue ? XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == cellValue), "name", row.routeName) : "" },
+        { field: "routeId", title: "工艺路线", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, clearable: true }, optionProps: { label: "name", value: "id" } }, formatter: ({ cellValue, row, column }) => cellValue ? XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == cellValue), "name", row.routeName) : "" },
         { field: "remark", title: "备注", minWidth: 200, editRender: { name: "VxeInput", props: { clearable: true, placeholder: "" } } }
     ],
     editRules: {

+ 134 - 0
src/views/production/dispatch/desc.vue

@@ -0,0 +1,134 @@
+<template>
+    <el-dialog v-model="visible" title="派工单详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="生产工单" name="order">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.productOrder.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.productOrder.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.productOrder.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(productionDic.orderStatus, descData.productOrder.status, descData.productOrder.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="计划开工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.productOrder.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划完工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.productOrder.endDate }}</el-descriptions-item>
+                        <el-descriptions-item label="交货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.productOrder.finishDate }}</el-descriptions-item>
+                        <el-descriptions-item label="概要" :span="ismobile ? 3 : 2" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="派工信息" name="dispatch">
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item label="派工产品" :span="ismobile ? 3 : 1" label-align="right">{{ descData.productBom.materialName }}</el-descriptions-item>
+                        <el-descriptions-item label="工序名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.stage.name }}</el-descriptions-item>
+                        <el-descriptions-item label="工序编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.stage.code }}</el-descriptions-item>
+                        <el-descriptions-item label="派工主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item label="派工编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="派工状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(productionDic.dispatchStatus, formatDispatch(descData)) }}</el-descriptions-item>
+                        <el-descriptions-item label="被派人员" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.map(descData.userItems, item => item.user.nickName).join() }}</el-descriptions-item>
+                        <el-descriptions-item label="派工数量" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderNum }}</el-descriptions-item>
+                        <el-descriptions-item label="计划开工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划完工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.endDate }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="条码" label-align="right">
+                            <!-- `[S]field[M]value[S]field[M]value[E]` -->
+                            <sc-qr-code :text="descData.code"></sc-qr-code>
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item v-if="descData.requisitionStatus" title="领料信息" name="requisition">
+                    <requisition-table v-bind="requisitionOptions"></requisition-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="报工信息" name="report">
+                </el-collapse-item>
+
+                <el-collapse-item v-if="descData.isInspection" title="质检信息" name="inspection">
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { productionDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+import requisitionTable from "@/views/warehouse/production/requisition/index";
+
+const $emit = defineEmits(["closed"]);
+const props = defineProps({
+    formatDispatch: { type: Function, default: () => {} }
+});
+
+const visible = ref(false);
+
+import store from "@/store";
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const activeNames = ref(["order", "dispatch", "requisition", "report", "inspection"]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    productOrder: { name: null, code: null, beginDate: null, endDate: null, finishDate: null, remark: null, status: "pending", createTime: null },
+    productBom: { materialName: null, materialCode: null },
+    stage: { name: null, code: null },
+    requisitionStatus: null,
+    name: null,
+    code: null,
+    beginDate: null,
+    endDate: null,
+    userItems: [],
+    orderNum: 1,
+    isInspection: true,
+    status: "pending",
+    createTime: null
+});
+
+const requisitionOptions = reactive({
+    detailable: true,
+    options: {
+        toolbarConfig: { enabled: false },
+        formConfig: { enabled: false },
+        paramsColums: [
+            { column: "orderBy", defaultValue: "createTime_desc" },
+            { column: "requisitionType", defaultValue: "auto" },
+            { column: "dispatchId" },
+            { column: "tenantId" }
+        ]
+    }
+});
+
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (XEUtils.includes(["productOrder", "productBom", "productOrderBom", "stage"], key)) XEUtils.objectEach(descData.value[key], (_, o_key) => XEUtils.set(descData.value[key], o_key, XEUtils.get(data[key], o_key)));
+        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+
+    requisitionOptions.options.paramsColums[2].defaultValue = data.id;
+    requisitionOptions.options.paramsColums[3].defaultValue = data.tenantId;
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content img {display: block;}
+</style>

+ 175 - 0
src/views/production/dispatch/detail.vue

@@ -0,0 +1,175 @@
+<template>
+    <el-dialog v-model="visible" title="修改派工单" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="生产工单" name="order">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据主题" :span="ismobile ? 3 : 1" label-align="right">{{ form.productOrder.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ form.productOrder.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ form.productOrder.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="计划开工日期" :span="ismobile ? 3 : 1" label-align="right">{{ form.productOrder.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划完工日期" :span="ismobile ? 3 : 1" label-align="right">{{ form.productOrder.endDate }}</el-descriptions-item>
+                        <el-descriptions-item label="交货日期" :span="ismobile ? 3 : 1" label-align="right">{{ form.productOrder.finishDate }}</el-descriptions-item>
+                        <el-descriptions-item label="派工产品" :span="ismobile ? 3 : 1" label-align="right">{{ form.productBom.materialName }}</el-descriptions-item>
+                        <el-descriptions-item label="工序名称" :span="ismobile ? 3 : 1" label-align="right">{{ form.stage.name }}</el-descriptions-item>
+                        <el-descriptions-item label="工序编号" :span="ismobile ? 3 : 1" label-align="right">{{ form.stage.code }}</el-descriptions-item>
+                        <el-descriptions-item label="概要" :span="3" label-align="right">{{ form.productOrder.remark }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="派工信息" name="dispatch">
+                    <el-row>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="派工主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入派工主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="派工编号" required>
+                                <el-input v-model="form.code" readonly show-word-limit></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="被派人员" prop="userIds">
+                                <el-select v-model="form.userIds" filterable multiple collapse-tags collapse-tags-tooltip placeholder="请选择被派人员">
+                                    <el-option v-for="item in users.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.nickName" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="派工数量" prop="orderNum">
+                                <el-input-number v-model="form.orderNum" :min="1" :max="form.productOrderBom.number" :readonly="XEUtils.includes(['partially', 'complete'], form.requisitionStatus)" step-strictly :controls="false" :precision="2" placeholder="请输入派工数量"></el-input-number>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划开工日期" prop="beginDate">
+                                <vxe-date-picker v-model="form.beginDate" :start-date="form.productOrder.beginDate" :end-date="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划开工日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划完工日期" prop="endDate">
+                                <vxe-date-picker v-model="form.endDate" :start-date="form.beginDate" :end-date="form.productOrder.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划完工日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="是否质检">
+                                <el-radio-group v-model="form.isInspection">
+                                    <el-radio label="是" :value="true"></el-radio>
+                                    <el-radio label="否" :value="false"></el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import API from "@/api";
+import store from "@/store";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["order", "dispatch"]);
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.tenantId), "name"));
+
+const users = ref([]);
+const form = reactive({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    orderId: null,
+    productOrder: { name: null, code: null, beginDate: null, endDate: null, finishDate: null, remark: null, createTime: null },
+    bomId: null,
+    productBom: { materialName: null },
+    productOrderBom: { number: null },
+    stageId: null,
+    stage: { name: null, code: null },
+    requisitionStatus: "pending",
+    name: null,
+    code: null,
+    beginDate: null,
+    endDate: null,
+    userIds: [],
+    orderNum: 1,
+    isInspection: true
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入单据主题" }],
+    userIds: [{ required: true, message: "请选择被派人员" }],
+    orderNum: [{ required: true, message: "请输入派工数量" }],
+    beginDate: [{ required: true, message: "请选择计划开工期" }],
+    endDate: [{ required: true, message: "请选择计划完工期" }]
+});
+
+const setData = data => {
+    visible.value = true;
+
+    XEUtils.objectEach(form, (_, key) => {
+        if (key == "userIds") XEUtils.set(form, key, XEUtils.map(XEUtils.get(data, "userItems"), item => item.user.id));
+        else if (XEUtils.includes(["productOrder", "productBom", "productOrderBom", "stage"], key)) XEUtils.objectEach(form[key], (_, o_key) => XEUtils.set(form[key], o_key, XEUtils.get(data[key], o_key)));
+        else XEUtils.set(form, key, XEUtils.get(data, key));
+    });
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            const data = XEUtils.pick(form, "orderId");
+            const items = [XEUtils.omit(form, ["orderId", "productOrder", "productBom", "productOrderBom", "stage", "requisitionStatus"])];
+            XEUtils.set(data, "items", items);
+            
+            isSaving.value = true;
+            API.production.dispatch.handler(data).then(res => {
+                ElMessage.success("操作成功");
+                isSaving.value = false;
+                visible.value = false;
+                $emit("success");
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .el-input-number :deep(.el-input__suffix) {font-size: 12px;}
+.el-form .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 158 - 0
src/views/production/dispatch/index.vue

@@ -0,0 +1,158 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.production.dispatch" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <template v-if="XEUtils.includes(['pending', 'processing'], row.status)">
+                    <el-button v-if="row.requisitionStatus == 'complete'" type="primary" link @click="table_report(row)">
+                        <template #icon><sc-iconify icon="mdi:transfer"></sc-iconify></template>按单汇报
+                    </el-button>
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                </template>
+                <el-button v-if="row.status == 'pending' && (!row.requisitionStatus || row.requisitionStatus == 'pending')" type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <order-detail v-if="dialog.detail" ref="orderRef" @success="refreshTable" @closed="dialogClose"></order-detail>
+    <order-desc v-if="dialog.desc" ref="orderDescRef" :formatDispatch="formatDispatch" @closed="dialog.desc = false"></order-desc>
+    <report-detail v-if="dialog.report" ref="dispatchRef" @success="refreshTable" @closed="dialog.report = false"></report-detail>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { productionDic, warehouseDic, qualityInspectionDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import orderDetail from "./detail";
+import orderDesc from "./desc";
+import reportDetail from "./report";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const formatDispatch = row => {
+    // 超期未开工
+    if (row.status === "pending" && moment().diff(row.beginDate) > 0) return "overduePending";
+    // 超期生产中 生产中 && 当前时间 > 计划完工时间 || row.status === "complete" && row.isInspection && 未质检
+    if (row.status === "processing" && moment().diff(row.endDate) > 0) return "overdueProcessing";
+    return "pending";
+}
+
+const formatInspection = row => {
+    if (!row.isInspection) return "noNeed";
+    return "pending";
+}
+
+const selectConfig = reactive({
+    options: XEUtils.omit(productionDic.dispatchStatus, "overduePending", "overdueProcessing"),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "派工主题"),
+        mapFormItemInput("codeLike", "派工编号"),
+        mapFormItemSelect("status", "派工状态", selectConfig),
+        mapFormItemDatePicker("beginDate", "计划开工日期", daterangeConfig),
+        mapFormItemDatePicker("endDate", "计划完工日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "status" },
+    { column: "beginDateBegin", field: "beginDate[0]" },
+    { column: "beginDateEnd", field: "beginDate[1]" },
+    { column: "endDateBegin", field: "endDate[0]" },
+    { column: "endDateEnd", field: "endDate[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { field: "name", title: "派工主题", fixed: "left", minWidth: 150, sortable: true },
+    { field: "code", title: "派工编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    { type: "html", field: "productBom.materialName", title: "派工产品", minWidth: 150, sortable: true },
+    { type: "html", field: "orderNum", title: "派工数量", minWidth: 100, sortable: true },
+    { type: "html", field: "beginDate", title: "计划开工日期", minWidth: 120, sortable: true },
+    { type: "html", field: "endDate", title: "计划完工日期", minWidth: 120, sortable: true },
+    { type: "html", field: "productOrder.finishDate", title: "交货日期", minWidth: 120, sortable: true },
+    { field: "status", title: "派工状态", minWidth: 120, editRender: { name: "$cell-tag", options: productionDic.dispatchStatus, formatter: row => formatDispatch(row) } },
+    { field: "requisitionStatus", title: "领料状态", minWidth: 120, editRender: { name: "$cell-tag", options: warehouseDic.requisition.status, formatter: row => row.requisitionStatus || "noNeed" } },
+    { field: "inspectionStatus", title: "质检状态", minWidth: 120, editRender: { name: "$cell-tag", options: qualityInspectionDic.status, formatter: row => formatInspection(row) } },
+    { type: "html", field: "userNames", title: "被派人员", minWidth: 100, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.filter(XEUtils.map(row.userItems, item => XEUtils.get(item, "user.nickName")), item => item).join() },
+    { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { title: "操作", fixed: "right", width: 240, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = () => (xGridTable.value.searchData(), xGridTable.value.reloadColumn(columns));
+
+const orderRef = ref();
+const orderDescRef = ref();
+const reportRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    report: false
+});
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => orderDescRef.value?.setData(row));
+}
+
+const table_report = row => {
+    dialog.report = true;
+    nextTick(() => reportRef.value?.setData(row));
+}
+
+const table_del = ({ id, orderId, requisitionStatus }) => {
+    ElMessageBox.confirm(XEUtils.template("是否确认删除该派工单{{ tip }}?", { tip: requisitionStatus == "pending" ? ",此操作将同时删除领料单" : "" }), "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.production.dispatch.handler({ orderId, items: [{ id }] }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 29 - 0
src/views/production/dispatch/main.js

@@ -0,0 +1,29 @@
+import XEUtils from "xe-utils"
+
+export const tableOptions = reactive({
+    tableKey: "bom",
+
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        { field: "materialCode", title: "产品编码", fixed: "left", minWidth: 180 },
+        { field: "materialName", title: "产品名称", fixed: "left", minWidth: 180 },
+        { field: "material.specification", title: "规格型号", minWidth: 150 },
+        { field: "material.unit", title: "单位", minWidth: 120 },
+        { field: "routeName", title: "工艺路线", minWidth: 120 },
+        { field: "number", title: "生产数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "inspectProgramId", title: "质检方案", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, clearable: true }, optionProps: { label: "name", value: "id" } }, formatter: ({ cellValue, row, column }) => cellValue ? XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == cellValue), "name", row.inspectProgramName) : "" }
+    ],
+
+    editRules: {
+        number: [{ required: true, message: "必须填写" }]
+    },
+
+    selectOptions: {
+        treeConfig: null,
+        paramsColums: [
+            { column: "status", defaultValue: "enable" }
+        ]
+    },
+
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, item => XEUtils.pick(item, "id", "materialCode", "materialName", "material", "routeId", "routeName", "inspectProgramId", "inspectProgramName"))
+})

+ 134 - 0
src/views/production/dispatch/report.vue

@@ -0,0 +1,134 @@
+<template>
+    <el-dialog v-model="visible" title="修改派工单" fullscreen :close-on-click-modal="false" @closed="$emit('closed', isDel)">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="工单信息" name="order">
+                </el-collapse-item>
+
+                <el-collapse-item title="派工信息" name="material">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户" @change="form.managerId = null">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="生产周期" prop="beginDate">
+                                <vxe-date-range-picker v-model:start-value="form.beginDate" v-model:end-value="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划开工/完工期"></vxe-date-range-picker>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { productionDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["order", "dispatch"]);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    planId: null,
+    saleOrderId: null,
+    name: null,
+    code: null,
+    orderDate: moment().format("YYYY-MM-DD"),
+    beginDate: null,
+    endDate: null,
+    finishDate: null,
+    priority: "medium",
+    bomList: [],
+    remark: null,
+    fileList: []
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入单据主题" }],
+    orderDate: [{ required: true, message: "请选择单据日期" }],
+    beginDate: [{ required: true, message: "请选择生产周期" }]
+});
+
+const setData = data => {
+    visible.value = true;
+
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "bomList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.productBom, routeId: item.routeId, inspectProgramId: item.inspectProgramId, number: item.number })));
+        else if (key == "fileList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+        else XEUtils.set(form.value, key, XEUtils.get(data, key));
+    });
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            const data = XEUtils.omit(form.value, "bomList");
+            const bomList = XEUtils.map(form.value.bomList, item => ({ bomId: item.id, routeId: item.routeId, inspectProgramId: item.inspectProgramId, number: item.number }));
+            XEUtils.set(data, "bomList", bomList);
+
+            isSaving.value = true;
+            API.production.dispatch.add(data).then(res => {
+                ElMessage.success("操作成功");
+                isSaving.value = false;
+                isDel.value = false;
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+const removeSuccess = () => form.value.id && (isDel.value = true);
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .el-input-number :deep(.el-input__suffix) {font-size: 12px;}
+.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 119 - 0
src/views/production/inspection/index.vue

@@ -0,0 +1,119 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action>
+                <!-- 入库申请/返工(派工后) -->
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>质检
+                </el-button>
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>指派
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { salesDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "质检任务主题"),
+            mapFormItemInput("codeLike", "质检任务编号"),
+            mapFormItemDatePicker("createTime", "计划周期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "status", defaultValue: "pending" },
+        { column: "tenantId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "createTimeBegin", field: "createTime[0]" },
+        { column: "createTimeEnd", field: "createTime[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "质检任务主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "质检任务编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { type: "html", field: "product", title: "质检产品", minWidth: 150, sortable: true },
+        { type: "html", field: "num", title: "数量", minWidth: 150, sortable: true },
+        { field: "status", title: "质检状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
+        { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+    ]
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const dispatchRef = ref();
+const dispatchDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => dispatchRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => dispatchRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => dispatchDescRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该派工单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>

+ 89 - 0
src/views/production/order/desc.vue

@@ -0,0 +1,89 @@
+<template>
+    <el-dialog v-model="visible" title="生产工单详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <!-- <el-descriptions-item label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item> -->
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(productionDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="计划开工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划完工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.endDate }}</el-descriptions-item>
+                        <el-descriptions-item label="交货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.finishDate }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label="附件" label-align="right">
+                            <sc-upload-file v-model="descData.fileList" hideAdd disabled></sc-upload-file>
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table v-model="descData.bomList" v-bind="tableOptions['order']" disabled></sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { productionDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+import store from "@/store";
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const activeNames = ref(["basic", "material"]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    // orderDate: null,
+    beginDate: null,
+    endDate: null,
+    finishDate: null,
+    bomList: [],
+    remark: null,
+    fileList: [],
+    status: "pending",
+    createTime: null
+});
+
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "fileList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+        else if (key == "bomList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.productBom, number: item.number })));
+        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 211 - 0
src/views/production/order/detail.vue

@@ -0,0 +1,211 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed', isDel)">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id && !form.planId" v-model="form.tenantId" filterable placeholder="请选择所属租户" @change="form.managerId = null">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入单据主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <!-- <el-col :md="8" :xs="24">
+                            <el-form-item label="单据日期" prop="orderDate">
+                                <el-date-picker v-model="form.orderDate" :clearable="false" value-format="YYYY-MM-DD" placeholder="请选择单据日期"></el-date-picker>
+                            </el-form-item>
+                        </el-col> -->
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划开工日期" prop="beginDate">
+                                <vxe-date-picker v-model="form.beginDate" :end-date="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划开工日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <!-- 算所有工序的总时间 -->
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划完工日期" prop="endDate">
+                                <vxe-date-picker v-model="form.endDate" :start-date="form.beginDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划完工日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="交货日期">
+                                <vxe-date-picker v-model="form.finishDate" :start-date="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择交货日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="优先级">
+                                <el-select v-model="form.priority" placeholder="请选择优先级">
+                                    <el-option v-for="(label, key) in productionDic.priority" :key="key" :label="label" :value="key"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.bomList" v-bind="tableOptions['order']" :disabled="!!form.planId"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他信息" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :xs="24">
+                            <el-form-item label="附件" label-width="100">
+                                <sc-upload-file v-model="form.fileList" @removeSuccess="removeSuccess">
+                                    <vxe-button status="primary" size="mini" content="上传附件"></vxe-button>
+                                </sc-upload-file>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { productionDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+const isDel = ref(false);
+
+const activeNames = ref(["basic", "material", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增生产工单",
+    edit: "修改生产工单"
+});
+
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    planId: null,
+    saleOrderId: null,
+    name: null,
+    code: null,
+    // orderDate: moment().format("YYYY-MM-DD"),
+    beginDate: null,
+    endDate: null,
+    finishDate: null,
+    priority: "medium",
+    bomList: [],
+    remark: null,
+    fileList: []
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入单据主题" }],
+    // orderDate: [{ required: true, message: "请选择单据日期" }],
+    beginDate: [{ required: true, message: "请选择计划开工日期" }],
+    endDate: [{ required: true, message: "请选择计划完工日期" }]
+});
+
+const setData = (data = {}, model = "add") => {
+    visible.value = true;
+    mode.value = model;
+
+    if (model === "add") {
+        if (!XEUtils.isEmpty(data)) {
+            const planData = {
+                tenantId: data.tenantId,
+                planId: data.id,
+                saleOrderId: data.saleOrderId,
+                beginDate: data.beginDate,
+                endDate: data.endDate,
+                bomList: XEUtils.map(data.bomList, item => ({ ...item.productBom, number: item.number, routeId: item.routeId, inspectProgramId: item.inspectProgramId }))
+            }
+    
+            XEUtils.objectEach(form.value, (_, key) => XEUtils.has(planData, key) && XEUtils.set(form.value, key, XEUtils.get(planData, key)));
+        }
+    } else {
+        XEUtils.objectEach(form.value, (_, key) => {
+            if (key == "bomList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.productBom, routeId: item.routeId, inspectProgramId: item.inspectProgramId, number: item.number })));
+            else if (key == "fileList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+            else XEUtils.set(form.value, key, XEUtils.get(data, key));
+        });
+    }
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            if (!form.value.bomList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "bomList", "fileList");
+                const bomList = XEUtils.map(form.value.bomList, item => ({ bomId: item.id, routeId: item.routeId, inspectProgramId: item.inspectProgramId, number: item.number }));
+                const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "productionOrderAttach" }));
+                XEUtils.set(data, "bomList", bomList);
+                fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+                isSaving.value = true;
+                API.production.order[mode.value](data).then(res => {
+                    ElMessage.success("操作成功");
+                    isSaving.value = false;
+                    isDel.value = false;
+                    visible.value = false;
+                    $emit("success", mode.value);
+                }).catch(() => isSaving.value = false);
+            }
+        } else {
+            return false;
+        }
+    });
+}
+
+const removeSuccess = () => form.value.id && (isDel.value = true);
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--inner::placeholder) {color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 162 - 0
src/views/production/order/dispatch.vue

@@ -0,0 +1,162 @@
+<template>
+    <el-dialog v-model="visible" title="生产派工" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="生产工单" name="order">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="计划开工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划完工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.endDate }}</el-descriptions-item>
+                        <el-descriptions-item label="交货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.finishDate }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="派工信息" name="dispatch">
+                    <sc-form-table v-model="descData.bomList" v-bind="tableOptions['dispatch']">
+                        <template #expand_content="{ row, rowIndex }">
+                            <sc-form-table :ref="el => el && (formTableRefs[rowIndex] = el)" v-model="row.dispatchList" v-bind="tableOptions['dispatchExpand']" @editActivated="editActivated">
+                                <template #edit_date_picker="{ row, column }">
+                                    <vxe-date-picker v-model="row[column.field]" :start-date="formatDateStart(row, column)" :end-date="formatDateEnd(row, column)"></vxe-date-picker>
+                                </template>
+                                <template #default_date_picker="{ row, column }">{{ row[column.field] }}</template>
+                            </sc-form-table>
+                        </template>
+                    </sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { productionDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["order", "dispatch"]);
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const users = ref([]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    beginDate: null,
+    endDate: null,
+    finishDate: null,
+    remark: null,
+    bomList: [],
+    createTime: null
+});
+
+const formatDateStart = (row, { field }) => field == "beginDate" ? descData.value.beginDate : row.beginDate;
+const formatDateEnd = (row, { field }) => field == "beginDate" ? row.endDate : descData.value.endDate;
+
+const setData = async data => {
+    const res = await API.production.dispatch.getSummary({ orderId: data.id });
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "bomList") {
+            XEUtils.set(descData.value, key, XEUtils.filter(XEUtils.map(XEUtils.get(data, key), item => {
+                // 当前bom 保留还有剩余数量的工序
+                item.dispatchList = XEUtils.filter(XEUtils.map(XEUtils.get(item, "processRoute.detailList"), stageItem => {
+                    // 当前工序 已派工列表
+                    const stageDispatched = XEUtils.filter(res, r => r.bomId == item.bomId && r.stageId == stageItem.stageId);
+                    return {
+                        ...stageItem,
+                        bomId: item.bomId,
+                        name: `转:${data.name}`,
+                        remainNum: XEUtils.subtract(item.number, XEUtils.sum(XEUtils.map(stageDispatched, d => d.orderNum))),
+                        orderNum: XEUtils.subtract(item.number, XEUtils.sum(XEUtils.map(stageDispatched, d => d.orderNum))),
+                        beginDate: data.beginDate,
+                        endDate: data.endDate,
+                        isInspection: true
+                    }
+                }), stageItem => stageItem.isReport && stageItem.orderNum > 0);
+
+                return item
+            }), item => item.dispatchList.length > 0));
+        } else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+
+    visible.value = true;
+}
+
+const formTableRefs = ref([]);
+const editActivated = ({ row, column }) => {
+    column.field == "userIds" && (column.editRender.options = XEUtils.filter(users.value, item => item.tenantId == descData.value.tenantId));
+    column.field == "orderNum" && (column.editRender.props.max = row.remainNum);
+}
+const submit = async () => {
+    const validResult = [];
+    for (const ref of formTableRefs.value) {
+        if (!await ref.validateFormTable()) {
+            validResult.push(false);
+            break;
+        }
+    }
+
+    if (!validResult.length) {
+        const data = {
+            orderId: descData.value.id,
+            tenantId: descData.value.tenantId,
+            items: XEUtils.map(XEUtils.flatten(XEUtils.map(descData.value.bomList, item => item.dispatchList)), item => ({ ...XEUtils.omit(item, "id", "stage", "remainNum")}))
+        }
+
+        isSaving.value = true;
+        API.production.dispatch.handler(data).then(res => {
+            ElMessage.success("操作成功");
+            isSaving.value = false;
+            visible.value = false;
+            $emit("success");
+        }).catch(() => isSaving.value = false);
+    }
+  
+}
+
+const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+fetchUser();
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+
+.el-collapse-item :deep(.el-collapse-item__content) .vxe-body--row-expanded-cell {padding: 16px;}
+.el-collapse-item :deep(.el-collapse-item__content) .vxe-body--row-expanded-cell .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-collapse-item :deep(.el-collapse-item__content) .vxe-body--row-expanded-cell .vxe-date-picker .vxe-date-picker--suffix {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-collapse-item :deep(.el-collapse-item__content) .vxe-body--row-expanded-cell .vxe-date-picker .vxe-date-picker--control-icon {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+</style>

+ 127 - 30
src/views/production/order/index.vue

@@ -2,55 +2,152 @@
 	<el-container class="is-vertical">
         <sc-page-header @add="table_add"></sc-page-header>
 
-        <scTable ref="xGridTable" :formConfig="formConfig" :columns="columns" :options="options">
-            <template #action>
-                <el-button type="primary" link>
-                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
-                </el-button>
-                <el-button type="primary" link>
-                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+        <scTable ref="xGridTable" :apiObj="$API.production.order" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <el-button v-if="row.canDispatch" type="primary" link @click="table_dispatch(row)">
+                    <template #icon><sc-iconify icon="mdi:transfer"></sc-iconify></template>派工
                 </el-button>
+                <template v-if="row.status === 'pending'">
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                    <el-button type="primary" link @click="table_del(row)">
+                        <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                    </el-button>
+                </template>
             </template>
         </scTable>
 	</el-container>
+
+    <order-detail v-if="dialog.detail" ref="orderRef" @success="refreshTable" @closed="dialogClose"></order-detail>
+    <order-desc v-if="dialog.desc" ref="orderDescRef" @closed="dialog.desc = false"></order-desc>
+    <dispatch-detail v-if="dialog.dispatch" ref="dispatchRef" @success="refreshTable" @closed="dialog.dispatch = false"></dispatch-detail>
 </template>
 
 <script setup>
+import moment from "moment";
 import XEUtils from "xe-utils";
-import { mapFormItemInput } from "@/components/scTable/helper";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { productionDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import orderDetail from "./detail";
+import orderDesc from "./desc";
+import dispatchDetail from "./dispatch";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const selectConfig = reactive({
+    options: productionDic.orderStatus,
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
 
 const formConfig = reactive({
     data: {},
     items: [
-        mapFormItemInput("name", "Name"),
-        mapFormItemInput("role", "Role"),
-        mapFormItemInput("sex", "Sex"),
-        mapFormItemInput("num", "Num")
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "单据主题"),
+        mapFormItemInput("codeLike", "单据编号"),
+        mapFormItemSelect("status", "单据状态", selectConfig),
+        // mapFormItemDatePicker("orderDate", "单据日期", daterangeConfig)
+        mapFormItemDatePicker("beginDate", "计划开工日期", daterangeConfig),
+        mapFormItemDatePicker("endDate", "计划完工日期", daterangeConfig)
     ]
 });
 
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "status" },
+    { column: "beginDateBegin", field: "beginDate[0]" },
+    { column: "beginDateEnd", field: "beginDate[1]" },
+    { column: "endDateBegin", field: "endDate[0]" },
+    { column: "endDateEnd", field: "endDate[1]" }
+]);
+
 const columns = reactive([
     { type: "seq", fixed: "left", width: 60 },
-    { type: "html", field: "name", title: "Name", sortable: true },
-    { type: "html", field: "role", title: "Role", sortable: true },
-    { type: "html", field: "sex", title: "Sex", sortable: true },
-    { type: "html", field: "num", title: "Num", sortable: true },
-    { type: "html", field: "address", title: "Address", sortable: true },
-    { title: "操作", fixed: "right", width: 160, slots: { default: "action" } }
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "单据主题", fixed: "left", minWidth: 150, sortable: true },
+    { field: "code", title: "单据编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    // { type: "html", field: "orderDate", title: "单据日期", minWidth: 120, sortable: true },
+    { field: "status", title: "单据状态", minWidth: 120, editRender: { name: "$cell-tag", options: productionDic.orderStatus } },
+    { type: "html", field: "beginDate", title: "计划开工日期", minWidth: 120, sortable: true },
+    { type: "html", field: "endDate", title: "计划完工日期", minWidth: 120, sortable: true },
+    { type: "html", field: "finishDate", title: "交货日期", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { visible: false, type: "html", field: "remark", title: "概要", minWidth: 300, sortable: true },
+    { title: "操作", fixed: "right", width: 200, slots: { default: "action" } }
 ]);
 
-const options = reactive({
-    data: [
-        { id: '10001', name: 'Test1', role: 'Develop', sex: 'Man', num: '28', address: 'test abc' },
-        { id: '10002', name: 'Test2', role: 'Test', sex: 'Women', num: '22', address: 'Guangzhou' },
-        { id: '10003', name: 'Test3', role: 'PM', sex: 'Man', num: '32', address: 'Shanghai' },
-        { id: '10004', name: 'Test4', role: 'Designer', sex: 'Women', num: '24', address: 'Shanghai' },
-        { id: '10005', name: 'Test5', role: 'Develop', sex: 'Man', num: '42', address: 'Guangzhou' },
-        { id: '10006', name: 'Test6', role: 'Test', sex: 'Women', num: '39', address: 'Shanghai' },
-        { id: '10007', name: 'Test7', role: 'Develop', sex: 'Man', num: '46', address: 'Shanghai' },
-        { id: '10008', name: 'Test8', role: 'PM', sex: 'Women', num: '49', address: 'Guangzhou' }
-    ]
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const orderRef = ref();
+const orderDescRef = ref();
+const dispatchRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    dispatch: false
 });
 
-const table_add = () => {};
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData(row, "edit"));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => orderDescRef.value?.setData(row));
+}
+
+const table_dispatch = row => {
+    dialog.dispatch = true;
+    nextTick(() => dispatchRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该生产工单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.production.order.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+const dialogClose = isDel => {
+    dialog.detail = false;
+    isDel && refreshTable();
+}
 </script>

+ 64 - 0
src/views/production/order/main.js

@@ -0,0 +1,64 @@
+import XEUtils from "xe-utils"
+
+export const tableOptions = reactive({
+    order: {
+        tableKey: "bom",
+    
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "materialCode", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "materialName", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "material.specification", title: "规格型号", minWidth: 150 },
+            { field: "material.unit", title: "单位", minWidth: 120 },
+            { field: "number", title: "生产数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        ],
+    
+        editRules: {
+            number: [{ required: true, message: "必须填写" }]
+        },
+    
+        selectOptions: {
+            treeConfig: null,
+            paramsColums: [
+                { column: "status", defaultValue: "enable" }
+            ]
+        },
+    
+        add_success: (oldValue, newValue) => XEUtils.map(newValue, item => XEUtils.pick(item, "id", "materialCode", "materialName", "material", "routeId", "inspectProgramId"))
+    },
+
+    dispatch: {
+        expandConfig: { expandAll: true },
+        columns: [
+            { type: "expand", fixed: "left", width: 40, align: "center", slots: { content: "expand_content" } },
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false },
+            { field: "productBom.materialCode", title: "产品编码", minWidth: 180 },
+            { field: "productBom.materialName", title: "产品名称", minWidth: 180 },
+            { field: "processRoute.name", title: "工艺路线", minWidth: 180 }
+        ]
+    },
+
+    dispatchExpand: {
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false },
+            { field: "stage.code", title: "工序编号", fixed: "left", minWidth: 180 },
+            { field: "stage.name", title: "工序名称", fixed: "left", minWidth: 180 },
+            { field: "name", title: "派工主题", minWidth: 180, editRender: { name: "VxeInput" } },
+            { field: "code", title: "派工编号", minWidth: 180, editRender: { name: "VxeInput", props: { maxLength: 50, placeholder: "不填将自动生成" } } },
+            { field: "userIds", title: "被派人员", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, multiple: true, multiCharOverflow: 1 }, optionProps: { label: "nickName", value: "id" } }, formatter: ({ cellValue, row, column }) => cellValue && cellValue.length ? XEUtils.map(cellValue, (id, index) => XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == id), "nickName")).join() : "" },
+            { field: "orderNum", title: "本次派工数量", minWidth: 100, editRender: { name: "VxeNumberInput", type: "float", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 1 } },
+            { field: "beginDate", title: "计划开工期", minWidth: 120, editRender: {}, slots: { edit: "edit_date_picker", default: "default_date_picker" } },
+            { field: "endDate", title: "计划完工期", minWidth: 120, editRender: {}, slots: { edit: "edit_date_picker", default: "default_date_picker" } },
+            { field: "isInspection", title: "是否质检", minWidth: 100, cellRender: { name: "VxeCheckbox" } }
+        ],
+
+        editRules: {
+            name: [{ required: true, message: "必须填写" }],
+            code: [{ required: true, validator: () => {} }],
+            userIds: [{ required: true, message: "必须填写" }],
+            orderNum: [{ required: true, message: "必须填写" }],
+            beginDate: [{ required: true, message: "必须填写" }],
+            endDate: [{ required: true, message: "必须填写" }]
+        }
+    }
+})

+ 18 - 40
src/views/production/plan/desc.vue

@@ -1,5 +1,5 @@
 <template>
-    <el-dialog v-model="visible" title="销售订单详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+    <el-dialog v-model="visible" title="生产计划详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
         <el-main>
             <el-collapse v-model="activeNames">
                 <el-collapse-item title="基本信息" name="basic">
@@ -7,35 +7,21 @@
                         <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item>
-                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(salesDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
-                        <el-descriptions-item label="客户名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.customerName }}</el-descriptions-item>
-                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
-                        <el-descriptions-item label="预计交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.planReceiveDate }}</el-descriptions-item>
-                        <el-descriptions-item label="实际交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.deliveryDate }}</el-descriptions-item>
-                        <el-descriptions-item label="业务员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.managerName }}</el-descriptions-item>
-                        <el-descriptions-item label="收货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.actualReceiveDate }}</el-descriptions-item>
-                        <el-descriptions-item label="客户收货地址" label-align="right" :span="ismobile ? 3 : 1">{{ descData.deliveryAddress }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="计划主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="计划编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="计划状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(productionDic.planStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="计划开工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划完工日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.endDate }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="1" label-width="140" border>
                         <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
-                        <el-descriptions-item label="附件" label-align="right">
-                            <sc-upload-file v-model="descData.fileList" hideAdd disabled></sc-upload-file>
-                        </el-descriptions-item>
+                        <el-descriptions-item v-if="descData.saleOrderId" label="来源单据" label-align="right">{{ descData.saleOrder.code }}</el-descriptions-item>
                     </el-descriptions>
                 </el-collapse-item>
 
                 <el-collapse-item title="产品信息" name="material">
-                    <sc-form-table v-model="descData.childrenList" v-bind="tableOptions" disabled></sc-form-table>
-                </el-collapse-item>
-
-                <el-collapse-item title="金额信息" name="amount">
-                    <el-descriptions :column="2" label-width="140" border>
-                        <el-descriptions-item label="整单折扣额" label-align="right">{{ descData.freePrice }}</el-descriptions-item>
-                        <el-descriptions-item label="成交金额" label-align="right">{{ descData.actualPrice }}</el-descriptions-item>
-                    </el-descriptions>
+                    <sc-form-table v-model="descData.bomList" v-bind="tableOptions" disabled></sc-form-table>
                 </el-collapse-item>
             </el-collapse>
         </el-main>
@@ -44,9 +30,8 @@
 
 <script setup>
 import XEUtils from "xe-utils";
-import { salesDic } from "@/utils/basicDic";
+import { productionDic } from "@/utils/basicDic";
 import { tableOptions } from "./main";
-import scUploadFile from "@/components/scUpload/file";
 
 const $emit = defineEmits(["closed"]);
 const visible = ref(false);
@@ -55,24 +40,18 @@ import store from "@/store";
 const ismobile = computed(() => store.state.global.ismobile);
 const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
 
-const activeNames = ref(["basic", "material", "amount"]);
+const activeNames = ref(["basic", "material"]);
 const descData = ref({
     id: null,
     tenantId: store.state.tenant.tenantId,
+    saleOrderId: null,
+    saleOrder: null,
+    name: null,
     code: null,
-    orderDate: null,
-    customerName: null,
-    contractNo: null,
-    planReceiveDate: null,
-    actualReceiveDate: null,
-    deliveryDate: null,
-    managerName: null,
-    deliveryAddress: null,
-    childrenList: [],
-    freePrice: null,
-    actualPrice: null,
+    beginDate: null,
+    endDate: null,
+    bomList: [],
     remark: null,
-    fileList: [],
     status: "pending",
     createTime: null
 });
@@ -80,8 +59,7 @@ const descData = ref({
 const setData = data => {
     visible.value = true;
     XEUtils.objectEach(descData.value, (_, key) => {
-        if (key == "fileList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
-        else if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, quantity: item.materialQuantity, price: item.materialPrice  })));
+        if (key == "bomList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.productBom, number: item.number })));
         else XEUtils.set(descData.value, key, XEUtils.get(data, key));
     });
 }

+ 50 - 69
src/views/production/plan/detail.vue

@@ -23,41 +23,37 @@
                             </el-form-item>
                         </el-col>
                         <el-col :md="8" :xs="24">
-                            <el-form-item label="负责人" prop="managerUserId">
-                                <el-select v-model="form.managerUserId" placeholder="请选择负责人">
-                                    <el-option v-for="item in users.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.nickName" :value="item.id" />
-                                </el-select>
+                            <el-form-item label="计划开工日期" prop="beginDate">
+                                <vxe-date-picker v-model="form.beginDate" :end-date="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划开工日期"></vxe-date-picker>
                             </el-form-item>
                         </el-col>
                         <el-col :md="8" :xs="24">
-                            <el-form-item label="计划周期" prop="beginDate">
-                                <vxe-date-range-picker v-model:start-value="form.beginDate" v-model:end-value="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划周期"></vxe-date-range-picker>
+                            <el-form-item label="计划完工日期" prop="endDate">
+                                <vxe-date-picker v-model="form.endDate" :start-date="form.beginDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划完工日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划优先级">
+                                <el-select v-model="form.priority" placeholder="请选择计划优先级">
+                                    <el-option v-for="(label, key) in productionDic.priority" :key="key" :label="label" :value="key"></el-option>
+                                </el-select>
                             </el-form-item>
                         </el-col>
                     </el-row>
                 </el-collapse-item>
 
                 <el-collapse-item title="产品信息" name="material">
-                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions">
-                        <template #bom="{ row }">
-                            <el-button type="primary" link @click="show_bom(row)">查看BOM清单</el-button>
-                        </template>
-                        <template #stock="{ row }">{{row}}
-                            <!-- <template v-if="!row.disabled">
-                                <template v-if="!row.warehouse.length">无库存</template>
-                                <template v-else>
-                                    <span>{{ formatWarehouseCount(row.warehouse, "number") }}</span>/
-                                    <span>{{ formatWarehouseCount(row.warehouse, "normalNumber") }}</span>/
-                                    <span>{{ formatWarehouseCount(row.warehouse, "lockedNumber") }}</span>
-                                    <el-button type="primary" link @click="table_stock(name, row)">查看</el-button>
-                                </template>
-                            </template> -->
-                        </template>
-                        <template #stockUse="{ row }">
-                            <el-button type="primary" link @click="table_stock(name, row, 'stockUse')">分配</el-button>
-                            <el-button type="info" link :disabled="!row.stockUseNum || row.stockUseNum == 0" @click="table_clear(row)">清空</el-button>
-                        </template>
-                    </sc-form-table>
+                    <sc-form-table ref="formTableRef" v-model="form.bomList" :disabled="!!form.saleOrderId" v-bind="tableOptions"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
                 </el-collapse-item>
             </el-collapse>
         </el-form>
@@ -75,6 +71,7 @@ import XEUtils from "xe-utils";
 
 import API from "@/api";
 import store from "@/store";
+import { productionDic } from "@/utils/basicDic";
 import { tableOptions } from "./main";
 
 const $emit = defineEmits(["success", "closed"]);
@@ -88,80 +85,65 @@ const titleMap = reactive({
     edit: "修改生产计划"
 });
 
-const users = ref([]);
 provide("tenantId", computed(() => form.value.tenantId));
 const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
 const form = ref({
     id: null,
     tenantId: store.state.tenant.tenantId,
+    saleOrderId: null,
     name: null,
     code: null,
-    managerUserId: null,
     beginDate: null,
     endDate: null,
-    childrenList: []
+    priority: "medium",
+    bomList: [],
+    remark: null
 });
 const rules = reactive({
     tenantId: [{ required: true, message: "请选择所属租户" }],
     name: [{ required: true, message: "请输入计划主题" }],
-    managerUserId: [{ required: true, message: "请选择负责人" }],
-    beginDate: [{ required: true, message: "请选择计划周期" }],
+    beginDate: [{ required: true, message: "请选择计划开工日期" }],
+    endDate: [{ required: true, message: "请选择计划完工日期" }]
 });
 
 const open = () => visible.value = true;
 const setData = data => {
     open();
     mode.value = "edit";
+    
     XEUtils.objectEach(form.value, (_, key) => {
-        if (key == "childrenList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, quantity: item.materialQuantity, price: item.materialPrice })));
+        if (key == "priority") XEUtils.set(form.value, key, XEUtils.get(data, key) || "medium");
+        else if (key == "bomList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.productBom, routeId: item.routeId, inspectProgramId: item.inspectProgramId, number: item.number })));
         else XEUtils.set(form.value, key, XEUtils.get(data, key));
     });
 }
 
-const show_bom = row => {
-
-    API.production.bom.getChild({ materialCode: row.code })
-    API.production.bom.get({materialCode: row.code})
-    // const bomTree = XEUtils.toArrayTree(bomList);
-    // const tableData = XEUtils.mapTree(bomTree, item => ({ ...item, total: XEUtils.reduce(XEUtils.map(XEUtils.get(XEUtils.findTree(bomTree, b => b.id == item.id), "nodes", []), b => b.parentId === "0" ? materialQuantity : b.quantity), (p, v) => XEUtils.multiply(p, v)) }));
-
-    // drawer.value = true;
-    // nextTick(() => drawerRef.value?.setData({ tableData }));
-}
-
-
 const formRef = ref();
 const formTableRef = ref();
 const submit = () => {
     formRef.value.validate(async valid => {
         if (valid) {
-            // if (!form.value.childrenList.length) return ElMessage.warning("请添加产品信息后再保存");
+            if (!form.value.bomList.length) return ElMessage.warning("请添加产品信息后再保存");
             
-            // if (await formTableRef.value.validateFormTable()) {
-            //     const data = XEUtils.omit(form.value, "customer", "childrenList", "fileList");
-            //     const childrenList = XEUtils.map(form.value.childrenList, item => ({ materialCode: item.code, materialQuantity: item.quantity, materialPrice: item.price }));
-            //     const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "saleOrderAttach" }));
-            //     XEUtils.set(data, "customerId", form.value.customer.id);
-            //     XEUtils.set(data, "childrenList", childrenList);
-            //     fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
-
-            //     isSaving.value = true;
-            //     API.production.plan[mode.value](data).then(res => {
-            //         ElMessage.success("操作成功");
-            //         isSaving.value = false;
-            //         visible.value = false;
-            //         $emit("success", mode.value);
-            //     }).catch(() => isSaving.value = false);
-            // }
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "bomList");
+                const bomList = XEUtils.map(form.value.bomList, item => ({ bomId: item.id, routeId: item.routeId, inspectProgramId: item.inspectProgramId, number: item.number }));
+                XEUtils.set(data, "bomList", bomList);
+
+                isSaving.value = true;
+                API.production.plan[mode.value](data).then(res => {
+                    ElMessage.success("操作成功");
+                    isSaving.value = false;
+                    visible.value = false;
+                    $emit("success", mode.value);
+                }).catch(() => isSaving.value = false);
+            }
         } else {
             return false;
         }
     });
 }
 
-const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
-fetchUser();
-
 defineExpose({
     open,
     setData
@@ -170,11 +152,10 @@ defineExpose({
 
 <style scoped>
 .el-form {padding-left: 16px;padding-right: 22px;}
-.el-form .el-input-number {width: 100%;}
-.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
-.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
-.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
-.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+.el-form .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--inner::placeholder) {color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
 
 .el-collapse {border: none;}
 .el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}

+ 47 - 20
src/views/production/plan/index.vue

@@ -7,16 +7,21 @@
                 <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
             </template>
             <template #order_link="{ row }">
-                <vxe-text status="primary" @click="table_order(row)">{{ row.saleOrder.code }}</vxe-text>
+                <vxe-text v-if="row.saleOrderId" status="primary" @click="table_order(row)">{{ row.saleOrder.code }}</vxe-text>
             </template>
 
             <template #action="{ row }">
-                <el-button type="primary" link @click="table_edit(row)">
-                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
-                </el-button>
-                <el-button type="primary" link @click="table_del(row)">
-                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
-                </el-button>
+                <template v-if="row.status === 'pending'">
+                    <el-button type="primary" link @click="table_production(row)">
+                        <template #icon><sc-iconify icon="material-symbols:inactive-order-outline"></sc-iconify></template>生成工单
+                    </el-button>
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                    <el-button v-if="!row.saleOrderId" type="primary" link @click="table_del(row)">
+                        <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                    </el-button>
+                </template>
             </template>
         </scTable>
 	</el-container>
@@ -24,6 +29,7 @@
     <plan-detail v-if="dialog.detail" ref="planRef" @success="refreshTable" @closed="dialog.detail = false"></plan-detail>
     <plan-desc v-if="dialog.desc" ref="planDescRef" @closed="dialog.desc = false"></plan-desc>
     <order-desc v-if="dialog.orderDesc" ref="orderDescRef" @closed="dialog.orderDesc = false"></order-desc>
+    <production-detail v-if="dialog.production" ref="productionRef" @success="refreshTable" @closed="dialog.production = false"></production-detail>
 </template>
 
 <script setup>
@@ -32,22 +38,31 @@ import XEUtils from "xe-utils";
 
 import API from "@/api";
 import TOOL from "@/utils/tool";
-import { salesDic } from "@/utils/basicDic";
+import { productionDic } from "@/utils/basicDic";
 import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+
+import orderDesc from "@/views/sales/order/desc";
+import productionDetail from "@/views/production/order/detail";
 import planDetail from "./detail";
 import planDesc from "./desc";
-import orderDesc from "@/views/sales/order/desc";
 
 import store from "@/store";
 watch(() => store.state.tenant.tenantId, () => refreshTable());
 
+const selectConfig = reactive({
+    options: productionDic.planStatus,
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
 const daterangeConfig = reactive({
     resetValue: () => [],
     props: {
         type: "daterange",
         startPlaceholder: "开始日期",
         endPlaceholder: "结束日期",
-        format: "YYYY-MM-DD"
+        valueFormat: "YYYY-MM-DD"
     }
 });
 
@@ -57,7 +72,9 @@ const formConfig = reactive({
         mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
         mapFormItemInput("nameLike", "计划主题"),
         mapFormItemInput("codeLike", "计划编号"),
-        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
+        mapFormItemSelect("status", "计划状态", selectConfig),
+        mapFormItemDatePicker("beginDate", "计划开工日期", daterangeConfig),
+        mapFormItemDatePicker("endDate", "计划完工日期", daterangeConfig)
     ]
 });
 
@@ -66,8 +83,11 @@ const paramsColums = reactive([
     { column: "tenantId" },
     { column: "nameLike" },
     { column: "codeLike" },
-    { column: "createTimeBegin", field: "createTime[0]" },
-    { column: "createTimeEnd", field: "createTime[1]" }
+    { column: "status" },
+    { column: "beginDateBegin", field: "beginDate[0]" },
+    { column: "beginDateEnd", field: "beginDate[1]" },
+    { column: "endDateBegin", field: "endDate[0]" },
+    { column: "endDateEnd", field: "endDate[1]" }
 ]);
 
 const columns = reactive([
@@ -75,12 +95,12 @@ const columns = reactive([
     { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
     { type: "html", field: "name", title: "计划主题", fixed: "left", minWidth: 150, sortable: true },
     { field: "code", title: "计划编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
-    { field: "orderCode", title: "来源单据", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "order_link" } },
-    { field: "status", title: "计划状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
-    { type: "html", field: "beginDate", title: "计划开日期", minWidth: 150, sortable: true },
-    { type: "html", field: "endDate", title: "计划结束日期", minWidth: 150, sortable: true },
-    { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
-    { title: "操作", fixed: "right", width: 140, slots: { default: "action" } }
+    { field: "orderCode", title: "来源单据", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "order_link" } },
+    { field: "status", title: "计划状态", minWidth: 120, editRender: { name: "$cell-tag", options: productionDic.planStatus } },
+    { type: "html", field: "beginDate", title: "计划开日期", minWidth: 150, sortable: true },
+    { type: "html", field: "endDate", title: "计划完工日期", minWidth: 150, sortable: true },
+    { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
 ]);
 
 // 显示隐藏 筛选表单
@@ -90,10 +110,12 @@ const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGrid
 const planRef = ref();
 const planDescRef = ref();
 const orderDescRef = ref();
+const productionRef = ref();
 const dialog = reactive({
     detail: false,
     desc: false,
-    orderDesc: false
+    orderDesc: false,
+    production: false
 });
 
 const table_add = () => {
@@ -116,6 +138,11 @@ const table_order = row => {
     nextTick(() => orderDescRef.value?.setData(row.saleOrder));
 }
 
+const table_production = row => {
+    dialog.production = true;
+    nextTick(() => productionRef.value?.setData(row));
+}
+
 const table_del = ({ id }) => {
     ElMessageBox.confirm("是否确认删除该生产计划?", "删除警告", {
         type: "warning",

+ 0 - 34
src/views/production/plan/main copy.js

@@ -1,34 +0,0 @@
-import XEUtils from "xe-utils"
-import { materialDic } from "@/utils/basicDic"
-
-export const tableOptions = reactive({
-    tableKey: "material",
-
-    // editDiasbled: { quantity: row => row.parentId !== "0" },
-    columns: [
-        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
-        // { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => !!row.children.length }, slots: { default: "seq_del" } },
-        { field: "code", title: "产品编码", fixed: "left", minWidth: 200 },
-        { field: "name", title: "产品名称", fixed: "left", minWidth: 150 },
-        { field: "bomList", title: "BOM清单", fixed: "left", minWidth: 150, slots: { default: "bom" } },
-        { field: "needType", title: "需求类型", fixed: "left", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(materialDic.needType, cellValue, cellValue) },
-        { field: "unit", title: "单位", minWidth: 120 },
-        { field: "quantity", title: "标准用量", minWidth: 100 },
-        { field: "stockNum", title: "库存明细(总/可用/锁定)", minWidth: 160, className: "vxe-table-edit-handler-cell", slots: { default: "stock" } },
-        { field: "stockUseNum", title: "仓库领用数量", minWidth: 120, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 }, slots: { edit: "stockUse" } },
-        { field: "allocateNum", title: "生产数量", minWidth: 120, editRender: { enabled: false, name: "VxeNumberInput", props: { controlConfig: { enabled: false } } } }
-    ],
-    editRules: {
-        stockUseNum: [{ required: true, message: "必须填写" }],
-        allocateNum: [{ required: true, message: "必须填写" }]
-    },
-
-    selectOptions: {
-        paramsColums: [
-            { column: "status", defaultValue: "enable" },
-            { column: "materialTypeIn", defaultValue: materialDic.typeRelation["outsourcing"] }
-        ]
-    },
-
-    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "tenantId", "code", "name", "needType", "quantity", "unit"))
-})

+ 8 - 10
src/views/production/plan/main.js

@@ -1,28 +1,26 @@
 import XEUtils from "xe-utils"
-import { materialDic } from "@/utils/basicDic"
 
 export const tableOptions = reactive({
     tableKey: "bom",
 
-    treeConfig: { transform: true, expandAll: true },
-    // editDiasbled: { quantity: row => row.parentId !== "0" },
     columns: [
-        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => !!row.children.length }, slots: { default: "seq_del" } },
-        { field: "material.code", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left" },
-        { field: "material.name", title: "产品名称", fixed: "left", minWidth: 150 },
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        { field: "materialCode", title: "产品编码", fixed: "left", minWidth: 180 },
+        { field: "materialName", title: "产品名称", fixed: "left", minWidth: 180 },
+        { field: "material.specification", title: "规格型号", minWidth: 150 },
         { field: "material.unit", title: "单位", minWidth: 120 },
-        { field: "material.needType", title: "需求类型", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(materialDic.needType, cellValue, cellValue) },
-        { field: "quantity", title: "单用量", minWidth: 100 }
+        { field: "number", title: "生产数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } }
     ],
     editRules: {
-        quantity: [{ required: true, message: "必须填写" }]
+        number: [{ required: true, message: "必须填写" }]
     },
 
     selectOptions: {
+        treeConfig: null,
         paramsColums: [
             { column: "status", defaultValue: "enable" }
         ]
     },
 
-    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "parentId", "material", "quantity"))
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, item => XEUtils.pick(item, "id", "materialCode", "materialName", "material", "routeId", "inspectProgramId"))
 })

+ 69 - 12
src/views/production/prePlan/detail.vue

@@ -7,8 +7,8 @@
                         <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="2" label-width="140" border>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.code }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.orderDate }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据日期" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.orderDate }}</el-descriptions-item>
                         <el-descriptions-item label="客户名称" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.customerName }}</el-descriptions-item>
                         <el-descriptions-item label="预计交期" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.planReceiveDate }}</el-descriptions-item>
                         <el-descriptions-item label="订单产品" :span="ismobile ? 2 : 1" label-align="right">
@@ -26,8 +26,13 @@
 
                     <el-row>
                         <el-col :md="8" :xs="24">
-                            <el-form-item label="计划周期" prop="planBeginDate" label-width="140" :rules="{ required: true, message: '请选择计划周期' }">
-                                <vxe-date-range-picker v-model:start-value="form.planBeginDate" v-model:end-value="form.planEndDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划周期"></vxe-date-range-picker>
+                            <el-form-item label="计划开始日期" prop="planBeginDate" label-width="140" :rules="{ required: true, message: '请选择计划开始日期' }">
+                                <vxe-date-picker v-model="form.planBeginDate" :end-date="form.planEndDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划开始日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划结束日期" prop="planEndDate" label-width="140" :rules="{ required: true, message: '请选择计划结束日期' }">
+                                <vxe-date-picker v-model="form.planEndDate" :start-date="form.planBeginDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划结束日期"></vxe-date-picker>
                             </el-form-item>
                         </el-col>
                     </el-row>
@@ -39,7 +44,7 @@
                     </template>
                     <el-row>
                         <el-col :md="8" :xs="24">
-                            <el-form-item :label="`${item.title}主题`" required>
+                            <el-form-item label="单据主题" required>
                                 <el-input v-model="form[`${item.fieldPrefix}Name`]" clearable placeholder="不填将自动生成"></el-input>
                             </el-form-item>
                         </el-col>
@@ -49,8 +54,10 @@
                             <template v-if="!row.disabled">
                                 <template v-if="!row.warehouse.length">无库存</template>
                                 <template v-else>
-                                    <span>{{ formatWarehouseCount(row.warehouse, "number") }}</span>/
-                                    <span>{{ formatWarehouseCount(row.warehouse, "normalNumber") }}</span>/
+                                    <span>{{ formatWarehouseCount(row.warehouse, "number") }}</span>
+                                    <span>/</span>
+                                    <span>{{ formatWarehouseCount(row.warehouse, "normalNumber") }}</span>
+                                    <span>/</span>
                                     <span>{{ formatWarehouseCount(row.warehouse, "lockedNumber") }}</span>
                                     <el-button type="primary" link @click="table_stock(name, row)">查看</el-button>
                                 </template>
@@ -58,7 +65,7 @@
                         </template>
                         <template #stockUse="{ row }">
                             <el-button type="primary" link @click="table_stock(name, row, 'stockUse')">分配</el-button>
-                            <el-button type="info" link :disabled="!row.stockUseNum || row.stockUseNum == 0" @click="table_clear(row)">清空</el-button>
+                            <el-button type="info" link :disabled="!row.stockUseNum || row.stockUseNum == 0" @click="table_clear(name, row)">清空</el-button>
                         </template>
                     </sc-form-table>
                 </el-collapse-item>
@@ -115,6 +122,50 @@ const form = reactive({
 
 const formatEditableCount = name => XEUtils.filter(XEUtils.toTreeArray(form[name]), item => item.material.needType == name).length;
 const formatWarehouseCount = (array, key) => XEUtils.sum(XEUtils.map(array, item => XEUtils.get(item, key)));
+// 仓库领用数变化
+const qtyLinkChange = {
+    // 子bom/采购/委外的需求数量、生产/采购/委外数量都会变化
+    self_made: function(data) {
+        XEUtils.eachTree(data.children, item => {
+            const parentAllocate = XEUtils.get(XEUtils.findTree(form.self_made, b => b.id == item.id), "parent.allocateNum");
+            item.quantity = XEUtils.multiply(parentAllocate, item.qty);
+            item.allocateNum = XEUtils.subtract(item.quantity, item.stockUseNum);
+        });
+
+        XEUtils.arrayEach(["outsourcing", "out_purchase"], key => {
+            const otherData = XEUtils.get(XEUtils.findTree(form[key], b => b.bomId == data.bomId), "item");
+            if (otherData) {
+                otherData.quantity = data.allocateNum;
+                this[key](otherData);
+            }
+        });
+    },
+    // 子bom/采购的需求数量、采购/委外数量都会变化
+    outsourcing: function(data) {
+        XEUtils.eachTree(data.children, item => {
+            const parentAllocate = XEUtils.get(XEUtils.findTree(form.outsourcing, b => b.id == item.id), "parent.allocateNum") || XEUtils.get(XEUtils.findTree(form.outsourcing, b => b.id == item.id), "parent.quantity");
+            item.quantity = XEUtils.multiply(parentAllocate, item.qty);
+            !item.disabled && (item.allocateNum = XEUtils.subtract(item.quantity, item.stockUseNum));
+        });
+
+        const otherData = XEUtils.get(XEUtils.findTree(form.out_purchase, b => b.bomId == data.bomId), "item");
+        if (otherData) {
+            otherData.quantity = data.allocateNum;
+            this.out_purchase(otherData);
+        }
+    },
+    out_purchase: function(data) {
+        XEUtils.eachTree(data.children, item => {
+            let parentAllocate = 0;
+            if (item.material.needType == "self_made") parentAllocate = XEUtils.get(XEUtils.findTree(form.self_made, b => b.bomId == item.bomId), "parent.allocateNum");
+            if (item.material.needType == "out_purchase") parentAllocate = XEUtils.get(XEUtils.findTree(form.out_purchase, b => b.id == item.id), "parent.quantity");
+            if (item.material.needType == "outsourcing") parentAllocate = XEUtils.divide(XEUtils.get(XEUtils.findTree(form.outsourcing, b => b.bomId == item.bomId), "item.allocateNum"), item.qty);
+            item.quantity = XEUtils.multiply(parentAllocate, item.qty);
+            !item.disabled && (item.allocateNum = XEUtils.subtract(item.quantity, item.stockUseNum));
+        });
+    }
+}
+
 const setData = data => {
     visible.value = true;
     XEUtils.objectEach(orderData, (_, key) => {
@@ -139,6 +190,7 @@ const setData = data => {
                         routeId: item.routeId,
                         material: item.material,
                         warehouse: item.warehouseMaterialList || [],
+                        qty: item.quantity,
                         quantity,
                         stockUseNum: disabled ? undefined: 0,
                         allocateNum: disabled ? undefined: quantity
@@ -170,12 +222,16 @@ const editSuccess = ({ table_Key, id, tableData }) => {
     treeItem.stockUseNum = XEUtils.sum(tableData, item => item.useNum);
     treeItem.allocateNum = XEUtils.subtract(treeItem.quantity, XEUtils.sum(tableData, item => item.useNum));
     treeItem.warehouse = tableData;
+
+    qtyLinkChange?.[table_Key]?.(treeItem);
 }
 
-const table_clear = row => {
+const table_clear = (table_Key, row) => {
     row.stockUseNum = 0;
     row.allocateNum = row.quantity;
     XEUtils.arrayEach(row.warehouse, item => item.useNum = undefined);
+
+    qtyLinkChange?.[table_Key]?.(row);
 }
 
 const formRef = ref();
@@ -228,9 +284,10 @@ defineExpose({
 .el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
 .el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row {margin-top: 20px;}
 .el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item {margin-bottom: 0;}
-.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
-.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker .vxe-date-range-picker--suffix {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
-.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker .vxe-date-range-picker--control-icon {padding-left: .5em;padding-right: 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-picker .vxe-date-picker--suffix {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-picker .vxe-date-picker--control-icon {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-picker .vxe-date-picker--inner::placeholder {color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
 .el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 260px;}
 .el-collapse-item :deep(.el-collapse-item__content) .order-product + .order-product {margin-top: 4px;padding-top: 4px;border-top: var(--el-descriptions-table-border);}
 </style>

+ 2 - 2
src/views/production/prePlan/index.vue

@@ -27,7 +27,7 @@ import store from "@/store";
 watch(() => store.state.tenant.tenantId, () => refreshTable());
 
 const customerConfig = reactive({
-    api: { key: "basic.customer", query: { orderBy: "createTime_desc", status: "enable" } },
+    api: { key: "basic.customer", query: { orderBy: "createTime_desc", customerType: "customer", status: "enable" } },
     optionProps: { label: "name", value: "id" },
     events: {
         change: data => XEUtils.merge(formConfig.data, data)
@@ -57,7 +57,7 @@ const formConfig = reactive({
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "orderDate_asc" },
+    { column: "orderBy", defaultValue: "orderDate_desc" },
     { column: "status", defaultValue: "pending" },
     { column: "tenantId" },
     { column: "codeLike" },

+ 5 - 5
src/views/production/prePlan/main.js

@@ -3,8 +3,8 @@ import { materialDic } from "@/utils/basicDic"
 
 const defaultColumns = [
     { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false },
-    { field: "material.code", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left" },
-    { field: "material.name", title: "产品名称", fixed: "left", minWidth: 150 },
+    { field: "material.code", title: "产品编码", fixed: "left", minWidth: 180, treeNode: true, headerAlign: "center", align: "left" },
+    { field: "material.name", title: "产品名称", fixed: "left", minWidth: 180 },
     { field: "material.unit", title: "单位", minWidth: 150 },
     { field: "quantity", title: "需求数量", minWidth: 100 },
     { field: "stockNum", title: "库存明细(总/可用/锁定)", minWidth: 160, className: "vxe-table-edit-handler-cell", slots: { default: "stock" } },
@@ -63,8 +63,8 @@ export const drawerDic = reactive({
             disabled: true,
             treeConfig: { expandAll: true },
             columns: [
-                { field: "material.code", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left" },
-                { field: "material.name", title: "产品名称", fixed: "left", minWidth: 150 },
+                { field: "material.code", title: "产品编码", fixed: "left", minWidth: 180, treeNode: true, headerAlign: "center", align: "left" },
+                { field: "material.name", title: "产品名称", fixed: "left", minWidth: 180 },
                 { field: "material.specification", title: "规格型号", minWidth: 150 },
                 { field: "material.unit", title: "单位", minWidth: 120 },
                 { field: "material.needType", title: "需求类型", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(materialDic.needType, cellValue, cellValue) },
@@ -94,7 +94,7 @@ export const drawerDic = reactive({
             columns: [
                 { field: "warehouse.name", title: "仓库名称", minWidth: 150, footerAlign: "right" },
                 { field: "normalNumber", title: "可用库存", minWidth: 100 },
-                { field: "useNum", title: "本次领用", minWidth: 120, editRender: { name: "VxeNumberInput", props: { min: 0, clearable: true, controlConfig: { enabled: false } }, defaultValue: 0 } }
+                { field: "useNum", title: "本次领用", minWidth: 120, editRender: { name: "VxeNumberInput", type: "float", props: { min: 0, clearable: true, controlConfig: { enabled: false } }, defaultValue: 0 } }
             ],
             footerField: [["warehouse.name", "useNum"]],
             mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 2 }]

+ 124 - 0
src/views/production/report/detail.vue

@@ -0,0 +1,124 @@
+<template>
+    <el-dialog v-model="visible" title="工序汇报" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="派工信息" name="basic">
+                </el-collapse-item>
+
+                <el-collapse-item title="汇报信息" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.bomList" v-bind="tableOptions"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "material", "other"]);
+
+const users = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    managerUserId: null,
+    beginDate: null,
+    endDate: null,
+    bomList: [{}],
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入计划主题" }],
+    beginDate: [{ required: true, message: "请选择计划周期" }],
+    managerUserId: [{ required: true, message: "请选择负责人" }]
+});
+
+const setData = data => {
+    visible.value = true;
+    // XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            // if (!form.value.bomList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            // if (await formTableRef.value.validateFormTable()) {
+            //     const data = XEUtils.omit(form.value, "customer", "bomList", "fileList");
+            //     const bomList = XEUtils.map(form.value.bomList, item => ({ materialCode: item.code, materialQuantity: item.quantity, materialPrice: item.price }));
+            //     const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "saleOrderAttach" }));
+            //     XEUtils.set(data, "customerId", form.value.customer.id);
+            //     XEUtils.set(data, "bomList", bomList);
+            //     fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+            //     isSaving.value = true;
+            //     API.production.plan[mode.value](data).then(res => {
+            //         ElMessage.success("操作成功");
+            //         isSaving.value = false;
+            //         visible.value = false;
+            //         $emit("success", mode.value);
+            //     }).catch(() => isSaving.value = false);
+            // }
+        } else {
+            return false;
+        }
+    });
+}
+
+const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 143 - 0
src/views/production/report/index.vue

@@ -0,0 +1,143 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+            <template #process_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">工序</vxe-text>
+                <div>
+                    <vxe-text status="primary" @click="table_detail(row)">分配</vxe-text>
+                    <vxe-text status="primary" @click="table_report(row)">汇报</vxe-text><!-- (报工)dialog(工序汇报) -->
+                </div>
+            </template>
+
+            <template #action>
+                <el-button type="primary" link @click="table_edit(row)">
+                    <!-- <template #icon><sc-iconify icon="mdi:transfer"></sc-iconify></template>送检 -->
+                    <!-- <template #icon><sc-iconify icon="mdi:transfer"></sc-iconify></template>入库申请 -->
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <report-detail v-if="dialog.report" ref="reportRef" @success="refreshTable" @closed="dialog.report = false"></report-detail>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { salesDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import reportDetail from "./detail";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "派工主题"),
+            mapFormItemInput("codeLike", "派工编号"),
+            mapFormItemDatePicker("planDate", "计划周期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "status", defaultValue: "pending" },
+        { column: "tenantId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "beginDateBegin", field: "planDate[0]" },
+        { column: "endDateEnd", field: "planDate[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "单据主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "派工产品", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { field: "process", title: "加工工序", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "process_link" } },
+        { field: "isReport", title: "是否汇报", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { type: "html", field: "customerName", title: "生产人员", minWidth: 150, sortable: true },
+        { type: "html", field: "num", title: "加工数量", minWidth: 150, sortable: true },
+        { type: "html", field: "num1", title: "已分配数量", minWidth: 150, sortable: true },
+        { type: "html", field: "num2", title: "未分配数量", minWidth: 150, sortable: true },
+        { type: "html", field: "num3", title: "合格数量", minWidth: 150, sortable: true },
+        { type: "html", field: "num4", title: "返工数量", minWidth: 150, sortable: true },
+        { type: "html", field: "num5", title: "报废数量", minWidth: 150, sortable: true },
+        { type: "html", field: "num6", title: "剩余数量", minWidth: 150, sortable: true },
+        { field: "status", title: "状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
+        { field: "status1", title: "质检状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
+        { type: "html", field: "beginDate", title: "计划开工日期", minWidth: 120, sortable: true },
+        { type: "html", field: "endDate", title: "计划完工日期", minWidth: 120, sortable: true },
+        { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { title: "操作", fixed: "right", width: 160, slots: { default: "action" } }
+    ],
+    options: {
+        data: [{}]
+    }
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const dispatchRef = ref();
+const reportRef = ref();
+const dispatchDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    report: false,
+    desc: false
+});
+
+const table_report = (row) => {
+    dialog.report = true;
+    nextTick(() => reportRef.value?.setData(row));
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => dispatchRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => dispatchDescRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该派工单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>

+ 32 - 0
src/views/production/report/main.js

@@ -0,0 +1,32 @@
+import XEUtils from "xe-utils"
+import { productionDic } from "@/utils/basicDic"
+
+function objectToArray(obj) {
+    return XEUtils.map(XEUtils.keys(obj), value => ({ label: XEUtils.get(obj, value), value }))
+}
+
+export const tableOptions = reactive({
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        { field: "code", title: "任务编号", fixed: "left", minWidth: 180, editRender: { name: "VxeInput", props: { clearable: true, placeholder: "" } } },
+        { field: "routeId", title: "生产人员", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, multiple: true, clearable: true }, optionProps: { label: "name", value: "id" } }, formatter: ({ cellValue, row, column }) => {
+            // cellValue ? XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == cellValue), "name", row.routeName) : "" 
+            }
+        },
+        { field: "deviceName", title: "设备", minWidth: 150 },
+        { field: "number", title: "合格数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "number1", title: "返工数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 } },
+        { field: "number2", title: "报废数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 } },
+        { field: "reason", title: "报废原因",  minWidth: 200, editRender: { name: "VxeInput", props: { clearable: true, placeholder: "" } } },
+        { field: "scrapType", title: "报废类型", minWidth: 150, editRender: { name: "VxeSelect", props: { clearable: true }, options: objectToArray(productionDic.scrapType) } },
+        { field: "isReport", title: "报废是否入库", minWidth: 100, cellRender: { name: "VxeCheckbox" } },
+        { field: "reworkMethod", title: "返工方式", minWidth: 150, editRender: { name: "VxeSelect", props: { clearable: true }, options: objectToArray(productionDic.reworkMethod) } },
+        { field: "endTime", title: "实际结束时间", minWidth: 150, editRender: { name: "VxeDatePicker", props: { type: "datetime" } } },
+        { field: "moveNum", title: "工时", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 0 } },
+        { field: "moveNumUnit", title: "工时单位", minWidth: 100 }, // 分钟
+        { field: "rule", title: "工资分摊规则", minWidth: 100 }, // 平分
+    ],
+    editRules: {
+        number: [{ required: true, message: "必须填写" }]
+    }
+})

+ 118 - 0
src/views/production/rework/index.vue

@@ -0,0 +1,118 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action>
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>按单汇报
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { salesDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "返工主题"),
+            mapFormItemInput("codeLike", "返工编号"),
+            mapFormItemDatePicker("planDate", "计划周期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "status", defaultValue: "pending" },
+        { column: "tenantId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "beginDateBegin", field: "planDate[0]" },
+        { column: "endDateEnd", field: "planDate[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "返工主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "返工编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { type: "html", field: "product", title: "返工产品", minWidth: 150, sortable: true },
+        { type: "html", field: "num", title: "返工数量", minWidth: 150, sortable: true },
+        { field: "status", title: "返工状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
+        { type: "html", field: "customerName", title: "被派人员", minWidth: 150, sortable: true },
+        { type: "html", field: "beginDate", title: "计划开工日期", minWidth: 120, sortable: true },
+        { type: "html", field: "endDate", title: "计划完工日期", minWidth: 120, sortable: true },
+        { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+    ]
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const dispatchRef = ref();
+const dispatchDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => dispatchRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => dispatchRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => dispatchDescRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该返工单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>

+ 151 - 0
src/views/purchase/inspection/entry.vue

@@ -0,0 +1,151 @@
+<template>
+    <el-dialog v-model="visible" title="入库申请" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-input v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="入库主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入入库主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="入库编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="申请时间" prop="applyDate">
+                                <el-date-picker v-model="form.applyDate" :clearable="false" value-format="YYYY-MM-DD" placeholder="请选择申请时间"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="入库仓库">
+                                <el-select v-model="form.warehouseId" placeholder="请选择入库仓库">
+                                    <el-option v-for="item in warehouses.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.name" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="入库信息" name="warehouse">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions['entry']"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "warehouse", "other"]);
+
+const warehouses = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    warehouseId: null,
+    applyDate: null,
+    childrenList: [{}],
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入入库主题" }],
+    applyDate: [{ required: true, message: "请选择申请时间" }]
+});
+
+const setData = data => {
+    visible.value = true;
+    // XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            // if (!form.value.bomList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            // if (await formTableRef.value.validateFormTable()) {
+            //     const data = XEUtils.omit(form.value, "customer", "bomList", "fileList");
+            //     const bomList = XEUtils.map(form.value.bomList, item => ({ materialCode: item.code, materialQuantity: item.quantity, materialPrice: item.price }));
+            //     const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "saleOrderAttach" }));
+            //     XEUtils.set(data, "customerId", form.value.customer.id);
+            //     XEUtils.set(data, "bomList", bomList);
+            //     fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+            //     isSaving.value = true;
+            //     API.production.plan[mode.value](data).then(res => {
+            //         ElMessage.success("操作成功");
+            //         isSaving.value = false;
+            //         visible.value = false;
+            //         $emit("success", mode.value);
+            //     }).catch(() => isSaving.value = false);
+            // }
+        } else {
+            return false;
+        }
+    });
+}
+
+// const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+// fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 107 - 0
src/views/purchase/inspection/index.vue

@@ -0,0 +1,107 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action>
+                <el-button type="primary" link @click="table_inspect(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>批量检/单检
+                </el-button>
+                <el-button type="primary" link @click="table_entry(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>入库申请
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <purchase-entry v-if="dialog.entry" ref="purchaseEntryRef" @success="refreshTable" @closed="dialog.entry = false"></purchase-entry>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { salesDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import purchaseEntry from "./entry";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "质检任务主题"),
+            mapFormItemInput("codeLike", "质检任务编号"),
+            mapFormItemDatePicker("createTime", "计划周期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "status", defaultValue: "pending" },
+        { column: "tenantId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "createTimeBegin", field: "createTime[0]" },
+        { column: "createTimeEnd", field: "createTime[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "质检任务主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "质检任务编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { type: "html", field: "product", title: "质检产品", minWidth: 150, sortable: true },
+        { type: "html", field: "num", title: "数量", minWidth: 150, sortable: true },
+        { field: "status", title: "质检状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
+        { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+    ]
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const purchaseRef = ref();
+const purchaseDescRef = ref();
+const purchaseEntryRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    entry: false
+});
+
+const table_inspect = row => {
+    dialog.detail = true;
+    nextTick(() => purchaseRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => purchaseDescRef.value?.setData(row));
+}
+
+const table_entry = row => {
+    dialog.entry = true;
+    nextTick(() => purchaseEntryRef.value?.setData(row));
+}
+</script>

+ 44 - 0
src/views/purchase/inspection/main.js

@@ -0,0 +1,44 @@
+import XEUtils from "xe-utils"
+import { productionDic } from "@/utils/basicDic"
+
+function objectToArray(obj) {
+    return XEUtils.map(XEUtils.keys(obj), value => ({ label: XEUtils.get(obj, value), value }))
+}
+
+export const tableOptions = reactive({
+    inspect: {
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "materialCode", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "materialName", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "material.specification", title: "规格型号", minWidth: 150 },
+            { field: "material.unit", title: "单位", minWidth: 120 },
+            { field: "number", title: "质检数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 1 } },
+            { field: "number1", title: "合格数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 } },
+            { field: "rate1", title: "合格率", minWidth: 100 },
+            { field: "number2", title: "不合格数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 } },
+            { field: "rate2", title: "不合格率", minWidth: 100 },
+            { field: "reason", title: "备注",  minWidth: 200, editRender: { name: "VxeInput", props: { clearable: true, placeholder: "" } } },
+            { field: "scrapType", title: "质检结果", minWidth: 150 }
+        ],
+        editRules: {
+            number: [{ required: true, message: "必须填写" }]
+        }
+    },
+
+    entry: {
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "materialCode", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "materialName", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "material.specification", title: "规格型号", minWidth: 150 },
+            { field: "material.unit", title: "单位", minWidth: 120 },
+            { field: "price", title: "单价", minWidth: 100 },
+            { field: "number", title: "数量", minWidth: 100 },
+            { field: "totalPrice", title: "总价", minWidth: 100 }
+        ],
+        editRules: {
+            number: [{ required: true, message: "必须填写" }]
+        }
+    }
+})

+ 108 - 0
src/views/purchase/order/desc.vue

@@ -0,0 +1,108 @@
+<template>
+    <el-dialog v-model="visible" title="采购订单详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item>
+                        <el-descriptions-item label="供应商名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.customerName }}</el-descriptions-item>
+                        <el-descriptions-item label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
+                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(purchaseDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="采购分类" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(purchaseDic.category, descData.category, descData.category) }}</el-descriptions-item>
+                        <el-descriptions-item label="采购人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.managerName }}</el-descriptions-item>
+                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="预计交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.planReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="实际交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.deliveryDate }}</el-descriptions-item>
+                        <el-descriptions-item label="收货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.actualReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="交货地址" label-align="right" :span="ismobile ? 3 : 1">{{ descData.deliveryAddress }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label="附件" label-align="right">
+                            <sc-upload-file v-model="descData.fileList" hideAdd disabled></sc-upload-file>
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table v-model="descData.childrenList" v-bind="tableOptions" disabled></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="金额信息" name="amount">
+                    <el-descriptions :column="2" label-width="140" border>
+                        <el-descriptions-item label="整单折扣额" label-align="right">{{ descData.freePrice }}</el-descriptions-item>
+                        <el-descriptions-item label="成交金额" label-align="right">{{ descData.actualPrice }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { purchaseDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+import store from "@/store";
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const activeNames = ref(["basic", "material", "amount"]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    orderDate: null,
+    customerName: null,
+    contractNo: null,
+    category: null,
+    managerName: null,
+    planReceiveDate: null,
+    actualReceiveDate: null,
+    deliveryDate: null,
+    deliveryAddress: null,
+    childrenList: [],
+    freePrice: null,
+    actualPrice: null,
+    remark: null,
+    fileList: [],
+    status: "pending",
+    createTime: null
+});
+
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "fileList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+        else if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, number: item.materialQuantity, price: item.materialPrice, isInspection: item.isInspection })));
+        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 265 - 0
src/views/purchase/order/detail.vue

@@ -0,0 +1,265 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed', isDel)">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id && !form.purchasePlanId" v-model="form.tenantId" filterable placeholder="请选择所属租户" @change="form.managerId = null">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入单据主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单据日期" prop="orderDate">
+                                <el-date-picker v-model="form.orderDate" :clearable="false" value-format="YYYY-MM-DD" placeholder="请选择单据日期"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="供应商" prop="supplier.id">
+                                <sc-table-input v-model="form.supplier" placeholder="选择供应商" v-bind="selectOptions"></sc-table-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="合同编号">
+                                <el-input v-model="form.contractNo" maxlength="50" show-word-limit placeholder="请输入合同编号"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="采购分类" prop="category">
+                                <el-select v-model="form.category" placeholder="请选择采购分类">
+                                    <el-option v-for="(label, key) in purchaseDic.category" :key="key" :label="label" :value="key"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="采购人员" prop="managerId">
+                                <el-select v-model="form.managerId" placeholder="请选择采购人员">
+                                    <el-option v-for="item in users.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.nickName" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="预计交期">
+                                <el-date-picker v-model="form.planReceiveDate" value-format="YYYY-MM-DD" placeholder="请选择预计交期"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="16" :xs="24">
+                            <el-form-item label="交货地址">
+                                <el-input v-model="form.deliveryAddress" type="textarea" maxlength="200" :rows="1" placeholder="请输入交货地址"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions">
+                        <template v-if="!!form.purchasePlanId" #top></template>
+                    </sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="金额信息" name="amount">
+                    <el-row>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="整单折扣额">
+                                <el-input-number v-model="form.freePrice" :min="0" :precision="2" :controls="false" clearable placeholder="请输入折扣金额">
+                                    <template #suffix>元</template>
+                                </el-input-number>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="采购金额" required>
+                                <el-input-number v-model="form.actualPrice" readonly :controls="false" placeholder="0">
+                                    <template #suffix>元</template>
+                                </el-input-number>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他信息" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :xs="24">
+                            <el-form-item label="附件" label-width="100">
+                                <sc-upload-file v-model="form.fileList" @removeSuccess="removeSuccess">
+                                    <vxe-button status="primary" size="mini" content="上传附件"></vxe-button>
+                                </sc-upload-file>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { purchaseDic } from "@/utils/basicDic";
+import { tableOptions, selectOptions } from "./main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+const isDel = ref(false);
+
+const activeNames = ref(["basic", "material", "amount", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增采购订单",
+    edit: "修改采购订单"
+});
+
+const users = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    purchasePlanId: null,
+    saleOrderId: null,
+    name: null,
+    code: null,
+    orderDate: moment().format("YYYY-MM-DD"),
+    supplier: { id: null, name: null },
+    contractNo: null,
+    category: null,
+    managerId: null,
+    planReceiveDate: null,
+    deliveryAddress: null,
+    childrenList: [],
+    freePrice: null,
+    incomePrice: null,
+    actualPrice: null,
+    remark: null,
+    fileList: []
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入单据主题" }],
+    orderDate: [{ required: true, message: "请选择单据日期" }],
+    "supplier.id": [{ required: true, message: "请选择供应商" }],
+    category: [{ required: true, message: "请选择采购分类" }],
+    managerId: [{ required: true, message: "请选择采购人员" }]
+});
+
+watch(() => XEUtils.pick(form.value, "childrenList", "freePrice"), value => {
+    if (!value.childrenList.length) form.value.incomePrice = null;
+    else {
+        form.value.incomePrice = XEUtils.sum(XEUtils.map(value.childrenList, item => XEUtils.multiply(item.number, item.price)));
+        form.value.actualPrice = XEUtils.subtract(form.value.incomePrice, value.freePrice);
+    }
+}, { deep: true });
+
+const setData = (data = {}, model = "add") => {
+    visible.value = true;
+    mode.value = model;
+
+    /**
+     * 从计划生成
+     * @childrenList purchasePlanId 判断整体除 price 外禁用
+    */ 
+    if (model === "add") {
+        if (!XEUtils.isEmpty(data)) {
+            const planData = {
+                tenantId: data.tenantId,
+                purchasePlanId: data.id,
+                saleOrderId: data.saleOrderId,
+                planReceiveDate: data.endDate,
+                childrenList: XEUtils.map(data.childrenList, item => ({ ...item.material, purchasePlanId: data.id, number: item.number, isInspection: true }))
+            }
+    
+            XEUtils.objectEach(form.value, (_, key) => XEUtils.has(planData, key) && XEUtils.set(form.value, key, XEUtils.get(planData, key)));
+        }
+    } else {
+        XEUtils.objectEach(form.value, (_, key) => {
+            if (key == "supplier") XEUtils.set(form.value, key, { id: XEUtils.get(data, "customerId"), name: XEUtils.get(data, "customerName") });
+            else if (key == "childrenList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, purchasePlanId: data.purchasePlanId, number: item.materialQuantity, price: item.materialPrice, isInspection: item.isInspection })));
+            else if (key == "fileList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+            else XEUtils.set(form.value, key, XEUtils.get(data, key));
+        });
+    }
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            if (!form.value.childrenList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "supplier", "childrenList", "fileList");
+                const childrenList = XEUtils.map(form.value.childrenList, item => ({ materialCode: item.code, materialQuantity: item.number, materialPrice: item.price, isInspection: item.isInspection }));
+                const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "purchaseOrderAttach" }));
+                XEUtils.set(data, "customerId", form.value.supplier.id);
+                XEUtils.set(data, "childrenList", childrenList);
+                fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+                isSaving.value = true;
+                API.purchase.order[mode.value](data).then(res => {
+                    ElMessage.success("操作成功");
+                    isSaving.value = false;
+                    isDel.value = false;
+                    visible.value = false;
+                    $emit("success", mode.value);
+                }).catch(() => isSaving.value = false);
+            }
+        } else {
+            return false;
+        }
+    });
+}
+
+const removeSuccess = () => form.value.id && (isDel.value = true);
+
+const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .el-input-number :deep(.el-input__suffix) {font-size: 12px;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 166 - 0
src/views/purchase/order/index.vue

@@ -0,0 +1,166 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.purchase.order" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <el-button v-if="row.status == 'processing'" type="primary" link @click="table_receipt(row)">
+                    <template #icon><sc-iconify icon="tabler:package-import"></sc-iconify></template>收货
+                </el-button>
+                <template v-if="row.status == 'pending'">
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                    <el-button type="primary" link @click="table_del(row)">
+                        <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                    </el-button>
+                </template>
+            </template>
+        </scTable>
+	</el-container>
+
+    <order-detail v-if="dialog.detail" ref="orderRef" @success="refreshTable" @closed="dialogClose"></order-detail>
+    <order-desc v-if="dialog.desc" ref="orderDescRef" @closed="dialog.desc = false"></order-desc>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { purchaseDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import orderDetail from "./detail";
+import orderDesc from "./desc";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const selectConfig = reactive({
+    options: purchaseDic.orderStatus,
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const customerConfig = reactive({
+    api: { key: "basic.customer", query: { orderBy: "createTime_desc", customerType: "supplier", status: "enable" } },
+    optionProps: { label: "name", value: "id" },
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "单据主题"),
+        mapFormItemInput("codeLike", "单据编号"),
+        mapFormItemSelect("status", "单据状态", selectConfig),
+        mapFormItemSelect("customerId", "供应商", customerConfig),
+        mapFormItemInput("contractNoLike", "合同编号"),
+        mapFormItemSelect("category", "采购分类", { ...selectConfig, options: purchaseDic.category }),
+        mapFormItemDatePicker("orderDate", "单据日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "status" },
+    { column: "customerId" },
+    { column: "contractNoLike" },
+    { column: "category" },
+    { column: "orderDateBegin", field: "orderDate[0]" },
+    { column: "orderDateEnd", field: "orderDate[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "单据主题", fixed: "left", minWidth: 150, sortable: true },
+    { field: "code", title: "单据编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    { type: "html", field: "orderDate", title: "单据日期", minWidth: 120, sortable: true },
+    { field: "status", title: "单据状态", minWidth: 120, editRender: { name: "$cell-tag", options: purchaseDic.orderStatus } },
+    { type: "html", field: "customerName", title: "供应商", minWidth: 150, sortable: true },
+    { type: "html", field: "contractNo", title: "合同编号", minWidth: 150, sortable: true },
+    { type: "html", field: "category", title: "采购分类", minWidth: 150, sortable: true, formatter: ({ cellValue }) => XEUtils.get(purchaseDic.category, cellValue, cellValue) },
+    { type: "html", field: "managerName", title: "采购人员", minWidth: 150, sortable: true },
+    { visible: false, type: "html", field: "freePrice", title: "整单折扣额", minWidth: 120, sortable: true },
+    { type: "html", field: "actualPrice", title: "采购金额", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.amountFormat(cellValue) || cellValue },
+    { type: "html", field: "planReceiveDate", title: "预计交期", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "deliveryDate", title: "实际交期", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "actualReceiveDate", title: "收货日期", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "deliveryAddress", title: "交货地址", minWidth: 300, sortable: true },
+    { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { visible: false, type: "html", field: "remark", title: "概要", minWidth: 300, sortable: true },
+    { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const orderRef = ref();
+const orderDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    receipt: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData(row, "edit"));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => orderDescRef.value?.setData(row));
+}
+
+const table_receipt = row => {
+    // 收货日期、是否需要质检
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该采购订单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.purchase.order.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+const dialogClose = isDel => {
+    dialog.detail = false;
+    isDel && refreshTable();
+}
+</script>

+ 53 - 0
src/views/purchase/order/main.js

@@ -0,0 +1,53 @@
+import XEUtils from "xe-utils"
+
+export const tableOptions = reactive({
+    tableKey: "material",
+
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => !!row.purchasePlanId }, slots: { default: "seq_del" } },
+        { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+        { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
+        { field: "specification", title: "规格型号", fixed: "left", minWidth: 150 },
+        { field: "unit", title: "单位", fixed: "left", minWidth: 120 },
+        { field: "number", title: "采购数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "price", title: "采购单价", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "isInspection", title: "是否质检", minWidth: 100, cellRender: { name: "VxeCheckbox" } }
+    ],
+    editDiasbled: { number: row => !!row.purchasePlanId },
+    editRules: {
+        number: [{ required: true, message: "必须填写" }],
+        price: [{ required: true, message: "必须填写" }]
+    },
+    footerField: [["number", "price"], ["number"]],
+    footerTitle: ["合计:", "订单金额:"],
+    footerMethod: [
+        (data, field) => XEUtils.sum(data, field),
+        (data, field) => XEUtils.sum(XEUtils.map(data, item => XEUtils.multiply(XEUtils.get(item, field), XEUtils.get(item, "price"))))
+    ],
+    mergeFooterItems: [
+        { row: 0, col: 0, rowspan: 1, colspan: 5 },
+        { row: 1, col: 0, rowspan: 1, colspan: 5 },
+        { row: 1, col: 5, rowspan: 1, colspan: 2 },
+        { row: 0, col: 7, rowspan: 1, colspan: 1 },
+        { row: 1, col: 7, rowspan: 1, colspan: 1 }
+    ],
+
+    selectOptions: {
+        paramsColums: [
+            { column: "status", defaultValue: "enable" },
+            { column: "needType", defaultValue: "out_purchase" }
+        ]
+    },
+
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, item => ({ ...XEUtils.pick(item, "id", "code", "name", "unit", "specification"), isInspection: true }))
+})
+
+export const selectOptions = reactive({
+    tableKey: "supplier",
+    valueKey: "name",
+    paramsColums: [
+        { column: "status", defaultValue: "enable" },
+        { column: "customerType", defaultValue: "supplier" },
+        { column: "typeIn", defaultValue: ["raw_material", "component", "mro", "capital"] }
+    ]
+})

+ 82 - 0
src/views/purchase/plan/desc.vue

@@ -0,0 +1,82 @@
+<template>
+    <el-dialog v-model="visible" title="采购计划详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="计划主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="计划编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="计划状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(productionDic.planStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="计划下单日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.beginDate }}</el-descriptions-item>
+                        <el-descriptions-item label="计划到货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.endDate }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item v-if="descData.saleOrderId" label="来源单据" label-align="right">{{ descData.saleOrder.code }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table v-model="descData.childrenList" v-bind="tableOptions" disabled></sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { productionDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+import store from "@/store";
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const activeNames = ref(["basic", "material"]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    saleOrderId: null,
+    saleOrder: null,
+    name: null,
+    code: null,
+    beginDate: null,
+    endDate: null,
+    childrenList: [],
+    remark: null,
+    status: "pending",
+    createTime: null
+});
+
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, number: item.number })));
+        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 156 - 0
src/views/purchase/plan/detail.vue

@@ -0,0 +1,156 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入计划主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划下单日期" prop="beginDate">
+                                <vxe-date-picker v-model="form.beginDate" :end-date="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划下单日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划到货日期" prop="endDate">
+                                <vxe-date-picker v-model="form.endDate" :start-date="form.beginDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划到货日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" :disabled="!!form.saleOrderId" v-bind="tableOptions"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "material", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增采购计划",
+    edit: "修改采购计划"
+});
+
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    saleOrderId: null,
+    name: null,
+    code: null,
+    beginDate: null,
+    endDate: null,
+    childrenList: [],
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入计划主题" }],
+    beginDate: [{ required: true, message: "请选择计划下单日期" }],
+    endDate: [{ required: true, message: "请选择计划到货日期" }]
+});
+
+const open = () => visible.value = true;
+const setData = data => {
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "childrenList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, number: item.number })));
+        else XEUtils.set(form.value, key, XEUtils.get(data, key));
+    });
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            if (!form.value.childrenList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "childrenList");
+                const childrenList = XEUtils.map(form.value.childrenList, item => ({ materialCode: item.code, number: item.number }));
+                XEUtils.set(data, "childrenList", childrenList);
+
+                isSaving.value = true;
+                API.purchase.plan[mode.value](data).then(res => {
+                    ElMessage.success("操作成功");
+                    isSaving.value = false;
+                    visible.value = false;
+                    $emit("success", mode.value);
+                }).catch(() => isSaving.value = false);
+            }
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--inner::placeholder) {color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 158 - 0
src/views/purchase/plan/index.vue

@@ -0,0 +1,158 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.purchase.plan" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+            <template #order_link="{ row }">
+                <vxe-text v-if="row.saleOrderId" status="primary" @click="table_order(row)">{{ row.saleOrder.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <template v-if="row.status === 'pending'">
+                    <el-button type="primary" link @click="table_purchase(row)">
+                        <template #icon><sc-iconify icon="material-symbols:inactive-order-outline"></sc-iconify></template>生成采购单
+                    </el-button>
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                    <el-button v-if="!row.saleOrderId" type="primary" link @click="table_del(row)">
+                        <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                    </el-button>
+                </template>
+            </template>
+        </scTable>
+	</el-container>
+
+    <plan-detail v-if="dialog.detail" ref="planRef" @success="refreshTable" @closed="dialog.detail = false"></plan-detail>
+    <plan-desc v-if="dialog.desc" ref="planDescRef" @closed="dialog.desc = false"></plan-desc>
+    <order-desc v-if="dialog.orderDesc" ref="orderDescRef" @closed="dialog.orderDesc = false"></order-desc>
+    <purchase-detail v-if="dialog.purchase" ref="purchaseRef" @success="refreshTable" @closed="dialog.purchase = false"></purchase-detail>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { productionDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemDatePicker, mapFormItemSelect, mapFormItemTenant } from "@/components/scTable/helper";
+
+import orderDesc from "@/views/sales/order/desc";
+import purchaseDetail from "@/views/purchase/order/detail";
+import planDetail from "./detail";
+import planDesc from "./desc";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const selectConfig = reactive({
+    options: productionDic.planStatus,
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "计划主题"),
+        mapFormItemInput("codeLike", "计划编号"),
+        mapFormItemSelect("status", "计划状态", selectConfig),
+        mapFormItemDatePicker("beginDate", "计划下单日期", daterangeConfig),
+        mapFormItemDatePicker("endDate", "计划到货日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "status" },
+    { column: "beginDateBegin", field: "beginDate[0]" },
+    { column: "beginDateEnd", field: "beginDate[1]" },
+    { column: "endDateBegin", field: "endDate[0]" },
+    { column: "endDateEnd", field: "endDate[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "计划主题", fixed: "left", minWidth: 150, sortable: true },
+    { field: "code", title: "计划编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    { field: "orderCode", title: "来源单据", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "order_link" } },
+    { field: "status", title: "计划状态", minWidth: 120, editRender: { name: "$cell-tag", options: productionDic.planStatus } },
+    { type: "html", field: "beginDate", title: "计划下单日期", minWidth: 150, sortable: true },
+    { type: "html", field: "endDate", title: "计划到货日期", minWidth: 150, sortable: true },
+    { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { title: "操作", fixed: "right", width: 240, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const planRef = ref();
+const planDescRef = ref();
+const orderDescRef = ref();
+const purchaseRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    orderDesc: false,
+    purchase: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => planRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => planRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => planDescRef.value?.setData(row));
+}
+
+const table_order = row => {
+    dialog.orderDesc = true;
+    nextTick(() => orderDescRef.value?.setData(row.saleOrder));
+}
+
+const table_purchase = row => {
+    dialog.purchase = true;
+    nextTick(() => purchaseRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该采购计划?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.purchase.plan.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 27 - 0
src/views/purchase/plan/main.js

@@ -0,0 +1,27 @@
+import XEUtils from "xe-utils"
+
+export const tableOptions = reactive({
+    tableKey: "material",
+
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+        { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
+        { field: "specification", title: "规格型号", minWidth: 150 },
+        { field: "unit", title: "单位", minWidth: 120 },
+        { field: "number", title: "采购数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        // 单独的预计交期
+    ],
+    editRules: {
+        number: [{ required: true, message: "必须填写" }]
+    },
+
+    selectOptions: {
+        paramsColums: [
+            { column: "status", defaultValue: "enable" },
+            { column: "needType", defaultValue: "out_purchase" }
+        ]
+    },
+
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, item => XEUtils.pick(item, "id", "code", "name", "unit", "specification"))
+})

+ 4 - 4
src/views/sales/order/desc.vue

@@ -7,9 +7,9 @@
                         <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item>
                         <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(salesDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
                         <el-descriptions-item label="客户名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.customerName }}</el-descriptions-item>
                         <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
@@ -81,7 +81,7 @@ const setData = data => {
     visible.value = true;
     XEUtils.objectEach(descData.value, (_, key) => {
         if (key == "fileList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
-        else if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, quantity: item.materialQuantity, price: item.materialPrice  })));
+        else if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, quantity: item.materialQuantity, price: item.materialPrice })));
         else XEUtils.set(descData.value, key, XEUtils.get(data, key));
     });
 }

+ 10 - 8
src/views/sales/order/index.vue

@@ -8,12 +8,14 @@
             </template>
             
             <template #action="{ row }">
-                <el-button v-if="row.status == 'pending'" type="primary" link @click="table_edit(row)">
-                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
-                </el-button>
-                <el-button type="primary" link @click="table_del(row)">
-                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
-                </el-button>
+                <template v-if="row.status == 'pending'">
+                    <el-button type="primary" link @click="table_edit(row)">
+                        <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                    </el-button>
+                    <el-button type="primary" link @click="table_del(row)">
+                        <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                    </el-button>
+                </template>
             </template>
         </scTable>
 	</el-container>
@@ -44,7 +46,7 @@ const selectConfig = reactive({
 });
 
 const customerConfig = reactive({
-    api: { key: "basic.customer", query: { orderBy: "createTime_desc", status: "enable" } },
+    api: { key: "basic.customer", query: { orderBy: "createTime_desc", customerType: "customer", status: "enable" } },
     optionProps: { label: "name", value: "id" },
     events: {
         change: data => XEUtils.merge(formConfig.data, data)
@@ -74,7 +76,7 @@ const formConfig = reactive({
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "orderDate_asc" },
+    { column: "orderBy", defaultValue: "orderDate_desc" },
     { column: "tenantId" },
     { column: "codeLike" },
     { column: "status" },

+ 8 - 5
src/views/sales/order/main.js

@@ -7,11 +7,11 @@ export const tableOptions = reactive({
 
     columns: [
         { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
-        { field: "code", title: "产品编码", fixed: "left", minWidth: 150 },
-        { field: "name", title: "产品名称", fixed: "left", minWidth: 150 },
+        { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+        { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
         { field: "unit", title: "单位", minWidth: 150 },
-        { field: "quantity", title: "数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
-        { field: "price", title: "单价", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } }
+        { field: "quantity", title: "销售数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "price", title: "销售单价", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } }
     ],
     editRules: {
         quantity: [{ required: true, message: "必须填写" }],
@@ -44,5 +44,8 @@ export const tableOptions = reactive({
 export const selectOptions = reactive({
     tableKey: "customer",
     valueKey: "name",
-    paramsColums: [{ column: "status", defaultValue: "enable" }]
+    paramsColums: [
+        { column: "status", defaultValue: "enable" },
+        { column: "customerType", defaultValue: "customer" }
+    ]    
 })

+ 1 - 1
src/views/sales/performance/components/line.vue

@@ -83,7 +83,7 @@ getEcharts();
 .el-header :deep(.vxe-date-range-picker) {flex-direction: row-reverse;}
 .el-header :deep(.vxe-date-range-picker) .vxe-date-range-picker--suffix {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
 .el-header :deep(.vxe-date-range-picker) .vxe-date-range-picker--inner {text-align: center;}
-.el-header :deep(.vxe-date-range-picker) .vxe-date-range-picker--control-icon {padding-left: .5em;padding-right: 0;}
+.el-header :deep(.vxe-date-range-picker) .vxe-date-range-picker--control-icon {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
 
 .echart-panel {flex: 1;}
 </style>

+ 1 - 1
src/views/sales/plan/detail.vue

@@ -176,7 +176,7 @@ defineExpose({
     
 .el-form .vxe-date-picker {width: 100%;flex-direction: row-reverse;}
 .el-form .vxe-date-picker :deep(.vxe-date-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
-.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
 
 .el-form .el-input-number {width: 100%;}
 .el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}

+ 6 - 6
src/views/sales/plan/index.vue

@@ -36,7 +36,7 @@ watch(() => store.state.tenant.tenantId, () => refreshTable());
 const formatStatus = row => {
     if (moment().isBefore(row.beginDate)) return "pending";
     if (moment().isAfter(row.endDate)) return "finished";
-    return "executing";
+    return "processing";
 }
 
 const selectConfig = reactive({
@@ -63,25 +63,25 @@ const formConfig = reactive({
         mapFormItemInput("nameLike", "计划名称"),
         mapFormItemInput("codeLike", "计划编号"),
         mapFormItemSelect("type", "计划类型", selectConfig),
-        mapFormItemDatePicker("beginDate", "计划开始日期", daterangeConfig)
+        mapFormItemDatePicker("planDate", "计划周期", daterangeConfig)
     ]
 });
 
 const options = reactive({
     paramsColums: [
-        { column: "orderBy", defaultValue: "beginDate_asc" },
+        { column: "orderBy", defaultValue: "beginDate_desc" },
         { column: "tenantId" },
         { column: "nameLike" },
         { column: "codeLike" },
         { column: "type" },
-        { column: "beginDateBegin", field: "beginDate[0]" },
-        { column: "beginDateEnd", field: "beginDate[1]" }
+        { column: "beginDateBegin", field: "planDate[0]" },
+        { column: "endDateEnd", field: "planDate[1]" }
     ],
     treeConfig: { transform: true },
     pagerConfig : { enabled: false },
     columns: [
         { type: "seq", fixed: "left", width: 80 },
-        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => row.parentId === "0" ? cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") : "" },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
         { type: "html", field: "name", title: "计划名称", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left", sortable: true },
         { type: "html", field: "code", title: "计划编号", fixed: "left", minWidth: 150, sortable: true },
         { type: "html", field: "type", title: "计划类型", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(salesDic.planType, cellValue, cellValue) },

+ 167 - 0
src/views/warehouse/inbound/confirm.vue

@@ -0,0 +1,167 @@
+<template>
+    <el-dialog v-model="visible" title="入库确认" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-input v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="确认结果" prop="result">
+                                <el-radio-group v-model="form.result">
+                                    <el-radio label="同意" value="approve"></el-radio>
+                                    <el-radio label="否决" value="reject"></el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="确认时间" prop="applyDate">
+                                <el-date-picker v-model="form.applyDate" :clearable="false" value-format="YYYY-MM-DD" placeholder="请选择申请时间"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="入库仓库">
+                                <el-select v-model="form.warehouseId" placeholder="请选择入库仓库">
+                                    <el-option v-for="item in warehouses.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.name" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="16" :xs="24">
+                            <el-form-item label="库管意见">
+                                <el-input v-model="form.comments" type="textarea" maxlength="200" :rows="1" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <!-- 原材料入库称重、成品出库过磅等业务流程中 -->
+                            <el-form-item label="磅单编号">
+                                <el-input v-model="tenantName" placeholder="请输入磅单编号"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <!-- 运输车辆编号 -->
+                            <el-form-item label="入库车号">
+                                <el-input v-model="tenantName" placeholder="请输入入库车号"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="入库信息" name="warehouse">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions[form.type]"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "@/views/purchase/inspection/main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "warehouse", "other"]);
+
+const warehouses = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    type: "purchase",
+    result: "approve",
+    comments: null,
+    warehouseId: null,
+    applyDate: null,
+    childrenList: [{}],
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入入库主题" }],
+    applyDate: [{ required: true, message: "请选择申请时间" }]
+});
+
+const setData = data => {
+    visible.value = true;
+    // XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            // if (!form.value.bomList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            // if (await formTableRef.value.validateFormTable()) {
+            //     const data = XEUtils.omit(form.value, "customer", "bomList", "fileList");
+            //     const bomList = XEUtils.map(form.value.bomList, item => ({ materialCode: item.code, materialQuantity: item.quantity, materialPrice: item.price }));
+            //     const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "saleOrderAttach" }));
+            //     XEUtils.set(data, "customerId", form.value.customer.id);
+            //     XEUtils.set(data, "bomList", bomList);
+            //     fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+            //     isSaving.value = true;
+            //     API.production.plan[mode.value](data).then(res => {
+            //         ElMessage.success("操作成功");
+            //         isSaving.value = false;
+            //         visible.value = false;
+            //         $emit("success", mode.value);
+            //     }).catch(() => isSaving.value = false);
+            // }
+        } else {
+            return false;
+        }
+    });
+}
+
+// const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+// fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 106 - 0
src/views/warehouse/inbound/detail.vue

@@ -0,0 +1,106 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_freeze(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>冻结
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+
+import store from "@/store";
+const route = useRoute();
+console.log('route',route)
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { enabled: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "产品名称"),
+            mapFormItemInput("codeLike", "产品编号"),
+            mapFormItemDatePicker("confirmDate", "确认日期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "confirmDateBegin", field: "confirmDate[0]" },
+        { column: "confirmDateEnd", field: "confirmDate[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { type: "html", field: "warehouseName", title: "仓库", fixed: "left", minWidth: 150, sortable: true },
+        { type: "html", field: "batchNumber", title: "批号", minWidth: 150, sortable: true },
+        { type: "html", field: "serialNumber", title: "序列号", minWidth: 150, sortable: true },
+        { type: "html", field: "productionDate", title: "生产日期", minWidth: 150, sortable: true },
+        { type: "html", field: "validityDate", title: "有效日期", minWidth: 150, sortable: true },
+        { type: "html", field: "num", title: "数量", minWidth: 150 },
+        { type: "html", field: "storageDate", title: "入库日期", minWidth: 150, sortable: true },
+        { field: "code", title: "关联入库单", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { title: "操作", fixed: "right", width: 120, slots: { default: "action" } }
+    ]
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const dispatchRef = ref();
+const dispatchDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false
+});
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => dispatchDescRef.value?.setData(row));
+}
+
+const table_freeze = ({ id }) => {
+    ElMessageBox.confirm("是否确认冻结该库存?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>
+
+<style scoped>
+.el-descriptions {padding-left: 10px;}
+.el-descriptions :deep(.el-descriptions__cell) {width: calc(100% / 4);}
+</style>

+ 134 - 0
src/views/warehouse/inbound/index.vue

@@ -0,0 +1,134 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action>
+                <el-button type="primary" link @click="$router.push({ path: `${$route.fullPath}/detail`, query: row })">
+                    <template #icon><sc-iconify icon="ant-design:unordered-list-outlined"></sc-iconify></template>查看详情
+                </el-button>
+                <el-button type="primary" link @click="table_inbound(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>入库
+                </el-button>
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { salesDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "入库主题"),
+            mapFormItemInput("codeLike", "入库编号"),
+            mapFormItemDatePicker("applyDate", "申请日期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "status", defaultValue: "pending" },
+        { column: "tenantId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "applyDateBegin", field: "applyDate[0]" },
+        { column: "applyDateEnd", field: "applyDate[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "入库主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "入库编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { type: "html", field: "num", title: "数量", minWidth: 150, sortable: true },
+        { type: "html", field: "type", title: "入库类别", minWidth: 120, sortable: true, formatter: ({ cellValue }) => cellValue },
+        { field: "status", title: "入库状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
+        // { type: "html", field: "warehouseName", title: "入库仓库", minWidth: 120, sortable: true, formatter: ({ cellValue }) => cellValue },
+        { visible: false, type: "html", field: "applicant", title: "申请人员", minWidth: 120, sortable: true },
+        { type: "html", field: "applyDate", title: "申请日期", minWidth: 120, sortable: true },
+        { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { title: "操作", fixed: "right", width: 280, slots: { default: "action" } }
+    ],
+    options: {
+        data: [{}]
+    }
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const confirmRef = ref();
+const inboundRef = ref();
+const inboundDescRef = ref();
+const dialog = reactive({
+    inbound: false,
+    detail: false,
+    desc: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => inboundRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => inboundRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => inboundDescRef.value?.setData(row));
+}
+
+const table_inbound = () => {
+    dialog.inbound = true;
+    nextTick(() => confirmRef.value?.open());
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该入库单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>

+ 53 - 0
src/views/warehouse/inbound/main.js

@@ -0,0 +1,53 @@
+import XEUtils from "xe-utils"
+import { productionDic } from "@/utils/basicDic"
+
+function objectToArray(obj) {
+    return XEUtils.map(XEUtils.keys(obj), value => ({ label: XEUtils.get(obj, value), value }))
+}
+
+export const tableOptions = reactive({
+    production: {
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "materialCode", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "materialName", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "material.specification", title: "规格型号", minWidth: 150 },
+            { field: "material.unit", title: "单位", minWidth: 120 },
+            { field: "number", title: "质检数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 1 } },
+            { field: "number1", title: "合格数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 } },
+            { field: "rate1", title: "合格率", minWidth: 100 },
+            { field: "number2", title: "不合格数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 } },
+            { field: "rate2", title: "不合格率", minWidth: 100 },
+            { field: "reason", title: "备注",  minWidth: 200, editRender: { name: "VxeInput", props: { clearable: true, placeholder: "" } } },
+            { field: "scrapType", title: "质检结果", minWidth: 150 }
+        ],
+        editRules: {
+            number: [{ required: true, message: "必须填写" }]
+        }
+    },
+
+    purchase: {
+        columns: [
+            { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+            { field: "materialCode", title: "产品编码", fixed: "left", minWidth: 180 },
+            { field: "materialName", title: "产品名称", fixed: "left", minWidth: 180 },
+            { field: "material.specification", title: "规格型号", minWidth: 150 },
+            { field: "material.unit", title: "单位", minWidth: 120 },
+            { field: "number", title: "数量", minWidth: 100 },
+            { field: "warehouse", title: "仓库", minWidth: 100 },
+            { field: "remaining", title: "剩余容量", minWidth: 100 },
+            { field: "batchNumber", title: "批号", minWidth: 100 },
+            { field: "serialNumber", title: "序列号", minWidth: 100 },
+            { field: "productionDate", title: "生产日期", minWidth: 100 },
+            { field: "validityDate", title: "有效日期", minWidth: 100 },
+            { field: "arrivalDate", title: "到货日期", minWidth: 100 },
+        ],
+        editRules: {
+            number: [{ required: true, message: "必须填写" }]
+        }
+    },
+
+    outsourcing: {
+
+    }
+})

+ 129 - 0
src/views/warehouse/inventory/detail.vue

@@ -0,0 +1,129 @@
+<template>
+    <el-dialog v-model="visible" title="库存明细" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="2" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="产品名称" label-align="right">{{ descData.materialName }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="产品编号" label-align="right">{{ descData.materialCode }}</el-descriptions-item>
+                        <el-descriptions-item label="规格型号" label-align="right">{{ descData.specification }}</el-descriptions-item>
+                        <el-descriptions-item label="单位" label-align="right">{{ descData.unit }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="入库明细" name="inbound">
+                    <scTable ref="xGridTable" v-bind="xGridOptions">
+                        <template #code_link="{ row }">
+                            <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+                        </template>
+
+                        <template #action="{ row }">
+                            <el-button type="primary" link @click="table_freeze(row)">
+                                <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>冻结
+                            </el-button>
+                        </template>
+                    </scTable>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import store from "@/store";
+import { mapFormItemInput, mapFormItemSelect } from "@/components/scTable/helper";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+const activeNames = ref(["basic", "inbound"]);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+const descData = ref({
+    tenantId: null,
+    materialName: null,
+    materialCode: null,
+    specification: null,
+    unit: null
+});
+
+const setData = async data => {
+    // const res = await API.warehouse.requisition.getStock({ requisitionId: data.id });
+    XEUtils.objectEach(descData.value, (_, key) => {
+        XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+
+    visible.value = true;
+}
+
+const selectConfig = reactive({
+    optionProps: { label: "name", value: "id" },
+    events: {
+        change: data => XEUtils.merge(xGridOptions.formConfig.data, data)
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { enabled: false },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { type: "html", field: "warehouseName", title: "仓库", fixed: "left", minWidth: 150, sortable: true },
+        { type: "html", field: "batchNumber", title: "批号", minWidth: 150, sortable: true },
+        { type: "html", field: "serialNumber", title: "序列号", minWidth: 150, sortable: true },
+        { type: "html", field: "productionDate", title: "生产日期", minWidth: 150, sortable: true },
+        { type: "html", field: "validityDate", title: "有效日期", minWidth: 150, sortable: true },
+        { type: "html", field: "num", title: "数量", minWidth: 150 },
+        { type: "html", field: "storageDate", title: "入库日期", minWidth: 150, sortable: true },
+        { field: "code", title: "关联入库单", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { title: "操作", fixed: "right", width: 120, slots: { default: "action" } }
+    ]
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const table_detail = row => {
+    // dialog.desc = true;
+    // nextTick(() => dispatchDescRef.value?.setData(row));
+}
+
+const table_freeze = ({ id }) => {
+    ElMessageBox.confirm("是否确认冻结该库存?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 82 - 0
src/views/warehouse/inventory/index.vue

@@ -0,0 +1,82 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_detail(row)">
+                    <template #icon><sc-iconify icon="ant-design:unordered-list-outlined"></sc-iconify></template>查看详情
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <inventory-desc v-if="dialog" ref="inventoryRef" @closed="dialog = false"></inventory-desc>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemInput, mapFormItemSelect } from "@/components/scTable/helper";
+import inventoryDesc from "./detail";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const selectConfig = reactive({
+    optionProps: { label: "name", value: "id" },
+    events: {
+        change: data => XEUtils.merge(xGridOptions.formConfig.data, data)
+    }
+});
+
+const xGridOptions = reactive({
+    apiObj: API.warehouse.inventory,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "产品名称"),
+            mapFormItemInput("codeLike", "产品编号"),
+            mapFormItemSelect("warehouseId", "仓库", selectConfig),
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "tenantId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "materialName", title: "产品名称", fixed: "left", minWidth: 150, sortable: true },
+        { field: "materialCode", title: "产品编号", fixed: "left", minWidth: 150, sortable: true },
+        { type: "html", field: "specification", title: "规格型号", minWidth: 120 },
+        { type: "html", field: "unit", title: "单位", minWidth: 100 },
+        { type: "html", field: "number", title: "库存数量", minWidth: 150 },
+        { type: "html", field: "num1", title: "车间剩余数量", titleSuffix: { content: "车间剩余数量为生产领料剩余数量" }, minWidth: 150 },
+        { type: "html", field: "lockedNumber", title: "预定数量", minWidth: 150 }, // 生产
+        { type: "html", field: "num3", title: "在途数量", minWidth: 150 }, // 采购
+        { type: "html", field: "frozenNumber", title: "冻结数量", minWidth: 150 }, // 详情冻结
+        { type: "html", field: "normalNumber", title: "可用数量", minWidth: 150 },
+        { type: "html", field: "warehouseName", title: "仓库", minWidth: 150, sortable: true },
+        { title: "操作", fixed: "right", width: 120, slots: { default: "action" } }
+    ]
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const inventoryRef = ref();
+const dialog = ref(false);
+
+const table_detail = row => {
+    dialog.value = true;
+    nextTick(() => inventoryRef.value?.setData(row));
+}
+</script>

+ 14 - 0
src/views/warehouse/inventory/main.js

@@ -0,0 +1,14 @@
+export const tableOptions = reactive({
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        { field: "warehouseName", title: "仓库", fixed: "left", minWidth: 180 },
+        { field: "batchNumber", title: "批号", fixed: "left", minWidth: 150 },
+        { field: "serialNumber", title: "序列号", fixed: "left", minWidth: 150 },
+        { field: "productionDate", title: "生产日期", minWidth: 120 },
+        { field: "validityDate", title: "有效日期", minWidth: 120 },
+        { field: "number", title: "数量", minWidth: 120 },
+        { field: "storageDate", title: "入库日期", minWidth: 120 },
+        { field: "code", title: "关联入库单", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { title: "操作", fixed: "right", width: 120, slots: { default: "action" } }
+    ],
+})

+ 149 - 0
src/views/warehouse/outbound/detail.vue

@@ -0,0 +1,149 @@
+<template>
+    <el-dialog v-model="visible" title="出库确认" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-input v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                       <el-col :md="8" :xs="24">
+                            <el-form-item label="出库主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入领料主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="出库编号" required>
+                                <el-input v-model="form.code" readonly show-word-limit></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="出库日期" prop="applyDate">
+                                <el-date-picker v-model="form.applyDate" :clearable="false" value-format="YYYY-MM-DD" placeholder="请选择申请时间"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="16" :xs="24">
+                            <el-form-item label="客户名称">
+                                <el-input v-model="form.customerName" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品明细" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions[form.type]"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "material", "other"]);
+
+const warehouses = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    type: "direct",
+    beginDate: null,
+    childrenList: [{}],
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入出库主题" }],
+    applyDate: [{ required: true, message: "请选择申请时间" }]
+});
+
+const setData = data => {
+    visible.value = true;
+    // XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            // if (!form.value.bomList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            // if (await formTableRef.value.validateFormTable()) {
+            //     const data = XEUtils.omit(form.value, "customer", "bomList", "fileList");
+            //     const bomList = XEUtils.map(form.value.bomList, item => ({ materialCode: item.code, materialQuantity: item.quantity, materialPrice: item.price }));
+            //     const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "saleOrderAttach" }));
+            //     XEUtils.set(data, "customerId", form.value.customer.id);
+            //     XEUtils.set(data, "bomList", bomList);
+            //     fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+            //     isSaving.value = true;
+            //     API.production.plan[mode.value](data).then(res => {
+            //         ElMessage.success("操作成功");
+            //         isSaving.value = false;
+            //         visible.value = false;
+            //         $emit("success", mode.value);
+            //     }).catch(() => isSaving.value = false);
+            // }
+        } else {
+            return false;
+        }
+    });
+}
+
+// const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+// fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 151 - 0
src/views/warehouse/outbound/index.vue

@@ -0,0 +1,151 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action>
+                <el-button type="primary" link @click="table_list(row)">
+                    <template #icon><sc-iconify icon="ant-design:unordered-list-outlined"></sc-iconify></template>出库明细
+                </el-button>
+                <el-button type="primary" link @click="table_outbound(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>出库
+                </el-button>
+                <!-- 发货 -->
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <outbound-detail v-if="dialog.detail" ref="outboundRef" @closed="dialog.detail = false"></outbound-detail>
+    <outbound-list v-if="dialog.list" ref="outboundListRef" @closed="dialog.list = false"></outbound-list>
+    <outbound-desc v-if="dialog.desc" ref="outboundDescRef" @closed="dialog.desc = false"></outbound-desc>
+    <outbound-review v-if="dialog.review" ref="outboundReviewRef" @closed="dialog.review = false"></outbound-review>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { warehouseDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import outboundDetail from "./detail";
+import outboundList from "./list";
+import outboundReview from "./review";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.warehouse.outbound,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "出库主题"),
+            mapFormItemInput("codeLike", "出库编号"),
+            mapFormItemDatePicker("applyDate", "申请日期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "status", defaultValue: "pending" },
+        { column: "tenantId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "applyDateBegin", field: "applyDate[0]" },
+        { column: "applyDateEnd", field: "applyDate[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "出库主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "出库编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { type: "html", field: "num", title: "数量", minWidth: 150, sortable: true },
+        { type: "html", field: "type", title: "出库类别", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(warehouseDic.outbound.type, cellValue, cellValue) },
+        { field: "status", title: "出库状态", minWidth: 120, editRender: { name: "$cell-tag", options: warehouseDic.outbound.status } },
+        { visible: false, type: "html", field: "applicant", title: "申请人员", minWidth: 120, sortable: true },
+        { type: "html", field: "applyDate", title: "申请日期", minWidth: 120, sortable: true },
+        { type: "html", field: "applyDate", title: "出库日期", minWidth: 120, sortable: true },
+        { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { title: "操作", fixed: "right", width: 280, slots: { default: "action" } }
+    ],
+
+    options: {
+        data: [{}]
+    }
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const outboundRef = ref();
+const outboundListRef = ref();
+const outboundDescRef = ref();
+const outboundReviewRef = ref();
+const dialog = reactive({
+    detail: false,
+    list: false,
+    desc: false,
+    review: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => outboundRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => outboundRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => outboundDescRef.value?.setData(row));
+}
+
+const table_list = row => {
+    dialog.list = true;
+    nextTick(() => outboundListRef.value?.setData(row));
+}
+
+const table_outbound = row => {
+    dialog.review = true;
+    nextTick(() => outboundReviewRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该出库单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>

+ 106 - 0
src/views/warehouse/outbound/list.vue

@@ -0,0 +1,106 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_freeze(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>冻结
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+
+import store from "@/store";
+const route = useRoute();
+console.log('route',route)
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    // apiObj: API.production.prePlan,
+    toolbarConfig: { enabled: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "产品名称"),
+            mapFormItemInput("codeLike", "产品编号"),
+            mapFormItemDatePicker("confirmDate", "确认日期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "confirmDateBegin", field: "confirmDate[0]" },
+        { column: "confirmDateEnd", field: "confirmDate[1]" }
+    ],
+    columns: [
+        { type: "seq", fixed: "left", width: 60 },
+        { type: "html", field: "warehouseName", title: "仓库", fixed: "left", minWidth: 150, sortable: true },
+        { type: "html", field: "batchNumber", title: "批号", minWidth: 150, sortable: true },
+        { type: "html", field: "serialNumber", title: "序列号", minWidth: 150, sortable: true },
+        { type: "html", field: "productionDate", title: "生产日期", minWidth: 150, sortable: true },
+        { type: "html", field: "validityDate", title: "有效日期", minWidth: 150, sortable: true },
+        { type: "html", field: "num", title: "数量", minWidth: 150 },
+        { type: "html", field: "storageDate", title: "入库日期", minWidth: 150, sortable: true },
+        { field: "code", title: "关联入库单", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { title: "操作", fixed: "right", width: 120, slots: { default: "action" } }
+    ]
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const dispatchRef = ref();
+const dispatchDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false
+});
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => dispatchDescRef.value?.setData(row));
+}
+
+const table_freeze = ({ id }) => {
+    ElMessageBox.confirm("是否确认冻结该库存?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.production.plan.del({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>
+
+<style scoped>
+.el-descriptions {padding-left: 10px;}
+.el-descriptions :deep(.el-descriptions__cell) {width: calc(100% / 4);}
+</style>

+ 23 - 0
src/views/warehouse/outbound/main.js

@@ -0,0 +1,23 @@
+import XEUtils from "xe-utils"
+
+export const tableOptions = reactive({
+    tableKey: "material",
+
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => !!row.purchasePlanId }, slots: { default: "seq_del" } },
+        { field: "code", title: "产品编码", fixed: "left", minWidth: 180 },
+        { field: "name", title: "产品名称", fixed: "left", minWidth: 180 },
+        { field: "specification", title: "规格型号", fixed: "left", minWidth: 150 },
+        { field: "unit", title: "单位", fixed: "left", minWidth: 120 },
+        { field: "price", title: "单价", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "number", title: "数量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "warehouseId", title: "仓库", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, clearable: true }, optionProps: { label: "name", value: "id" } }, formatter: ({ cellValue, row, column }) => cellValue ? XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == cellValue), "name", row.warehouseName) : "" }
+    ],
+    editRules: {
+        price: [{ required: true, message: "必须填写" }],
+        number: [{ required: true, message: "必须填写" }],
+        warehouseId: [{ required: true, message: "必须填写" }]
+    },
+    footerField: [["price", "number", "warehouseId"]],
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 5 }]
+})

+ 177 - 0
src/views/warehouse/outbound/review.vue

@@ -0,0 +1,177 @@
+<template>
+    <el-dialog v-model="visible" title="出库确认" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-input v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="确认结果" prop="result">
+                                <el-radio-group v-model="form.result">
+                                    <el-radio label="是" value="approve"></el-radio>
+                                    <el-radio label="否" value="reject"></el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="确认时间" prop="applyDate">
+                                <el-date-picker v-model="form.applyDate" :clearable="false" value-format="YYYY-MM-DD" placeholder="请选择申请时间"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="出库仓库">
+                                <el-select v-model="form.warehouseId" placeholder="请选择出库仓库">
+                                    <el-option v-for="item in warehouses.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.name" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="16" :xs="24">
+                            <el-form-item label="库管意见">
+                                <el-input v-model="form.comments" type="textarea" maxlength="200" :rows="1" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="是否出库质检" prop="result">
+                                <el-radio-group v-model="form.result">
+                                    <el-radio label="是" value="approve"></el-radio>
+                                    <el-radio label="否" value="reject"></el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="质检人员" prop="result">
+                                <el-input v-model="form.comments" type="textarea" maxlength="200" :rows="1" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+                <el-collapse-item title="收货地址" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="出库明细" name="warehouse">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions[form.type]"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "@/views/purchase/inspection/main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "warehouse", "other"]);
+
+const warehouses = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    type: "purchase",
+    result: "approve",
+    comments: null,
+    warehouseId: null,
+    applyDate: null,
+    childrenList: [{}],
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入入库主题" }],
+    applyDate: [{ required: true, message: "请选择申请时间" }]
+});
+
+const setData = data => {
+    visible.value = true;
+    // XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            // if (!form.value.bomList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            // if (await formTableRef.value.validateFormTable()) {
+            //     const data = XEUtils.omit(form.value, "customer", "bomList", "fileList");
+            //     const bomList = XEUtils.map(form.value.bomList, item => ({ materialCode: item.code, materialQuantity: item.quantity, materialPrice: item.price }));
+            //     const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "saleOrderAttach" }));
+            //     XEUtils.set(data, "customerId", form.value.customer.id);
+            //     XEUtils.set(data, "bomList", bomList);
+            //     fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+            //     isSaving.value = true;
+            //     API.production.plan[mode.value](data).then(res => {
+            //         ElMessage.success("操作成功");
+            //         isSaving.value = false;
+            //         visible.value = false;
+            //         $emit("success", mode.value);
+            //     }).catch(() => isSaving.value = false);
+            // }
+        } else {
+            return false;
+        }
+    });
+}
+
+// const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+// fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 108 - 0
src/views/warehouse/production/requisition/desc.vue

@@ -0,0 +1,108 @@
+<template>
+    <el-dialog v-model="visible" :title="`${titleMap[descData.requisitionType]}详情`" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据主题" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item :label-class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" :class-name="$store.state.tenant.tenantId === '0' && 'no-border-top'" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="出库状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(warehouseDic.outbound.status, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="领料人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.receiverName }}</el-descriptions-item>
+                        <el-descriptions-item label="领料日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.requisitionDate }}</el-descriptions-item>
+                        <el-descriptions-item label="概要" :span="3" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="领料明细" name="material">
+                    <sc-form-table v-model="descData.items" v-bind="tableOptions" :mergeCells="mergeCells" disabled></sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import API from "@/api";
+import store from "@/store";
+
+import { warehouseDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+const titleMap = reactive({
+    auto: "生产领料",
+    manual: "生产补料"
+});
+
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+const activeNames = ref(["basic", "material"]);
+
+const mergeCells = ref([]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    requisitionType: "auto",
+    name: null,
+    code: null,
+    receiverName: null,
+    requisitionDate: null,
+    items: [],
+    remark: null,
+    status: "pending",
+    createTime: null
+});
+
+const setData = async data => {
+    const res = await API.warehouse.requisition.getStock({ requisitionId: data.id });
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "items") {
+            let row = 0, merges = []; // 单元格合并
+
+            const items = XEUtils.map(XEUtils.get(data, key), item => {
+                const stockItem = XEUtils.get(res, item.materialCode);
+                !stockItem.warehouses.length && (stockItem.warehouses = [{}]);
+
+                // 单元格合并
+                if (stockItem.warehouses.length > 1) merges.push(Array.from({ length: 5 }, (_, index) => ({ row, col: index + 1, rowspan: stockItem.warehouses.length, colspan: 1 })))
+                row += stockItem.warehouses.length;
+
+                return XEUtils.map(stockItem.warehouses, (warehouse, index) => {
+                    return XEUtils.omit({
+                        ...item, ...warehouse,
+                        remainingQuantity: stockItem.requiredQuantity, // 领料单需求数量
+                        quantity: 0 // 出库数量
+                    }, "id");
+                });
+            });
+            
+            XEUtils.set(descData.value, key, XEUtils.flatten(items));
+            mergeCells.value = XEUtils.flatten(merges);
+        } else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+
+    visible.value = true;
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 198 - 0
src/views/warehouse/production/requisition/detail.vue

@@ -0,0 +1,198 @@
+<template>
+    <el-dialog v-model="visible" :title="`${titleMap[form.requisitionType]}`" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" required>
+                                <el-input v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="领料主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入领料主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="领料编号" required>
+                                <el-input v-model="form.code" readonly show-word-limit></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="领料人员" prop="receiverId">
+                                <el-select v-model="form.receiverId" placeholder="请选择领料人员">
+                                    <el-option v-for="item in users.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.nickName" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="领料日期" prop="requisitionDate">
+                                <vxe-date-picker v-model="form.requisitionDate" value-format="yyyy-MM-dd" transfer placeholder="请选择领料日期"></vxe-date-picker>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="领料明细" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.items" v-bind="tableOptions" :mergeCells="mergeCells" @editActivated="editActivated"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他信息" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+const titleMap = reactive({
+    auto: "生产领料",
+    manual: "生产补料"
+});
+
+const activeNames = ref(["basic", "material", "other"]);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+
+const users = ref([]);
+const mergeCells = ref([]);
+const form = ref({
+    tenantId: store.state.tenant.tenantId,
+    requisitionType: "auto",
+    orderId: null,
+    dispatchId: null,
+    bomId: null,
+    name: null,
+    code: null,
+    receiverId: null,
+    requisitionDate: null,
+    items: [],
+    remark: null
+});
+const rules = reactive({
+    name: [{ required: true, message: "请输入领料主题" }],
+    receiverId: [{ required: true, message: "请选择领料人员" }],
+    requisitionDate: [{ required: true, message: "请选择领料日期" }]
+});
+
+const setData = async data => {
+    const res = await API.warehouse.requisition.getStock({ requisitionId: data.id });
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "items") {
+            let row = 0, merges = []; // 单元格合并
+
+            const items = XEUtils.map(XEUtils.get(data, key), item => {
+                const stockItem = XEUtils.get(res, item.materialCode);
+                !stockItem.warehouses.length && (stockItem.warehouses = [{}]);
+                
+                // 单元格合并
+                if (stockItem.warehouses.length > 1) merges.push(Array.from({ length: 5 }, (_, index) => ({ row, col: index + 1, rowspan: stockItem.warehouses.length, colspan: 1 })))
+                row += stockItem.warehouses.length;
+
+                return XEUtils.map(stockItem.warehouses, (warehouse, index) => {
+                    const max = XEUtils.min([stockItem.remainingQuantity, warehouse.normalQuantity]);
+                    const otherLocked = XEUtils.sum(stockItem.warehouses.slice(1), item => item.lockedQuantity);
+
+                    return XEUtils.omit({
+                        ...item, ...warehouse,
+                        remainingQuantity: stockItem.remainingQuantity, // 剩余领料数量
+                        // 领料数量和可用数量最小值 - 其他仓库锁定库存数
+                        quantity: index == 0 ? max < otherLocked ? max : XEUtils.subtract(max, otherLocked) : warehouse.lockedQuantity
+                    }, "id");
+                });
+            });
+
+            XEUtils.set(form.value, key, XEUtils.flatten(items));
+            mergeCells.value = XEUtils.flatten(merges);
+        } else XEUtils.set(form.value, key, XEUtils.get(data, key));
+    });
+
+    visible.value = true;
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const editActivated = ({ row, column }) => {    
+    if (column.field == "quantity") {
+        // 领料数量、锁定数量、可用数量最小值(必须先使用锁定库存)
+        const max = XEUtils.min([row.remainingQuantity, row.normalQuantity]);
+        const otherArr = XEUtils.filter(form.value.items, item => (item.id != row.id && item.materialCode == row.materialCode));
+        const otherLocked = XEUtils.sum(otherArr, item => item.lockedQuantity);
+        column.editRender.props.max = max < otherLocked ? max : XEUtils.subtract(max, otherLocked);
+    }
+}
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "items");
+                const items = [];
+                XEUtils.arrayEach(form.value.items, item => {
+                    console.log(item)
+
+                    // 可用库存是否 ≥ 出库数量。
+                })
+                // console.log(XEUtils.filter(item.actualQuantity > 0))
+                // const items = XEUtils.map(form.value.items, item => ({  }));
+                // XEUtils.set(data, "items", items);
+
+                // isSaving.value = true;
+                // API.warehouse.requisition.add(data).then(res => {
+                //     ElMessage.success("操作成功");
+                //     isSaving.value = false;
+                //     visible.value = false;
+                //     $emit("success");
+                // }).catch(() => isSaving.value = false);
+            }
+        } else {
+            return false;
+        }
+    });
+}
+
+const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .vxe-date-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--control-icon) {padding-left: .5em;padding-right: 0;color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+.el-form .vxe-date-picker :deep(.vxe-date-picker--inner::placeholder) {color: var(--el-input-placeholder-color,var(--el-text-color-placeholder));}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 135 - 0
src/views/warehouse/production/requisition/index.vue

@@ -0,0 +1,135 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header v-if="!detailable"></sc-page-header>
+
+        <scTable ref="xGridTable" v-bind="xGridOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+           
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_requisition(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>领料
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <order-detail v-if="dialog.detail" ref="orderRef" @success="refreshTable" @closed="dialog.detail = false"></order-detail>
+    <order-desc v-if="dialog.desc" ref="orderDescRef" @closed="dialog.desc = false"></order-desc>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { warehouseDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import orderDetail from "./detail";
+import orderDesc from "./desc";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const props = defineProps({
+    detailable: { type: Boolean, default: false },
+    options: { type: Object, default: () => ({}) }
+});
+
+const selectConfig = reactive({
+    options: warehouseDic.outbound.status,
+    events: {
+        change: data => XEUtils.merge(xGridOptions.formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const xGridOptions = reactive({
+    apiObj: API.warehouse.requisition,
+    toolbarConfig: { export: false },
+    formConfig: {
+        data: {},
+        items: [
+            mapFormItemInput("nameLike", "领料主题"),
+            mapFormItemInput("codeLike", "领料编号"),
+            mapFormItemSelect("status", "出库状态", selectConfig),
+            mapFormItemDatePicker("requisitionDate", "领料日期", daterangeConfig)
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "createTime_desc" },
+        { column: "tenantId" },
+        { column: "requisitionType", defaultValue: "auto" },
+        { column: "dispatchId" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "status" },
+        { column: "requisitionDateBegin", field: "requisitionDate[0]" },
+        { column: "requisitionDateEnd", field: "requisitionDate[1]" }
+    ],
+    columns: computed(() => props.detailable ? [
+        { type: "seq", fixed: "left", width: 60 },
+        { type: "html", field: "name", title: "领料主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "领料编号", fixed: "left", minWidth: 150, sortable: true },
+        { type: "html", field: "receiverName", title: "领料人", minWidth: 150, sortable: true },
+        { type: "html", field: "requisitionDate", title: "领料日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { field: "status", title: "出库状态", minWidth: 120, editRender: { name: "$cell-tag", options: warehouseDic.outbound.status } },
+    ]: [
+        { type: "seq", fixed: "left", width: 60 },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "领料主题", fixed: "left", minWidth: 150, sortable: true },
+        { field: "code", title: "领料编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+        { type: "html", field: "receiverName", title: "领料人", minWidth: 150, sortable: true },
+        { type: "html", field: "requisitionDate", title: "领料日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { field: "status", title: "出库状态", minWidth: 120, editRender: { name: "$cell-tag", options: warehouseDic.outbound.status } },
+        { visible: false, type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+        { title: "操作", fixed: "right", width: 120, slots: { default: "action" } }
+    ]),
+    ...props.options
+});
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const orderRef = ref();
+const orderDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false
+});
+
+const table_requisition = row => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => orderDescRef.value?.setData(row));
+}
+
+const table_invalid = ({ id }) => {
+    ElMessageBox.confirm("是否确认作废该领料单?", "作废警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        // API.({ id }).then(() => {
+        //     ElMessage.success("操作成功");
+        //     refreshTable();
+        // });
+    }).catch(() => {});
+}
+</script>

+ 42 - 0
src/views/warehouse/production/requisition/main.js

@@ -0,0 +1,42 @@
+import XEUtils from "xe-utils"
+
+export const tableOptions = reactive({
+    tableKey: "bom",
+
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        { field: "materialCode", title: "物料编码", fixed: "left", minWidth: 180 },
+        { field: "materialName", title: "物料名称", fixed: "left", minWidth: 180 },
+        { field: "specification", title: "规格型号", fixed: "left", minWidth: 150 },
+        { field: "unit", title: "单位", fixed: "left", minWidth: 120 },
+        { field: "remainingQuantity", title: "领料数量", minWidth: 100 },
+        { field: "quantity", title: "出库数量", minWidth: 100, 
+            editRender: { name: "VxeNumberInput", type: "float", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 1, events: { 
+                lazyChange: ({ $grid, row, column }) => {
+                    const otherArr = XEUtils.filter($grid.getTableData().tableData, item => (item.id != row.id && item.materialCode == row.materialCode));
+                    // 同物料不同仓库数量变化
+                    if (otherArr.length) {
+                        const remainingSum = XEUtils.subtract(XEUtils.min([row.remainingQuantity, row.normalQuantity]), row.quantity);
+                        XEUtils.arrayEach(otherArr, (item, index) => {
+                            if (item.quantity > remainingSum) {
+                                if (index == 0) item.quantity = remainingSum;
+                                else item.quantity = 0;
+                            }
+                        });
+                    }
+                } 
+            } }
+        },
+        { field: "returnQuantity", title: "退料数量", minWidth: 100 },
+        { field: "wasteQuantity", title: "废料数量", minWidth: 100 },
+        { field: "warehouseName", title: "仓库名称", minWidth: 180 },
+        { field: "lockedQuantity", title: "已锁定库存", minWidth: 100 },
+        { field: "normalQuantity", title: "可用库存", minWidth: 100 }
+    ],
+
+    editRules: {
+        quantity: [{ required: true, message: "必须填写" }]
+    },
+    footerField: [["remainingQuantity", "quantity", "returnQuantity", "wasteQuantity"]],
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 5 }]
+})

+ 4 - 4
vue.config.js

@@ -2,10 +2,10 @@ const { defineConfig } = require("@vue/cli-service")
 
 module.exports = defineConfig({
 	//设置为空打包后不分更目录还是多级目录
-	publicPath: "/easydo/mes",
-    outputDir: "easydo/mes",
-    // publicPath: "/",
-    // outputDir: "dist",
+    publicPath: "/mesWeb",
+    outputDir: "dist/mesWeb",
+    // publicPath: "/", // win7
+    // outputDir: "dist", // win7
 	//build编译后存放静态文件的目录
 	// assetsDir: "static",