diff --git a/db_migrations/deploy_production.sql b/db_migrations/deploy_production.sql index bdb0dd6..b4c1571 100644 --- a/db_migrations/deploy_production.sql +++ b/db_migrations/deploy_production.sql @@ -12,10 +12,13 @@ BEGIN; -- 1. 采购申请表新增 base_id(关联物料基础信息) ALTER TABLE purchase_request ADD COLUMN IF NOT EXISTS base_id INTEGER; --- 2. 采购入库表新增 request_id(关联采购申请单) +-- 2. 采购申请表新增 tax_rate(税率) +ALTER TABLE purchase_request ADD COLUMN IF NOT EXISTS tax_rate NUMERIC(5,2) DEFAULT 0; + +-- 3. 采购入库表新增 request_id(关联采购申请单) ALTER TABLE stock_buy ADD COLUMN IF NOT EXISTS request_id INTEGER; --- 3. 角色权限表新增 company_name(支持公司级别权限隔离) +-- 4. 角色权限表新增 company_name(支持公司级别权限隔离) ALTER TABLE sys_role_permission ADD COLUMN IF NOT EXISTS company_name VARCHAR(255); @@ -188,6 +191,57 @@ FROM sys_menu WHERE code IN ('system_mgmt', 'system_permission', 'system_audit') AND NOT EXISTS (SELECT 1 FROM sys_role_permission rp WHERE rp.role_code = 'SUPERVISOR' AND rp.target_code = sys_menu.code AND rp.type = 'menu'); +-- ============================================================ +-- 三补、借库管理菜单 + 权限码 + 角色分配 +-- ============================================================ + +-- 借库选单 & 借库审批 菜单 +INSERT INTO sys_menu (code, name, parent_id, is_visible) +SELECT 'op_borrow_apply', '借库选单', m.id, true +FROM sys_menu m WHERE m.code = 'operation_mgmt' +AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE code = 'op_borrow_apply'); + +INSERT INTO sys_menu (code, name, parent_id, is_visible) +SELECT 'op_borrow_approval', '借库审批', m.id, true +FROM sys_menu m WHERE m.code = 'operation_mgmt' +AND NOT EXISTS (SELECT 1 FROM sys_menu WHERE code = 'op_borrow_approval'); + +-- 借库选单元素权限 +INSERT INTO sys_element (menu_code, name, code, element_type) SELECT 'op_borrow_apply', '物品名称', 'op_borrow_apply:name', 'column' WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'op_borrow_apply:name'); +INSERT INTO sys_element (menu_code, name, code, element_type) SELECT 'op_borrow_apply', '规格型号', 'op_borrow_apply:spec', 'column' WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'op_borrow_apply:spec'); +INSERT INTO sys_element (menu_code, name, code, element_type) SELECT 'op_borrow_apply', '可用库存', 'op_borrow_apply:available_quantity', 'column' WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'op_borrow_apply:available_quantity'); +INSERT INTO sys_element (menu_code, name, code, element_type) SELECT 'op_borrow_apply', '操作/编辑', 'op_borrow_apply:operation', 'column' WHERE NOT EXISTS (SELECT 1 FROM sys_element WHERE code = 'op_borrow_apply:operation'); + +-- 角色分配:有 operation_mgmt 的角色 → 获得借库选单/审批菜单 + 元素 +INSERT INTO sys_role_permission (role_code, target_code, type) +SELECT DISTINCT rp.role_code, 'op_borrow_apply', 'menu' +FROM sys_role_permission rp WHERE rp.target_code = 'operation_mgmt' +AND NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = rp.role_code AND target_code = 'op_borrow_apply'); + +INSERT INTO sys_role_permission (role_code, target_code, type) +SELECT DISTINCT rp.role_code, 'op_borrow_approval', 'menu' +FROM sys_role_permission rp WHERE rp.target_code = 'operation_mgmt' +AND NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = rp.role_code AND target_code = 'op_borrow_approval'); + +INSERT INTO sys_role_permission (role_code, target_code, type) +SELECT rp.role_code, elem.code, 'element' +FROM (SELECT DISTINCT role_code FROM sys_role_permission WHERE target_code = 'operation_mgmt') rp +CROSS JOIN (SELECT unnest(ARRAY['op_borrow_apply:name','op_borrow_apply:spec','op_borrow_apply:available_quantity','op_borrow_apply:operation']) AS code) elem +WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission rp2 WHERE rp2.role_code = rp.role_code AND rp2.target_code = elem.code); + +-- 补借库管理菜单给 SUPERVISOR(如缺) +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'operation_mgmt', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'operation_mgmt'); +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'op_borrow_apply', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'op_borrow_apply'); +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'op_borrow_approval', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'op_borrow_approval'); +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'op_borrow', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'op_borrow'); +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'op_return', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'op_return'); +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'op_records', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'op_records'); + +-- 出库管理菜单补 SUPERVISOR +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'outbound_mgmt', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'outbound_mgmt'); +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', 'outbound_approval', 'menu' WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = 'outbound_approval'); +INSERT INTO sys_role_permission (role_code, target_code, type) SELECT 'SUPERVISOR', elem.code, 'element' FROM (SELECT unnest(ARRAY['outbound_approval:request_no','outbound_approval:applicant','outbound_approval:approver','outbound_approval:status','outbound_approval:operation']) AS code) elem WHERE NOT EXISTS (SELECT 1 FROM sys_role_permission WHERE role_code = 'SUPERVISOR' AND target_code = elem.code); + -- ============================================================ -- 四、验证 -- ============================================================ @@ -196,7 +250,7 @@ SELECT '=== 验证 ===' AS info; SELECT '表结构' AS item, column_name, data_type FROM information_schema.columns WHERE table_name IN ('purchase_request', 'stock_buy', 'sys_role_permission') - AND column_name IN ('base_id', 'request_id', 'company_name') + AND column_name IN ('base_id', 'request_id', 'company_name', 'tax_rate') ORDER BY table_name, column_name; SELECT '权限码总数' AS item, count(*) FROM sys_element; diff --git a/inventory-backend/app/models/purchase.py b/inventory-backend/app/models/purchase.py index f689543..2f43f0d 100644 --- a/inventory-backend/app/models/purchase.py +++ b/inventory-backend/app/models/purchase.py @@ -20,8 +20,9 @@ class PurchaseRequest(db.Model): supplier_link = db.Column(db.String(500), comment='商家地址链接') remark = db.Column(db.Text, comment='备注信息') images = db.Column(db.Text, comment='图片列表JSON') - unit_price = db.Column(db.Numeric(19, 4), default=0, comment='单价') - total_price = db.Column(db.Numeric(19, 4), default=0, comment='总价') + unit_price = db.Column(db.Numeric(19, 4), default=0, comment='不含税单价') + total_price = db.Column(db.Numeric(19, 4), default=0, comment='不含税总价') + tax_rate = db.Column(db.Numeric(5, 2), default=0, comment='税率') status = db.Column(db.Integer, default=0, nullable=False) requester_id = db.Column(db.Integer, nullable=False, index=True) approver_id = db.Column(db.Integer, index=True) @@ -88,6 +89,7 @@ class PurchaseRequest(db.Model): 'images': self.get_images(), 'unit_price': float(self.unit_price) if self.unit_price else 0, 'total_price': float(self.total_price) if self.total_price else 0, + 'tax_rate': float(self.tax_rate) if self.tax_rate else 0, 'status': self.status, 'status_text': ['待审批', '已通过', '已驳回', '已完成'][self.status] if self.status in [0, 1, 2, 3] else '未知', 'requester_id': self.requester_id, diff --git a/inventory-backend/app/services/purchase_service.py b/inventory-backend/app/services/purchase_service.py index 3127307..e2005ac 100644 --- a/inventory-backend/app/services/purchase_service.py +++ b/inventory-backend/app/services/purchase_service.py @@ -79,6 +79,7 @@ class PurchaseService: images=json.dumps(data.get('images', []), ensure_ascii=False) if data.get('images') else '[]', unit_price=float(data.get('unit_price', 0) or 0), total_price=float(data.get('total_price', 0) or 0), + tax_rate=float(data.get('tax_rate', 0) or 0), requester_id=requester_id, approver_id=data.get('approver_id'), status=0 diff --git a/inventory-web/src/api/purchase.ts b/inventory-web/src/api/purchase.ts index a908720..fc2ed11 100644 --- a/inventory-web/src/api/purchase.ts +++ b/inventory-web/src/api/purchase.ts @@ -13,6 +13,7 @@ export interface PurchaseItem { images?: string[] unit_price?: number total_price?: number + tax_rate?: number status: number status_text?: string requester_id?: number diff --git a/inventory-web/src/views/outbound/create.vue b/inventory-web/src/views/outbound/create.vue index 1167861..e943a0f 100644 --- a/inventory-web/src/views/outbound/create.vue +++ b/inventory-web/src/views/outbound/create.vue @@ -79,25 +79,37 @@
-
- - 点击开启全屏扫码 -
-
- - 无扫码权限 -
- -
- + +
+
diff --git a/inventory-web/src/views/purchase/index.vue b/inventory-web/src/views/purchase/index.vue index 52818b1..116859f 100644 --- a/inventory-web/src/views/purchase/index.vue +++ b/inventory-web/src/views/purchase/index.vue @@ -24,12 +24,19 @@ - + + + + + + + @@ -126,16 +133,26 @@ - - + + - - + + + + + + + + + + + + @@ -311,6 +328,7 @@ const form = ref({ remark: '', unit_price: undefined as number | undefined, total_price: undefined as number | undefined, + tax_rate: 0, approver_id: undefined as number | undefined, images: [] as string[] }) @@ -381,7 +399,7 @@ const openCreateDialog = (prefill?: { materialId?: number; name?: string; spec?: quantity: 1, purchase_date: new Date().toISOString().split('T')[0], supplier_link: '', remark: '', - unit_price: undefined, total_price: undefined, + unit_price: undefined, total_price: undefined, tax_rate: 0, approver_id: undefined, images: [] } @@ -506,11 +524,16 @@ const onUnitPriceChange = (val: number | undefined) => { } const onTotalPriceChange = (_val: number | undefined) => { - // 用户手动修改总价时:标记为已手动修改,不再反向强制覆盖单价 - // 允许用户输入打包一口价(如单价3元,买2个算5元),保留用户填入的单价和总价 totalPriceManuallyEdited.value = true } +const onTaxRateChange = () => { + // 税率变化时:保持不含税单价不变,重算不含税总价 = 单价×数量 + if (form.value.unit_price && form.value.quantity) { + form.value.total_price = Number((form.value.unit_price * form.value.quantity).toFixed(2)) + } +} + // --- 上传 --- const beforeAvatarUpload = (rawFile: any) => { const isTypeValid = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'].includes(rawFile.type) diff --git a/inventory-web/src/views/stock/inbound/buy.vue b/inventory-web/src/views/stock/inbound/buy.vue index 644cdf1..bb9d003 100644 --- a/inventory-web/src/views/stock/inbound/buy.vue +++ b/inventory-web/src/views/stock/inbound/buy.vue @@ -580,14 +580,18 @@ + + 自动计算,点击输入可覆盖 + @@ -1178,8 +1182,7 @@ const form = reactive({ supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '', arrival_photo: [] as string[], inspection_report: [] as string[], print_copies: 1, - request_id: undefined as number | undefined, // [新增] 关联采购申请单 - _post_tax_total_price: undefined as number | undefined // 含税总价(仅前端输入用,不提交后端) + request_id: undefined as number | undefined // [新增] 关联采购申请单 }) // ------------------------------------ @@ -1382,15 +1385,49 @@ const handleEntryModeChange = (val: string) => { if(formRef.value) formRef.value.clearValidate('batch_number') } } -// 价格联动:含税单价为主输入,不含税价格自动反算 -// 追踪用户最后编辑的是含税还是不含税('pre' | 'post') -const lastPriceEdit = ref<'pre' | 'post'>('post') // 默认以含税为主 +// 价格联动:含税法计算 +// 含税总价 = 数量 × 含税单价 +// 不含税总价 = 含税总价 ÷ (1 + 税率) +// 不含税单价 = 不含税总价 ÷ 数量 +const lastPriceEdit = ref<'pre' | 'post'>('post') + +// 含税总价:自动计算(含税单价 × 数量)+ 支持手动覆盖 +const postTaxTotalManuallySet = ref(false) +const postTaxTotalInput = ref(undefined) +const postTaxTotalAuto = computed(() => { + if (form.post_tax_unit_price != null && form.in_quantity > 0) { + return Number((form.post_tax_unit_price * form.in_quantity).toFixed(2)) + } + return 0 +}) + +const onPostTaxTotalChange = (val: number | undefined) => { + if (val != null && val > 0 && form.in_quantity > 0) { + postTaxTotalManuallySet.value = true + // 含税总价 → 含税单价 → 不含税单价/总价 + form.post_tax_unit_price = Number((val / form.in_quantity).toFixed(2)) + const taxMultiplier = 1 + (form.tax_rate || 0) / 100 + form.unit_price = Number((form.post_tax_unit_price / taxMultiplier).toFixed(4)) + form.total_price = Number((form.unit_price * form.in_quantity).toFixed(2)) + } +} + +const onPostTaxTotalClear = () => { + postTaxTotalManuallySet.value = false + postTaxTotalInput.value = undefined + // 重新用含税单价反算 + if (form.post_tax_unit_price != null && form.in_quantity > 0) { + const taxMultiplier = 1 + (form.tax_rate || 0) / 100 + form.unit_price = Number((form.post_tax_unit_price / taxMultiplier).toFixed(4)) + form.total_price = Number((form.unit_price * form.in_quantity).toFixed(2)) + } +} const updatePrices = (source: string) => { const taxMultiplier = 1 + (form.tax_rate || 0) / 100; if (source === 'pre') { - // 用户编辑了不含税单价 → 反推含税 + // 编辑了不含税单价 → 反推含税单价 lastPriceEdit.value = 'pre' if (form.unit_price != null) { form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2)); @@ -1398,7 +1435,7 @@ const updatePrices = (source: string) => { form.post_tax_unit_price = undefined; } } else if (source === 'post') { - // 用户编辑了含税单价 → 反推不含税 + // 编辑了含税单价 → 反推不含税 lastPriceEdit.value = 'post' if (form.post_tax_unit_price != null) { form.unit_price = Number((form.post_tax_unit_price / taxMultiplier).toFixed(4)); @@ -1406,43 +1443,51 @@ const updatePrices = (source: string) => { form.unit_price = undefined; } } else if (source === 'tax') { - // 税率变化:保持用户最后编辑的那个价格不变,重算另一个 + // 税率变化:保持最后编辑的价格不变,重算另一个 if (lastPriceEdit.value === 'post' && form.post_tax_unit_price != null) { form.unit_price = Number((form.post_tax_unit_price / taxMultiplier).toFixed(4)); } else if (form.unit_price != null) { form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2)); } - } else if (source === 'post_total') { - // 用户输入了含税总价 → 反推不含税总价和单价 - lastPriceEdit.value = 'post' - const postTaxTotal = form._post_tax_total_price - if (postTaxTotal != null && form.in_quantity > 0) { - const preTaxTotal = postTaxTotal / taxMultiplier - form.total_price = Number(preTaxTotal.toFixed(2)) - form.unit_price = Number((preTaxTotal / form.in_quantity).toFixed(4)) - form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2)) - } - return // 已完整计算,跳过下面的总价逻辑 } - // 计算不含税总价 - if (form.in_quantity > 0 && form.unit_price != null) { + // 含税单价优先计算总价 + // 不含税总价 = (含税单价 / (1+税率)) × 数量 + if (form.in_quantity > 0 && form.post_tax_unit_price != null) { + const preTax = form.post_tax_unit_price / taxMultiplier + form.unit_price = Number(preTax.toFixed(4)) + form.total_price = Number((preTax * form.in_quantity).toFixed(2)) + } else if (form.in_quantity > 0 && form.unit_price != null) { form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2)); } else { form.total_price = undefined; } + + if (postTaxTotalManuallySet.value && postTaxTotalInput.value != null) { + onPostTaxTotalChange(postTaxTotalInput.value) + return + } + if (!postTaxTotalManuallySet.value) { + postTaxTotalInput.value = undefined + } } -// 数量或不含税单价变化 → 自动推总价和含税单价 -watch(() => [form.in_quantity, form.unit_price], () => { - if (form.unit_price != null && form.in_quantity > 0) { - form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2)); - const taxMultiplier = 1 + (form.tax_rate || 0) / 100; - form.post_tax_unit_price = Number((form.unit_price * taxMultiplier).toFixed(2)); +// 数量或单价变化 → 以含税单价为主计算 +watch(() => [form.in_quantity, form.post_tax_unit_price], () => { + if (form.post_tax_unit_price != null && form.in_quantity > 0) { + const taxMultiplier = 1 + (form.tax_rate || 0) / 100 + form.unit_price = Number((form.post_tax_unit_price / taxMultiplier).toFixed(4)) + form.total_price = Number((form.unit_price * form.in_quantity).toFixed(2)) + } else if (form.unit_price != null && form.in_quantity > 0) { + form.total_price = Number((form.in_quantity * form.unit_price).toFixed(2)) + form.post_tax_unit_price = Number((form.unit_price * (1 + (form.tax_rate || 0) / 100)).toFixed(2)) } else { form.total_price = undefined; form.post_tax_unit_price = undefined; } + if (!postTaxTotalManuallySet.value) { + postTaxTotalInput.value = undefined + } }) // 注意:入库数量和打印份数现在是独立的字段,不再自动同步 @@ -2046,9 +2091,10 @@ const resetForm = () => { tax_rate: 0, currency: 'CNY', exchange_rate: 1.00, supplier_name: '', purchaser: '', purchaser_email: '', source_link: '', detail_link: '', arrival_photo: [], inspection_report: [], print_copies: 1, - request_id: undefined, - _post_tax_total_price: undefined + request_id: undefined }) + postTaxTotalInput.value = undefined + postTaxTotalManuallySet.value = false } const getStatusType = (status: string) => { const map: any = {'在库': 'success', '出库': 'info', '损耗': 'danger'}; return map[status] || 'warning' } diff --git a/inventory-web/src/views/transaction/borrow.vue b/inventory-web/src/views/transaction/borrow.vue index 7e8ee1e..65d430e 100644 --- a/inventory-web/src/views/transaction/borrow.vue +++ b/inventory-web/src/views/transaction/borrow.vue @@ -64,25 +64,37 @@
-
- - 点击开启全屏扫码 -
-
- - 无扫码权限 -
- -
- + +
+