我正在设计特许经营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)
收到的所有发票付款都存储在Payments
表格中(例如现金,信用卡,支票,银行存款)。如果可以从Invoices
表中推断出与给定作业的发票相关的所有收入,那么在Payments
表中存储“付费”状态是否有意义?
我可以通过将状态更改以及发票总额和审核用户存储在发票修订表中来跟踪发票的修订(请参阅上面的InvoiceRevisions
),但跟踪发票行修订表感觉很难维护。思考? 修改订单项应该是不可变的。这适用于“草稿”发票。
存储发票数据时,如何纳入销售税(或SA中的14%增值税)?
编辑:很好的反馈,伙计们。 发票和发票行根据定义是不可变的,因此跟踪更改是不明智的。但是,“草稿”发票必须可以由多个人编辑(例如,经理在技术人员创建发票后应用折扣)才能发布...
......限制在一个方向上改变?
答案 0 :(得分:51)
我的建议是大约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 (...)
这些是您的基本“草稿”表格。他们可以改变。要跟踪更改,您应创建历史记录表,其中包含原始Orders
和OrderDetails
表中的所有列,以及上次修改的用户,日期和修改类型的审计列(插入,更新或删除)。
正如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
- 如果你还记得的话,我们不会允许在它离开之后进行更改活跃状态。
这正确地代表了现实世界中正在发生的事情。发送/过帐发票后,将无法收回。它就在那里。如果您要取消它,您必须向A / R(如果您的系统支持此类事情)或作为负面发票发布撤消以满足您的财务报告。如果这样做,您实际上可以查看发生的事情,而无需深入了解每张发票的审计历史记录;你只需要查看发票本身。
仍有一个问题,即开发人员必须记得在将订单状态作为发票过帐后更改订单状态,但我们可以通过触发器来解决此问题:
CREATE TRIGGER tr_Invoices_UpdateOrderStatus
ON Invoices
FOR INSERT
AS
UPDATE Orders
SET OrderStatus = 2
WHERE OrderID IN (SELECT OrderID FROM inserted)
现在您的数据对于粗心的用户甚至是粗心的开发人员都是安全的。发票不再含糊不清;你不必担心漏洞,因为有人忘了查看发票状态,因为没有状态。
所以只是重新总结并解释其中的一些问题:为什么我只是因为某些发票历史而遇到了所有麻烦?
因为尚未发布的发票不是真正的交易。它们是交易“状态” - 正在进行的交易。它们不属于您的交易数据。通过像这样保持它们分开,你将解决很多潜在的未来问题。
免责声明:这一切都来自我的个人经历,我还没有看到世界上的每个发票系统。我无法100%确定这是否适合您的特定应用。我只能通过混合状态数据和交易数据来重申我从“未决”发票概念中看到的大黄蜂问题。
与在互联网上找到的所有其他设计一样,您应该将此作为一种可能的选项进行调查,并评估它是否真的适合您。
答案 1 :(得分:7)
通常,发票行不会更改。即订单(采购订单或工单)成为发票。一旦发票开具,可以取消发票,或者可以应用付款和贷记凭证,但这通常是关于它的。
您的情况可能略有不同,但我认为这是通常的约定 - 毕竟,当您收到xyz发票时,您不希望文档所基于的数据以任何方式被更改。
根据我的经验,税收存储在发票级别并在发票开具时确定。
至于订单在成为发票之前发生变化,通常我看到的不比基本的数据库级审计复杂 - 通常应用程序不会将该历史记录暴露给用户。
如果你想要一个相对领域不可知的直接审计跟踪,你可以查看AutoAudit - 一个基于触发器的审计跟踪。
我们通常没有“草稿发票”。这很诱人,因为订单和发票之间有很多相似之处。但实际上,最好是在单独的表格中没有成为发票的订单。发票往往会有一些差异(即状态变化实际上是从一个实体到另一个实体的转换),有时参考完整性,您实际上只希望加入“真实”发票。
所以我们通常总是有PurchaseOrder,PurchaseOrderLine,Invoice和InvoiceLine。在某些情况下,我认为PO方面的行为更像是购物车 - 价格没有存储,并且浮动产品表和其他情况,它们更像是价格报价,一旦传送到客户。在查看业务工作流程和要求时,这些细微之处可能很重要。
答案 2 :(得分:3)
为什么不创建要审核的表的副本而不是原始表创建的triggres会在每次插入,更新,删除时将行复制到表副本?
触发器通常看起来像这样:
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
答案 3 :(得分:3)
我同意Aaronaught关于发票“不变性”的评论。
如果您接受该建议,那么我会考虑将“待审核”,“已批准”和“无效”作为状态。 “待审核”就是这样。 “已批准”被认为是正确的,并由客户支付。 “无效”只是:发票不再有效,且不由客户支付。然后,您可以从Payments
中的记录中推断出是否全额支付了发票,并且您不会重复信息。
除此之外,您的修订建议没有真正的问题。
您可以在InvoiceLines
中将税收仅作为另一条记录。