UNPKG

@accounter/shaam-uniform-format-generator

Version:

Fully typed application that generates, parses, and validates SHAAM uniform format tax reports (INI.TXT and BKMVDATA.TXT).

329 lines (328 loc) • 12.9 kB
"use strict"; /** * Main API for generating SHAAM uniform format reports */ Object.defineProperty(exports, "__esModule", { value: true }); exports.generateUniformFormatReport = generateUniformFormatReport; const encoder_js_1 = require("../generator/format/encoder.js"); const index_js_1 = require("../generator/records/index.js"); const errors_js_1 = require("../validation/errors.js"); const validate_input_js_1 = require("../validation/validate-input.js"); /** * Generates SHAAM uniform format report files (INI.TXT and BKMVDATA.TXT) * from a high-level JSON input object. * * @param input - The report input data * @param options - Generation options * @returns Report output with generated file content */ function generateUniformFormatReport(input, options = {}) { // Validate input data first const validationMode = options.validationMode || 'fail-fast'; const validationErrors = (0, validate_input_js_1.validateInput)(input, validationMode); if (validationErrors.length > 0) { if (validationMode === 'fail-fast') { // This should have already thrown, but just in case throw new errors_js_1.ShaamFormatError('Validation failed', validationErrors); } // For collect-all mode, we still throw if there are errors // The user can catch and inspect the errors if needed throw new errors_js_1.ShaamFormatError('Input validation failed', validationErrors); } const records = []; const iniRecords = []; const recordCounts = {}; let recordNumber = 1; // Helper function to count and add data record const addRecord = (recordType, recordContent) => { records.push(recordContent); recordCounts[recordType] = (recordCounts[recordType] || 0) + 1; recordNumber++; }; // Helper function to add INI record const addIniRecord = (recordType, recordContent) => { iniRecords.push(recordContent); recordCounts[recordType] = (recordCounts[recordType] || 0) + 1; }; // Generate A000 record for INI.TXT const fileHeaderRecord = { reservedFuture: '', totalRecords: '0', // Will be updated later with actual count vatId: input.business.taxId, softwareRegNumber: '12345678', // TODO: Use actual software registration number softwareName: 'SHAAM Generator', softwareVersion: '1.0.0', vendorVatId: '123456789', // TODO: Use actual vendor VAT ID vendorName: 'Accounter', softwareType: '2', // Multi-year software fileOutputPath: options.fileNameBase || 'report', accountingType: '2', // Double-entry accounting balanceRequired: '1', companyRegId: '', withholdingFileNum: '', reserved1017: '', businessName: input.business.name, businessStreet: '', businessHouseNum: '', businessCity: '', businessZip: '', taxYear: '', startDate: input.business.reportingPeriod.startDate.replace(/-/g, ''), endDate: input.business.reportingPeriod.endDate.replace(/-/g, ''), processStartDate: new Date().toISOString().slice(0, 10).replace(/-/g, ''), processStartTime: new Date().toTimeString().slice(0, 5).replace(':', ''), languageCode: '0', // Hebrew characterEncoding: '1', // ISO-8859-8-i compressionSoftware: '', reserved1031: '', baseCurrency: 'ILS', reserved1033: '', branchInfoFlag: '0', // No branches for now reserved1035: '', }; addIniRecord('A000', (0, index_js_1.encodeA000)(fileHeaderRecord)); // Add A000Sum records for each record type to INI.TXT // Calculate expected counts based on input data const expectedCounts = { A100: 1, C100: input.documents.length, D110: input.documents.length, D120: input.documents.length, B100: input.journalEntries.length, B110: input.accounts.length, M100: input.inventory.length, Z900: 1, }; // Add summary records for each record type for (const [recordType, count] of Object.entries(expectedCounts)) { if (count > 0) { const summaryRecord = { code: recordType, recordCount: count.toString(), }; addIniRecord('A000Sum', (0, index_js_1.encodeA000Sum)(summaryRecord)); } } // 1. Business metadata - A100 record const businessMetadata = { recordNumber: recordNumber.toString(), vatId: input.business.taxId, reserved: '', }; addRecord('A100', (0, index_js_1.encodeA100)(businessMetadata)); // TODO: 2. Summaries... (not implemented yet) // 3. For each document in input.documents for (const document of input.documents) { // Document header - C100 record const documentRecord = { code: 'C100', recordNumber: recordNumber.toString(), vatId: input.business.taxId, documentType: document.type, documentId: document.id, documentIssueDate: document.date.replace(/-/g, ''), // Convert YYYY-MM-DD to YYYYMMDD documentIssueTime: '', customerName: '', customerStreet: '', customerHouseNumber: '', customerCity: '', customerPostCode: '', customerCountry: '', customerCountryCode: '', customerPhone: '', customerVatId: '', documentValueDate: '', foreignCurrencyAmount: '', currencyCode: '', amountBeforeDiscount: '', documentDiscount: '', amountAfterDiscountExcludingVat: '', vatAmount: '', amountIncludingVat: document.amount.toFixed(2), // Format as decimal with 2 places withholdingTaxAmount: '', customerKey: '', matchingField: '', cancelledAttribute1: '', cancelledDocument: '', cancelledAttribute2: '', documentDate: '', branchKey: '', cancelledAttribute3: '', actionExecutor: '', lineConnectingField: '', reserved: '', }; addRecord('C100', (0, index_js_1.encodeC100)(documentRecord)); // Document line - D110 record const documentLineRecord = { code: 'D110', recordNumber: recordNumber.toString(), vatId: input.business.taxId, documentType: document.type, documentNumber: document.id, lineNumber: '1', baseDocumentType: '', baseDocumentNumber: '', transactionType: '', internalCatalogCode: '', goodsServiceDescription: document.description || 'Item', manufacturerName: '', serialNumber: '', unitOfMeasureDescription: '', quantity: '1', unitPriceExcludingVat: '', lineDiscount: '', lineTotal: '', vatRatePercent: '', reserved1: '', branchId: '1', reserved2: '', documentDate: document.date.replace(/-/g, ''), headerLinkField: '', baseDocumentBranchId: '', reserved3: '', }; addRecord('D110', (0, index_js_1.encodeD110)(documentLineRecord)); // Payment record - D120 record (if applicable) const paymentRecord = { code: 'D120', recordNumber: recordNumber.toString(), vatId: input.business.taxId, documentType: document.type, documentNumber: document.id, lineNumber: '1', paymentMethod: '1', // Default payment method bankNumber: '', branchNumber: '', accountNumber: '', checkNumber: '', paymentDueDate: '', lineAmount: document.amount.toFixed(2), // Format as decimal with 2 places acquirerCode: '', cardBrand: '', creditTransactionType: '', firstPaymentAmount: '', installmentsCount: '', additionalPaymentAmount: '', reserved1: '', branchId: '1', reserved2: '', documentDate: document.date.replace(/-/g, ''), headerLinkField: '', reserved: '', }; addRecord('D120', (0, index_js_1.encodeD120)(paymentRecord)); } // 4. Journal lines: for each entry and its lines for (const entry of input.journalEntries) { const journalRecord = { code: 'B100', recordNumber: recordNumber.toString(), vatId: input.business.taxId, transactionNumber: entry.id.replace(/\D/g, '') || '1', // Extract only digits, default to '1' transactionLineNumber: '1', batchNumber: '', transactionType: '', referenceDocument: '', referenceDocumentType: '', referenceDocument2: '', referenceDocumentType2: '', details: entry.description || '', date: entry.date.replace(/-/g, ''), valueDate: entry.date.replace(/-/g, ''), // Same as date for simplicity accountKey: entry.accountId, counterAccountKey: '', debitCreditIndicator: (entry.amount >= 0 ? '1' : '2'), currencyCode: '', transactionAmount: Math.abs(entry.amount).toFixed(2), // Format as decimal with 2 places foreignCurrencyAmount: '', quantityField: '', matchingField1: '', matchingField2: '', branchId: '', entryDate: entry.date.replace(/-/g, ''), operatorUsername: '', reserved: '', }; addRecord('B100', (0, index_js_1.encodeB100)(journalRecord)); } // 5. Accounts: B110 records for (const account of input.accounts) { const accountRecord = { code: 'B110', recordNumber: recordNumber.toString(), vatId: input.business.taxId, accountKey: account.id, accountName: account.name, trialBalanceCode: account.type, trialBalanceCodeDescription: account.type, customerSupplierAddressStreet: '', customerSupplierAddressHouseNumber: '', customerSupplierAddressCity: '', customerSupplierAddressZip: '', customerSupplierAddressCountry: '', countryCode: '', parentAccountKey: '', accountOpeningBalance: '0', totalDebits: '0', totalCredits: '0', accountingClassificationCode: '', supplierCustomerTaxId: '', branchId: '', openingBalanceForeignCurrency: '', foreignCurrencyCode: '', reserved: '', }; addRecord('B110', (0, index_js_1.encodeB110)(accountRecord)); } // 6. Inventory: M100 records for (const item of input.inventory) { const inventoryRecord = { code: 'M100', recordNumber: recordNumber.toString(), vatId: input.business.taxId, universalItemCode: '', supplierItemCode: '', internalItemCode: item.id, itemName: item.name, classificationCode: '', classificationDescription: '', unitOfMeasure: '', openingStock: '0', totalStockIn: '0', totalStockOut: item.quantity.toString(), endPeriodCostNonBonded: '', endPeriodCostBonded: '', reserved: '', }; addRecord('M100', (0, index_js_1.encodeM100)(inventoryRecord)); } // 7. Closing record: Z900 const closingRecord = { recordNumber: recordNumber.toString(), vatId: input.business.taxId, totalRecords: records.length.toString(), // Count of records before Z900 is added reserved: '', }; addRecord('Z900', (0, index_js_1.encodeZ900)(closingRecord)); // Build iniText and dataText using the assembler const iniText = (0, encoder_js_1.assembleFile)(iniRecords); const dataText = (0, encoder_js_1.assembleFile)(records); // Create virtual File objects const iniFile = new File([iniText], `${options.fileNameBase || 'report'}.INI.TXT`, { type: 'text/plain', }); const dataFile = new File([dataText], `${options.fileNameBase || 'report'}.BKMVDATA.TXT`, { type: 'text/plain', }); return { iniText, dataText, iniFile, dataFile, summary: { totalRecords: records.length + iniRecords.length, perType: recordCounts, }, }; }