52

我正在为特许经营的 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表格中(例如现金、信用卡、支票、银行存款)。Invoices如果可以从表中推断出与给定工作发票相关的所有收入,那么在表中存储“已支付”状态是否有意义Payments

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

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

3. 税收

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


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

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

  1. 草案
  2. 发布
  3. 作废

...被迫向一个方向改变?

4

4 回答 4

68

我不得不使用其他人设计的发票系统的后端大约 4 年的建议:不要在发票上具有“待定”状态。它会让你发疯。

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

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

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

首先,创建一个草稿表(我们称之为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- 如果您还记得的话,我们不允许在发票离开活动状态后对其进行更改。

This correctly represents what's going on in the real world. Once an invoice is sent/posted, it can't be taken back. It's out there. If you want to cancel it, you have to post a reversal, either to an A/R (if your system supports that sort of thing) or as a negative invoice to satisfy your financial reporting. And if this is done, you can actually see what happened without having to dig into the audit history for each invoice; you just have to look at the invoices themselves.

There's still the problem that developers have to remember to change the order status after it's been posted as an invoice, but we can remedy that with a trigger:

CREATE TRIGGER tr_Invoices_UpdateOrderStatus
ON Invoices
FOR INSERT
AS

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

Now your data is safe from careless users and even careless developers. And invoices are no longer ambiguous; you don't have to be worry about bugs creeping in because somebody forgot to check the invoice status, because there is no status.

So just to re-summarize and paraphrase some of this: Why have I gone to all this trouble just for some invoice history?

Because invoices that haven't been posted yet aren't real transactions. They are transaction "state" - transactions in progress. They don't belong with your transactional data. By keeping them separate like this, you will solve a lot of potential future problems.

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

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

于 2010-04-21T00:20:02.963 回答
7

通常发票行不会更改。即订单(采购订单或工​​作订单)成为发票。一旦开出发票,它就可以作废,或者可以应用付款和贷项通知单,但这通常是关于它的。

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

至于税收,通常以我的经验,存储在发票级别并在发票过帐时确定。

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

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

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

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

于 2010-04-20T23:24:35.230 回答
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
于 2010-04-20T23:24:13.243 回答
3

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

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

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

您可以将税收作为另一条记录包含在InvoiceLines.

于 2010-04-20T23:33:28.567 回答