尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

采购制度数字化落地:从 PDF 规则到状态机、审批链与三单匹配

采购制度数字化落地:从 PDF 规则到状态机、审批链与三单匹配 简介这份PDF文档系统整理了恒大中国控股有限公司的采购管理制度面向企业采购、成本管理、供应链及内控审计等岗位的从业者与学习者可用于制度搭建参考、流程梳理和岗位培训。全文从制度目的与适用范围切入逐层展开术语定义、供应商资质审查与绩效评价、集中采购与联合采购两种模式、公开招标与竞争性谈判等采购方式并对战略合作的实施要求、集团成本管理中心与区域各主管部门的职责划分、使用集中采购成果的管理要求、内部控制与监督、应急处理与风险管理等模块作出规定目录层级清晰便于按章节检索。资源包内共1个PDF文件约168KB内容为完整制度文本可直接用于阅读、摘录与对标。目前已有81人学习适合需要了解大型房企采购规则与合规框架的读者参考。1. 制度是一份 PDF系统要的是状态机《恒大集团采购管理-恒大制度.pdf》这类文件在企业里很常见几十页写满「单笔金额超过 50 万元须报集团审批」「同一供应商年度累计采购不得超过约定额度」「无合同不得付款」这类条款。业务方把它当依据IT 把它当需求说明书双方都以为对方看懂了。真正的落差在于——PDF 描述的是「应该怎样」而采购系统必须回答谁在什么条件下点什么按钮、数据落到哪张表、审批链由谁生成、异常怎么回滚。把一份采购管理制度做成可运行的系统本质是三次翻译制度条款翻译成实体和字段审批层级翻译成规则表和状态机比价与结算要求翻译成带约束的 SQL 与巡检脚本。它解决的不是「文档电子化」而是把口头规则变成机器可判定的约束让越权采购在提交那一刻就被拦住而不是在审计时被翻出来。这篇文章适合正在做 SRM、采购中台或内部审批流的后端与产品工程师也适合被临时拉去「把制度上线」的开发者。你不需要重写制度只需要给它一个可执行的内核剩下的工作全部是数据建模和状态流转。2. 采购制度条款到数据库表的映射2.1 把每一条制度标注成三类约束一份采购管理制度动辄上百条逐条实现是灾难。我一般先做一遍分类标注把每条条款归到三类里再决定它落到代码的哪一层。分类做完之后工作量会立刻从「一百条需求」收缩成「三张清单」。数据约束字段必填、金额精度、供应商资质有效期、税率取值、付款比例上限。这类条款落到表结构、唯一索引和 CHECK 约束上改一次库就永久生效。流程约束金额分级审批、先审批后下单、无合同不付款、验收入库后才可结算。这类条款落到状态机与规则表上是所有采购系统里最容易写歪的部分。权限约束采购员不得自审自己提交的单据、跨组织不可见他人需求、供应商联系人仅采购主管可改。这类条款落到角色定义和行级数据范围上。制度条款类型典型原文特征系统落地形态可验证方式数据约束「必须」「不得低于」「不得超过」表字段、CHECK、唯一索引插一条脏数据看是否被拒流程约束「经……审批后方可」「分级审批」状态机 规则参数表造一笔超额单看审批链权限约束「由……负责」「不得由本人」RBAC 数据范围 SQL换账号登录验证可见性值得注意的是「同一供应商年度累计不得超过」这类条款既像数据约束又像流程约束实际做法是两者都要累计值用聚合查询算出来判断逻辑放在下单前的前置校验里不放进数据库约束——因为累计值会随其他单据变化写进约束会让历史单据无法重算。2.2 采购订单与供应商主数据的最小建表 SQL先立主数据再立单据。供应商表和采购订单表是整个系统的地基字段设计错了后面全是补丁。-- 供应商主数据资质有效期是制度里最常见的硬约束 CREATE TABLE supplier ( id BIGSERIAL PRIMARY KEY, supplier_code VARCHAR(32) NOT NULL UNIQUE, -- 供应商编码对账与合同引用 supplier_name VARCHAR(128) NOT NULL, credit_code VARCHAR(32), -- 统一社会信用代码 qual_expire_at DATE NOT NULL, -- 资质到期日过期不可下单 status SMALLINT NOT NULL DEFAULT 1, -- 1 正常 2 冻结 3 黑名单 created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT chk_supplier_status CHECK (status IN (1, 2, 3)) ); -- 采购订单主表金额用 numeric绝不用 float CREATE TABLE purchase_order ( id BIGSERIAL PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, -- 单号业务侧可见 source_req_id BIGINT NOT NULL, -- 来源需求单幂等键的一部分 supplier_id BIGINT NOT NULL REFERENCES supplier(id), org_id BIGINT NOT NULL, -- 采购组织决定数据可见范围 total_amount NUMERIC(18,2) NOT NULL, currency CHAR(3) NOT NULL DEFAULT CNY, status VARCHAR(16) NOT NULL DEFAULT DRAFT, created_by BIGINT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT chk_po_amount CHECK (total_amount 0), CONSTRAINT chk_po_status CHECK ( status IN (DRAFT,SUBMITTED,APPROVING,APPROVED,REJECTED,CLOSED) ), CONSTRAINT uk_po_source UNIQUE (source_req_id, supplier_id) -- 防重复下单 );这份 DDL 里有三处是踩过坑才加上的。第一金额必须用NUMERIC(18,2)用FLOAT会在多行汇总时出现 0.01 的偏差三单匹配时对不上。第二uk_po_source这个唯一约束是幂等控制的核心同一个需求单对同一供应商只能生成一张订单接口重试时数据库直接兜住。第三状态用字符串而不是数字枚举排障时肉眼可读代价是索引略大采购系统的数据量级完全撑得住。2.3 审批阈值别写死在代码里制度参数表怎么建制度会改代码不该跟着改。金额阈值、审批层级、付款比例上限这类数字一律进参数表由采购管理岗维护并在变更时留操作日志。参数键含义示例值变更影响po.approval.level1.max一级审批金额上限50000影响新提单不回溯历史单po.approval.level2.max二级审批金额上限500000同上po.approval.level3.max三级审批金额上限5000000超出走集团审批supplier.qual.warn.days资质到期预警天数30影响提醒任务payment.ratio.max预付款比例上限0.3影响付款申请校验参数表建议带上effective_from和effective_to两个时间字段审批时按单据提交时间取当时生效的版本。这样制度调整不会污染在途单据审计追溯时也能回答「这笔单当时按哪版规则批的」。3. 采购分级审批把审批链生成写成可测的规则函数3.1 金额区间与组织维度的双层规则建模分级审批最常见的建模错误是只按金额分层。真实制度里通常还有组织维度分公司权限内的单子分公司批超出部分上收到集团同一金额在采购组织和行政组织下的审批人还不一样。所以规则要拆成两层——先按组织定位审批域的边界再按金额在域内定位层级。金额区间元审批节点会签要求备注0 ~ 50,000部门负责人单人系统内闭环50,000 ~ 500,000部门负责人 采购总监顺序审批后一级可见前一级意见500,000 ~ 5,000,000上述 分公司总经理顺序审批需附比价记录5,000,000 以上上述 集团采购委员会会签需附合同草案审批链生成必须是纯函数输入金额、组织路径、参数快照输出一串有序节点。纯函数意味着可单测、可回放、可在审批流卡住时重算比对。反过来如果把审批链生成写在 Controller 里顺手查库拼装一旦出现「为什么这单少了一级审批」的问题基本只能靠日志考古。3.2 用 Python 生成审批链的最小实现from decimal import Decimal from dataclasses import dataclass dataclass class ApprovalNode: role: str # 审批角色编码 level: int # 层级用于顺序控制 need_sign: bool False # 是否需要会签 def build_chain(amount: Decimal, org_path: list[str], params: dict) - list[ApprovalNode]: amount: 订单含税总额org_path: 如 [集团,华南分公司,采购部] chain [ApprovalNode(roleDEPT_HEAD, level1)] l1 Decimal(params[po.approval.level1.max]) l2 Decimal(params[po.approval.level2.max]) l3 Decimal(params[po.approval.level3.max]) if amount l1: chain.append(ApprovalNode(rolePROCURE_DIRECTOR, level2)) if amount l2: # 组织维度分公司单上收到总经理集团本部单直接进委员会 if len(org_path) 2 and org_path[0] 集团: chain.append(ApprovalNode(roleBRANCH_GM, level3)) if amount l3: chain.append(ApprovalNode(roleGROUP_COMMITTEE, level4, need_signTrue)) # 审批人与提单人同一人时向上追加一级满足“不得自审” return chain这段代码有两个设计点值得说明。参数params从参数表按单据提交时间取出快照传入函数内部不查库保证同一输入永远得到同一输出方便回放比对。层级level用于顺序审批控制引擎只在低层级全部通过后才激活下一层need_signTrue的节点需要全部委员表决任一反对即整链驳回。org_path的长度判断是个简化写法真实场景里应该由组织维度的规则表决定别硬编码字符串比较。「不得自审」这条在代码里注释了但没展开正确做法是在生成后做一次冲突检测如果链上某节点的候选审批人集合包含提单人本人就把该节点替换为其上级角色并写一条approval_conflict_log这个日志在审计时非常有用。3.3 驳回、加签、超时状态机最容易漏的三个回写审批流上线后真正出问题的从来不是正常路径而是这三件事驳回到哪一级、加签后原节点怎么算、超时后单据算什么状态。迁移动作起始状态目标状态关键回写提交DRAFTSUBMITTED写规则快照、生成审批链首节点通过SUBMITTEDAPPROVING激活下一节点、记时长中段驳回APPROVINGREJECTED保留已通过节点的意见不清空退回修改APPROVINGDRAFT清空审批链重提时重新生成加签APPROVINGAPPROVING原节点标记为 DELEGATED不覆盖超时APPROVINGAPPROVING只发提醒不改状态核心原则是驳回保留意见、退回清空链路。很多系统把两者合成一个动作结果是重新提交时旧审批记录还在审计时无法区分「这单批了几轮」。超时只提醒不改状态是因为采购单据一旦被系统自动终止重新走流程的成本远高于继续挂着等人工处理。加签用DELEGATED标记而不是删除原节点保证审批链的完整时间线可导出。4. 询比价、订单落库与三单匹配的实战链路4.1 询比价数据模型与最低价校验 SQL制度里几乎必有「三家以上比价取最低价」这类条款。落地方式是把报价明细拆成独立表一张询价单对多行报价用窗口函数一次算出排名和最低价同时把异常标出来。-- 找出每个询价行里报价最低的供应商并标记是否存在围标嫌疑报价完全相同 SELECT q.quote_no, q.item_id, q.supplier_id, q.price, RANK() OVER (PARTITION BY q.item_id ORDER BY q.price ASC) AS price_rank, COUNT(*) OVER (PARTITION BY q.item_id, q.price) AS same_price_cnt, CASE WHEN COUNT(*) OVER (PARTITION BY q.item_id, q.price) 1 THEN CHECK_BUNDLING ELSE OK END AS risk_flag FROM quote_detail q WHERE q.invalid_flag 0 AND q.quote_no :quote_no; -- 绑定参数避免拼接逻辑说明PARTITION BY q.item_id让每个物料行独立排名price_rank 1即最低价中标候选same_price_cnt统计同一物料下价格完全相同的报价条数大于 1 时打上CHECK_BUNDLING标记交给采购主管人工复核——这是同源报价的一种典型特征。参数:quote_no必须用绑定变量不要字符串拼接quote_no否则单号里的特殊字符会直接变成注入点。执行计划上quote_detail表建议在(quote_no, item_id, price)上建联合索引几十万行量级下排序不会走磁盘。4.2 采购订单生成与幂等控制从审批通过的询价单生成采购订单是接口重试最容易出重复单的环节。除了前面提到的唯一约束应用层也要做一次前置判断把错误在事务开始前拦掉。def create_po_from_quote(session, req_id: int, supplier_id: int, items: list[dict]): # 前置幂等同一需求单 同一供应商只允许一张有效订单 exist session.execute( SELECT order_no FROM purchase_order WHERE source_req_id :r AND supplier_id :s AND status CANCELLED, {r: req_id, s: supplier_id} ).scalar() if exist: return exist # 直接返回已有单号不报错方便前端重试 total sum(Decimal(str(i[price])) * i[qty] for i in items) if total 0: raise ValueError(订单金额必须大于 0) order_no fPO{datetime.now():%Y%m%d}{random.randint(100000, 999999)} # 落库交给唯一约束 uk_po_source 兜底捕获唯一键冲突后回查 ... return order_no参数说明req_id是需求单主键supplier_id是中标供应商两者组合构成业务幂等键items每项含price与qty金额用Decimal(str(...))转换以避免浮点误差。返回值设计成「已存在也返回单号」是为了让调用方重试时行为一致——接口重试拿到 500 然后前端报错是采购系统里最常见的客服投诉来源。数据库层的唯一约束是最后一道防线捕获唯一键冲突后回查一次即可返回正确单号。4.3 三单匹配订单、入库、发票差异定位结算环节的制度要求通常是「订单、入库单、发票三者一致方可付款」。差异定位不要写成三个 if 嵌套用一次全连接把差异行摊平输出更清晰。-- 三单匹配以订单行为基准找出数量或金额不一致的行 SELECT o.order_no, o.item_id, o.qty AS order_qty, COALESCE(g.qty, 0) AS grn_qty, -- 入库数量 COALESCE(i.qty, 0) AS inv_qty, -- 发票数量 o.amount - COALESCE(i.amount, 0) AS amount_diff, CASE WHEN COALESCE(g.qty,0) o.qty THEN SHORT_RECEIPT WHEN COALESCE(i.qty,0) COALESCE(g.qty,0) THEN INVOICE_MISMATCH WHEN ABS(o.amount - COALESCE(i.amount,0)) 0.01 THEN AMOUNT_DIFF ELSE MATCHED END AS match_result FROM po_item o LEFT JOIN grn_item g ON g.po_item_id o.id LEFT JOIN invoice_item i ON i.po_item_id o.id WHERE o.order_no :order_no;三个判定分支的顺序很重要先看入库是否短收短收优先于发票不一致——因为短收是收货环节的事实问题发票问题是后续环节先解决前者可以让后者的差异自然消失。金额比较用 0.01而不是 0是为了容忍分位舍入numeric类型在跨币种折算后偶尔会差一分。LEFT JOIN保证即使没入库、没开票的订单行也会出现在结果里这对「款项挂账挂太久」的催办很有用。5. 制度数字化的进阶版本对齐与越权采购巡检5.1 制度版本与系统规则的映射表制度修订是常态系统最怕的是「制度改了但规则没改」或者「规则改了但没人知道依据是哪一版」。解决办法是建一张映射表把制度文档的条款号、版本号和系统里的规则 ID、生效时间串起来。制度版本条款号系统规则 ID生效时间变更说明V2023-01第 4.2 条rule_po_approval_l22023-01-01二级审批上限 50 万V2024-03第 4.2 条rule_po_approval_l22024-03-01上限下调至 30 万V2024-03第 6.1 条rule_pay_ratio2024-03-01预付款比例上限 30%有了这张表规则冲突时可以直接回答「这版规则来自哪一条」。维护成本也不高制度发布时由采购管理岗在后台勾选受影响条款系统自动把对应规则的effective_from设为发布日旧版本自动失效但不删除。审批时按单据提交时间取规则快照历史单据永远按当时的规则解释。5.2 用巡检 SQL 抓拆单与越权采购制度落地后最隐蔽的漏洞是拆单把一笔 60 万的采购拆成两笔 30 万绕开第二级审批。这种行为的特征是同一供应商、同一需求人、短时间窗口内多笔金额接近阈值的订单。定时跑一条聚合查询就能筛出来。-- 拆单巡检同供应商 同提交人 7 天内多笔订单合计超过一级阈值 SELECT p.created_by, p.supplier_id, DATE_TRUNC(day, p.created_at) AS order_day, COUNT(*) AS order_cnt, SUM(p.total_amount) AS sum_amount FROM purchase_order p JOIN sys_rule_param r ON r.param_key po.approval.level1.max WHERE p.created_at now() - INTERVAL 7 days AND p.status IN (APPROVED, CLOSED) GROUP BY p.created_by, p.supplier_id, DATE_TRUNC(day, p.created_at) HAVING SUM(p.total_amount) (r.param_value::numeric) AND COUNT(*) 1 ORDER BY sum_amount DESC;参数上7 天窗口和一级阈值都是可调的窗口太短抓不到跨周拆单太长会淹没在正常高频采购里一般先用 7 天试跑一周看误报率。status只统计已通过和已关闭的单草稿和被驳回的不算。阈值从参数表读避免巡检脚本自己硬编码一个数字后跟审批规则脱节。巡检结果建议落到一张日报表并推给采购稽核岗人工复核而不是自动冻结供应商——误判冻结造成的业务中断比漏掉一笔拆单的代价更大。类似的巡检还能扩展到「合同过期仍下单」「资质过期供应商中标」「预付款比例超限」写法和上面这条几乎一致差别只在关联表和阈值参数。把这几条挂到每日定时任务里跑出的差异行直接推给对应岗位比事后翻 PDF 逐条对快得多也让制度从一份静态文档变成每天在跑的约束。本文还有配套的精品资源点击获取
返回列表