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

Field Property Name
Audit Trail No AuditTrailNo
Doc Type DocType
Year/Doc No YearDocNo
Operation OperationName
Performed By UserName
Performed At ModifiedAt

Audit Trail Details

Field Property Name
Audit Trail No AuditTrailNo
Doc Type DocType
Year/Doc No YearDocNo
Operation OperationName
Performed By UserName
Performed At ModifiedAt
Notes Notes

Audit Trail Line Index

Fields Property Name
Audit Trail No AuditTrailNo
Particulars DisplayName
OldValue OldValue
NewValue NewValue
Updated By UpdatedBy

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):
AuditTrailNo DocType Year/DocNo Performed By Performed At Operation
00000001 Journal Voucher 2024-25/000006 Bhumika 2024-11-28 09:00 Modify Main Changed the Discount Amountshown in details
Example Output in Audit Trail Index1(Line Level):
AuditTrailNo Particular OldValue newValue Updatedby
00000001 Unit Name VAPI BENGLURU User
00000001 JV Date 08-Apr-24 07-Apr-24 User

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. Particulars Old Value New Value Updated By
00000002 Ac Particulars ADVANCE TAX - 2016-2017 - [078002] ADANI ANDHERI OFFICE LEASE DEPOSIT - [076050] user
00000002 Ref No 012345 user
00000002 Ref Date 17-Mar-25 16-Mar-25 user
00000002 Amount 520.00 200.00 user
00000002 Debit/Credit Type Credit Debit user
00000002 Debit Amount 0.00 200.00 System
00000002 Credit Amount 520.00 0.00 System

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):
AuditTrailNo DocType Year/DocNo Performed By Performed At Operation Notes
00000001 Journal Voucher 2024-25/000006 Bhumika 2024-11-28 09:00 Delete Line Created by Mistakeshown in details
Example Output in Audit Trail Index(Line Level):
Audit Trail No. Particulars Old Value New Value Updated By
00000003 Ac Particulars ADVANCE TAX - [780050] user
00000003 Ref Date Oct 31 2024 12:00AM Apr 8 2025 6:31PM user
00000003 Amount 1435000.00 0 user
00000003 Debit/Credit Type Credit user
00000003 Credit Amount 1902350.00 467350.00 System

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. Particulars Old Value New Value Updated By
00000005 Unit Name NA user
00000005 JV Date 31-Oct-25 30-Oct-25 user
00000005 Debit Amount 1902350.00 0 user
00000005 Credit Amount 27000.00 0 user
00000005 Debit/Credit Type D user
00000005 Ref No user
00000005 Ref Date 31-Oct-25 user
00000005 Amount 1900000.00 user
00000005 Account Particulars 125080 user
00000005 Debit/Credit Type D user
00000005 Ref No user
00000005 Ref Date 31-Oct-25 user
00000005 Amount 1900000.00 user
00000005 Account Particulars 125080 user

File Names

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