Audit Trail - JV

Audit Trail - JV

Why we need Audit Trail

An audit trail is essential to comply with government regulations and industry standards, ensuring transparency and accountability. It tracks changes to critical data like what was changed, who made the change, and when,to meet legal requirements, prevent fraud, and support audits effectively.

Applicability of Audit Trail

Audit trail functionality will be implemented for the following financial documents:

  1. Invoice
  2. Bill
  3. DNCN (Debit/Credit Note)
  4. Voucher
  5. Journal Voucher
  6. Charts of Accounts
  7. Party Master
  8. Opening Balances
    • Audit trail applies only for Delete All,Import & CarryForwards (logs also need to be maintained in ERP and MFG).
    • Delete & Import we will allow for the First Year Only
  9. Opening Outstanding (Opg Os)
    • Audit trail applies only for Delete All,Import & CarryForwards (logs also need to be maintained in ERP and MFG).
    • Delete & Import we will allow for the First Year Only

Audit Trail table Structure & Script

CREATE TABLE AuditTrail 

(
    id INT PRIMARY KEY IDENTITY(1,1),
    AuditTrailNo NVARCHAR(8),---generated by the system
    DocTypeCode NVARCHAR(5),---for eg. V, , DNCN1
    yearDocNo NVARCHAR(12),----for eg.YearVoucherNo
    Operationcode NVARCHAR(2),-----for Eg. Modify, Delete, recreated
    operationDate DATETIME,-----On what date it was Modified or delete
    username NVARCHAR(50),----who deleted/Modify
    Notes NVARCHAR(250)---user will mention reason for delete
);

CREATE TABLE AuditTrail1 (
    id INT PRIMARY KEY IDENTITY(1,1),
    AuditTrailNo NVARCHAR(8),--for joining purpose
    displayname NVARCHAR(100),----what will be visible to the user
    oldvalue NVARCHAR(500),
    newvalue NVARCHAR(500),
    updatedBy NVARCHAR(1),---- ind will be U and S (user and System)
);
-----used only for modify

CREATE TABLE AuditTrailMetaData 
(
    id INT PRIMARY KEY IDENTITY(1,1),
    tableName NVARCHAR(50),--- will be the table name
    propertyname NVARCHAR(50),---name which has been used in the Property
    displayname NVARCHAR(100),-- will be the name which is visible to the user for eg Voucher Amount
    ind NVARCHAR(3),---->pending to add description
    coulmnname NVARCHAR(50),---->pending to add description
    internalcolumn NVARCHAR(50)---> for deleting purpose
);


CREATE TABLE AuditTrailDocType 
(
    id INT PRIMARY KEY IDENTITY(1,1),
    docType NVARCHAR(4),--- for eg B (for Bill), V(for Voucher)
    docTypeName NVARCHAR(50),---for eg Bill, Voucer
);

---FOR PARTYMST 
INSERT INTO AuditTrailDocType (docType, docTypeName)
VALUES ('I', 'Invoice ')
INSERT INTO AuditTrailDocType (docType, docTypeName)
VALUES ('B', 'Bill')
INSERT INTO AuditTrailDocType (docType, docTypeName)
VALUES ('D', 'Debit/Credit Note')
INSERT INTO AuditTrailDocType (docType, docTypeName)
VALUES ('V', 'Voucher')
INSERT INTO AuditTrailDocType (docType, docTypeName)
VALUES ('J', 'Journal Voucher ')
INSERT INTO AuditTrailDocType (docType, docTypeName)
VALUES ('A', 'Charts of Account')
INSERT INTO AuditTrailDocType (docType, docTypeName)
VALUES ('P', 'PartyMst')


