web-dev-qa-db-ja.com

請求書、請求書明細、改訂版のデータベース設計

フランチャイズのCRM(多くのリファクタリングを含む)のリレーショナルデータベースの2番目の主要な反復を設計しており、job invoicesおよびinvoice lines強力な監査証跡各請求書に加えられた変更について。

現在のスキーマ

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%のVAT)をどのように組み込む必要がありますか?


編集:良いフィードバック、みんな。 請求書と請求書の行は定義により不変ですなので、変更の追跡は賢明ではありません。ただし、「ドラフト」請求書は、発行する前に複数の人が編集できる必要があります(たとえば、技術者が請求書を作成した後にマネージャーが割引を適用する)。

4.請求書ステータスを定義および追跡する最良の方法は?

  1. ドラフト
  2. 発行済み
  3. 無効

...一方向に変化するように制約されていますか?

42
Petrus Theron

誰かelseが設計した請求システムのバックエンドで作業しなければならない約4年間の私のアドバイス:請求書のステータスを「保留」にしないでください。それはあなたを狂気に駆り立てます。

保留中の請求書を通常の請求書(「保留中」フラグ/ステータス付き)として保存する場合の問題は、数百のオペレーション/レポートが考慮されることになっていることです投稿済み請求書すべてのステータスexcept保留中。つまり、このステータスを確認する必要がありますすべて。シングル。 time。そして誰かが忘れようとしている。そして、誰もが気づくまでに数週間かかります。

保留中のフィルタを組み込んだActiveInvoicesビューを作成できますが、それは問題をシフトするだけです。誰かがテーブルの代わりにビューを使用することを忘れるでしょう。

保留中の請求書は請求書ではありません。それは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 (...)

これらは基本的な「ドラフト」テーブルです。変更することができます。変更を追跡するには、元の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

請求書は2レベルの階層であるため、詳細については、同様のやや複雑なトリガーが必要です。

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

私がここで何をしたか見てください?私たちの請求書は原始的な神聖な存在であり、実務経験のあるカスタマーサービス担当者によるarbitrary意的な変更によって汚されていません。 リスクなしがここでめちゃくちゃになっています。ただし、必要に応じて、元の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)

これで、データは不注意なユーザーや不注意な開発者からも安全です。また、請求書はあいまいではなくなりました。 is no statusがあるため、誰かが請求書のステータスを確認するのを忘れたため、バグが忍び寄る心配はありません。

それで、これのいくつかを再要約し、言い換えれば、請求書の履歴のためだけにこの問題にすべて行ったのはなぜですか?

まだ投稿されていない請求書実際の取引ではないであるためです。それらはトランザクションの「状態」-進行中のトランザクションです。それらはトランザクションデータに属していません。このように別々に保つことにより、潜在的な将来の問題のalotを解決します。

免責事項:これはすべてmy個人的な経験であり、私は見たことがないevery請求書発行システム世界中。これが特定のアプリケーションに適していることを100%確実に保証することはできません。状態データとトランザクションデータを混在させることにより、「保留中」の請求書の概念に起因する、ホーネットの問題のネストを繰り返すことができます。

インターネットで見つける他のすべてのデザインと同様に、これを1つの可能なオプションとして調査し、実際に機能するかどうかを評価する必要があります。

52
Aaronaught

通常、請求書の行は変更されません。つまり、注文(購入注文または作業注文)が請求書になります。請求書が発行されると、その請求書は無効になるか、支払いとクレジットメモを適用できますが、それは通常それに関するものです。

あなたの状況は少し異なるかもしれませんが、これは通常の慣習だと思います-結局、請求書xyzを受け取ったとき、ドキュメントの基になっているデータが何らかの方法で変更されることを期待していません。

私の経験では通常、税に関しては、請求書レベルで保存され、請求書の転記時に決定されます。

請求書になる前に注文が変わる限り、通常、基本的なデータベースレベルの監査よりも複雑なものはありません。通常、アプリケーションはその履歴をユーザーに公開しません。

比較的ドメインに依存しない直接的な監査証跡が必要な場合は、 AutoAudit -トリガーベースの監査証跡を調べることができます。

通常、「ドラフト請求書」はありません。注文と請求書には多くの類似点があるため、魅力的です。しかし、実際には、請求書になっていない注文は別のテーブルに入れる方が良いでしょう。請求書にはいくつかの違いがある傾向があります(つまり、状態の変化は実際には1つのエンティティから別のエンティティへの変換です)。

したがって、通常、PurchaseOrder、PurchaseOrderLine、Invoice、およびInvoiceLineが常にあります。場合によっては、PO側をショッピングカートのように振る舞わせました-価格は保存されず、製品テーブルに浮かびます。また、その他の場合は、価格が見積りに送信されてから、クライアント。これらの微妙さは、ビジネスワークフローと要件を見るときに重要になることがあります。

7
Cade Roux

監査したいテーブルのコピーを作成するだけでなく、元のテーブルでトリガーを作成して、挿入、更新、削除のたびにテーブルコピーに行をコピーするのはなぜですか?

通常、トリガーは次のようになります。

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
Dean Kuga

請求書の「不変性」に関する上記のAaronaughtのコメントに同意します。

そのアドバイスを受け取った場合、ステータスとして「保留中のレビュー」、「承認済み」、および「無効」を検討します。 「保留中のレビュー」はまさにそれです。 「承認済み」は正しいとみなされ、クライアントが支払い可能です。 「無効」とは、請求書がもはや有効ではなく、クライアントが支払うことができないことです。その後、請求書がPaymentsのレコードから全額支払われるかどうかを推測できますが、情報は繰り返しません。

それを除けば、あなたの修正案に本当の問題はありません。

InvoiceLinesの単なる別のレコードとして税を含めることができます。

3
John