sql server-发票,发票行和修订的数据库设计

我正在为一家特许经营的CRM设计一个关系数据库的第二次主要迭代(需要大量重构),我需要有关最佳数据库设计实践的帮助,以存储作业发票和发票行,并对每项所做的任何更改都具有很强的审计追踪能力 发票。

当前模式

InvoiceRevisions桌子

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))

InvoiceRevisions Table

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.存储发票的“已付款”或“待处理”状态是否明智?

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

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

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

3.税收

存储发票数据时,应如何计入营业税(或SA中的14%增值税)?


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

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

  1. 草案
  2. 发行
  3. 虚空

...必须朝一个方向改变?

4个解决方案
53 votes

我的建议来自必须使用别人设计的发票系统后端大约4年的经验:不要在发票上拥有“挂起”状态。 它会让你发疯。

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

您可以使用内置的未决过滤器创建Order视图,但这可以解决问题。 有人会忘记使用视图而不是表格。

待处理的发票不是发票。 在问题注释中正确地将其作为草稿(或命令,请求等,所有相同的概念)陈述。 能够修改这些草稿的需求是绝对可以理解的。 所以这是我的建议。

首先,创建草稿表(我们将其称为Order):

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 (...)

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

正如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%地确定这适合您的特定应用。 我只能重申大黄蜂所带来的一系列问题,这些问题是由于“待处理”发票的概念,状态数据与事务数据的混合而引起的。

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

Aaronaught answered 2020-08-11T21:36:04Z
7 votes

通常,发票行不会更改。 即订单(采购订单或工作订单)成为发票。 发票一旦发出,就可以作废,也可以应用付款和贷项凭证,但这通常就可以了。

您的情况可能有所不同,但是我相信这是通常的惯例-毕竟,当您收到发票xyz时,您不希望文档所依据的数据有任何改变。

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

至于在成为发票之前更改订单,通常我发现没有什么比基本数据库级审核更复杂的-通常应用程序不会向用户公开该历史记录。

如果您希望获得一个相对域无关的简单审计跟踪,则可以考虑使用基于触发器的审计跟踪AutoAudit。

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

因此,我们通常总是具有PurchaseOrder,PurchaseOrderLine,Invoice和InvoiceLine。 在某些情况下,我让采购方的行为更像是购物车-价格没有存储并随产品表浮动,而在另一些情况下,价格更像是报价,一旦将价格传递给商品, 客户。 在查看业务工作流程和需求时,这些细微之处可能很重要。

Cade Roux answered 2020-08-11T21:36:53Z
3 votes

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

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

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
Dean Kuga answered 2020-08-11T21:37:18Z
3 votes

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

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

除此之外,您的修订构想没有任何实际问题。

您可以将税收作为InvoiceLines中的另一个记录。

John answered 2020-08-11T21:37:52Z
translate from https://stackoverflow.com:/questions/2679333/database-design-for-invoices-invoice-lines-revisions