---JV
INSERT INTO AuditTrailMetaData (tablename, propertyname, displayname, ind, internalcolumn)
VALUES 
('journalvoucher', 'UnitCode', 'Unit Code', 'MJV', 'unitcode'),
('journalvoucher', 'dated', 'JV Date', 'MJV', 'dated'),
('journalvoucher', 'debitamount', 'Debit Amount', 'MJV', 'debit'),
('journalvoucher', 'creditamount', 'Credit Amount', 'MJV', 'credit'),
('journalvoucher', 'jvtypename', 'JV Type', 'MJV', 'jvtype'),
('journalvoucher', 'divisioncode', 'Division Code', 'MJV', 'divisionid'),
('journalvoucher1', 'drcrname', 'Debit/Credit Type', 'LJV', 'drcr'),
('journalvoucher1', 'refno', 'Ref No', 'LJV', 'refno'),
('journalvoucher1', 'refdate', 'Ref Date', 'LJV', 'refdate'),
('journalvoucher1', 'amount', 'Amount', 'LJV', 'amount'),
('journalvoucher1', 'mainacnamecode', 'Account Head', 'LJV', 'mainac'),
('journalvoucher1', 'subacnamecode', 'Sub Account Head', 'LJV', 'subac'),
('journalvoucher1', 'refdocname', 'Ref Doc', 'LJV', 'refdoc');

INSERT INTO sysparameters (shortname, name, syscondition, dated, isactive, docno, status) VALUES ('AuditTrail', 'Enable Audit Trail', 'Y', null, 'Y', null, null)

ALTER TABLE Audittrail1 ADD docId INT

Implementation Mechanism

The following mechanisms will be applied to track changes across documents for delete, delete1, modify, modify1, modify other info or any other features where necessary(eg. UpdateTaxableAmount)

Audit Trail Main Index

FieldProperty Name
Audit Trail NoAuditTrailNo
Doc TypeDocType
Year/Doc NoYearDocNo
OperationOperationName
Performed ByUserName
Performed AtModifiedAt

Audit Trail Details

FieldProperty Name
Audit Trail NoAuditTrailNo
Doc TypeDocType
Year/Doc NoYearDocNo
OperationOperationName
Performed ByUserName
Performed AtModifiedAt
NotesNotes

Audit Trail Line Index

FieldsProperty Name
Audit Trail NoAuditTrailNo
ParticularsDisplayName
OldValueOldValue
NewValueNewValue
Updated ByUpdatedBy

Audit Trail Queries

--------Audit Trail Index------------------------------
SELECT AuditTrailNo, docTypeName,
        CONCAT(SUBSTRING(yeardocno, 1, 4), '-', SUBSTRING(yeardocno, 5, 2), '/', RIGHT(yeardocno, 6)) AS yeardocNo, 
        IIF(Operationcode = 'RC', 'Re-Created', 
        IIF(Operationcode = 'MM', 'Modify Main', 
            IIF(Operationcode = 'ML', 'Modify Line', 
                IIF(Operationcode = 'DM', 'Delete Main', 'Delete Line')
            )
        )
        ) AS operationname, operationDate, username, A.id
        FROM AuditTrail A 
        LEFT JOIN AuditTrailDocType AD ON A.DocTypeCode = AD.docType
        ORDER BY A.id DESC

--------------Audit Trail Details-----------------------
SELECT AuditTrailNo, docTypeName,
        CONCAT(SUBSTRING(yeardocno, 1, 4), '-', SUBSTRING(yeardocno, 5, 2), '/', RIGHT(yeardocno, 6)) AS yeardocNo, 
        IIF(Operationcode = 'RC', 'Re-Created', 
        IIF(Operationcode = 'MM', 'Modify Main', 
            IIF(Operationcode = 'ML', 'Modify Line', 
                IIF(Operationcode = 'DM', 'Delete Main', 'Delete Line')
            )
        )
        ) AS operationname, operationDate, username, A.id, A.Notes
        FROM AuditTrail A 
        LEFT JOIN AuditTrailDocType AD ON A.DocTypeCode = AD.docType
        WHERE A.id = @id

------------------Audit Trail1 Index----------------------------------
SELECT AuditTrailNo, oldvalue, newvalue, displayname, 
        IIF(updatedby = 'U', 'user', 'System') AS updatedby, id
        FROM 
        AuditTrail1
        WHERE audittrailNo = @audittrailno
        ORDER BY id DESC

