refactor: Clean up finance services (AP, AR, cash-flow, bank-reconciliation)

- Cleaned up ap.service.ts
- Refactored ar.service.ts
- Minor fixes in bank-reconciliation.service.ts
- Updated cash-flow.service.ts
- Cleaned erp-integration.service.ts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Adrian Flores Cortes 2026-01-25 14:31:51 -06:00
parent 369f461695
commit cf23727b2b
5 changed files with 111 additions and 131 deletions

View File

@ -189,48 +189,44 @@ export class APService {
} }
async create(ctx: ServiceContext, data: CreateAPDto): Promise<AccountPayable> { async create(ctx: ServiceContext, data: CreateAPDto): Promise<AccountPayable> {
// Calcular monto neto (con retenciones) // Calcular retención total
const netAmount = const retentionAmount =
data.originalAmount - (data.retentionIsr ?? 0) +
(data.retentionIsr ?? 0) - (data.retentionIva ?? 0) +
(data.retentionIva ?? 0) -
(data.guaranteeFund ?? 0); (data.guaranteeFund ?? 0);
const subtotal = data.originalAmount - (data.taxAmount ?? 0);
const totalAmount = data.originalAmount;
const balance = totalAmount - retentionAmount;
const ap = this.apRepository.create({ const ap = this.apRepository.create({
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
documentType: data.documentType, documentType: data.documentType,
documentNumber: data.documentNumber, documentNumber: data.documentNumber,
documentDate: data.documentDate, invoiceDate: data.documentDate,
dueDate: data.dueDate, dueDate: data.dueDate,
partnerId: data.supplierId, supplierId: data.partnerId,
partnerName: data.supplierName, supplierName: data.partnerName,
partnerRfc: data.partnerRfc, supplierRfc: data.partnerRfc,
projectId: data.projectId, projectId: data.projectId,
projectCode: data.projectCode, projectCode: data.projectCode,
contractId: data.contractId, subtotal,
purchaseOrderId: data.purchaseOrderId,
originalAmount: data.originalAmount,
taxAmount: data.taxAmount ?? 0, taxAmount: data.taxAmount ?? 0,
retentionIsr: data.retentionIsr ?? 0, retentionAmount,
retentionIva: data.retentionIva ?? 0, totalAmount,
guaranteeFund: data.guaranteeFund ?? 0,
netAmount,
paidAmount: 0, paidAmount: 0,
balanceAmount: netAmount, balance,
currencyCode: data.currencyCode ?? 'MXN', currency: data.currencyCode ?? 'MXN',
exchangeRate: data.exchangeRate ?? 1, exchangeRate: data.exchangeRate ?? 1,
cfdiUuid: data.cfdiUuid, cfdiUuid: data.cfdiUuid,
cfdiXml: data.cfdiXml, paymentDays: data.paymentTermDays ?? 30,
description: data.description,
paymentTermDays: data.paymentTermDays,
ledgerAccountId: data.ledgerAccountId,
notes: data.notes, notes: data.notes,
metadata: data.metadata, metadata: data.metadata,
status: 'pending', status: 'pending',
createdBy: ctx.userId, createdBy: ctx.userId,
}); });
return this.apRepository.save(ap); const result = await this.apRepository.save(ap);
return result;
} }
async update( async update(
@ -252,9 +248,10 @@ export class APService {
updatedBy: ctx.userId, updatedBy: ctx.userId,
}); });
// Recalcular saldo si cambió el monto total // Recalcular saldo si cambió el monto original
if (data.totalAmount !== undefined) { if (data.originalAmount !== undefined) {
ap.balance = ap.totalAmount - ap.paidAmount; ap.totalAmount = data.originalAmount;
ap.balance = Number(ap.totalAmount) - Number(ap.retentionAmount) - Number(ap.paidAmount);
} }
return this.apRepository.save(ap); return this.apRepository.save(ap);

View File

