web-dev-qa-db-fra.com

Conception de base de données pour les factures, les lignes de facturation et les révisions

Je conçois la 2e itération majeure d'une base de données relationnelle pour le CRM d'une franchise (avec beaucoup de refactoring) et j'ai besoin d'aide sur les meilleures pratiques de conception de base de données pour le stockage factures de travail et lignes de facture avec une forte piste d'audit de toutes les modifications apportées à chaque facture.

Schéma actuel

Invoices Tableau

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 Tableau

LineId (int) // Primary key
InvoiceId (int) // related to Invoices above
Quantity (decimal(9,4))
Title (nvarchar(512))
Comment (nvarchar(512))
UnitPrice (smallmoney)

Schéma de révision

InvoiceRevisions Tableau

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)

Considérations de conception de schéma

1. Est-il judicieux d'enregistrer le statut Payé ou En attente d'une facture?

Tous les paiements reçus pour une facture sont stockés dans une table Payments (par ex. Espèces, carte de crédit, chèque, dépôt bancaire). Est-il utile de stocker un statut "Payé" dans la table Invoices si tous les revenus liés aux factures d'un travail donné peuvent être déduits de la table Payments?

2. Comment suivre les révisions des éléments de ligne de facture?

Je peux suivre les révisions d'un facture en stockant les changements de statut avec le total de la facture et l'utilisateur d'audit dans un tableau de révision de la facture (voir InvoiceRevisions ci-dessus), mais garder une trace d'une table de révision de ligne de facture est difficile à maintenir. Pensées? Modifier: les éléments de campagne doivent être immuables. Cela s'applique à un "projet" de facture.

3. Taxe

Comment dois-je intégrer la taxe de vente (ou la TVA de 14% en SA) lors du stockage des données de facturation?


Edit: Bonne rétroaction, les gars. Les factures et les lignes de facture sont par définition immuables, donc le suivi des modifications n'est pas judicieux. Cependant, une "ébauche" de facture doit être modifiable par plusieurs personnes (par exemple, le gestionnaire applique une remise après que le technicien a créé la facture) avant qu'elle ne soit émise ...

4. La meilleure façon de définir et de suivre l'état de la facture?

  1. Brouillon
  2. Publié
  3. Annulé

... contraint de changer dans une direction?

42
Petrus Theron

Mon conseil d'environ 4 ans d'avoir à travailler avec le back-end d'un système de facturation que quelqu'un sinon a conçu: Ne pas avoir un statut "en attente" sur les factures. Cela vous rendra fou.

Le problème avec le stockage des factures en attente en tant que factures ordinaires (avec un indicateur/statut "en attente") est qu'il y aura des centaines d'opérations/rapports qui ne sont censés prendre en compte - publié factures, qui signifie littéralement chaque état sauf pour en attente. Ce qui signifie que ce statut doit être vérifié chaque. Célibataire. temps. Et quelqu'un va oublier. Et il faudra des semaines avant que quiconque s'en rende compte.

Vous pouvez créer une vue ActiveInvoices avec le filtre en attente intégré, mais cela ne fait que déplacer le problème; quelqu'un oubliera d'utiliser la vue au lieu de la table.

Une facture en attente n'est pas une facture. Il est correctement indiqué dans les commentaires de la question comme un brouillon (ou une commande, une demande, etc., tout de même concept). La nécessité de pouvoir modifier ces ébauches est parfaitement compréhensible. Voici donc ma recommandation.

Créez d'abord un projet de table (nous l'appellerons 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 (...)

Ce sont vos "ébauches" de base. Ils peuvent être modifiés. Pour suivre les modifications, vous devez créer des tables d'historique, qui contiennent toutes les colonnes des tables Orders et OrderDetails d'origine, ainsi que des colonnes d'audit pour le dernier utilisateur modifié, la date et la modification type (insérer, mettre à jour ou supprimer).

Comme le mentionne Cade, vous pouvez utiliser AutoAudit pour automatiser la plupart de ce processus.

Vous souhaiterez également un déclencheur pour empêcher les mises à jour des brouillons qui ne sont plus actifs (en particulier les brouillons qui sont publiés et qui sont devenus des factures). Il est important de garder ces données cohérentes:

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

Étant donné que les factures sont une hiérarchie à deux niveaux, vous avez besoin d'un déclencheur similaire et légèrement plus compliqué pour les détails:

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

Cela peut sembler beaucoup de travail, mais maintenant vous pouvez le faire:

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

Tu vois ce que j'ai fait ici? Nos factures sont des entités vierges et sacrées, non souillées par des changements arbitraires de la part d'un employé du service à la clientèle le premier jour. Il y a pas de risque de vissage ici. Mais, si nous en avons besoin, nous pouvons toujours découvrir tout "l'historique" d'une facture, car elle renvoie à son Order d'origine - qui, si vous vous en souvenez, nous n'autorisons pas les modifications après quitte le statut actif.

