发票、发票行和修订的数据库设计

Posted

技术标签:

【中文标题】发票、发票行和修订的数据库设计【英文标题】:Database design for invoices, invoice lines & revisions 【发布时间】:2011-02-10 09:10:06 【问题描述】:

我正在为特许经营的 CRM 设计关系数据库的第二次主要迭代(进行了大量重构),我需要有关存储工作发票的最佳数据库设计实践的帮助发票行,对每张发票所做的任何更改都有强大的审计跟踪

当前架构

Invoices

InvoiceId (int) // Primary key
JobId (int)
StatusId (tinyint) // Pending, Paid or Deleted
UserId (int) // auditing user
Reference (nvarchar(256)) // unique natural string key with invoice number
Date (datetime)
Comments (nvarchar(MAX))

InvoiceLines

LineId (int) // Primary key
InvoiceId (int) // related to Invoices above
Quantity (decimal(9,4))
Title (nvarchar(512))
Comment (nvarchar(512))
UnitPrice (smallmoney)

修订架构

InvoiceRevisions

RevisionId (int) // Primary key
InvoiceId (int)
JobId (int)
StatusId (tinyint) // Pending, Paid or Deleted
UserId (int) // auditing user
Reference (nvarchar(256)) // unique natural string key with invoice number
Date (datetime)
Total (smallmoney)

架构设计注意事项

1。存储发票的已付款或待处理状态是否明智?

收到的所有发票付款都存储在Payments 表中(例如现金、信用卡、支票、银行存款)。如果与给定工作的发票相关的所有收入都可以从Payments 表中推断出来,那么在Invoices 表中存储“已支付”状态是否有意义?

2。如何跟踪发票行项目修订?

我可以通过将状态更改连同发票总数和审计用户存储在发票修订表中来跟踪发票的修订(参见上面的InvoiceRevisions),但是跟踪发票行修订表感觉很难维护。想法? 编辑:订单项应该是不可变的。这适用于“草稿”发票。

3。税收

在存储发票数据时,我应该如何纳入销售税(或 SA 中的 14% 增值税)?


编辑:反馈很好,伙计们。 根据定义,发票和发票行是不可变的,因此跟踪更改是不明智的。但是,一张“草稿”发票在开具前必须可由多人编辑(例如,经理在技术人员创建发票后应用折扣)...

4。定义和跟踪发票状态的最佳方式?

    草稿 已发布 作废

...被限制在一个方向改变?

【问题讨论】:

这听起来像是一个愚蠢的问题,但为什么要跟踪发票的更改呢?发票往往是不可变的东西,它代表完整的购买/合同;如果上面有错误,则将其作废并创建一个新的。 +1 @Aaronaught:很好,但是必须有某种形式的起草机制,因为发票可以由多个用户更新(例如,技术人员创建它并且经理在将其签发给之前应用折扣)客户)。可以说,这应该由不同的数据结构来处理。我考虑以这种方式跟踪更改的原因是因为发票(在旧模型中)具有状态(待处理、已支付、已删除)并且用户对更新它的人感兴趣(经理开始通过已支付发票跟踪技术人员的绩效,这也是不好的做法)。 有趣的是,您没有 products 表。产品/服务有那么独特吗? 看看这个问题:***.com/questions/163517/… 和 Fowler 的文章martinfowler.com/apsupp/accounting.pdf 由于发票在定义上是不可变的,因此如果订单项更改或稍后添加折扣,我会选择作废发票并创建新发票。 【参考方案1】:

我必须使用某人 else 设计的发票系统后端大约 4 年的建议:不要在发票上设置“待处理”状态。它会让你发疯。

将待处理发票存储为普通发票(带有“待处理”标志/状态)的问题在于,将有数百个操作/报告只应考虑已发布发票,这实际上意味着待处理的每个状态 except。这意味着必须每次检查此状态。单身的。时间。有人会忘记。任何人都需要几周的时间才能意识到这一点。

您可以使用内置的待处理过滤器创建ActiveInvoices 视图,但这只是转移了问题;有人会忘记使用视图而不是表格。

待处理发票不是发票。在问题 cmets 中正确地将其表述为 draft(或订单、请求等,都是相同的概念)。能够修改这些草案的需要是可以理解的,当然。所以这是我的建议。

首先,创建一个草稿表(我们称之为Orders):

CREATE TABLE Orders
(
    OrderID int NOT NULL IDENTITY(1, 1)
        CONSTRAINT PK_Orders PRIMARY KEY CLUSTERED,
    OrderDate datetime NOT NULL
        CONSTRAINT DF_Orders_OrderDate DEFAULT GETDATE(),
    OrderStatus tinyint NOT NULL,  -- 0 = Active, 1 = Canceled, 2 = Invoiced
    ...
)

