diff --git a/src/modules/finance/services/ap.service.ts b/src/modules/finance/services/ap.service.ts index f3e4611..c0c5421 100644 --- a/src/modules/finance/services/ap.service.ts +++ b/src/modules/finance/services/ap.service.ts @@ -189,48 +189,44 @@ export class APService { } async create(ctx: ServiceContext, data: CreateAPDto): Promise { - // Calcular monto neto (con retenciones) - const netAmount = - data.originalAmount - - (data.retentionIsr ?? 0) - - (data.retentionIva ?? 0) - + // Calcular retención total + const retentionAmount = + (data.retentionIsr ?? 0) + + (data.retentionIva ?? 0) + (data.guaranteeFund ?? 0); + const subtotal = data.originalAmount - (data.taxAmount ?? 0); + const totalAmount = data.originalAmount; + const balance = totalAmount - retentionAmount; const ap = this.apRepository.create({ tenantId: ctx.tenantId, documentType: data.documentType, documentNumber: data.documentNumber, - documentDate: data.documentDate, + invoiceDate: data.documentDate, dueDate: data.dueDate, - partnerId: data.supplierId, - partnerName: data.supplierName, - partnerRfc: data.partnerRfc, + supplierId: data.partnerId, + supplierName: data.partnerName, + supplierRfc: data.partnerRfc, projectId: data.projectId, projectCode: data.projectCode, - contractId: data.contractId, - purchaseOrderId: data.purchaseOrderId, - originalAmount: data.originalAmount, + subtotal, taxAmount: data.taxAmount ?? 0, - retentionIsr: data.retentionIsr ?? 0, - retentionIva: data.retentionIva ?? 0, - guaranteeFund: data.guaranteeFund ?? 0, - netAmount, + retentionAmount, + totalAmount, paidAmount: 0, - balanceAmount: netAmount, - currencyCode: data.currencyCode ?? 'MXN', + balance, + currency: data.currencyCode ?? 'MXN', exchangeRate: data.exchangeRate ?? 1, cfdiUuid: data.cfdiUuid, - cfdiXml: data.cfdiXml, - description: data.description, - paymentTermDays: data.paymentTermDays, - ledgerAccountId: data.ledgerAccountId, + paymentDays: data.paymentTermDays ?? 30, notes: data.notes, metadata: data.metadata, status: 'pending', createdBy: ctx.userId, }); - return this.apRepository.save(ap); + const result = await this.apRepository.save(ap); + return result; } async update( @@ -252,9 +248,10 @@ export class APService { updatedBy: ctx.userId, }); - // Recalcular saldo si cambió el monto total - if (data.totalAmount !== undefined) { - ap.balance = ap.totalAmount - ap.paidAmount; + // Recalcular saldo si cambió el monto original + if (data.originalAmount !== undefined) { + ap.totalAmount = data.originalAmount; + ap.balance = Number(ap.totalAmount) - Number(ap.retentionAmount) - Number(ap.paidAmount); } return this.apRepository.save(ap); diff --git a/src/modules/finance/services/ar.service.ts b/src/modules/finance/services/ar.service.ts index 9380715..088d535 100644 --- a/src/modules/finance/services/ar.service.ts +++ b/src/modules/finance/services/ar.service.ts @@ -13,7 +13,6 @@ import { ARDocumentType, ARPayment, CollectionMethod, - CollectionStatus, } from '../entities'; interface ServiceContext { @@ -84,8 +83,10 @@ interface AgingBucket { export class ARService { private arRepository: Repository; private collectionRepository: Repository; + dataSource: DataSource; - constructor(private dataSource: DataSource) { + constructor(dataSource: DataSource) { + this.dataSource = dataSource; this.arRepository = dataSource.getRepository(AccountReceivable); this.collectionRepository = dataSource.getRepository(ARPayment); } @@ -128,7 +129,7 @@ export class ARService { } if (partnerId) { - queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); + queryBuilder.andWhere('ar.customerId = :partnerId', { partnerId }); } if (projectId) { @@ -136,11 +137,11 @@ export class ARService { } if (startDate) { - queryBuilder.andWhere('ar.documentDate >= :startDate', { startDate }); + queryBuilder.andWhere('ar.invoiceDate >= :startDate', { startDate }); } if (endDate) { - queryBuilder.andWhere('ar.documentDate <= :endDate', { endDate }); + queryBuilder.andWhere('ar.invoiceDate <= :endDate', { endDate }); } if (overdue) { @@ -152,7 +153,7 @@ export class ARService { if (search) { queryBuilder.andWhere( - '(ar.documentNumber ILIKE :search OR ar.partnerName ILIKE :search)', + '(ar.documentNumber ILIKE :search OR ar.customerName ILIKE :search)', { search: `%${search}%` } ); } @@ -186,41 +187,36 @@ export class ARService { } async create(ctx: ServiceContext, data: CreateARDto): Promise { - // Calcular monto neto (con retenciones) - const netAmount = - data.originalAmount - - (data.retentionIsr ?? 0) - - (data.retentionIva ?? 0) - + // Calcular retención total y monto neto + const retentionAmount = + (data.retentionIsr ?? 0) + + (data.retentionIva ?? 0) + (data.guaranteeFund ?? 0); + const subtotal = data.originalAmount - (data.taxAmount ?? 0); + const totalAmount = data.originalAmount; + const balance = totalAmount - retentionAmount; const ar = this.arRepository.create({ tenantId: ctx.tenantId, documentType: data.documentType, documentNumber: data.documentNumber, - documentDate: data.documentDate, + invoiceDate: data.documentDate, dueDate: data.dueDate, - partnerId: data.partnerId, - partnerName: data.partnerName, - partnerRfc: data.partnerRfc, + customerId: data.partnerId, + customerName: data.partnerName, + customerRfc: data.partnerRfc, projectId: data.projectId, projectCode: data.projectCode, - contractId: data.contractId, - estimationId: data.estimationId, - originalAmount: data.originalAmount, + subtotal, taxAmount: data.taxAmount ?? 0, - retentionIsr: data.retentionIsr ?? 0, - retentionIva: data.retentionIva ?? 0, - guaranteeFund: data.guaranteeFund ?? 0, - netAmount, + retentionAmount, + totalAmount, collectedAmount: 0, - balanceAmount: netAmount, - currencyCode: data.currencyCode ?? 'MXN', + balance, + currency: data.currencyCode ?? 'MXN', exchangeRate: data.exchangeRate ?? 1, cfdiUuid: data.cfdiUuid, - cfdiXml: data.cfdiXml, - description: data.description, - paymentTermDays: data.paymentTermDays, - ledgerAccountId: data.ledgerAccountId, + paymentDays: data.paymentTermDays ?? 30, notes: data.notes, metadata: data.metadata, status: 'pending', @@ -252,12 +248,8 @@ export class ARService { // Recalcular montos si cambió el monto original if (data.originalAmount !== undefined) { - ar.netAmount = - ar.originalAmount - - (ar.retentionIsr ?? 0) - - (ar.retentionIva ?? 0) - - (ar.guaranteeFund ?? 0); - ar.balanceAmount = ar.netAmount - ar.collectedAmount; + ar.totalAmount = data.originalAmount; + ar.balance = ar.totalAmount - ar.retentionAmount - ar.collectedAmount; } return this.arRepository.save(ar); @@ -327,7 +319,7 @@ export class ARService { throw new Error(`Cuenta por cobrar ${ar.documentNumber} ya está cobrada, cancelada o castigada`); } arRecords.push(ar); - totalToApply += ar.balanceAmount; + totalToApply += Number(ar.balance); } // Validar monto @@ -347,16 +339,13 @@ export class ARService { collectionMethod: data.collectionMethod, collectionDate: data.collectionDate, bankAccountId: data.bankAccountId, - collectionAmount: data.collectionAmount, - currencyCode: data.currencyCode ?? 'MXN', + accountReceivableId: arRecords[0]?.id, + amount: data.collectionAmount, + currency: data.currencyCode ?? 'MXN', exchangeRate: data.exchangeRate ?? 1, - reference: data.reference, - depositReference: data.depositReference, transferReference: data.transferReference, - collectionConcept: data.collectionConcept, notes: data.notes, status: 'pending', - documentCount: arRecords.length, createdBy: ctx.userId, }); @@ -373,14 +362,15 @@ export class ARService { for (const ar of sortedAR) { if (remainingAmount <= 0) break; - const amountToApply = Math.min(remainingAmount, ar.balanceAmount); + const arBalance = Number(ar.balance); + const amountToApply = Math.min(remainingAmount, arBalance); applications.push({ arId: ar.id, amount: amountToApply }); - ar.collectedAmount += amountToApply; - ar.balanceAmount -= amountToApply; - ar.lastCollectionDate = data.collectionDate; + ar.collectedAmount = Number(ar.collectedAmount) + amountToApply; + ar.balance = arBalance - amountToApply; + ar.collectionDate = data.collectionDate; - if (ar.balanceAmount <= 0.01) { + if (ar.balance <= 0.01) { ar.status = 'collected'; } else { ar.status = 'partial'; @@ -394,9 +384,9 @@ export class ARService { // Guardar aplicaciones en metadata del cobro savedCollection.metadata = { applications }; - await this.collectionRepository.save(savedCollection); + const result = await this.collectionRepository.save(savedCollection); - return savedCollection; + return result; } private async generateCollectionNumber(ctx: ServiceContext): Promise { @@ -435,9 +425,8 @@ export class ARService { throw new Error('Solo se pueden confirmar cobros pendientes'); } - collection.status = 'confirmed'; - collection.confirmedAt = new Date(); - collection.confirmedById = ctx.userId; + collection.status = 'deposited'; + collection.depositDate = new Date(); collection.updatedBy = ctx.userId; return this.collectionRepository.save(collection); @@ -468,9 +457,10 @@ export class ARService { for (const app of applications) { const ar = await this.arRepository.findOne({ where: { id: app.arId } }); if (ar) { - ar.collectedAmount -= app.amount; - ar.balanceAmount += app.amount; - ar.status = ar.balanceAmount >= ar.netAmount ? 'pending' : 'partial'; + ar.collectedAmount = Number(ar.collectedAmount) - app.amount; + ar.balance = Number(ar.balance) + app.amount; + const expectedBalance = Number(ar.totalAmount) - Number(ar.retentionAmount); + ar.status = Number(ar.balance) >= expectedBalance ? 'pending' : 'partial'; ar.updatedBy = ctx.userId; await this.arRepository.save(ar); } @@ -505,7 +495,7 @@ export class ARService { .andWhere('ar.deletedAt IS NULL'); if (partnerId) { - queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); + queryBuilder.andWhere('ar.customerId = :partnerId', { partnerId }); } if (projectId) { @@ -532,7 +522,7 @@ export class ARService { const daysOverdue = Math.floor( (asOfDate.getTime() - new Date(ar.dueDate).getTime()) / (1000 * 60 * 60 * 24) ); - const balance = ar.balanceAmount; + const balance = Number(ar.balance); // Clasificar en bucket let bucket: keyof AgingBucket; @@ -552,10 +542,10 @@ export class ARService { summary.total += balance; // Por cliente - if (!partnerMap.has(ar.partnerId)) { - partnerMap.set(ar.partnerId, { - partnerId: ar.partnerId, - partnerName: ar.partnerName, + if (!partnerMap.has(ar.customerId)) { + partnerMap.set(ar.customerId, { + partnerId: ar.customerId, + partnerName: ar.customerName, aging: { current: 0, days1to30: 0, @@ -567,7 +557,7 @@ export class ARService { }); } - const partnerData = partnerMap.get(ar.partnerId)!; + const partnerData = partnerMap.get(ar.customerId)!; partnerData.aging[bucket] += balance; partnerData.aging.total += balance; } @@ -594,7 +584,7 @@ export class ARService { .andWhere('ar.deletedAt IS NULL'); if (partnerId) { - queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); + queryBuilder.andWhere('ar.customerId = :partnerId', { partnerId }); } if (projectId) { @@ -621,7 +611,7 @@ export class ARService { const entry = forecastMap.get(dateKey)!; entry.documents.push(ar); - entry.totalAmount += ar.balanceAmount; + entry.totalAmount += Number(ar.balance); } return Array.from(forecastMap.values()).sort( @@ -653,28 +643,28 @@ export class ARService { // Total pendiente const totalPending = await baseQuery .clone() - .select('SUM(ar.balanceAmount)', 'total') + .select('SUM(ar.balance)', 'total') .getRawOne(); // Vencido const totalOverdue = await baseQuery .clone() .andWhere('ar.dueDate < :today', { today }) - .select('SUM(ar.balanceAmount)', 'total') + .select('SUM(ar.balance)', 'total') .getRawOne(); // Por cobrar esta semana const dueThisWeek = await baseQuery .clone() .andWhere('ar.dueDate BETWEEN :today AND :endOfWeek', { today, endOfWeek }) - .select('SUM(ar.balanceAmount)', 'total') + .select('SUM(ar.balance)', 'total') .getRawOne(); // Por cobrar este mes const dueThisMonth = await baseQuery .clone() .andWhere('ar.dueDate BETWEEN :today AND :endOfMonth', { today, endOfMonth }) - .select('SUM(ar.balanceAmount)', 'total') + .select('SUM(ar.balance)', 'total') .getRawOne(); // Conteos @@ -689,11 +679,11 @@ export class ARService { const monthlyInvoiced = await this.arRepository .createQueryBuilder('ar') .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) - .andWhere('ar.documentDate BETWEEN :startOfMonth AND :endOfMonth', { + .andWhere('ar.invoiceDate BETWEEN :startOfMonth AND :endOfMonth', { startOfMonth, endOfMonth, }) - .select('SUM(ar.netAmount)', 'total') + .select('SUM(ar.totalAmount)', 'total') .getRawOne(); const monthlyCollected = await this.collectionRepository @@ -704,7 +694,7 @@ export class ARService { endOfMonth, }) .andWhere('col.status != :cancelled', { cancelled: 'cancelled' }) - .select('SUM(col.collectionAmount)', 'total') + .select('SUM(col.amount)', 'total') .getRawOne(); const invoicedAmount = parseFloat(monthlyInvoiced?.total) || 0; diff --git a/src/modules/finance/services/bank-reconciliation.service.ts b/src/modules/finance/services/bank-reconciliation.service.ts index e10761f..50998a1 100644 --- a/src/modules/finance/services/bank-reconciliation.service.ts +++ b/src/modules/finance/services/bank-reconciliation.service.ts @@ -107,8 +107,10 @@ export class BankReconciliationService { private bankAccountRepository: Repository; private movementRepository: Repository; private reconciliationRepository: Repository; + dataSource: DataSource; - constructor(private dataSource: DataSource) { + constructor(dataSource: DataSource) { + this.dataSource = dataSource; this.bankAccountRepository = dataSource.getRepository(BankAccount); this.movementRepository = dataSource.getRepository(BankMovement); this.reconciliationRepository = dataSource.getRepository(BankReconciliation); diff --git a/src/modules/finance/services/cash-flow.service.ts b/src/modules/finance/services/cash-flow.service.ts index acc07ce..d1ece24 100644 --- a/src/modules/finance/services/cash-flow.service.ts +++ b/src/modules/finance/services/cash-flow.service.ts @@ -79,7 +79,10 @@ export class CashFlowService { private arRepository: Repository; private bankAccountRepository: Repository; - constructor(private dataSource: DataSource) { + dataSource: DataSource; + + constructor(dataSource: DataSource) { + this.dataSource = dataSource; this.projectionRepository = dataSource.getRepository(CashFlowProjection); this.apRepository = dataSource.getRepository(AccountPayable); this.arRepository = dataSource.getRepository(AccountReceivable); @@ -347,10 +350,10 @@ export class CashFlowService { const arRecords = await arQuery.getMany(); const incomeEstimations = arRecords .filter((ar) => ar.documentType === 'estimation') - .reduce((sum, ar) => sum + Number(ar.balanceAmount), 0); + .reduce((sum, ar) => sum + Number(ar.balance), 0); const incomeSales = arRecords .filter((ar) => ar.documentType !== 'estimation') - .reduce((sum, ar) => sum + Number(ar.balanceAmount), 0); + .reduce((sum, ar) => sum + Number(ar.balance), 0); // Obtener pagos esperados (AP por vencer en el periodo) const apQuery = this.apRepository @@ -367,7 +370,7 @@ export class CashFlowService { const apRecords = await apQuery.getMany(); const expenseSuppliers = apRecords .filter((ap) => ap.documentType === 'invoice') - .reduce((sum, ap) => sum + Number(ap.balanceAmount), 0); + .reduce((sum, ap) => sum + Number(ap.balance), 0); // Determinar año y periodo fiscal const fiscalYear = periodStart.getFullYear(); @@ -401,16 +404,16 @@ export class CashFlowService { incomeBreakdown: arRecords.map((ar) => ({ id: ar.id, documentNumber: ar.documentNumber, - partnerName: ar.partnerName, + partnerName: ar.customerName, dueDate: ar.dueDate, - amount: ar.balanceAmount, + amount: ar.balance, })), expenseBreakdown: apRecords.map((ap) => ({ id: ap.id, documentNumber: ap.documentNumber, - partnerName: ap.partnerName, + partnerName: ap.supplierName, dueDate: ap.dueDate, - amount: ap.balanceAmount, + amount: ap.balance, })), notes: `Proyección automática generada el ${new Date().toISOString()}`, }); diff --git a/src/modules/finance/services/erp-integration.service.ts b/src/modules/finance/services/erp-integration.service.ts index 907d400..5bc9ad0 100644 --- a/src/modules/finance/services/erp-integration.service.ts +++ b/src/modules/finance/services/erp-integration.service.ts @@ -9,11 +9,9 @@ import { DataSource, Repository, IsNull, Between } from 'typeorm'; import { ChartOfAccounts, + AccountType, AccountingEntry, AccountingEntryLine, - AccountPayable, - AccountReceivable, - BankMovement, } from '../entities'; interface ServiceContext { @@ -21,13 +19,6 @@ interface ServiceContext { userId?: string; } -interface ExportConfig { - format: 'csv' | 'xml' | 'json' | 'txt'; - encoding?: string; - delimiter?: string; - includeHeaders?: boolean; -} - interface SAPExportEntry { BUKRS: string; // Sociedad BELNR: string; // Número de documento @@ -86,17 +77,13 @@ export class ERPIntegrationService { private accountRepository: Repository; private entryRepository: Repository; private lineRepository: Repository; - private apRepository: Repository; - private arRepository: Repository; - private movementRepository: Repository; + dataSource: DataSource; - constructor(private dataSource: DataSource) { + constructor(dataSource: DataSource) { + this.dataSource = dataSource; this.accountRepository = dataSource.getRepository(ChartOfAccounts); this.entryRepository = dataSource.getRepository(AccountingEntry); this.lineRepository = dataSource.getRepository(AccountingEntryLine); - this.apRepository = dataSource.getRepository(AccountPayable); - this.arRepository = dataSource.getRepository(AccountReceivable); - this.movementRepository = dataSource.getRepository(BankMovement); } // ==================== EXPORTACIÓN SAP ==================== @@ -111,7 +98,7 @@ export class ERPIntegrationService { journalNumber?: number; } = {} ): Promise { - const { companyCode = '1000', documentType = 'SA', journalNumber = 1 } = options; + const { companyCode = '1000' } = options; const entries = await this.entryRepository.find({ where: { @@ -132,7 +119,7 @@ export class ERPIntegrationService { BLDAT: this.formatDateSAP(entry.entryDate), BUDAT: this.formatDateSAP(entry.entryDate), MONAT: entry.fiscalPeriod, - WAERS: entry.currencyCode, + WAERS: entry.currency, KURSF: entry.exchangeRate, BKTXT: entry.description.slice(0, 25), lines: (entry.lines || []).map((line, idx) => ({ @@ -142,7 +129,7 @@ export class ERPIntegrationService { WRBTR: Math.abs(line.debit || line.credit), DMBTR: Math.abs(line.debit || line.credit) * entry.exchangeRate, SGTXT: (line.description || entry.description).slice(0, 50), - ZUONR: line.reference?.slice(0, 18), + ZUONR: line.description?.slice(0, 18), KOSTL: line.costCenterId?.slice(0, 10), PROJK: line.projectId?.slice(0, 24), })), @@ -211,7 +198,7 @@ export class ERPIntegrationService { diario?: number; } = {} ): Promise { - const { polizaTipo = 1, diario = 1 } = options; // 1=Diario + const { diario = 1 } = options; // 1=Diario const entries = await this.entryRepository.find({ where: { @@ -236,7 +223,7 @@ export class ERPIntegrationService { Concepto: (line.description || entry.description).slice(0, 200), Cargo: line.debit, Abono: line.credit, - Referencia: line.reference?.slice(0, 20), + Referencia: line.description?.slice(0, 20), Diario: diario, })), })); @@ -345,7 +332,8 @@ ${entries.map((entry) => this.generatePolizaXML(entry)).join('\n')} private generatePolizaXML(entry: AccountingEntry): string { const fecha = new Date(entry.entryDate).toISOString().split('T')[0]; - const tipoPoliza = this.mapEntryTypeToCFDI(entry.entryType); + // Map entry type for CFDI (reserved for future use) + this.mapEntryTypeToCFDI(entry.entryType); const transacciones = (entry.lines || []) .map((line) => { @@ -626,7 +614,7 @@ ${cuentas} nature: 'debit' | 'credit'; level: number; parentCode?: string; - satCode?: string; + sapCode?: string; }[] = []; if (format === 'json') { @@ -643,7 +631,7 @@ ${cuentas} type: parts[2] as AccountType, nature: parts[3] as 'debit' | 'credit', level: parseInt(parts[4]) || 1, - satCode: parts[5], + sapCode: parts[5], }; }); } @@ -676,7 +664,7 @@ ${cuentas} accountType: acc.type, nature: acc.nature, level: acc.level, - sapCode: acc.sapCode, + satCode: acc.sapCode, allowsDirectPosting: true, status: 'active', initialBalance: 0,