Create Operations

  • Once the create Operation is completed we will check if the YearDocNo exists for the same doctypeName for any Delete Operation in Audit Trail
  • If it exist then we will Insert in Audit Trail main Level with Operation Re-Created(RC)
  • If the doc no doesnot exist in Audit Trail then Logs will be captured from the ERP log system.Already added in system
  • This feature will be only for Main level Create

Code for Audit Trail Re-Create Method

//------------------------------------HOW TO ADD IN CREATE FEATURE----------------------------------------

        [HttpPost("{yearLabel},{dbname},{userName}")]
        public ActionResult JournalVoucherCreate(JournalVoucher _JournalVoucher, string yearLabel, string dbname, string userName)
        {
            if (ModelState.IsValid)
            {
                var maxJvNo = _IJournalVoucherRepository.GetMaxJvNo(dbname, yearLabel);
                int jvNoLength = string.IsNullOrEmpty(maxJvNo) ? 6 : maxJvNo.Length;
                var nextJvNo = _IUtilityMethodsRepository.GetNextDocno(maxJvNo, jvNoLength);
                _JournalVoucher.JvNo = nextJvNo;

                _JournalVoucher.YearJvNo = $"{yearLabel.Replace("-", "")}{_JournalVoucher.JvNo}";

                _IJournalVoucherRepository.JournalVoucherCreate(_JournalVoucher, dbname);

                var auditTrailSysparameter = _IUtilityMethodsRepository.GetSysParameterValue(dbname, "AuditTrail");
                if(auditTrailSysparameter.IsActive == "Y")
                {          
                    //--------------------AUDIT TRAIL BLOCK-------------------------------------------
                    var yearDocNoCnt = _IAuditTrailRepository.GetYearDocNoCnt(_JournalVoucher.YearJvNo, dbname, "J");

                    if(yearDocNoCnt > 0)
                    {
                        var nextAuditTrailNo = _AuditTrailController.GetNextAuditTrailNo(dbname);

                        // assigning values for main level audit trail create
                        AuditTrail _AuditTrail = new()
                        {
                            AuditTrailNo = nextAuditTrailNo,
                            DocTypeCode = "J",
                            YearDocNo = _JournalVoucher.YearJvNo,
                            UserName = userName,
                            OperationCode = "RC",
                            Notes = $"Re-created by {userName}",
                        };
                        
                        _IAuditTrailRepository.AuditTrailCreate(dbname, _AuditTrail);// for creating main level
                        _IAuditTrailRepository.AuditTrail1DynamicCreate(dbname, _AuditTrail.YearDocNo, "journalvoucher", _AuditTrail.AuditTrailNo, "YearJvNo", data.Id);// for creating line level
                    }
                }

                _IUtilityMethodsRepository.InsertMFGLog(dbname, userName, _JournalVoucher.JvNo, "JV", "Add", yearLabel, "");

            }
            return Ok(_JournalVoucher);
        }

YearDocNoCntQuery

--- to check record exist or not for delete feature 
--Paramteres : Yeardocno - eg. PartyId, YearVoucherNo etc..., docTypeCode = P for PartyMst, V for Voucher, OperationCode = D(for Delete)
SELECT COUNT(*) FROM audittrail
WHERE yeardocno = @yeardocno AND 
doctypecode = @doctypecode AND operationCode = 'DM'

AuditTrailCreate


-- to insert at main level
-- Paramteres : 
-- auditrailNo - getting from getnextaudittrailno, 
-- docTypeCode = P for PartyMst, V for Voucher, 
-- OperationCode = RC(Re-Created),
-- OperationDate = DateTime.Now 
-- UserName - coming from postlogin
-- Notes = $"Recreated by {_AuditChange.UserName}"

INSERT INTO AuditTrail(AuditTrailNo,DocTypeCode,yearDocNo, OperationCode, operationDate, username, Notes) 
        VALUES (@AuditTrailNo,@DocTypeCode, @yearDocNo, @Operationcode, @operationDate, @username, @Notes)

