前年给一家做五金件的客户做系统梳理,财务部给我看了一个月的对账邮件:三百二十七封,涉及四十一家供应商,附件有 Excel、有扫描件、有手机拍的送货单。三个会计核了六天,核出十一笔差错,其中四笔是同一张单在两个附件里金额不一样。
财务主管说得很直白:不是人不行,是每个月都要重新把同一件事拼一遍。我当时给的建议不是"上个门户",而是先把对账的对象从邮件变成单据。门户只是单据的载体,载体换了,对象没换,照样乱。
后来这套东西做了一年,对账周期从六天压到一天半,差错从每月十一笔降到两三笔。更重要的变化是争议有了归属:哪张单、谁提的、什么理由、什么时候解决的,都能查。这篇把当时的设计思路和数据模型写下来。
一、邮件对账的三个结构性缺陷
1.1 数据在正文和附件里,不在结构里
邮件的正文是给人看的,不是给程序看的。"本月共计发出 47 单,其中 3 单有退货,退货部分已在 9 月 12 日那封邮件里说明"——这句话里至少有三个数字,但它们在文本里,取不出来。
附件更麻烦。同一份对账单,供应商发来的格式每家的都不一样,有的一行一单,有的一行多单合并,有的把运费单列,有的含在单价里。要核对,只能人眼看,靠经验判断这行对应的是哪张单。
1.2 没有共同的主键
我们这边认的是采购单号,供应商认的是他们的发货单号。两套编号在邮件里各说各的,靠"金额加日期"去猜。金额一样、日期一样的单子一个月里总有几对,猜错一次,后面全歪。
这个问题的本质不是编码规则不统一,是双方没有约定一个共同的识别物。约定共同主键这件事,比开发任何功能都重要,因为它是所有自动核对的前提。
1.3 争议没有留痕
邮件里的争议是这样的:财务发一封"这单金额不对",供应商回一封"我们查一下",然后再回一封"是按新价格算的"。三封邮件散在收件箱里,一个月后要复盘,得靠关键词搜。
更常见的情况是争议根本没被记录,只是在电话里说了一句"这单先挂着"。挂到月底,双方都忘了为什么挂,只好按一方的主张结掉。
二、门户不是把邮件搬到网页上
2.1 三条线:订单、收货、发票
对账之所以对不上,是因为三方各自记了一笔:采购说订了多少,仓库说收了多少,供应商说发了多少并开了多少票。这三条线在邮件时代各自独立,门户要做的是把它们放在同一张对账单上。
我们的做法是先固定"以收货为准"这条原则。有收货记录的数量按收货算,没有收货记录的发货不算,有收货没开票的挂账。这条原则写进供应商协议,比任何技术方案都管用,因为它消除了"以谁为准"的争论。
2.2 谁生成、谁确认
对账单由采购方生成,供应商确认,这个方向不能反。反过来让供应商生成,采购方要核的内容就从"确认数字"变成"确认对方的口径",工作量反而更大。
确认动作要有时间戳和操作人。我们要求供应商用子账号登录,一个企业可以有多个子账号,但确认动作只允许主账号做。这样既方便日常查询,又保证确认这件事有人负责。
2.3 差异要能挂在单据上
差异不是一个总数,是挂在某张单上的一个金额。这句话听起来像废话,实际做的时候很容易做错:很多系统只记"本月差异 3800 元",不记这 3800 是哪几张单加起来。
我们的处理是差异必须落到明细行,行上带原因码:单价不符、数量不符、运费争议、退货未冲、票据未到。原因码固定六七个,不允许自由填写。自由填写的字段,三个月后就会变成几十种说法,统计不了。
三、数据模型
3.1 主体与单据
供应商表存主体信息,结算日放在这里,因为不同供应商的结算日不同。单据表存三类单据:采购单、收货单、发票,用 doc_type 区分,同一张单在系统里只有一行,靠供应商加类型加单号做唯一约束。
唯一约束这一条很重要。邮件时代同一张发票被转发五次,在系统里就是五条记录。有了唯一约束,重复导入会被挡住,导入程序据此写"已存在则更新",而不是"插入新行"。
3.2 对账单与明细
对账单按供应商加期间唯一,一个供应商一个月一张。明细行指向单据,带金额和差异金额。这样一张对账单既能汇总,也能下钻到具体单据。
期间用字符型存年月,不用日期范围。用日期范围的话,每张单属于哪个期间要靠计算,跨月发货、次月开票的情况会让归属变得含糊。字符型的期间是人为划定的,划完就固定,不会有歧义。
CREATE TABLE supplier (
id bigserial PRIMARY KEY,
code text NOT NULL UNIQUE,
name text NOT NULL,
settle_day smallint NOT NULL DEFAULT 25,
status text NOT NULL DEFAULT 'ACTIVE'
);
CREATE TABLE supplier_doc (
id bigserial PRIMARY KEY,
supplier_id bigint NOT NULL REFERENCES supplier(id),
doc_type text NOT NULL CHECK (doc_type IN ('PO','RECEIPT','INVOICE')),
doc_no text NOT NULL,
doc_date date NOT NULL,
amount numeric(14,2) NOT NULL,
source text NOT NULL,
status text NOT NULL DEFAULT 'OPEN',
UNIQUE (supplier_id, doc_type, doc_no)
);
CREATE TABLE statement (
id bigserial PRIMARY KEY,
supplier_id bigint NOT NULL REFERENCES supplier(id),
period char(7) NOT NULL,
total_amount numeric(14,2) NOT NULL DEFAULT 0,
status text NOT NULL DEFAULT 'DRAFT'
CHECK (status IN ('DRAFT','SENT','CONFIRMED','DISPUTED')),
sent_at timestamptz,
confirmed_at timestamptz,
UNIQUE (supplier_id, period)
);
CREATE TABLE statement_line (
id bigserial PRIMARY KEY,
statement_id bigint NOT NULL REFERENCES statement(id),
doc_id bigint NOT NULL REFERENCES supplier_doc(id),
amount numeric(14,2) NOT NULL,
diff_amount numeric(14,2) NOT NULL DEFAULT 0,
reason_code text,
UNIQUE (statement_id, doc_id)
);
CREATE TABLE dispute (
id bigserial PRIMARY KEY,
line_id bigint NOT NULL REFERENCES statement_line(id),
raised_by text NOT NULL,
reason_code text NOT NULL,
note text,
resolved_at timestamptz,
resolution text
);
3.3 差异与处理记录
争议表挂在明细行上,不挂在对账单上。挂在对账单上,一个争议就对应整张单,说不清是哪几笔。挂在明细行上,一个争议对应一张单据,金额和原因都能对上。
处理结果也存两段:resolved_at 是时间,resolution 是结论。结论不写"已解决",要写具体动作,比如"按新单价重开票"、"退货已入账,下期冲减"。写具体动作的目的是让下个月的人能看懂。
四、上线节奏
4.1 先做只读
上线头一个月只做一件事:把采购单、收货单、发票导进去,让供应商能登录看到自己的单据。不提确认,不提对账,只让人能查。这一步的目的是让供应商愿意登录,而不是马上改变流程。
只读阶段我们拉了使用率,四十一家供应商里有三十四家在上线那周登录过。剩下七家,五家是老板自己管账、不上网,两家是规模太小、没有固定的人。这七家决定了后面的兜底方案。
4.2 再上确认
第二个月开确认功能,只对登录过的三十四家开放。确认窗口设五天,五天不确认自动进入争议状态,采购方介入。这个规则推下去遇到的阻力比预想小,因为供应商也想早点确认早点拿钱。
确认率从六成涨到九成用了三个月。涨不上去的那一个月,我们查了原因,是通知发到了子账号,主账号没看到。改成主账号必收、子账号可选之后,确认率就上来了。
4.3 遗留供应商的兜底
那七家不上网的,不能硬推。我们的做法是让采购员代录,代录的记录标记 source 为人工,并且在门户上仍然生成一张对账单,打印出来盖章回传。纸还在,但它对应系统里的一条记录,不再是孤立的一张纸。
这条兜底很重要。做门户常犯的错是假设所有人都会上网,实际上总有一批人不会或者不愿意。给这批人留一条人工通道,比强推的成功率高得多。
五、复盘
5.1 数字上的变化
对账周期从六天到一天半,参与人数从三个减到一个半。差错从每月十一笔降到两三笔,剩下的差错基本都在代录的那七家里。争议平均解决时长从九天到三天,因为责任明确了。
还有个数字没那么显眼:每月邮件数量从三百多封降到八十封左右。剩下的八十封,多是价格调整、合同变更这类确实需要人来沟通的事。这说明系统把机械的部分接住了,留下的都是需要判断的。
5.2 组织上的两个前提
一个是采购部得认"以收货为准"。这条不认,对账单永远对不上,因为两边算的基数不同。另一个是财务得愿意把确认权放给供应商,而不是自己核完再通知。
这两条都不是技术问题。我在项目启动会上花了两个小时只讨论这两条,当时有人觉得跑题。后来回过头看,那两个小时省掉了后面三个月的扯皮。
常见问题
问:供应商不愿意用门户,怎么办?
答:先给好处再提要求。只读阶段让他们能查单据、能下载对账单,这是好处;确认阶段才提要求。另外把确认和对账周期绑定,确认快的先排款,比发通知有用。
问:同一张发票被重复开票,系统怎么发现?
答:靠供应商加类型加单号的唯一约束。重复导入时程序识别到冲突,转人工核。另外加一条金额校验,同一供应商同一期间出现两张金额完全相同的发票,标记待核。
问:对账单生成后供应商改了数,怎么处理?
答:不允许直接改。已发出的对账单锁定,要改只能作废重开,作废记录保留。这样能看出一个期间被改过几次,改得频繁的供应商值得单独看一眼。
问:多币种的供应商怎么对账?
答:按单据币种分别汇总,对账单上分币种列示,不合并成一个本币数。汇率在结算日锁定,锁定后写进对账单,避免确认之后汇率波动导致重新核对。