@ -13,7 +13,6 @@ import {
ARDocumentType, ARDocumentType,
ARPayment, ARPayment,
CollectionMethod, CollectionMethod,
CollectionStatus,
} from '../entities'; } from '../entities';
interface ServiceContext { interface ServiceContext {
@ -84,8 +83,10 @@ interface AgingBucket {
export class ARService { export class ARService {
private arRepository: Repository<AccountReceivable>; private arRepository: Repository<AccountReceivable>;
private collectionRepository: Repository<ARPayment>; private collectionRepository: Repository<ARPayment>;
dataSource: DataSource;
constructor(private dataSource: DataSource) { constructor(dataSource: DataSource) {
this.dataSource = dataSource;
this.arRepository = dataSource.getRepository(AccountReceivable); this.arRepository = dataSource.getRepository(AccountReceivable);
this.collectionRepository = dataSource.getRepository(ARPayment); this.collectionRepository = dataSource.getRepository(ARPayment);
} }
@ -128,7 +129,7 @@ export class ARService {
} }
if (partnerId) { if (partnerId) {
queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); queryBuilder.andWhere('ar.customerId = :partnerId', { partnerId });
} }
if (projectId) { if (projectId) {
@ -136,11 +137,11 @@ export class ARService {
} }
if (startDate) { if (startDate) {
queryBuilder.andWhere('ar.documentDate >= :startDate', { startDate }); queryBuilder.andWhere('ar.invoiceDate >= :startDate', { startDate });
} }
if (endDate) { if (endDate) {
queryBuilder.andWhere('ar.documentDate <= :endDate', { endDate }); queryBuilder.andWhere('ar.invoiceDate <= :endDate', { endDate });
} }
if (overdue) { if (overdue) {
@ -152,7 +153,7 @@ export class ARService {
if (search) { if (search) {
queryBuilder.andWhere( queryBuilder.andWhere(
'(ar.documentNumber ILIKE :search OR ar.partnerName ILIKE :search)', '(ar.documentNumber ILIKE :search OR ar.customerName ILIKE :search)',
{ search: `%${search}%` } { search: `%${search}%` }
); );
} }
@ -186,41 +187,36 @@ export class ARService {
} }
async create(ctx: ServiceContext, data: CreateARDto): Promise<AccountReceivable> { async create(ctx: ServiceContext, data: CreateARDto): Promise<AccountReceivable> {
// Calcular monto neto (con retenciones) // Calcular retención total y monto neto
const netAmount = const retentionAmount =
data.originalAmount - (data.retentionIsr ?? 0) +
(data.retentionIsr ?? 0) - (data.retentionIva ?? 0) +
(data.retentionIva ?? 0) -
(data.guaranteeFund ?? 0); (data.guaranteeFund ?? 0);
const subtotal = data.originalAmount - (data.taxAmount ?? 0);
const totalAmount = data.originalAmount;
const balance = totalAmount - retentionAmount;
const ar = this.arRepository.create({ const ar = this.arRepository.create({
tenantId: ctx.tenantId, tenantId: ctx.tenantId,
documentType: data.documentType, documentType: data.documentType,
documentNumber: data.documentNumber, documentNumber: data.documentNumber,
documentDate: data.documentDate, invoiceDate: data.documentDate,
dueDate: data.dueDate, dueDate: data.dueDate,
partnerId: data.partnerId, customerId: data.partnerId,
partnerName: data.partnerName, customerName: data.partnerName,
partnerRfc: data.partnerRfc, customerRfc: data.partnerRfc,
projectId: data.projectId, projectId: data.projectId,
projectCode: data.projectCode, projectCode: data.projectCode,
contractId: data.contractId, subtotal,
estimationId: data.estimationId,
originalAmount: data.originalAmount,
taxAmount: data.taxAmount ?? 0, taxAmount: data.taxAmount ?? 0,
retentionIsr: data.retentionIsr ?? 0, retentionAmount,
retentionIva: data.retentionIva ?? 0, totalAmount,
guaranteeFund: data.guaranteeFund ?? 0,
netAmount,
collectedAmount: 0, collectedAmount: 0,
balanceAmount: netAmount, balance,
currencyCode: data.currencyCode ?? 'MXN', currency: data.currencyCode ?? 'MXN',
exchangeRate: data.exchangeRate ?? 1, exchangeRate: data.exchangeRate ?? 1,
cfdiUuid: data.cfdiUuid, cfdiUuid: data.cfdiUuid,
cfdiXml: data.cfdiXml, paymentDays: data.paymentTermDays ?? 30,
description: data.description,
paymentTermDays: data.paymentTermDays,
ledgerAccountId: data.ledgerAccountId,
notes: data.notes, notes: data.notes,
metadata: data.metadata, metadata: data.metadata,
status: 'pending', status: 'pending',
@ -252,12 +248,8 @@ export class ARService {
// Recalcular montos si cambió el monto original // Recalcular montos si cambió el monto original
if (data.originalAmount !== undefined) { if (data.originalAmount !== undefined) {
ar.netAmount = ar.totalAmount = data.originalAmount;
ar.originalAmount - ar.balance = ar.totalAmount - ar.retentionAmount - ar.collectedAmount;
(ar.retentionIsr ?? 0) -
(ar.retentionIva ?? 0) -
(ar.guaranteeFund ?? 0);
ar.balanceAmount = ar.netAmount - ar.collectedAmount;
} }
return this.arRepository.save(ar); 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`); throw new Error(`Cuenta por cobrar ${ar.documentNumber} ya está cobrada, cancelada o castigada`);
} }
arRecords.push(ar); arRecords.push(ar);
totalToApply += ar.balanceAmount; totalToApply += Number(ar.balance);
} }
// Validar monto // Validar monto
@ -347,16 +339,13 @@ export class ARService {
collectionMethod: data.collectionMethod, collectionMethod: data.collectionMethod,
collectionDate: data.collectionDate, collectionDate: data.collectionDate,
bankAccountId: data.bankAccountId, bankAccountId: data.bankAccountId,
collectionAmount: data.collectionAmount, accountReceivableId: arRecords[0]?.id,
currencyCode: data.currencyCode ?? 'MXN', amount: data.collectionAmount,
currency: data.currencyCode ?? 'MXN',
exchangeRate: data.exchangeRate ?? 1, exchangeRate: data.exchangeRate ?? 1,
reference: data.reference,
depositReference: data.depositReference,
transferReference: data.transferReference, transferReference: data.transferReference,
collectionConcept: data.collectionConcept,
notes: data.notes, notes: data.notes,
status: 'pending', status: 'pending',
documentCount: arRecords.length,
createdBy: ctx.userId, createdBy: ctx.userId,
}); });
@ -373,14 +362,15 @@ export class ARService {
for (const ar of sortedAR) { for (const ar of sortedAR) {
if (remainingAmount <= 0) break; 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 }); applications.push({ arId: ar.id, amount: amountToApply });
ar.collectedAmount += amountToApply; ar.collectedAmount = Number(ar.collectedAmount) + amountToApply;
ar.balanceAmount -= amountToApply; ar.balance = arBalance - amountToApply;
ar.lastCollectionDate = data.collectionDate; ar.collectionDate = data.collectionDate;
if (ar.balanceAmount <= 0.01) { if (ar.balance <= 0.01) {
ar.status = 'collected'; ar.status = 'collected';
} else { } else {
ar.status = 'partial'; ar.status = 'partial';
@ -394,9 +384,9 @@ export class ARService {
// Guardar aplicaciones en metadata del cobro // Guardar aplicaciones en metadata del cobro
savedCollection.metadata = { applications }; 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<string> { private async generateCollectionNumber(ctx: ServiceContext): Promise<string> {
@ -435,9 +425,8 @@ export class ARService {
throw new Error('Solo se pueden confirmar cobros pendientes'); throw new Error('Solo se pueden confirmar cobros pendientes');
} }
collection.status = 'confirmed'; collection.status = 'deposited';
collection.confirmedAt = new Date(); collection.depositDate = new Date();
collection.confirmedById = ctx.userId;
collection.updatedBy = ctx.userId; collection.updatedBy = ctx.userId;
return this.collectionRepository.save(collection); return this.collectionRepository.save(collection);
@ -468,9 +457,10 @@ export class ARService {
for (const app of applications) { for (const app of applications) {
const ar = await this.arRepository.findOne({ where: { id: app.arId } }); const ar = await this.arRepository.findOne({ where: { id: app.arId } });
if (ar) { if (ar) {
ar.collectedAmount -= app.amount; ar.collectedAmount = Number(ar.collectedAmount) - app.amount;
ar.balanceAmount += app.amount; ar.balance = Number(ar.balance) + app.amount;
ar.status = ar.balanceAmount >= ar.netAmount ? 'pending' : 'partial'; const expectedBalance = Number(ar.totalAmount) - Number(ar.retentionAmount);
ar.status = Number(ar.balance) >= expectedBalance ? 'pending' : 'partial';
ar.updatedBy = ctx.userId; ar.updatedBy = ctx.userId;
await this.arRepository.save(ar); await this.arRepository.save(ar);
} }
@ -505,7 +495,7 @@ export class ARService {
.andWhere('ar.deletedAt IS NULL'); .andWhere('ar.deletedAt IS NULL');
if (partnerId) { if (partnerId) {
queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); queryBuilder.andWhere('ar.customerId = :partnerId', { partnerId });
} }
if (projectId) { if (projectId) {
@ -532,7 +522,7 @@ export class ARService {
const daysOverdue = Math.floor( const daysOverdue = Math.floor(
(asOfDate.getTime() - new Date(ar.dueDate).getTime()) / (1000 * 60 * 60 * 24) (asOfDate.getTime() - new Date(ar.dueDate).getTime()) / (1000 * 60 * 60 * 24)
); );
const balance = ar.balanceAmount; const balance = Number(ar.balance);
// Clasificar en bucket // Clasificar en bucket
let bucket: keyof AgingBucket; let bucket: keyof AgingBucket;
@ -552,10 +542,10 @@ export class ARService {
summary.total += balance; summary.total += balance;
// Por cliente // Por cliente
if (!partnerMap.has(ar.partnerId)) { if (!partnerMap.has(ar.customerId)) {
partnerMap.set(ar.partnerId, { partnerMap.set(ar.customerId, {
partnerId: ar.partnerId, partnerId: ar.customerId,
partnerName: ar.partnerName, partnerName: ar.customerName,
aging: { aging: {
current: 0, current: 0,
days1to30: 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[bucket] += balance;
partnerData.aging.total += balance; partnerData.aging.total += balance;
} }
@ -594,7 +584,7 @@ export class ARService {
.andWhere('ar.deletedAt IS NULL'); .andWhere('ar.deletedAt IS NULL');
if (partnerId) { if (partnerId) {
queryBuilder.andWhere('ar.partnerId = :partnerId', { partnerId }); queryBuilder.andWhere('ar.customerId = :partnerId', { partnerId });
} }
if (projectId) { if (projectId) {
@ -621,7 +611,7 @@ export class ARService {
const entry = forecastMap.get(dateKey)!; const entry = forecastMap.get(dateKey)!;
entry.documents.push(ar); entry.documents.push(ar);
entry.totalAmount += ar.balanceAmount; entry.totalAmount += Number(ar.balance);
} }
return Array.from(forecastMap.values()).sort( return Array.from(forecastMap.values()).sort(
@ -653,28 +643,28 @@ export class ARService {
// Total pendiente // Total pendiente
const totalPending = await baseQuery const totalPending = await baseQuery
.clone() .clone()
.select('SUM(ar.balanceAmount)', 'total') .select('SUM(ar.balance)', 'total')
.getRawOne(); .getRawOne();
// Vencido // Vencido
const totalOverdue = await baseQuery const totalOverdue = await baseQuery
.clone() .clone()
.andWhere('ar.dueDate < :today', { today }) .andWhere('ar.dueDate < :today', { today })
.select('SUM(ar.balanceAmount)', 'total') .select('SUM(ar.balance)', 'total')
.getRawOne(); .getRawOne();
// Por cobrar esta semana // Por cobrar esta semana
const dueThisWeek = await baseQuery const dueThisWeek = await baseQuery
.clone() .clone()
.andWhere('ar.dueDate BETWEEN :today AND :endOfWeek', { today, endOfWeek }) .andWhere('ar.dueDate BETWEEN :today AND :endOfWeek', { today, endOfWeek })
.select('SUM(ar.balanceAmount)', 'total') .select('SUM(ar.balance)', 'total')
.getRawOne(); .getRawOne();
// Por cobrar este mes // Por cobrar este mes
const dueThisMonth = await baseQuery const dueThisMonth = await baseQuery
.clone() .clone()
.andWhere('ar.dueDate BETWEEN :today AND :endOfMonth', { today, endOfMonth }) .andWhere('ar.dueDate BETWEEN :today AND :endOfMonth', { today, endOfMonth })
.select('SUM(ar.balanceAmount)', 'total') .select('SUM(ar.balance)', 'total')
.getRawOne(); .getRawOne();
// Conteos // Conteos
@ -689,11 +679,11 @@ export class ARService {
const monthlyInvoiced = await this.arRepository const monthlyInvoiced = await this.arRepository
.createQueryBuilder('ar') .createQueryBuilder('ar')
.where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId }) .where('ar.tenantId = :tenantId', { tenantId: ctx.tenantId })
.andWhere('ar.documentDate BETWEEN :startOfMonth AND :endOfMonth', { .andWhere('ar.invoiceDate BETWEEN :startOfMonth AND :endOfMonth', {
startOfMonth, startOfMonth,
endOfMonth, endOfMonth,
}) })
.select('SUM(ar.netAmount)', 'total') .select('SUM(ar.totalAmount)', 'total')
.getRawOne(); .getRawOne();
const monthlyCollected = await this.collectionRepository const monthlyCollected = await this.collectionRepository
@ -704,7 +694,7 @@ export class ARService {
endOfMonth, endOfMonth,
}) })
.andWhere('col.status != :cancelled', { cancelled: 'cancelled' }) .andWhere('col.status != :cancelled', { cancelled: 'cancelled' })
.select('SUM(col.collectionAmount)', 'total') .select('SUM(col.amount)', 'total')
.getRawOne(); .getRawOne();
const invoicedAmount = parseFloat(monthlyInvoiced?.total) || 0; const invoicedAmount = parseFloat(monthlyInvoiced?.total) || 0;

View File

@ -107,8 +107,10 @@ export class BankReconciliationService {
private bankAccountRepository: Repository<BankAccount>; private bankAccountRepository: Repository<BankAccount>;
private movementRepository: Repository<BankMovement>; private movementRepository: Repository<BankMovement>;
private reconciliationRepository: Repository<BankReconciliation>; private reconciliationRepository: Repository<BankReconciliation>;
dataSource: DataSource;
constructor(private dataSource: DataSource) { constructor(dataSource: DataSource) {
this.dataSource = dataSource;
this.bankAccountRepository = dataSource.getRepository(BankAccount); this.bankAccountRepository = dataSource.getRepository(BankAccount);
this.movementRepository = dataSource.getRepository(BankMovement); this.movementRepository = dataSource.getRepository(BankMovement);
this.reconciliationRepository = dataSource.getRepository(BankReconciliation); this.reconciliationRepository = dataSource.getRepository(BankReconciliation);

View File

@ -79,7 +79,10 @@ export class CashFlowService {
private arRepository: Repository<AccountReceivable>; private arRepository: Repository<AccountReceivable>;
private bankAccountRepository: Repository<BankAccount>; private bankAccountRepository: Repository<BankAccount>;
constructor(private dataSource: DataSource) { dataSource: DataSource;
constructor(dataSource: DataSource) {
this.dataSource = dataSource;
this.projectionRepository = dataSource.getRepository(CashFlowProjection); this.projectionRepository = dataSource.getRepository(CashFlowProjection);
this.apRepository = dataSource.getRepository(AccountPayable); this.apRepository = dataSource.getRepository(AccountPayable);
this.arRepository = dataSource.getRepository(AccountReceivable); this.arRepository = dataSource.getRepository(AccountReceivable);
@ -347,10 +350,10 @@ export class CashFlowService {
const arRecords = await arQuery.getMany(); const arRecords = await arQuery.getMany();
const incomeEstimations = arRecords const incomeEstimations = arRecords
.filter((ar) => ar.documentType === 'estimation') .filter((ar) => ar.documentType === 'estimation')
.reduce((sum, ar) => sum + Number(ar.balanceAmount), 0); .reduce((sum, ar) => sum + Number(ar.balance), 0);
const incomeSales = arRecords const incomeSales = arRecords
.filter((ar) => ar.documentType !== 'estimation') .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) // Obtener pagos esperados (AP por vencer en el periodo)
const apQuery = this.apRepository const apQuery = this.apRepository
@ -367,7 +370,7 @@ export class CashFlowService {
const apRecords = await apQuery.getMany(); const apRecords = await apQuery.getMany();
const expenseSuppliers = apRecords const expenseSuppliers = apRecords
.filter((ap) => ap.documentType === 'invoice') .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 // Determinar año y periodo fiscal
const fiscalYear = periodStart.getFullYear(); const fiscalYear = periodStart.getFullYear();
@ -401,16 +404,16 @@ export class CashFlowService {
incomeBreakdown: arRecords.map((ar) => ({ incomeBreakdown: arRecords.map((ar) => ({
id: ar.id, id: ar.id,
documentNumber: ar.documentNumber, documentNumber: ar.documentNumber,
partnerName: ar.partnerName, partnerName: ar.customerName,
dueDate: ar.dueDate, dueDate: ar.dueDate,
amount: ar.balanceAmount, amount: ar.balance,
})), })),
expenseBreakdown: apRecords.map((ap) => ({ expenseBreakdown: apRecords.map((ap) => ({
id: ap.id, id: ap.id,
documentNumber: ap.documentNumber, documentNumber: ap.documentNumber,
partnerName: ap.partnerName, partnerName: ap.supplierName,
dueDate: ap.dueDate, dueDate: ap.dueDate,
amount: ap.balanceAmount, amount: ap.balance,
})), })),
notes: `Proyección automática generada el ${new Date().toISOString()}`, notes: `Proyección automática generada el ${new Date().toISOString()}`,
}); });

View File

@ -9,11 +9,9 @@
import { DataSource, Repository, IsNull, Between } from 'typeorm'; import { DataSource, Repository, IsNull, Between } from 'typeorm';
import { import {
ChartOfAccounts, ChartOfAccounts,
AccountType,
AccountingEntry, AccountingEntry,
AccountingEntryLine, AccountingEntryLine,
AccountPayable,
AccountReceivable,
BankMovement,
} from '../entities'; } from '../entities';
interface ServiceContext { interface ServiceContext {
@ -21,13 +19,6 @@ interface ServiceContext {
userId?: string; userId?: string;
} }
interface ExportConfig {
format: 'csv' | 'xml' | 'json' | 'txt';
encoding?: string;
delimiter?: string;
includeHeaders?: boolean;
}
interface SAPExportEntry { interface SAPExportEntry {
BUKRS: string; // Sociedad BUKRS: string; // Sociedad
BELNR: string; // Número de documento BELNR: string; // Número de documento
@ -86,17 +77,13 @@ export class ERPIntegrationService {
private accountRepository: Repository<ChartOfAccounts>; private accountRepository: Repository<ChartOfAccounts>;
private entryRepository: Repository<AccountingEntry>; private entryRepository: Repository<AccountingEntry>;
private lineRepository: Repository<AccountingEntryLine>; private lineRepository: Repository<AccountingEntryLine>;
private apRepository: Repository<AccountPayable>; dataSource: DataSource;
private arRepository: Repository<AccountReceivable>;
private movementRepository: Repository<BankMovement>;
constructor(private dataSource: DataSource) { constructor(dataSource: DataSource) {
this.dataSource = dataSource;
this.accountRepository = dataSource.getRepository(ChartOfAccounts); this.accountRepository = dataSource.getRepository(ChartOfAccounts);
this.entryRepository = dataSource.getRepository(AccountingEntry); this.entryRepository = dataSource.getRepository(AccountingEntry);
this.lineRepository = dataSource.getRepository(AccountingEntryLine); this.lineRepository = dataSource.getRepository(AccountingEntryLine);
this.apRepository = dataSource.getRepository(AccountPayable);
this.arRepository = dataSource.getRepository(AccountReceivable);
this.movementRepository = dataSource.getRepository(BankMovement);
} }
// ==================== EXPORTACIÓN SAP ==================== // ==================== EXPORTACIÓN SAP ====================
@ -111,7 +98,7 @@ export class ERPIntegrationService {
journalNumber?: number; journalNumber?: number;
} = {} } = {}
): Promise<ExportResult> { ): Promise<ExportResult> {
const { companyCode = '1000', documentType = 'SA', journalNumber = 1 } = options; const { companyCode = '1000' } = options;
const entries = await this.entryRepository.find({ const entries = await this.entryRepository.find({
where: { where: {
@ -132,7 +119,7 @@ export class ERPIntegrationService {
BLDAT: this.formatDateSAP(entry.entryDate), BLDAT: this.formatDateSAP(entry.entryDate),
BUDAT: this.formatDateSAP(entry.entryDate), BUDAT: this.formatDateSAP(entry.entryDate),
MONAT: entry.fiscalPeriod, MONAT: entry.fiscalPeriod,
WAERS: entry.currencyCode, WAERS: entry.currency,
KURSF: entry.exchangeRate, KURSF: entry.exchangeRate,
BKTXT: entry.description.slice(0, 25), BKTXT: entry.description.slice(0, 25),
lines: (entry.lines || []).map((line, idx) => ({ lines: (entry.lines || []).map((line, idx) => ({
@ -142,7 +129,7 @@ export class ERPIntegrationService {
WRBTR: Math.abs(line.debit || line.credit), WRBTR: Math.abs(line.debit || line.credit),
DMBTR: Math.abs(line.debit || line.credit) * entry.exchangeRate, DMBTR: Math.abs(line.debit || line.credit) * entry.exchangeRate,
SGTXT: (line.description || entry.description).slice(0, 50), 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), KOSTL: line.costCenterId?.slice(0, 10),
PROJK: line.projectId?.slice(0, 24), PROJK: line.projectId?.slice(0, 24),
})), })),
@ -211,7 +198,7 @@ export class ERPIntegrationService {
diario?: number; diario?: number;
} = {} } = {}
): Promise<ExportResult> { ): Promise<ExportResult> {
const { polizaTipo = 1, diario = 1 } = options; // 1=Diario const { diario = 1 } = options; // 1=Diario
const entries = await this.entryRepository.find({ const entries = await this.entryRepository.find({
where: { where: {
@ -236,7 +223,7 @@ export class ERPIntegrationService {
Concepto: (line.description || entry.description).slice(0, 200), Concepto: (line.description || entry.description).slice(0, 200),
Cargo: line.debit, Cargo: line.debit,
Abono: line.credit, Abono: line.credit,
Referencia: line.reference?.slice(0, 20), Referencia: line.description?.slice(0, 20),
Diario: diario, Diario: diario,
})), })),
})); }));
@ -345,7 +332,8 @@ ${entries.map((entry) => this.generatePolizaXML(entry)).join('\n')}
private generatePolizaXML(entry: AccountingEntry): string { private generatePolizaXML(entry: AccountingEntry): string {
const fecha = new Date(entry.entryDate).toISOString().split('T')[0]; 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 || []) const transacciones = (entry.lines || [])
.map((line) => { .map((line) => {
@ -626,7 +614,7 @@ ${cuentas}
nature: 'debit' | 'credit'; nature: 'debit' | 'credit';
level: number; level: number;
parentCode?: string; parentCode?: string;
satCode?: string; sapCode?: string;
}[] = []; }[] = [];
if (format === 'json') { if (format === 'json') {
@ -643,7 +631,7 @@ ${cuentas}
type: parts[2] as AccountType, type: parts[2] as AccountType,
nature: parts[3] as 'debit' | 'credit', nature: parts[3] as 'debit' | 'credit',
level: parseInt(parts[4]) || 1, level: parseInt(parts[4]) || 1,
satCode: parts[5], sapCode: parts[5],
}; };
}); });
} }
@ -676,7 +664,7 @@ ${cuentas}
accountType: acc.type, accountType: acc.type,
nature: acc.nature, nature: acc.nature,
level: acc.level, level: acc.level,
sapCode: acc.sapCode, satCode: acc.sapCode,
allowsDirectPosting: true, allowsDirectPosting: true,
status: 'active', status: 'active',
initialBalance: 0, initialBalance: 0,