CREATE TABLE OrderDetails
(
    -- Optional, if individual details need to be referenced
    OrderDetailID int NOT NULL IDENTITY(1, 1)
        CONSTRAINT PK_OrderDetails PRIMARY KEY CLUSTERED,
    OrderID int NOT NULL
        CONSTRAINT FK_OrderDetails_Orders FOREIGN KEY
            REFERENCES Orders (OrderID)
            ON UPDATE CASCADE
            ON DELETE CASCADE,
    ...
)

CREATE INDEX IX_OrderDetails
ON OrderDetails (OrderID)
INCLUDE (...)

这些是您的基本“草稿”表格。它们可以更改。要跟踪更改,您应该创建历史表,其中包含原始 OrdersOrderDetails 表中的所有列,以及最后修改的用户、日期和修改类型(插入、更新、或删除)。

正如 Cade 所提到的,您可以使用 AutoAudit 来自动化大部分流程。

您还需要一个触发器,以防止更新不再有效的草稿(尤其是已过帐并已成为发票的草稿)。保持这些数据的一致性很重要:

CREATE TRIGGER tr_Orders_ActiveUpdatesOnly
ON Orders
FOR UPDATE, DELETE
AS

IF EXISTS
(
    SELECT 1
    FROM deleted
    WHERE OrderStatus <> 0
)
BEGIN
    RAISERROR('Cannot modify a posted/canceled order.', 16, 1)
    ROLLBACK
END

由于发票是两级层次结构,因此您需要一个类似但稍微复杂一点的触发器来获取详细信息:

CREATE TRIGGER tr_OrderDetails_ActiveUpdatesOnly
ON OrderDetails
FOR INSERT, UPDATE, DELETE
AS

IF EXISTS
(
    SELECT 1
    FROM
    (
        SELECT OrderID FROM deleted
        UNION ALL
        SELECT OrderID FROM inserted
    ) d
    INNER JOIN Orders o
        ON o.OrderID = d.OrderID
    WHERE o.OrderStatus <> 0
)
BEGIN
    RAISERROR('Cannot change details for a posted/canceled order.', 16, 1)
    ROLLBACK
END

这可能看起来需要做很多工作,但现在您可以这样做了:

CREATE TABLE Invoices
(
    InvoiceID int NOT NULL IDENTITY(1, 1)
        CONSTRAINT PK_Invoices PRIMARY KEY CLUSTERED,
    OrderID int NOT NULL
        CONSTRAINT FK_Invoices_Orders FOREIGN KEY
            REFERENCES Orders (OrderID),
    InvoiceDate datetime NOT NULL
        CONSTRAINT DF_Invoices_Date DEFAULT GETDATE(),
    IsPaid bit NOT NULL
        CONSTRAINT DF_Invoices_IsPaid DEFAULT 0,
    ...
)

看看我在这里做了什么?我们的发票是原始的、神圣的实体,没有被一些第一天上班的客户服务人员任意更改所玷污。 没有风险在这里搞砸了。但是,如果需要,我们仍然可以找到发票的整个“历史记录”,因为它链接回其原始 Order - 如果您还记得的话,我们不允许在它离开活动状态后进行更改状态。

这正确地代表了现实世界中正在发生的事情。发票一经发送/过帐,便无法收回。它在外面。如果您想取消它,您必须将冲销过帐到应收帐款(如果您的系统支持这种事情)或作为负发票来满足您的财务报告。如果这样做了,您实际上可以查看发生了什么,而无需深入了解每张发票的审计历史;您只需查看发票本身即可。

仍然存在开发人员必须记住在将订单状态作为发票过帐后更改订单状态的问题,但我们可以通过触发器解决此问题:

CREATE TRIGGER tr_Invoices_UpdateOrderStatus
ON Invoices
FOR INSERT
AS

UPDATE Orders
SET OrderStatus = 2
WHERE OrderID IN (SELECT OrderID FROM inserted)

现在您的数据不会受到粗心的用户甚至粗心的开发人员的影响。并且发票不再模棱两可;您不必担心由于有人忘记检查发票状态而出现的错误,因为 没有状态

所以只是为了重新总结和解释其中的一些内容:为什么我为了一些发票历史而遇到所有这些麻烦?

因为尚未过帐的发票不是真正的交易。它们是交易“状态”——正在进行的交易。它们不属于您的交易数据。通过像这样将它们分开,您将解决很多潜在的未来问题。

免责声明:这一切都来自我的个人经验,我还没有见过世界上每一个的发票系统。我不能 100% 保证这适用于您的特定应用。我只能重申我所看到的由“待处理”发票的概念、状态数据与交易数据混合导致的问题。

与您在互联网上找到的所有其他设计一样,您应该将其作为一种可能的选择进行调查,并评估它是否真的适合您。

【讨论】:

评论后:我不想进入 excruciating 详细信息,但如果您正在跟踪付款,则不需要 IsPaid 列。最好在支付单张发票时应用付款,如果单笔付款涵盖多张发票,则将它们分成多个桶,并维护从付款明细到发票的参考,并包括溢出桶。也就是说,您可能仍希望将 IsPaidPaidDate 列作为非规范化的一种形式,因为未付发票是老化流程的常见输入。 @FreshCode:这取决于您的会计观点和政策。如果有人多付了钱,你必须以某种方式对其进行解释。通常,这会进入溢出存储桶,以针对未来的发票(“商店信用”)应用。如何解释该存储桶取决于您的会计师... 代码示例总是胜出 :) 感谢您提供有见地的反馈。 我同意,开发人员经常忽略状态。为草稿使用不同的表格更安全。 是的四年,即使我确实为未决发票而苦苦挣扎,但我发现它没有用。报告变得更加复杂。【参考方案2】:

通常不会更改发票行。即订单(采购订单或工​​作订单)成为发票。发票一经开出,就可以作废,也可以应用付款和贷项通知单,但通常仅此而已。

您的情况可能有点不同,但我相信这是通常的惯例 - 毕竟,当您收到 xyz 发票时,您不希望文档所基于的数据会以任何方式被更改。

就税收而言,根据我的经验,通常存储在发票级别并在发票过帐时确定。

至于订单在成为发票之前发生变化,通常我没有看到比基本的数据库级审计更复杂的了 - 通常应用程序不会向用户公开该历史记录。

如果您想要一个与域无关的直接审计跟踪,您可以查看AutoAudit - 一个基于触发器的审计跟踪。

我们通常没有“草稿发票”。这很诱人,因为订单和发票之间有很多相似之处。但实际上,最好将尚未成为发票的订单放在单独的表格中。发票往往有一些差异(即状态变化实际上是从一个实体到另一个实体的转换),并且有时您真的只希望将事物加入“真实”发票。

所以我们通常总是有 PurchaseOrder、PurchaseOrderLine、Invoice 和 InvoiceLine。在某些情况下,我让 PO 端的行为更像是一个购物车 - 价格没有存储并随产品表浮动,而在其他情况下,它们更像是价格报价,一旦它们被传输到必须兑现客户。在查看业务工作流程和需求时,这些细微之处可能很重要。

【讨论】:

【参考方案3】:

为什么不只创建要审核的表的副本,而不是在原始表上创建触发器,以便在每次插入、更新、删除时将行复制到表副本?

触发器通常看起来像这样:

CREATE TRIGGER Trg_MyTrigger
   ON  MyTable
   AFTER UPDATE,DELETE
AS
BEGIN
    -- SET NOCOUNT ON added to prevent extra result sets from
    -- interfering with SELECT statements.
    SET NOCOUNT ON;

    INSERT INTO [DB].[dbo].[MyTable_Audit]
           (Field1, Field2)
     SELECT Field1, Field2
    FROM DELETED
END
GO

【讨论】:

+1 最初想在代码中执行此操作以将我的数据与 ORM 分开,但后来我想起我并不害怕 SQL,也不太可能停止使用 SQL Server :)跨度> 【参考方案4】:

我同意 Aaronaught 关于发票“不变性”的上述评论。

如果您接受该建议,那么我会考虑将“待审核”、“已批准”和“已作废”作为状态。 “待审核”就是这样。 “已批准”被认为是正确的,并由客户支付。 “作废”就是:发票不再有效,不再由客户支付。然后你就可以从Payments的记录中推断出发票是否已足额支付,而且你没有重复信息。

除此之外,您的修订想法没有真正的问题。

您可以在InvoiceLines 中将税作为另一条记录。

【讨论】:

+1 表示“无效”布尔条目,以跟踪已删除的发票。作废应该是单向的,所以那时真的不需要审计跟踪。 抱歉编辑了我的答案!欣赏赞成票;希望我无论如何改进了答案!错过了您对经理折扣/批准的渴望,因此我也尝试将其纳入。 这可能值得一个单独的问题,但是您会建议如何处理收到的发票随后作废的付款?应该删除这些付款还是创建负现金付款(在Payments 表中)以表示退款? @FreshCode 如果发票在收到付款后作废(但不确定这是否应该被允许),它应该有一个贷方余额,并且将对其应用退款支票以采取余额归零。我想我在某处有一本书介绍了标准会计系统的数据库设计模式。

以上是关于发票、发票行和修订的数据库设计的主要内容,如果未能解决你的问题,请参考以下文章

设计具有混合粒度的事实表

为应收账款寻找简单的数据库设计[关闭]

订单/发票/付款数据库建模

产品场景梳理方法与基于场景的产品设计方法

使用 JOIN 而不是 NOT IN 优化 SQL 查询

验证码识别,发票编号识别(转)