Cela représente correctement ce qui se passe dans le monde réel. Une fois qu'une facture est envoyée/postée, elle ne peut pas être reprise. C'est là-bas. Si vous souhaitez l'annuler, vous devez enregistrer une annulation, soit dans un A/R (si votre système prend en charge ce genre de chose), soit sous forme de facture négative pour satisfaire vos rapports financiers. Et si cela est fait, vous pouvez réellement voir ce qui s'est passé sans avoir à fouiller dans l'historique d'audit pour chaque facture; il suffit de regarder les factures elles-mêmes.

Il y a toujours le problème que les développeurs doivent se rappeler de changer le statut de la commande après qu'elle a été publiée en tant que facture, mais nous pouvons y remédier avec un déclencheur:

CREATE TRIGGER tr_Invoices_UpdateOrderStatus
ON Invoices
FOR INSERT
AS

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

Vos données sont désormais à l'abri des utilisateurs imprudents et même des développeurs imprudents. Et les factures ne sont plus ambiguës; vous n'avez pas à vous soucier des bogues qui s'introduisent parce que quelqu'un a oublié de vérifier l'état de la facture, car il n'y a aucun état.

Donc, juste pour résumer et paraphraser une partie de ceci: Pourquoi suis-je allé à tous ces problèmes juste pour un historique de facturation?

Parce que les factures qui n'ont pas encore été postées ne sont pas de vraies transactions. Ce sont des transactions "état" - transactions en cours. Ils n'appartiennent pas à vos données transactionnelles. En les gardant séparés comme ceci, vous résoudrez beaucoup de futurs problèmes potentiels.

Avertissement: Tout cela parle de mon expérience personnelle et je n'ai pas vu tous système de facturation dans le monde. Je ne peux garantir avec 100% de certitude que cela convient à votre application particulière. Je ne peux que réitérer le nid de frelons de problèmes que j'ai vu résultant de la notion de factures "en attente", du mélange des données d'état avec des données transactionnelles.

Comme pour tout autre design que vous trouvez sur Internet, vous devriez étudier cela comme une option possible et évaluer si cela peut vraiment fonctionner pour vous.

52
Aaronaught

En règle générale, les lignes de facturation ne sont pas modifiées. c'est-à-dire qu'une commande (bon de commande ou bon de travail) devient une facture. Une fois qu'une facture est émise, elle peut être annulée ou des paiements et des notes de crédit peuvent être appliqués, mais c'est généralement à peu près tout.

Votre situation peut être un peu différente, mais je pense que c'est la convention habituelle - après tout, lorsque vous recevez une facture xyz, vous ne vous attendez pas à ce que les données sur lesquelles le document était basé soient modifiées de quelque manière que ce soit.

En ce qui concerne la taxe, généralement d'après mon expérience, qui est stockée au niveau de la facture et déterminée au moment où la facture est validée.

En ce qui concerne les commandes changeant avant de devenir des factures, je n'ai généralement rien vu de plus complexe que l'audit de base de base de données - généralement, l'application n'expose pas cet historique aux utilisateurs.

Si vous voulez une piste d'audit directe qui soit relativement indépendante du domaine, vous pouvez regarder AutoAudit - une piste d'audit basée sur un déclencheur.

Nous n'avons généralement pas de "factures provisoires". C'est tentant car vous avez beaucoup de similitudes entre les commandes et les factures. Mais en réalité, il est préférable d'avoir des commandes qui ne sont pas devenues des factures dans un tableau séparé. Les factures ont tendance à avoir quelques différences (c'est-à-dire que le changement d'état est en fait une transformation d'une entité à une autre) et avec l'intégrité référentielle, parfois vous ne voulez vraiment que les choses se joignent à de "vraies" factures.

Nous avons donc généralement toujours PurchaseOrder, PurchaseOrderLine, Invoice et InvoiceLine. Dans certains cas, j'ai eu le côté PO se comporter plus comme un panier d'achat - où le prix n'est pas stocké et flotte avec le tableau des produits et d'autres cas où ils ressemblent plus à des devis qui doivent être honorés une fois qu'ils sont transmis au client. Ces subtilités peuvent être importantes lors de l'examen du flux de travail et des exigences de l'entreprise.

7
Cade Roux

Pourquoi ne pas simplement créer des copies des tables que vous souhaitez auditer et que sur les tables d'origine créer des triggres qui copieront une ligne dans des copies de table à chaque insertion, mise à jour, suppression?

Le déclencheur ressemble généralement à ceci:

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

Je suis d'accord avec le commentaire d'Aaronaught ci-dessus concernant "l'immuabilité" de la facture.

Si vous suivez ce conseil, j'envisagerais d'avoir les statuts "Examen en attente", "Approuvé" et "Annulé". "Examen en attente" est juste cela. "Approuvé" est réputé être correct et payable par le client. "Annulé" n'est que cela: la facture n'est plus valide et n'est pas payable par le client. Vous pouvez ensuite déduire si la facture est entièrement payée à partir des enregistrements dans Payments, et vous ne répétez pas les informations.

Mis à part cela, aucun problème réel avec votre idée de révision cependant.

Vous pouvez inclure la taxe comme un autre enregistrement dans InvoiceLines.

3
John