AuditTrailDynamicCreate

        "DECLARE 
		@columns NVARCHAR(MAX),
		@castedColumns NVARCHAR(MAX),
		@query NVARCHAR(MAX);

	    -- Step 1: Build column list dynamically based on tablename
	    SELECT 
		@columns = STRING_AGG(internalcolumn, ', '),
		@castedColumns = STRING_AGG('CAST(' + internalcolumn + ' AS NVARCHAR(MAX)) AS ' + internalcolumn, ', ')
	    FROM AuditTrailMetaData
	    WHERE tablename = @tableName;

	    -- Step 2: Build the dynamic SQL
	    SET @query = 'WITH A (dynamicdocno, ' + @columns + ') AS
        (
		    SELECT  ' + QUOTENAME(@tableYearDocNameId) + ' AS dynamicdocno, '  + @castedColumns + '
		    FROM {tableName}
	    ),
	    B(internalcolumn, value) AS 
        (
		    SELECT internalcolumn, value
		    FROM A
		    UNPIVOT (value FOR internalcolumn IN (' + @columns + ')) AS U
		    WHERE dynamicdocno = @yearDocNoId
	    )
	    INSERT INTO audittrail1 (audittrailno, displayname, oldvalue, updatedby, docid)
	    SELECT @audittrailno, M.displayname, B.value, @updatedby, @docid
	    FROM B
	    JOIN AuditTrailMetaData M ON M.internalcolumn = B.internalcolumn 
	    AND M.tablename = @tableName;';

        -- Execute the dynamic SQL
        EXEC sp_executesql @query, 
        N'@yearDocNoId NVARCHAR(MAX), @audittrailno NVARCHAR(MAX), @tableName NVARCHAR(MAX), @updatedby NVARCHAR(10)', 
        @yearDocNoId, @audittrailno, @tableName, 'U', @docid";

Modify Main/ Modify Line Operations

  • Operation: Modify Main/ Modify Line
1. Capture Old Values Before Modification
  • Add a field in Modify page for Audit Trail Notes
  • When the Save button is clicked in the controller:
    • Main-Level Details:
      Fetch the current Main-level details (existing records) from the database into a model class, referred to as OldValues.
2. Detect Line-Level Changes
  • Pass the following to a Detect Changes method:
    • OldValues (fetched old values)
    • New values coming from the Modify Razor Page (user-modified data).
  • The Detect Changes method compares the old and new values for each field which needs to be tracked and identifies only those fields where the data has been changed.
3. Update Audit Trail for Main-Level Changes
  • Pass the list of changes (from the Detect Changes method) to an UpdateAuditTrail method.
  • Insert these changes into a temporary table in the database.
    • Each record will contain:
      • PropertyName (Field Name)
      • OldValue (Previous Value)
      • NewValue (Updated Value)
Example Output in Audit Trail Index(Main Level):
AuditTrailNoDocTypeYear/DocNoPerformed ByPerformed AtOperation
00000001Journal Voucher2024-25/000006Bhumika2024-11-28 09:00Modify MainChanged the Discount Amountshown in details
Example Output in Audit Trail Index1(Line Level):
AuditTrailNoParticularOldValuenewValueUpdatedby
00000001Unit NameVAPIBENGLURUUser
00000001JV Date08-Apr-2407-Apr-24User

In Case computation is also there in Modify Line

  • Execute the Compute method to process any modifications or
  • Fetch the updated main-level details into a new model class called NewValuesMain.
  • Pass both OldValuesMain and NewValuesMain to the Detect Changes method.
  • Pass the changes (from the Detect Changes method) along with the Audit Trail No to the AuditTrailCreate method.
Example Output in Audit Trail Index(Line Level):

Audit Trail Item Details

Audit Trail No.ParticularsOld ValueNew ValueUpdated By
00000002Ac ParticularsADVANCE TAX - 2016-2017 - [078002]ADANI ANDHERI OFFICE LEASE DEPOSIT - [076050]user
00000002Ref No012345user
00000002Ref Date17-Mar-2516-Mar-25user
00000002Amount520.00200.00user
00000002Debit/Credit TypeCreditDebituser
00000002Debit Amount0.00200.00System
00000002Credit Amount520.000.00System

