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

268 lines (257 loc) • 11.2 kB
import { z } from 'zod'; import { CRLF } from '../../format/index.js'; import { formatField, formatNumericField } from '../format/encoder.js'; /** * B110 Record Schema - Account record * Fields 1400-1424 based on SHAAM 1.31 specification table */ export const B110Schema = z.object({ // Field 1400: Record code (4) - Required - Alphanumeric code: z.literal('B110').describe('Record type code - always "B110"'), // Field 1401: Record number in file (9) - Required - Numeric recordNumber: z .string() .min(1) .max(9) .regex(/^\d+$/) .describe('Sequential record number in file'), // Field 1402: Tax ID (9) - Required - Numeric vatId: z.string().min(1).max(9).regex(/^\d+$/).describe('Tax identification number'), // Field 1403: Account key (15) - Required - Alphanumeric accountKey: z.string().min(1).max(15).describe('Account key - must be unique'), // Field 1404: Account name (50) - Required - Alphanumeric accountName: z.string().min(1).max(50).describe('Account name'), // Field 1405: Trial balance code (15) - Required - Alphanumeric trialBalanceCode: z.string().min(1).max(15).describe('Trial balance code'), // Field 1406: Trial balance code description (30) - Required - Alphanumeric trialBalanceCodeDescription: z.string().min(1).max(30).describe('Trial balance code description'), // Field 1407: Customer/Supplier address - street (50) - Optional - Alphanumeric customerSupplierAddressStreet: z .string() .max(50) .default('') .describe('Customer/Supplier address - street (only for customer/supplier accounts)'), // Field 1408: Customer/Supplier address - house number (10) - Optional - Alphanumeric customerSupplierAddressHouseNumber: z .string() .max(10) .default('') .describe('Customer/Supplier address - house number (only for customer/supplier accounts)'), // Field 1409: Customer/Supplier address - city (30) - Optional - Alphanumeric customerSupplierAddressCity: z .string() .max(30) .default('') .describe('Customer/Supplier address - city (only for customer/supplier accounts)'), // Field 1410: Customer/Supplier address - ZIP (8) - Optional - Alphanumeric customerSupplierAddressZip: z .string() .max(8) .default('') .describe('Customer/Supplier address - ZIP (only for customer/supplier accounts)'), // Field 1411: Customer/Supplier address - country (30) - Optional - Alphanumeric customerSupplierAddressCountry: z .string() .max(30) .default('') .describe('Customer/Supplier address - country (only for customer/supplier accounts)'), // Field 1412: Country code (2) - Optional - Alphanumeric countryCode: z .string() .max(2) .default('') .describe('Country code (only for customer/supplier accounts, see Appendix 3)'), // Field 1413: Parent account key (15) - Optional - Alphanumeric parentAccountKey: z.string().max(15).default('').describe('Parent account key'), // Field 1414: Account opening balance (15) - Optional - Alphanumeric - Format: X9(12)V99 accountOpeningBalance: z .string() .max(15) .default('') .describe('Account opening balance (positive = debit, negative = credit)'), // Field 1415: Total debits (15) - Optional - Alphanumeric - Format: X9(12)V99 totalDebits: z.string().max(15).default('').describe('Total debits (excludes opening balance)'), // Field 1416: Total credits (15) - Optional - Alphanumeric - Format: X9(12)V99 totalCredits: z.string().max(15).default('').describe('Total credits (excludes opening balance)'), // Field 1417: Accounting classification code (4) - Conditional - Numeric accountingClassificationCode: z .string() .max(4) .regex(/^$|^\d+$/) .default('') .describe('Accounting classification code (mandatory if 6111 report required)'), // Field 1418: Reserved field (0) - Deprecated - Alphanumeric - Skipped as it has 0 length // Field 1419: Supplier/Customer tax ID (9) - Conditional - Numeric supplierCustomerTaxId: z .string() .max(9) .regex(/^$|^\d+$/) .default('') .describe( 'Supplier/Customer tax ID (required if double-entry bookkeeping, code 2 in field 1013)', ), // Field 1420: Reserved field (0) - Deprecated - Alphanumeric - Skipped as it has 0 length // Field 1421: Branch ID (7) - Conditional - Alphanumeric branchId: z .string() .max(7) .default('') .describe('Branch ID (required if field 1034 = 1, see Note 3)'), // Field 1422: Opening balance in foreign currency (15) - Optional - Alphanumeric - Format: X9(12)V99 openingBalanceForeignCurrency: z .string() .max(15) .default('') .describe('Opening balance in foreign currency'), // Field 1423: Foreign currency code (3) - Optional - Alphanumeric foreignCurrencyCode: z .string() .max(3) .default('') .describe('Foreign currency code (refers to field 1422, see Appendix 2)'), // Field 1424: Reserved field (16) - Optional - Alphanumeric reserved: z.string().max(16).default('').describe('Reserved field for future use'), }); export type B110 = z.infer<typeof B110Schema>; /** * Encodes a B110 record to fixed-width string format * Total line width: 376 characters + CRLF */ export function encodeB110(input: B110): string { const fields = [ formatField(input.code, 4, 'left'), // Field 1400: Record code (4) formatNumericField(input.recordNumber, 9), // Field 1401: Record number (9) formatNumericField(input.vatId, 9), // Field 1402: VAT ID (9) formatField(input.accountKey, 15, 'left'), // Field 1403: Account key (15) formatField(input.accountName, 50, 'left'), // Field 1404: Account name (50) formatField(input.trialBalanceCode, 15, 'left'), // Field 1405: Trial balance code (15) formatField(input.trialBalanceCodeDescription, 30, 'left'), // Field 1406: Trial balance code description (30) formatField(input.customerSupplierAddressStreet, 50, 'left'), // Field 1407: Customer/Supplier address - street (50) formatField(input.customerSupplierAddressHouseNumber, 10, 'left'), // Field 1408: Customer/Supplier address - house number (10) formatField(input.customerSupplierAddressCity, 30, 'left'), // Field 1409: Customer/Supplier address - city (30) formatField(input.customerSupplierAddressZip, 8, 'left'), // Field 1410: Customer/Supplier address - ZIP (8) formatField(input.customerSupplierAddressCountry, 30, 'left'), // Field 1411: Customer/Supplier address - country (30) formatField(input.countryCode, 2, 'left'), // Field 1412: Country code (2) formatField(input.parentAccountKey, 15, 'left'), // Field 1413: Parent account key (15) formatField(input.accountOpeningBalance, 15, 'right'), // Field 1414: Account opening balance (15) - right-aligned for amount formatField(input.totalDebits, 15, 'right'), // Field 1415: Total debits (15) - right-aligned for amount formatField(input.totalCredits, 15, 'right'), // Field 1416: Total credits (15) - right-aligned for amount formatNumericField(input.accountingClassificationCode, 4), // Field 1417: Accounting classification code (4) // Field 1418: Reserved field (0) - skipped as it has 0 length formatNumericField(input.supplierCustomerTaxId, 9), // Field 1419: Supplier/Customer tax ID (9) // Field 1420: Reserved field (0) - skipped as it has 0 length formatField(input.branchId, 7, 'left'), // Field 1421: Branch ID (7) formatField(input.openingBalanceForeignCurrency, 15, 'right'), // Field 1422: Opening balance in foreign currency (15) - right-aligned for amount formatField(input.foreignCurrencyCode, 3, 'left'), // Field 1423: Foreign currency code (3) formatField(input.reserved, 16, 'left'), // Field 1424: Reserved field (16) ]; return fields.join('') + CRLF; } /** * Parses a fixed-width B110 record line back to object * Expected line length: 376 characters (excluding CRLF) */ export function parseB110(line: string): B110 { // Remove CRLF if present const cleanLine = line.replace(/\r?\n$/, ''); if (cleanLine.length !== 376) { throw new Error(`Invalid B110 record length: expected 376 characters, got ${cleanLine.length}`); } // Extract fields at their fixed positions let pos = 0; const code = cleanLine.slice(pos, pos + 4).trim(); pos += 4; const recordNumber = cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || '0'; pos += 9; const vatId = cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || '0'; pos += 9; const accountKey = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const accountName = cleanLine.slice(pos, pos + 50).trim(); pos += 50; const trialBalanceCode = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const trialBalanceCodeDescription = cleanLine.slice(pos, pos + 30).trim(); pos += 30; const customerSupplierAddressStreet = cleanLine.slice(pos, pos + 50).trim(); pos += 50; const customerSupplierAddressHouseNumber = cleanLine.slice(pos, pos + 10).trim(); pos += 10; const customerSupplierAddressCity = cleanLine.slice(pos, pos + 30).trim(); pos += 30; const customerSupplierAddressZip = cleanLine.slice(pos, pos + 8).trim(); pos += 8; const customerSupplierAddressCountry = cleanLine.slice(pos, pos + 30).trim(); pos += 30; const countryCode = cleanLine.slice(pos, pos + 2).trim(); pos += 2; const parentAccountKey = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const accountOpeningBalance = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const totalDebits = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const totalCredits = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const accountingClassificationCode = cleanLine .slice(pos, pos + 4) .trim() .replace(/^0+/, '') || ''; pos += 4; // Field 1418: Reserved field (0) - skipped as it has 0 length const supplierCustomerTaxId = cleanLine .slice(pos, pos + 9) .trim() .replace(/^0+/, '') || ''; pos += 9; // Field 1420: Reserved field (0) - skipped as it has 0 length const branchId = cleanLine.slice(pos, pos + 7).trim(); pos += 7; const openingBalanceForeignCurrency = cleanLine.slice(pos, pos + 15).trim(); pos += 15; const foreignCurrencyCode = cleanLine.slice(pos, pos + 3).trim(); pos += 3; const reserved = cleanLine.slice(pos, pos + 16).trim(); pos += 16; // Validate the code field if (code !== 'B110') { throw new Error(`Invalid B110 record code: expected "B110", got "${code}"`); } const parsed: B110 = { code, recordNumber, vatId, accountKey, accountName, trialBalanceCode, trialBalanceCodeDescription, customerSupplierAddressStreet, customerSupplierAddressHouseNumber, customerSupplierAddressCity, customerSupplierAddressZip, customerSupplierAddressCountry, countryCode, parentAccountKey, accountOpeningBalance, totalDebits, totalCredits, accountingClassificationCode, supplierCustomerTaxId, branchId, openingBalanceForeignCurrency, foreignCurrencyCode, reserved, }; // Validate against schema return B110Schema.parse(parsed); }