52

フランチャイズの CRM 用にリレーショナル データベースの 2 回目の主要なイテレーションを設計しています (多くのリファクタリングを使用)。ジョブ インボイスインボイス ラインを格納するためのデータベース設計のベスト プラクティスと、それぞれに加えられた変更の強力な監査証跡についてサポートが必要です。請求書。

現在のスキーマ

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. 無効化

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

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

これらは基本的な「ドラフト」テーブルです。それらは変更できます。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,
    ...
)

私がここで何をしたかわかりますか?私たちの請求書は手付かずの神聖な存在であり、入社初日の顧客サービス担当者による恣意的な変更によって汚されることはありません。ここで失敗するリスクはありません。ただし、必要に応じて、元の請求書にリンクされているため、請求書の「履歴」全体を確認することができ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 つの可能なオプションとして調査し、それが実際に機能するかどうかを評価する必要があります。

于 2010-04-21T00:20:02.963 に答える
7

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

あなたの状況は少し異なるかもしれませんが、これは通常の慣例だと思います。結局のところ、請求書 xyz を受け取ったとき、文書の元になったデータが何らかの形で変更されることはないと思います。

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

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

比較的ドメインにとらわれない簡単な監査証跡が必要な場合は、トリガーベースの監査証跡であるAutoAuditを調べることができます。

通常、「下書き請求書」はありません。注文と請求書の間に多くの類似点があるため、魅力的です。しかし、実際には、請求書になっていない注文は別のテーブルにある方がよいでしょう。請求書にはいくつかの違いがある傾向があり (つまり、状態の変化は実際にはあるエンティティから別のエンティティへの変換です)、参照整合性を使用すると、「実際の」請求書に結合するだけでよい場合があります。

そのため、通常は PurchaseOrder、PurchaseOrderLine、Invoice、および InvoiceLine が常に存在します。場合によっては、PO 側がショッピング カートのように動作するようにしました。価格は保存されず、products テーブルと共にフロートし、その他のケースでは、価格見積もりの​​ように、一度送信されたら尊重する必要があります。クライアント。これらの微妙な点は、ビジネス ワークフローと要件を検討する際に重要になる可能性があります。

于 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 に答える