Example of Code in JV modify1


        [HttpPut("{dbname},{yearlabel},{username}")]
        public ActionResult JournalVoucher1Modify(JournalVoucher1 _JournalVoucher1, string dbname, string yearlabel, string username)
        {
            if (ModelState.IsValid)
            {
                // picking old values for audit trail 
                JournalVoucher1 _LineOldValues = _IJournalVoucherRepository.JournalVoucher1Details(_JournalVoucher1.Id, dbname).Result;

                _JournalVoucher1.RefNo = !string.IsNullOrEmpty(_JournalVoucher1.RefNo) ? _JournalVoucher1.RefNo.PadLeft(6, '0') : "";
                _IJournalVoucherRepository.JournalVoucher1Modify(_JournalVoucher1, dbname);

                JournalVoucher _MainOldValues = new();
                AuditTrail _AuditTrail = new();

                var auditTrailSysparameter = _IUtilityMethodsRepository.GetSysParameterValue(dbname, "AuditTrail");
                if(auditTrailSysparameter.IsActive == "Y")
                {
                    //------------------------AUDIT TRAIL BLOCK------------------------------------------------------------------------
                    // need to call to fetch new values else it will not consider combo values
                    JournalVoucher1 _LineNewValues = _IJournalVoucherRepository.JournalVoucher1Details(_JournalVoucher1.Id, dbname).Result;

                    //calling detect changes for audit trail to detect changes made
                    var detectedChanges = _AuditTrailController.DetectChanges(_LineOldValues, _LineNewValues, dbname, "Journalvoucher1");

                    if (detectedChanges.Count != 0)//only going forward if change is done
                    {
                        //assigning the values 
                        _AuditTrail.DocTypeCode = "J";
                        _AuditTrail.YearDocNo = _JournalVoucher1.YearJvNo;
                        _AuditTrail.UserName = username;
                        _AuditTrail.OperationCode = "ML";
                        _AuditTrail.Notes = _JournalVoucher1.AuditTrailNotes;
                        _AuditTrail.AuditTrailNo = _AuditTrailController.GetNextAuditTrailNo(dbname);
                    
                        _IAuditTrailRepository.AuditTrailCreate(dbname, _AuditTrail);
                        _IAuditTrailRepository.AuditTrail1Create(detectedChanges1, dbname, _AuditTrail.AuditTrailNo, "S", _JournalVoucher1.Id);
                    }
                    // calling main level details to get values before updating
                    _MainOldValues = _IJournalVoucherRepository.JournalVoucherDetails(_JournalVoucher1.MainId, dbname).Result;
                }

                _IJournalVoucherRepository.UpdateAmount(_JournalVoucher1.YearJvNo, dbname);

                if(auditTrailSysparameter.IsActive == "Y")
                {
                    // calling main level details to get values after updating
                    JournalVoucher _MainNewValues = _IJournalVoucherRepository.JournalVoucherDetails(_JournalVoucher1.MainId, dbname).Result;

                    var detectedChanges1 = _AuditTrailController.DetectChanges(_MainOldValues, _MainNewValues, dbname, "Journalvoucher");

                    if (detectedChanges1.Count != 0)
                    {
                        _IAuditTrailRepository.AuditTrail1Create(detectedChanges1, dbname, _AuditTrail.AuditTrailNo, "S", _JournalVoucher1.Id);
                    }  
                }

                _IUtilityMethodsRepository.InsertMFGLog(dbname, username, _JournalVoucher1.JvNo, "JV", "Edit Line", yearlabel, "");
            }

            return Ok();
        }

Line Delete

  • Operation: DeleteLine
1. Capture Old Values Before Deletion
  • When the Delete button is clicked in the controller: - Assign all the values for main level Audit Trail and call AuditTrailCreate and AuditTrailDynamicCreate mentioned above - Fetch the current line-level details (existing records) from the database into a model class, referred to as OldValues before deleting it - Delete the records - Call the UpdateAmount method - Call the details method after Update to fetch the new values - Call the Detect Changes method after that call AuditTrail1Create Method
Example Output in Audit Trail Index(Main Level):
AuditTrailNoDocTypeYear/DocNoPerformed ByPerformed AtOperationNotes
00000001Journal Voucher2024-25/000006Bhumika2024-11-28 09:00Delete LineCreated by Mistakeshown in details
Example Output in Audit Trail Index(Line Level):
Audit Trail No.ParticularsOld ValueNew ValueUpdated By
00000003Ac ParticularsADVANCE TAX - [780050]user
00000003Ref DateOct 31 2024 12:00AMApr 8 2025 6:31PMuser
00000003Amount1435000.000user
00000003Debit/Credit TypeCredituser
00000003Credit Amount1902350.00467350.00System

Example of Audit Trail in JV Line Delete

        [HttpDelete("{id},{mainId},{dbname},{userName},{yearLabel},{deleteReason}")]
        public void JournalVoucher1Delete(int id, int mainId, string dbname, string userName, string yearLabel, string deleteReason)
        {
            var data = _IJournalVoucherRepository.JournalVoucherDetails(mainId, dbname).Result;

            var auditTrailSysparameter = _IUtilityMethodsRepository.GetSysParameterValue(dbname, "AuditTrail");
            AuditTrail _AuditTrail = new();
            if(auditTrailSysparameter.IsActive == "Y")
            {
      
                _AuditTrail.AuditTrailNo = _AuditTrailController.GetNextAuditTrailNo(dbname);
                //assigning the values 
                _AuditTrail.DocTypeCode = "J";
                _AuditTrail.YearDocNo = data.YearJvNo;
                _AuditTrail.UserName = userName;
                _AuditTrail.OperationCode = "DL";
                _AuditTrail.Notes = deleteReason;

                // for creating main level audit trail
                _IAuditTrailRepository.AuditTrailCreate(dbname, _AuditTrail);

                // for inserting records from line level jv
                _IAuditTrailRepository.AuditTrail1DynamicCreate(dbname, $"{id}", "journalvoucher1", _AuditTrail.AuditTrailNo, "id", id);

            }
            
            _IJournalVoucherRepository.JournalVoucher1Delete(id, dbname);

            _IJournalVoucherRepository.UpdateAmount(data.YearJvNo, dbname);

            if(auditTrailSysparameter.IsActive == "Y")
            {
                // calling main level details to get values after updating
                JournalVoucher _jvDataAfterUpdate = _IJournalVoucherRepository.JournalVoucherDetails(mainId, dbname).Result;

                var detectedChanges1 = _AuditTrailController.DetectChanges(data, _jvDataAfterUpdate, dbname, "Journalvoucher");

                if (detectedChanges1.Count != 0)
                {
                    _IAuditTrailRepository.AuditTrail1Create(detectedChanges1, dbname, _AuditTrail.AuditTrailNo, "S", id);
                }  

            }

            _IUtilityMethodsRepository.InsertMFGLog(dbname, userName, data.JvNo, "JV", "Delete Line", yearLabel, deleteReason);
        }

Detect Changes

        public List<AuditTrail1> DetectChanges<T>(T original, T updated, string dbName, string tableName)
        {
            var changes = new List<AuditTrail1>();

            // Fetch the property names that should be tracked from AuditTrailInfo table
            var auditTrailProperties = _IAuditTrailRepository.AuditTrailMetaData(dbName, tableName); // Ensure it's a List<string>

            foreach (var propertyName in auditTrailProperties)  // Loop through properties from DB
            {
                var property = typeof(T).GetProperty(propertyName.PropertyName, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance);

                var oldValue = property.GetValue(original);
                var newValue = property.GetValue(updated);

                if (!Equals(oldValue, newValue)) // Log only if values are different
                {
                    changes.Add(new AuditTrail1
                    {
                        DisplayName = propertyName.DisplayName,
                        OldValue = oldValue,
                        NewValue = newValue
                    });
                }
            }

            return changes;
        }

AuditTrail1Create

INSERT INTO AuditTrail1(audittrailno, displayname, oldvalue, newvalue, updatedby) 
        VALUES (@audittrailno, @displayname, @oldvalue, @newvalue, @updatedby)

        -- foreach (var dataLine in detectedChangesList)
        -- {
        --     connection.Query<AuditTrail1>(insertAuditTrail1,
        --     new
        --     {
        --         auditTrailNo,
        --         dataLine.DisplayName,
        --         dataLine.OldValue,
        --         dataLine.NewValue,
        --         updatedBy

        --     });
        -- }

Main Delete

  • Operation: DeleteMain
1. Capture Old Values Before Deletion
  • When the Delete button is clicked in the controller:
  • Assign all the values for main level Audit Trail and call AuditTrailCreate and AuditTrailDynamicCreate(For main and line level) mentioned above

Code Example

        [HttpDelete("{id},{dbname},{userName},{yearLabel},{deleteReason}")]
        public void JournalVoucherDelete(int id, string dbname, string userName, string yearLabel, string deleteReason)
        {
            var data = _IJournalVoucherRepository.JournalVoucherDetails(id, dbname).Result;

            var auditTrailSysparameter = _IUtilityMethodsRepository.GetSysParameterValue(dbname, "AuditTrail");
            if(auditTrailSysparameter.IsActive == "Y")
            {
                //-------------------AUDIT TRAIL BLOCK--------------------------------------------------------
                AuditTrail _AuditTrail = new();
     
                _AuditTrail.AuditTrailNo = _AuditTrailController.GetNextAuditTrailNo(dbname);
                //assigning the values 
                _AuditTrail.DocTypeCode = "J";
                _AuditTrail.YearDocNo = data.YearJvNo;
                _AuditTrail.UserName = userName;
                _AuditTrail.OperationCode = "DM";
                _AuditTrail.Notes = deleteReason;

                // for creating main level audit trail
                _IAuditTrailRepository.AuditTrailCreate(dbname, _AuditTrail);

                // for inserting records from main level jv
                _IAuditTrailRepository.AuditTrail1DynamicCreate(dbname, _AuditTrail.YearDocNo, "journalvoucher", _AuditTrail.AuditTrailNo, "YearJvNo", id);

                // checking line level records are there or not 
                var journalVoucher1Cnt = _IUtilityMethodsRepository.GetRecordCount(dbname, data.YearJvNo, "journalvoucher1", "yearjvno");

                if(journalVoucher1Cnt > 0)
                {
                    // for inserting records from line level jv
                    _IAuditTrailRepository.AuditTrail1DynamicCreate(dbname, _AuditTrail.YearDocNo, "journalvoucher1", _AuditTrail.AuditTrailNo, "YearJvNo", id);
                }

            }

            _IJournalVoucherRepository.JournalVoucherDelete(data.YearJvNo, dbname);
            _IUtilityMethodsRepository.InsertMFGLog(dbname, userName, data.JvNo, "JV", "Delete", yearLabel, deleteReason);
        }

Example of how it is stored at line level

Audit Trail No.ParticularsOld ValueNew ValueUpdated By
00000005Unit NameNAuser
00000005JV Date31-Oct-2530-Oct-25user
00000005Debit Amount1902350.000user
00000005Credit Amount27000.000user
00000005Debit/Credit TypeDuser
00000005Ref Nouser
00000005Ref Date31-Oct-25user
00000005Amount1900000.00user
00000005Account Particulars125080user
00000005Debit/Credit TypeDuser
00000005Ref Nouser
00000005Ref Date31-Oct-25user
00000005Amount1900000.00user
00000005Account Particulars125080user

File Names

File Name
Controller - AuditTrailController
Repository - IAuditTrailRepository, AuditTrailRepository
Service - IAuditTrailService, AuditTrailService
Method Names
- AuditTrailIndex
- AuditTrailDetails
- AuditTrail1Index
- GetYearDocNoCnt
- AuditTrailCreate
- GetMaxAuditTrailNo
- GetAuditTrailProperties
- AuditTrailDelete
- DetectChanges
- GetNextAuditTrailNo