feat: implement organization fiscal profile management including ARCA activity search and fiscal data configuration
This commit is contained in:
+2
-2
@@ -69,7 +69,7 @@ FILE_SERVER_URL = http://localhost:4000/
|
|||||||
FILE_SERVER_URL_prod = https://files.turnosxpress.com.ar/
|
FILE_SERVER_URL_prod = https://files.turnosxpress.com.ar/
|
||||||
|
|
||||||
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
|
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
|
||||||
ARCA_WSAA_HOMOLOGATION_URL=https://wsaahomo.afip.gov.ar/ws/services/LoginCms
|
ARCA_WSAA_URL=https://wsaahomo.afip.gov.ar/ws/services/LoginCms
|
||||||
ARCA_WSFE_HOMOLOGATION_URL=https://wswhomo.afip.gov.ar/wsfev1/service.asmx
|
ARCA_WSFE_URL=https://wswhomo.afip.gov.ar/wsfev1/service.asmx
|
||||||
|
|
||||||
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
||||||
|
|||||||
+2
-2
@@ -67,7 +67,7 @@ SYSTEM_KEY = ad7c956a-76bf-45hdr60-8a50-2ca7b2180997
|
|||||||
FILE_SERVER_URL = https://files.turnosxpress.com.ar/
|
FILE_SERVER_URL = https://files.turnosxpress.com.ar/
|
||||||
|
|
||||||
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
|
SYS_ADMIN_API_PUBLIC_KEY = keys/tx-sysadmin-public.key
|
||||||
ARCA_WSAA_HOMOLOGATION_URL=https://wsaahomo.afip.gov.ar/ws/services/LoginCms
|
ARCA_WSAA_URL=https://wsaa.afip.gov.ar/ws/services/LoginCms
|
||||||
ARCA_WSFE_HOMOLOGATION_URL=https://wswhomo.afip.gov.ar/wsfev1/service.asmx
|
ARCA_WSFE_URL=https://servicios1.afip.gov.ar/wsfev1/service.asmx
|
||||||
|
|
||||||
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
ARCA_CREDENTIALS_MASTER_KEY=ZmFrZS1kZXYtYXJjYS1tYXN0ZXIta2V5LTEyMzQ1Njc=
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { TextObjectFilterResult } from "../TextObjectFilter.model";
|
||||||
|
|
||||||
|
export type ArcaActivity = {
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ARCA_ACTIVITIES: ArcaActivity[] = [
|
||||||
|
{ code: "011111", description: "Cultivo de arroz" },
|
||||||
|
{ code: "011112", description: "Cultivo de trigo" },
|
||||||
|
{ code: "011119", description: "Cultivo de cereales n.c.p., excepto los de uso forrajero" },
|
||||||
|
{ code: "011121", description: "Cultivo de maíz" },
|
||||||
|
{ code: "011129", description: "Cultivo de cereales de uso forrajero n.c.p." },
|
||||||
|
{ code: "011130", description: "Cultivo de oleaginosas, excepto soja" },
|
||||||
|
{ code: "011140", description: "Cultivo de pastos de uso forrajero" },
|
||||||
|
{ code: "011211", description: "Cultivo de papa, batata y mandioca" },
|
||||||
|
{ code: "011221", description: "Cultivo de tomate" },
|
||||||
|
{ code: "011229", description: "Cultivo de bulbos, brotes, raíces y hortalizas de fruto n.c.p." },
|
||||||
|
{ code: "011240", description: "Cultivo de legumbres" },
|
||||||
|
{ code: "012110", description: "Cultivo de vid para vinificar" },
|
||||||
|
{ code: "012121", description: "Cultivo de uva de mesa" },
|
||||||
|
{ code: "012200", description: "Cultivo de frutas cítricas" },
|
||||||
|
{ code: "012311", description: "Cultivo de manzana y pera" },
|
||||||
|
{ code: "012320", description: "Cultivo de frutas de carozo" },
|
||||||
|
{ code: "012410", description: "Cultivo de frutas tropicales y subtropicales" },
|
||||||
|
{ code: "012510", description: "Cultivo de frutas secas" },
|
||||||
|
{ code: "013011", description: "Producción de semillas híbridas de cereales y oleaginosas" },
|
||||||
|
{ code: "013019", description: "Producción de semillas varietales o autofecundadas de cereales, oleaginosas, y forrajeras" },
|
||||||
|
{ code: "014113", description: "Cría de ganado bovino, excepto la realizada en cabañas y para la producción de leche" },
|
||||||
|
{ code: "014114", description: "Invernada de ganado bovino excepto el engorde en corrales (feed-lot)" },
|
||||||
|
{ code: "014115", description: "Engorde en corrales (feed-lot)" },
|
||||||
|
{ code: "014211", description: "Cría de ganado equino, excepto la realizada en haras" },
|
||||||
|
{ code: "014300", description: "Cría de ganado camélido" },
|
||||||
|
{ code: "014410", description: "Cría de ganado ovino, excepto en cabañas y para la producción de lana" },
|
||||||
|
{ code: "014420", description: "Cría de ganado ovino realizada en cabañas" },
|
||||||
|
{ code: "014430", description: "Cría de ganado ovino para la producción de lana" },
|
||||||
|
{ code: "014510", description: "Cría de ganado porcino, excepto la realizada en cabañas" },
|
||||||
|
{ code: "014520", description: "Cría de ganado porcino realizada en cabañas" },
|
||||||
|
{ code: "014610", description: "Producción de leche bovina" },
|
||||||
|
{ code: "014620", description: "Producción de leche de oveja y de cabra" },
|
||||||
|
{ code: "014710", description: "Producción de huevos" },
|
||||||
|
{ code: "014720", description: "Cría de aves para producción de carnes" },
|
||||||
|
{ code: "014740", description: "Producción de pollitos BB" },
|
||||||
|
{ code: "014810", description: "Cría de abejas" },
|
||||||
|
{ code: "014820", description: "Producción de pelos de ganado" },
|
||||||
|
{ code: "014910", description: "Cría de animales para la obtención de pieles y cueros" },
|
||||||
|
{ code: "014990", description: "Cría de animales y obtención de productos de origen animal n.c.p." },
|
||||||
|
{ code: "016111", description: "Servicios de labranza, siembra, transplante y cuidados culturales" },
|
||||||
|
{ code: "016112", description: "Servicios de pulverización, desinfección y fumigación terrestre" },
|
||||||
|
{ code: "016113", description: "Servicios de pulverización, desinfección y fumigación aérea" },
|
||||||
|
{ code: "016119", description: "Servicios de maquinaria agrícola n.c.p., excepto los de cosecha mecánica" },
|
||||||
|
{ code: "016120", description: "Servicios de cosecha mecánica" },
|
||||||
|
{ code: "016130", description: "Servicios de contratistas de mano de obra agrícola" },
|
||||||
|
{ code: "016140", description: "Servicios de post cosecha" },
|
||||||
|
{ code: "016150", description: "Servicios de procesamiento de semillas para su siembra" },
|
||||||
|
{ code: "016190", description: "Servicios de apoyo agrícolas n.c.p." },
|
||||||
|
{ code: "016210", description: "Inseminación artificial y servicios n.c.p. para mejorar la reproducción de los animales y el rendimiento de sus productos" },
|
||||||
|
{ code: "016220", description: "Servicios de contratistas de mano de obra pecuaria" },
|
||||||
|
{ code: "016230", description: "Servicios de esquila de animales" },
|
||||||
|
{ code: "016291", description: "Servicios para el control de plagas, baños parasiticidas, etc." },
|
||||||
|
{ code: "016292", description: "Albergue y cuidado de animales de terceros" },
|
||||||
|
{ code: "016299", description: "Servicios de apoyo pecuarios n.c.p." },
|
||||||
|
{ code: "551010", description: "Servicios de alojamiento por hora" },
|
||||||
|
{ code: "551021", description: "Servicios de alojamiento en pensiones" },
|
||||||
|
{ code: "551022", description: "Servicios de alojamiento en hoteles, hosterías y residenciales similares, excepto por hora, que incluyen servicio de restaurante al público" },
|
||||||
|
{ code: "551023", description: "Servicios de alojamiento en hoteles, hosterías y residenciales similares, excepto por hora, que no incluyen servicio de restaurante al público" },
|
||||||
|
{ code: "561011", description: "Servicios de restaurantes y cantinas sin espectáculo" },
|
||||||
|
{ code: "561012", description: "Servicios de restaurantes y cantinas con espectáculo" },
|
||||||
|
{ code: "561013", description: "Servicios de fast food y locales de venta de comidas y bebidas al paso" },
|
||||||
|
{ code: "561014", description: "Servicios de expendio de bebidas en bares" },
|
||||||
|
{ code: "561019", description: "Servicios de expendio de comidas y bebidas en establecimientos con servicio de mesa y/o en mostrador n.c.p." },
|
||||||
|
{ code: "620100", description: "Servicios de consultores en informática y suministros de programas de informática" },
|
||||||
|
{ code: "620200", description: "Servicios de consultores en equipo de informática" },
|
||||||
|
{ code: "620300", description: "Servicios de consultores en tecnología de la información" },
|
||||||
|
{ code: "620900", description: "Servicios de informática n.c.p." },
|
||||||
|
{ code: "681098", description: "Servicios inmobiliarios realizados por cuenta propia, con bienes urbanos propios o arrendados n.c.p." },
|
||||||
|
{ code: "681099", description: "Servicios inmobiliarios realizados por cuenta propia, con bienes rurales propios o arrendados n.c.p." },
|
||||||
|
{ code: "691001", description: "Servicios jurídicos" },
|
||||||
|
{ code: "692000", description: "Servicios de contabilidad, auditoría y asesoría fiscal" },
|
||||||
|
{ code: "702091", description: "Servicios de asesoramiento, dirección y gestión empresarial realizados por integrantes de los órganos de administración y/o fiscalización en sociedades anónimas" },
|
||||||
|
{ code: "702092", description: "Servicios de asesoramiento, dirección y gestión empresarial realizados por integrantes de cuerpos de dirección en sociedades excepto las anónimas" },
|
||||||
|
{ code: "702099", description: "Servicios de asesoramiento, dirección y gestión empresarial n.c.p." },
|
||||||
|
{ code: "749009", description: "Actividades profesionales, científicas y técnicas n.c.p." },
|
||||||
|
{ code: "851010", description: "Guarderías y jardines maternales" },
|
||||||
|
{ code: "851020", description: "Enseñanza inicial, jardín de infantes y primaria" },
|
||||||
|
{ code: "852100", description: "Enseñanza secundaria de formación general" },
|
||||||
|
{ code: "852200", description: "Enseñanza secundaria de formación técnica y profesional" },
|
||||||
|
{ code: "853100", description: "Enseñanza terciaria" },
|
||||||
|
{ code: "853201", description: "Enseñanza universitaria excepto formación de posgrado" },
|
||||||
|
{ code: "853300", description: "Formación de posgrado" },
|
||||||
|
{ code: "854910", description: "Enseñanza de idiomas" },
|
||||||
|
{ code: "854920", description: "Enseñanza de cursos relacionados con informática" },
|
||||||
|
{ code: "854930", description: "Enseñanza para adultos, excepto discapacitados" },
|
||||||
|
{ code: "854940", description: "Enseñanza especial y para discapacitados" },
|
||||||
|
{ code: "854950", description: "Enseñanza de gimnasia, deportes y actividades físicas" },
|
||||||
|
{ code: "854960", description: "Enseñanza artística" },
|
||||||
|
{ code: "854990", description: "Servicios de enseñanza n.c.p." },
|
||||||
|
{ code: "855000", description: "Servicios de apoyo a la educación" },
|
||||||
|
{ code: "861010", description: "Servicios de internación" },
|
||||||
|
{ code: "862110", description: "Servicios de consulta médica" },
|
||||||
|
{ code: "862120", description: "Servicios de atención médica en dispensarios, salitas, vacunatorios y otros locales de atención primaria de la salud" },
|
||||||
|
{ code: "862130", description: "Servicios de atención médica en otros establecimientos n.c.p." },
|
||||||
|
{ code: "862200", description: "Servicios odontológicos" },
|
||||||
|
{ code: "869010", description: "Servicios de prácticas de diagnóstico en laboratorios" },
|
||||||
|
{ code: "869090", description: "Servicios relacionados con la salud humana n.c.p." },
|
||||||
|
{ code: "900011", description: "Producción de espectáculos teatrales y musicales" },
|
||||||
|
{ code: "900021", description: "Composición y representación de obras teatrales, musicales y artísticas" },
|
||||||
|
{ code: "931010", description: "Servicios de organización, dirección y gestión de prácticas deportivas y explotación de las instalaciones" },
|
||||||
|
{ code: "931020", description: "Servicios prestados por profesionales y técnicos para la realización de prácticas deportivas" },
|
||||||
|
{ code: "960201", description: "Servicios de peluquería" },
|
||||||
|
{ code: "960202", description: "Servicios de tratamiento de belleza, excepto los de peluquería" },
|
||||||
|
{ code: "960990", description: "Servicios personales n.c.p." },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const findArcaActivityByCode = (code: string): ArcaActivity | undefined => {
|
||||||
|
return ARCA_ACTIVITIES.find((activity) => activity.code === code.trim());
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterArcaActivities = (name?: string): TextObjectFilterResult[] => {
|
||||||
|
const normalizedFilter = (name || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
return ARCA_ACTIVITIES.filter((activity) => {
|
||||||
|
if (!normalizedFilter) return true;
|
||||||
|
|
||||||
|
return (
|
||||||
|
activity.code.includes(normalizedFilter) ||
|
||||||
|
activity.description.toLowerCase().includes(normalizedFilter)
|
||||||
|
);
|
||||||
|
}).map((activity) => ({
|
||||||
|
id: activity.code,
|
||||||
|
name: activity.description,
|
||||||
|
description: activity.code,
|
||||||
|
}));
|
||||||
|
};
|
||||||
@@ -49,6 +49,11 @@ export type ArcaWsfeIssueFacturaCParams = {
|
|||||||
companyId: string;
|
companyId: string;
|
||||||
amount: number;
|
amount: number;
|
||||||
billingDate: Date;
|
billingDate: Date;
|
||||||
|
servicePeriodFrom: Date;
|
||||||
|
servicePeriodTo: Date;
|
||||||
|
paymentDueDate: Date;
|
||||||
|
receiverDocumentType?: number | string;
|
||||||
|
receiverDocumentNumber?: number | string;
|
||||||
sessionUser: string;
|
sessionUser: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -177,6 +177,29 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private escapeXml(value: string): string {
|
||||||
|
return value
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """)
|
||||||
|
.replace(/'/g, "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseXmlInteger(value: number | string, fieldName: string): number {
|
||||||
|
if (typeof value === "string" && value.trim() === "") {
|
||||||
|
throw new Error(`${fieldName} debe ser un número entero válido`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = typeof value === "string" ? Number(value.trim()) : value;
|
||||||
|
|
||||||
|
if (!Number.isInteger(parsedValue) || !Number.isFinite(parsedValue) || parsedValue < 0) {
|
||||||
|
throw new Error(`${fieldName} debe ser un número entero válido`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedValue;
|
||||||
|
}
|
||||||
|
|
||||||
private signTraCms(traXml: string, certificatePem: string, privateKeyPem: string): string {
|
private signTraCms(traXml: string, certificatePem: string, privateKeyPem: string): string {
|
||||||
const p7 = forge.pkcs7.createSignedData();
|
const p7 = forge.pkcs7.createSignedData();
|
||||||
p7.content = forge.util.createBuffer(traXml, "utf8");
|
p7.content = forge.util.createBuffer(traXml, "utf8");
|
||||||
@@ -198,7 +221,7 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildLoginCmsSoap(cms: string): string {
|
private buildLoginCmsSoap(cms: string): string {
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>\n<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsaa="http://wsaa.view.sua.dvadac.desein.afip.gov">\n<soapenv:Header/>\n<soapenv:Body>\n<wsaa:loginCms>\n<wsaa:in0>${cms}</wsaa:in0>\n</wsaa:loginCms>\n</soapenv:Body>\n</soapenv:Envelope>`;
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:wsaa="http://wsaa.view.sua.dvadac.desein.afip.gov">\n<soapenv:Header/>\n<soapenv:Body>\n<wsaa:loginCms>\n<wsaa:in0>${this.escapeXml(cms)}</wsaa:in0>\n</wsaa:loginCms>\n</soapenv:Body>\n</soapenv:Envelope>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private extractXmlValue(xml: string, tagName: string): string | undefined {
|
private extractXmlValue(xml: string, tagName: string): string | undefined {
|
||||||
@@ -271,10 +294,10 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
const privateKeyPem = this.decryptPrivateKey(credential);
|
const privateKeyPem = this.decryptPrivateKey(credential);
|
||||||
const { traXml } = this.buildTraXml(service);
|
const { traXml } = this.buildTraXml(service);
|
||||||
const cms = this.signTraCms(traXml, credential.certificatePem, privateKeyPem);
|
const cms = this.signTraCms(traXml, credential.certificatePem, privateKeyPem);
|
||||||
const wsaaUrl = process.env.ARCA_WSAA_HOMOLOGATION_URL;
|
const wsaaUrl = process.env.ARCA_WSAA_URL;
|
||||||
|
|
||||||
if (!wsaaUrl) {
|
if (!wsaaUrl) {
|
||||||
throw new Error("ARCA_WSAA_HOMOLOGATION_URL no está configurada");
|
throw new Error("ARCA_WSAA_URL no está configurada");
|
||||||
}
|
}
|
||||||
|
|
||||||
let response;
|
let response;
|
||||||
@@ -315,18 +338,21 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
pointOfSale: number;
|
pointOfSale: number;
|
||||||
voucherType: 11;
|
voucherType: 11;
|
||||||
}): string {
|
}): string {
|
||||||
|
const pointOfSale = this.parseXmlInteger(data.pointOfSale, "PtoVta");
|
||||||
|
const voucherType = this.parseXmlInteger(data.voucherType, "CbteTipo");
|
||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
<soapenv:Header/>
|
<soapenv:Header/>
|
||||||
<soapenv:Body>
|
<soapenv:Body>
|
||||||
<FECompUltimoAutorizado xmlns="http://ar.gov.afip.dif.FEV1/">
|
<FECompUltimoAutorizado xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||||
<Auth>
|
<Auth>
|
||||||
<Token>${data.token}</Token>
|
<Token>${this.escapeXml(data.token)}</Token>
|
||||||
<Sign>${data.sign}</Sign>
|
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||||
<Cuit>${data.cuit}</Cuit>
|
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||||
</Auth>
|
</Auth>
|
||||||
<PtoVta>${data.pointOfSale}</PtoVta>
|
<PtoVta>${pointOfSale}</PtoVta>
|
||||||
<CbteTipo>${data.voucherType}</CbteTipo>
|
<CbteTipo>${voucherType}</CbteTipo>
|
||||||
</FECompUltimoAutorizado>
|
</FECompUltimoAutorizado>
|
||||||
</soapenv:Body>
|
</soapenv:Body>
|
||||||
</soapenv:Envelope>`;
|
</soapenv:Envelope>`;
|
||||||
@@ -347,9 +373,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
|
|
||||||
private parseArcaDate(value: string): Date {
|
private parseArcaDate(value: string): Date {
|
||||||
const year = Number(value.slice(0, 4));
|
const year = Number(value.slice(0, 4));
|
||||||
const month = Number(value.slice(4, 6)) - 1;
|
const month = Number(value.slice(4, 6));
|
||||||
const day = Number(value.slice(6, 8));
|
const day = Number(value.slice(6, 8));
|
||||||
return new Date(year, month, day);
|
return new Date(Date.UTC(year, month - 1, day, 12));
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildWsfeIssueFacturaCSoap(data: {
|
private buildWsfeIssueFacturaCSoap(data: {
|
||||||
@@ -360,8 +386,17 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
voucherNumber: number;
|
voucherNumber: number;
|
||||||
amount: number;
|
amount: number;
|
||||||
voucherDate: string;
|
voucherDate: string;
|
||||||
|
servicePeriodFrom: string;
|
||||||
|
servicePeriodTo: string;
|
||||||
|
paymentDueDate: string;
|
||||||
|
receiverDocumentType: number | string;
|
||||||
|
receiverDocumentNumber: number | string;
|
||||||
}): string {
|
}): string {
|
||||||
const amount = data.amount.toFixed(2);
|
const amount = data.amount.toFixed(2);
|
||||||
|
const pointOfSale = this.parseXmlInteger(data.pointOfSale, "PtoVta");
|
||||||
|
const voucherNumber = this.parseXmlInteger(data.voucherNumber, "CbteDesde");
|
||||||
|
const receiverDocumentType = this.parseXmlInteger(data.receiverDocumentType, "DocTipo");
|
||||||
|
const receiverDocumentNumber = this.parseXmlInteger(data.receiverDocumentNumber, "DocNro");
|
||||||
|
|
||||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||||
@@ -369,23 +404,23 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
<soapenv:Body>
|
<soapenv:Body>
|
||||||
<FECAESolicitar xmlns="http://ar.gov.afip.dif.FEV1/">
|
<FECAESolicitar xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||||
<Auth>
|
<Auth>
|
||||||
<Token>${data.token}</Token>
|
<Token>${this.escapeXml(data.token)}</Token>
|
||||||
<Sign>${data.sign}</Sign>
|
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||||
<Cuit>${data.cuit}</Cuit>
|
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||||
</Auth>
|
</Auth>
|
||||||
<FeCAEReq>
|
<FeCAEReq>
|
||||||
<FeCabReq>
|
<FeCabReq>
|
||||||
<CantReg>1</CantReg>
|
<CantReg>1</CantReg>
|
||||||
<PtoVta>${data.pointOfSale}</PtoVta>
|
<PtoVta>${pointOfSale}</PtoVta>
|
||||||
<CbteTipo>11</CbteTipo>
|
<CbteTipo>11</CbteTipo>
|
||||||
</FeCabReq>
|
</FeCabReq>
|
||||||
<FeDetReq>
|
<FeDetReq>
|
||||||
<FECAEDetRequest>
|
<FECAEDetRequest>
|
||||||
<Concepto>1</Concepto>
|
<Concepto>3</Concepto>
|
||||||
<DocTipo>${FINAL_CONSUMER_DOCUMENT_TYPE}</DocTipo>
|
<DocTipo>${receiverDocumentType}</DocTipo>
|
||||||
<DocNro>${FINAL_CONSUMER_DOCUMENT_NUMBER}</DocNro>
|
<DocNro>${receiverDocumentNumber}</DocNro>
|
||||||
<CbteDesde>${data.voucherNumber}</CbteDesde>
|
<CbteDesde>${voucherNumber}</CbteDesde>
|
||||||
<CbteHasta>${data.voucherNumber}</CbteHasta>
|
<CbteHasta>${voucherNumber}</CbteHasta>
|
||||||
<CbteFch>${data.voucherDate}</CbteFch>
|
<CbteFch>${data.voucherDate}</CbteFch>
|
||||||
<ImpTotal>${amount}</ImpTotal>
|
<ImpTotal>${amount}</ImpTotal>
|
||||||
<ImpTotConc>0.00</ImpTotConc>
|
<ImpTotConc>0.00</ImpTotConc>
|
||||||
@@ -393,6 +428,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
<ImpOpEx>0.00</ImpOpEx>
|
<ImpOpEx>0.00</ImpOpEx>
|
||||||
<ImpTrib>0.00</ImpTrib>
|
<ImpTrib>0.00</ImpTrib>
|
||||||
<ImpIVA>0.00</ImpIVA>
|
<ImpIVA>0.00</ImpIVA>
|
||||||
|
<FchServDesde>${data.servicePeriodFrom}</FchServDesde>
|
||||||
|
<FchServHasta>${data.servicePeriodTo}</FchServHasta>
|
||||||
|
<FchVtoPago>${data.paymentDueDate}</FchVtoPago>
|
||||||
<MonId>PES</MonId>
|
<MonId>PES</MonId>
|
||||||
<MonCotiz>1</MonCotiz>
|
<MonCotiz>1</MonCotiz>
|
||||||
<CondicionIVAReceptorId>${FINAL_CONSUMER_IVA_CONDITION}</CondicionIVAReceptorId>
|
<CondicionIVAReceptorId>${FINAL_CONSUMER_IVA_CONDITION}</CondicionIVAReceptorId>
|
||||||
@@ -421,9 +459,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
<soapenv:Body>
|
<soapenv:Body>
|
||||||
<FEParamGetTiposCbte xmlns="http://ar.gov.afip.dif.FEV1/">
|
<FEParamGetTiposCbte xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||||
<Auth>
|
<Auth>
|
||||||
<Token>${data.token}</Token>
|
<Token>${this.escapeXml(data.token)}</Token>
|
||||||
<Sign>${data.sign}</Sign>
|
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||||
<Cuit>${data.cuit}</Cuit>
|
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||||
</Auth>
|
</Auth>
|
||||||
</FEParamGetTiposCbte>
|
</FEParamGetTiposCbte>
|
||||||
</soapenv:Body>
|
</soapenv:Body>
|
||||||
@@ -437,9 +475,9 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
<soapenv:Body>
|
<soapenv:Body>
|
||||||
<FEParamGetPtosVenta xmlns="http://ar.gov.afip.dif.FEV1/">
|
<FEParamGetPtosVenta xmlns="http://ar.gov.afip.dif.FEV1/">
|
||||||
<Auth>
|
<Auth>
|
||||||
<Token>${data.token}</Token>
|
<Token>${this.escapeXml(data.token)}</Token>
|
||||||
<Sign>${data.sign}</Sign>
|
<Sign>${this.escapeXml(data.sign)}</Sign>
|
||||||
<Cuit>${data.cuit}</Cuit>
|
<Cuit>${this.escapeXml(data.cuit)}</Cuit>
|
||||||
</Auth>
|
</Auth>
|
||||||
</FEParamGetPtosVenta>
|
</FEParamGetPtosVenta>
|
||||||
</soapenv:Body>
|
</soapenv:Body>
|
||||||
@@ -447,10 +485,10 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async postWsfe(soapBody: string, soapAction: string): Promise<string> {
|
private async postWsfe(soapBody: string, soapAction: string): Promise<string> {
|
||||||
const wsfeUrl = process.env.ARCA_WSFE_HOMOLOGATION_URL;
|
const wsfeUrl = process.env.ARCA_WSFE_URL;
|
||||||
|
|
||||||
if (!wsfeUrl) {
|
if (!wsfeUrl) {
|
||||||
throw new Error("ARCA_WSFE_HOMOLOGATION_URL no está configurada");
|
throw new Error("ARCA_WSFE_URL no está configurada");
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await axios.post(wsfeUrl, soapBody, {
|
const response = await axios.post(wsfeUrl, soapBody, {
|
||||||
@@ -613,7 +651,12 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
return { ok: false, configuredPointOfSale, error: errorMessage };
|
return { ok: false, configuredPointOfSale, error: errorMessage };
|
||||||
}
|
}
|
||||||
|
|
||||||
const points = Array.from(xml.matchAll(/<(?:\w+:)?PtoVenta>(\d+)<\/(?:\w+:)?PtoVenta>/g)).map((match) => Number(match[1]));
|
const points = Array.from(xml.matchAll(/<(?:\w+:)?PtoVenta(?:\s[^>]*)?>([\s\S]*?)<\/(?:\w+:)?PtoVenta>/g))
|
||||||
|
.map((match) => {
|
||||||
|
const nestedNumber = this.extractXmlValue(match[1], "Nro");
|
||||||
|
return Number(nestedNumber || match[1].trim());
|
||||||
|
})
|
||||||
|
.filter((point) => Number.isInteger(point));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -817,6 +860,14 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
|
const fiscalProfile = await this.requireActiveFiscalProfile(data.companyId, data.sessionUser);
|
||||||
const loginTicket = await this.getWsaaLoginTicket(data.companyId);
|
const loginTicket = await this.getWsaaLoginTicket(data.companyId);
|
||||||
const voucherType = 11;
|
const voucherType = 11;
|
||||||
|
const receiverDocumentType = this.parseXmlInteger(
|
||||||
|
data.receiverDocumentType ?? FINAL_CONSUMER_DOCUMENT_TYPE,
|
||||||
|
"DocTipo"
|
||||||
|
);
|
||||||
|
const receiverDocumentNumber = this.parseXmlInteger(
|
||||||
|
data.receiverDocumentNumber ?? FINAL_CONSUMER_DOCUMENT_NUMBER,
|
||||||
|
"DocNro"
|
||||||
|
);
|
||||||
let lastVoucherNumber: number;
|
let lastVoucherNumber: number;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -848,6 +899,11 @@ class ArcaCredentialsManager implements IArcaCredentialsManager {
|
|||||||
voucherNumber,
|
voucherNumber,
|
||||||
amount: data.amount,
|
amount: data.amount,
|
||||||
voucherDate: this.formatArcaDate(data.billingDate),
|
voucherDate: this.formatArcaDate(data.billingDate),
|
||||||
|
servicePeriodFrom: this.formatArcaDate(data.servicePeriodFrom),
|
||||||
|
servicePeriodTo: this.formatArcaDate(data.servicePeriodTo),
|
||||||
|
paymentDueDate: this.formatArcaDate(data.paymentDueDate),
|
||||||
|
receiverDocumentType,
|
||||||
|
receiverDocumentNumber,
|
||||||
}),
|
}),
|
||||||
"FECAESolicitar"
|
"FECAESolicitar"
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -42,8 +42,25 @@ export class InvoicesAdapterMongoose implements IInvoicesAdapter {
|
|||||||
type: { type: String, required: true, enum: Object.values(INVOICE_TYPE) },
|
type: { type: String, required: true, enum: Object.values(INVOICE_TYPE) },
|
||||||
status: { type: String, required: true, enum: Object.values(INVOICE_STATUS), index: true },
|
status: { type: String, required: true, enum: Object.values(INVOICE_STATUS), index: true },
|
||||||
billingDate: { type: Date, required: true },
|
billingDate: { type: Date, required: true },
|
||||||
paymentMethod: { type: String, required: true },
|
servicePeriodFrom: { type: Date, required: true },
|
||||||
description: { type: String, required: true },
|
servicePeriodTo: { type: Date, required: true },
|
||||||
|
paymentDueDate: { type: Date, required: true },
|
||||||
|
activityCode: { type: String, required: true },
|
||||||
|
activityDescription: { type: String, required: true },
|
||||||
|
receiverIvaCondition: { type: Number, required: true, default: 5 },
|
||||||
|
receiverDocumentType: { type: Schema.Types.Mixed, required: false },
|
||||||
|
receiverDocumentNumber: { type: Schema.Types.Mixed, required: false },
|
||||||
|
receiverName: { type: String, required: false },
|
||||||
|
receiverAddress: { type: String, required: false },
|
||||||
|
receiverEmail: { type: String, required: false },
|
||||||
|
saleCondition: { type: String, required: true },
|
||||||
|
itemDescription: { type: String, required: true },
|
||||||
|
itemQuantity: { type: Number, required: true, default: 1 },
|
||||||
|
itemUnit: { type: String, required: false },
|
||||||
|
itemUnitPrice: { type: Number, required: true },
|
||||||
|
itemDiscountPercent: { type: Number, required: true, default: 0 },
|
||||||
|
itemDiscountAmount: { type: Number, required: true, default: 0 },
|
||||||
|
itemSubtotal: { type: Number, required: true },
|
||||||
cae: { type: String, required: false },
|
cae: { type: String, required: false },
|
||||||
caeExpiresAt: { type: Date, required: false },
|
caeExpiresAt: { type: Date, required: false },
|
||||||
pointOfSale: { type: Number, required: false },
|
pointOfSale: { type: Number, required: false },
|
||||||
|
|||||||
@@ -12,19 +12,42 @@ export enum INVOICE_STATUS {
|
|||||||
FAILED = "FAILED",
|
FAILED = "FAILED",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum INVOICE_PAYMENT_METHOD {
|
export enum INVOICE_SALE_CONDITION {
|
||||||
CASH = "cash",
|
CASH = "cash",
|
||||||
|
CONTADO = "contado",
|
||||||
BANK_TRANSFER = "bank_transfer",
|
BANK_TRANSFER = "bank_transfer",
|
||||||
CREDIT_CARD = "credit_card",
|
CREDIT_CARD = "credit_card",
|
||||||
DEBIT_CARD = "debit_card",
|
DEBIT_CARD = "debit_card",
|
||||||
|
CURRENT_ACCOUNT = "current_account",
|
||||||
|
CHECK = "check",
|
||||||
OTHER = "other",
|
OTHER = "other",
|
||||||
|
ELECTRONIC_PAYMENT = "electronic_payment",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const FINAL_CONSUMER_IVA_CONDITION_ID = 5;
|
||||||
|
|
||||||
export type CreateInvoiceFromCashMovementParams = {
|
export type CreateInvoiceFromCashMovementParams = {
|
||||||
cashMovementId: string;
|
cashMovementId: string;
|
||||||
billingDate: Date | string;
|
billingDate: Date | string;
|
||||||
paymentMethod: INVOICE_PAYMENT_METHOD | string;
|
servicePeriodFrom: Date | string;
|
||||||
description: string;
|
servicePeriodTo: Date | string;
|
||||||
|
paymentDueDate: Date | string;
|
||||||
|
activityCode: string;
|
||||||
|
activityDescription: string;
|
||||||
|
receiverIvaCondition?: 5;
|
||||||
|
receiverDocumentType?: number | string;
|
||||||
|
receiverDocumentNumber?: number | string;
|
||||||
|
receiverName?: string;
|
||||||
|
receiverAddress?: string;
|
||||||
|
receiverEmail?: string;
|
||||||
|
saleCondition: INVOICE_SALE_CONDITION | string;
|
||||||
|
itemDescription: string;
|
||||||
|
itemQuantity?: number;
|
||||||
|
itemUnit?: string;
|
||||||
|
itemUnitPrice: number;
|
||||||
|
itemDiscountPercent?: number;
|
||||||
|
itemDiscountAmount?: number;
|
||||||
|
itemSubtotal: number;
|
||||||
sessionUser: string;
|
sessionUser: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -48,8 +71,25 @@ export type CreateInvoiceParams = {
|
|||||||
type: INVOICE_TYPE;
|
type: INVOICE_TYPE;
|
||||||
status: INVOICE_STATUS;
|
status: INVOICE_STATUS;
|
||||||
billingDate: Date;
|
billingDate: Date;
|
||||||
paymentMethod: INVOICE_PAYMENT_METHOD | string;
|
servicePeriodFrom: Date;
|
||||||
description: string;
|
servicePeriodTo: Date;
|
||||||
|
paymentDueDate: Date;
|
||||||
|
activityCode: string;
|
||||||
|
activityDescription: string;
|
||||||
|
receiverIvaCondition: 5;
|
||||||
|
receiverDocumentType?: number | string;
|
||||||
|
receiverDocumentNumber?: number | string;
|
||||||
|
receiverName?: string;
|
||||||
|
receiverAddress?: string;
|
||||||
|
receiverEmail?: string;
|
||||||
|
saleCondition: INVOICE_SALE_CONDITION | string;
|
||||||
|
itemDescription: string;
|
||||||
|
itemQuantity: number;
|
||||||
|
itemUnit?: string;
|
||||||
|
itemUnitPrice: number;
|
||||||
|
itemDiscountPercent: number;
|
||||||
|
itemDiscountAmount: number;
|
||||||
|
itemSubtotal: number;
|
||||||
cae?: string;
|
cae?: string;
|
||||||
caeExpiresAt?: Date;
|
caeExpiresAt?: Date;
|
||||||
pointOfSale?: number;
|
pointOfSale?: number;
|
||||||
@@ -75,8 +115,25 @@ export interface IInvoice {
|
|||||||
type: INVOICE_TYPE;
|
type: INVOICE_TYPE;
|
||||||
status: INVOICE_STATUS;
|
status: INVOICE_STATUS;
|
||||||
billingDate: Date;
|
billingDate: Date;
|
||||||
paymentMethod: INVOICE_PAYMENT_METHOD | string;
|
servicePeriodFrom: Date;
|
||||||
description: string;
|
servicePeriodTo: Date;
|
||||||
|
paymentDueDate: Date;
|
||||||
|
activityCode: string;
|
||||||
|
activityDescription: string;
|
||||||
|
receiverIvaCondition: 5;
|
||||||
|
receiverDocumentType?: number | string;
|
||||||
|
receiverDocumentNumber?: number | string;
|
||||||
|
receiverName?: string;
|
||||||
|
receiverAddress?: string;
|
||||||
|
receiverEmail?: string;
|
||||||
|
saleCondition: INVOICE_SALE_CONDITION | string;
|
||||||
|
itemDescription: string;
|
||||||
|
itemQuantity: number;
|
||||||
|
itemUnit?: string;
|
||||||
|
itemUnitPrice: number;
|
||||||
|
itemDiscountPercent: number;
|
||||||
|
itemDiscountAmount: number;
|
||||||
|
itemSubtotal: number;
|
||||||
cae?: string;
|
cae?: string;
|
||||||
caeExpiresAt?: Date;
|
caeExpiresAt?: Date;
|
||||||
pointOfSale?: number;
|
pointOfSale?: number;
|
||||||
|
|||||||
@@ -12,17 +12,24 @@ import {
|
|||||||
CreateInvoiceFromCashMovementParams,
|
CreateInvoiceFromCashMovementParams,
|
||||||
CreateInvoiceParams,
|
CreateInvoiceParams,
|
||||||
FindInvoicesParams,
|
FindInvoicesParams,
|
||||||
|
FINAL_CONSUMER_IVA_CONDITION_ID,
|
||||||
IInvoice,
|
IInvoice,
|
||||||
IInvoicesManager,
|
IInvoicesManager,
|
||||||
INVOICE_PAYMENT_METHOD,
|
INVOICE_SALE_CONDITION,
|
||||||
INVOICE_STATUS,
|
INVOICE_STATUS,
|
||||||
INVOICE_TYPE,
|
INVOICE_TYPE,
|
||||||
} from "./Invoices.Interface";
|
} from "./Invoices.Interface";
|
||||||
|
|
||||||
const FACTURA_C_VOUCHER_TYPE = "11";
|
const FACTURA_C_VOUCHER_TYPE = "11";
|
||||||
const ARGENTINA_TIME_ZONE = "America/Argentina/Buenos_Aires";
|
const ARGENTINA_TIME_ZONE = "America/Argentina/Buenos_Aires";
|
||||||
const DESCRIPTION_MAX_LENGTH = 500;
|
const TEXT_MAX_LENGTH = 500;
|
||||||
|
const RECEIVER_TEXT_MAX_LENGTH = 200;
|
||||||
|
const CENTS_TOLERANCE = 0.01;
|
||||||
const YYYY_MM_DD_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
const YYYY_MM_DD_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||||
|
const DNI_DOCUMENT_TYPE = 96;
|
||||||
|
const FINAL_CONSUMER_DOCUMENT_TYPE = 99;
|
||||||
|
const FINAL_CONSUMER_DOCUMENT_NUMBER = 0;
|
||||||
|
const FINAL_CONSUMER_COMPATIBLE_DOCUMENT_TYPES = new Set([DNI_DOCUMENT_TYPE, FINAL_CONSUMER_DOCUMENT_TYPE]);
|
||||||
|
|
||||||
class InvoicesManager implements IInvoicesManager {
|
class InvoicesManager implements IInvoicesManager {
|
||||||
invoices: InvoicesAdapterMongoose;
|
invoices: InvoicesAdapterMongoose;
|
||||||
@@ -65,30 +72,51 @@ class InvoicesManager implements IInvoicesManager {
|
|||||||
return `${year}-${month}-${day}`;
|
return `${year}-${month}-${day}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseBillingDate(billingDate: Date | string): Date {
|
private parseArgentinaCalendarDate(dateValue: Date | string, requiredMessage: string, invalidMessage: string): Date {
|
||||||
if (!billingDate) {
|
if (!dateValue) {
|
||||||
throw new Error("Se requiere la fecha del comprobante");
|
throw new Error(requiredMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parsedDate: Date;
|
let parsedDate: Date;
|
||||||
|
|
||||||
if (typeof billingDate === "string") {
|
if (typeof dateValue === "string") {
|
||||||
const localDateMatch = billingDate.match(YYYY_MM_DD_PATTERN);
|
const localDateMatch = dateValue.match(YYYY_MM_DD_PATTERN);
|
||||||
|
|
||||||
if (localDateMatch) {
|
if (!localDateMatch) {
|
||||||
const [, year, month, day] = localDateMatch;
|
throw new Error(invalidMessage);
|
||||||
parsedDate = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), 12));
|
}
|
||||||
} else {
|
|
||||||
parsedDate = new Date(billingDate);
|
const [, year, month, day] = localDateMatch;
|
||||||
|
const yearNumber = Number(year);
|
||||||
|
const monthNumber = Number(month);
|
||||||
|
const dayNumber = Number(day);
|
||||||
|
parsedDate = new Date(Date.UTC(yearNumber, monthNumber - 1, dayNumber, 12));
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedDate.getUTCFullYear() !== yearNumber ||
|
||||||
|
parsedDate.getUTCMonth() !== monthNumber - 1 ||
|
||||||
|
parsedDate.getUTCDate() !== dayNumber
|
||||||
|
) {
|
||||||
|
throw new Error(invalidMessage);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
parsedDate = billingDate;
|
parsedDate = dateValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Number.isNaN(parsedDate.getTime())) {
|
if (Number.isNaN(parsedDate.getTime())) {
|
||||||
throw new Error("La fecha del comprobante no es válida");
|
throw new Error(invalidMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return parsedDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseBillingDate(billingDate: Date | string): Date {
|
||||||
|
const parsedDate = this.parseArgentinaCalendarDate(
|
||||||
|
billingDate,
|
||||||
|
"Se requiere la fecha del comprobante",
|
||||||
|
"La fecha del comprobante no es válida"
|
||||||
|
);
|
||||||
|
|
||||||
const selectedDate = this.formatArgentinaCalendarDate(parsedDate);
|
const selectedDate = this.formatArgentinaCalendarDate(parsedDate);
|
||||||
const todayDate = this.formatArgentinaCalendarDate(new Date());
|
const todayDate = this.formatArgentinaCalendarDate(new Date());
|
||||||
|
|
||||||
@@ -99,12 +127,66 @@ class InvoicesManager implements IInvoicesManager {
|
|||||||
return parsedDate;
|
return parsedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
private validatePaymentMethod(paymentMethod: string): INVOICE_PAYMENT_METHOD {
|
private validateSaleCondition(saleCondition: string): INVOICE_SALE_CONDITION {
|
||||||
if (!Object.values(INVOICE_PAYMENT_METHOD).includes(paymentMethod as INVOICE_PAYMENT_METHOD)) {
|
if (!Object.values(INVOICE_SALE_CONDITION).includes(saleCondition as INVOICE_SALE_CONDITION)) {
|
||||||
throw new Error("El medio de pago de la factura no es válido");
|
throw new Error("La condición de venta de la factura no es válida");
|
||||||
}
|
}
|
||||||
|
|
||||||
return paymentMethod as INVOICE_PAYMENT_METHOD;
|
return saleCondition as INVOICE_SALE_CONDITION;
|
||||||
|
}
|
||||||
|
|
||||||
|
private trimOptional(value?: string): string | undefined {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
return trimmed || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseReceiverDocumentCode(value: number | string, fieldName: string): number {
|
||||||
|
if (typeof value === "string" && value.trim() === "") {
|
||||||
|
throw new Error(`${fieldName} del receptor no es válido`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedValue = typeof value === "string" ? Number(value.trim()) : value;
|
||||||
|
|
||||||
|
if (!Number.isInteger(parsedValue) || !Number.isFinite(parsedValue) || parsedValue < 0) {
|
||||||
|
throw new Error(`${fieldName} del receptor no es válido`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateReceiverDocumentPair(data: CreateInvoiceFromCashMovementParams): {
|
||||||
|
receiverDocumentType: number;
|
||||||
|
receiverDocumentNumber: number;
|
||||||
|
} {
|
||||||
|
const hasDocumentType = data.receiverDocumentType !== undefined && data.receiverDocumentType !== null && data.receiverDocumentType !== "";
|
||||||
|
const hasDocumentNumber = data.receiverDocumentNumber !== undefined && data.receiverDocumentNumber !== null && data.receiverDocumentNumber !== "";
|
||||||
|
|
||||||
|
if (hasDocumentType !== hasDocumentNumber) {
|
||||||
|
throw new Error("Tipo y número de documento del receptor deben informarse juntos");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasDocumentType && !hasDocumentNumber) {
|
||||||
|
return {
|
||||||
|
receiverDocumentType: FINAL_CONSUMER_DOCUMENT_TYPE,
|
||||||
|
receiverDocumentNumber: FINAL_CONSUMER_DOCUMENT_NUMBER,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const receiverDocumentType = this.parseReceiverDocumentCode(data.receiverDocumentType as number | string, "El tipo de documento");
|
||||||
|
const receiverDocumentNumber = this.parseReceiverDocumentCode(data.receiverDocumentNumber as number | string, "El número de documento");
|
||||||
|
|
||||||
|
if (!FINAL_CONSUMER_COMPATIBLE_DOCUMENT_TYPES.has(receiverDocumentType)) {
|
||||||
|
throw new Error("El tipo de documento del receptor no es compatible con Consumidor Final");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (receiverDocumentType === FINAL_CONSUMER_DOCUMENT_TYPE && receiverDocumentNumber !== FINAL_CONSUMER_DOCUMENT_NUMBER) {
|
||||||
|
throw new Error("El número de documento debe ser 0 cuando el tipo es Consumidor Final");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
receiverDocumentType,
|
||||||
|
receiverDocumentNumber,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createFromCashMovement(data: CreateInvoiceFromCashMovementParams): Promise<IInvoice> {
|
public async createFromCashMovement(data: CreateInvoiceFromCashMovementParams): Promise<IInvoice> {
|
||||||
@@ -115,21 +197,71 @@ class InvoicesManager implements IInvoicesManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const billingDate = this.parseBillingDate(data.billingDate);
|
const billingDate = this.parseBillingDate(data.billingDate);
|
||||||
const description = data.description?.trim();
|
const servicePeriodFrom = this.parseArgentinaCalendarDate(
|
||||||
|
data.servicePeriodFrom,
|
||||||
|
"Se requiere la fecha de inicio del período facturado",
|
||||||
|
"La fecha de inicio del período facturado no es válida"
|
||||||
|
);
|
||||||
|
const servicePeriodTo = this.parseArgentinaCalendarDate(
|
||||||
|
data.servicePeriodTo,
|
||||||
|
"Se requiere la fecha de fin del período facturado",
|
||||||
|
"La fecha de fin del período facturado no es válida"
|
||||||
|
);
|
||||||
|
const paymentDueDate = this.parseArgentinaCalendarDate(
|
||||||
|
data.paymentDueDate,
|
||||||
|
"Se requiere el vencimiento de pago",
|
||||||
|
"El vencimiento de pago no es válido"
|
||||||
|
);
|
||||||
|
const servicePeriodFromValue = this.formatArgentinaCalendarDate(servicePeriodFrom);
|
||||||
|
const servicePeriodToValue = this.formatArgentinaCalendarDate(servicePeriodTo);
|
||||||
|
const billingDateValue = this.formatArgentinaCalendarDate(billingDate);
|
||||||
|
const paymentDueDateValue = this.formatArgentinaCalendarDate(paymentDueDate);
|
||||||
|
|
||||||
if (!description) {
|
if (servicePeriodFromValue > servicePeriodToValue) {
|
||||||
throw new Error("Se requiere el detalle de la factura");
|
throw new Error("El inicio del período facturado no puede ser posterior al fin");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (description.length > DESCRIPTION_MAX_LENGTH) {
|
if (paymentDueDateValue < billingDateValue) {
|
||||||
throw new Error(`El detalle de la factura no puede superar los ${DESCRIPTION_MAX_LENGTH} caracteres`);
|
throw new Error("El vencimiento de pago no puede ser anterior a la fecha del comprobante");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data.paymentMethod || !data.paymentMethod.trim()) {
|
const activityCode = data.activityCode?.trim();
|
||||||
throw new Error("Se requiere el medio de pago de la factura");
|
const activityDescription = data.activityDescription?.trim();
|
||||||
|
const itemDescription = data.itemDescription?.trim();
|
||||||
|
|
||||||
|
if (!activityCode) {
|
||||||
|
throw new Error("Se requiere el código de actividad");
|
||||||
}
|
}
|
||||||
|
|
||||||
const paymentMethod = this.validatePaymentMethod(data.paymentMethod.trim());
|
if (!activityDescription) {
|
||||||
|
throw new Error("Se requiere la descripción de la actividad");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!itemDescription) {
|
||||||
|
throw new Error("Se requiere la descripción del ítem facturado");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([activityCode, activityDescription, itemDescription].some((value) => value.length > TEXT_MAX_LENGTH)) {
|
||||||
|
throw new Error(`Los datos de actividad e ítem no pueden superar los ${TEXT_MAX_LENGTH} caracteres`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.saleCondition || !data.saleCondition.trim()) {
|
||||||
|
throw new Error("Se requiere la condición de venta");
|
||||||
|
}
|
||||||
|
|
||||||
|
const saleCondition = this.validateSaleCondition(data.saleCondition.trim());
|
||||||
|
const receiverDocument = this.validateReceiverDocumentPair(data);
|
||||||
|
const itemQuantity = data.itemQuantity ?? 1;
|
||||||
|
const itemDiscountPercent = data.itemDiscountPercent ?? 0;
|
||||||
|
const itemDiscountAmount = data.itemDiscountAmount ?? 0;
|
||||||
|
|
||||||
|
if (!Number.isFinite(data.itemUnitPrice) || !Number.isFinite(data.itemSubtotal)) {
|
||||||
|
throw new Error("El importe del ítem no es válido");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemQuantity !== 1 || data.itemUnitPrice <= 0 || itemDiscountPercent !== 0 || itemDiscountAmount !== 0) {
|
||||||
|
throw new Error("El MVP permite un único ítem de servicio sin descuentos por el total del movimiento");
|
||||||
|
}
|
||||||
|
|
||||||
const movement = await CashFlow.movements.movementList.findOne({
|
const movement = await CashFlow.movements.movementList.findOne({
|
||||||
_id: data.cashMovementId,
|
_id: data.cashMovementId,
|
||||||
@@ -147,6 +279,17 @@ class InvoicesManager implements IInvoicesManager {
|
|||||||
throw new Error("El movimiento de caja no es facturable");
|
throw new Error("El movimiento de caja no es facturable");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const expectedSubtotal = Number((itemQuantity * data.itemUnitPrice - itemDiscountAmount).toFixed(2));
|
||||||
|
const sentSubtotal = Number(data.itemSubtotal.toFixed(2));
|
||||||
|
const movementAmount = Number(movement.amount.toFixed(2));
|
||||||
|
|
||||||
|
if (
|
||||||
|
Math.abs(expectedSubtotal - sentSubtotal) > CENTS_TOLERANCE ||
|
||||||
|
Math.abs(sentSubtotal - movementAmount) > CENTS_TOLERANCE
|
||||||
|
) {
|
||||||
|
throw new Error("El total del ítem debe coincidir con el importe del movimiento");
|
||||||
|
}
|
||||||
|
|
||||||
const fiscalProfiles = await OrganizationFiscalProfiles.find({
|
const fiscalProfiles = await OrganizationFiscalProfiles.find({
|
||||||
companyId,
|
companyId,
|
||||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
||||||
@@ -195,8 +338,25 @@ class InvoicesManager implements IInvoicesManager {
|
|||||||
type: INVOICE_TYPE.FACTURA_C,
|
type: INVOICE_TYPE.FACTURA_C,
|
||||||
status: INVOICE_STATUS.PENDING_ARCA,
|
status: INVOICE_STATUS.PENDING_ARCA,
|
||||||
billingDate,
|
billingDate,
|
||||||
paymentMethod,
|
servicePeriodFrom,
|
||||||
description,
|
servicePeriodTo,
|
||||||
|
paymentDueDate,
|
||||||
|
activityCode,
|
||||||
|
activityDescription,
|
||||||
|
receiverIvaCondition: FINAL_CONSUMER_IVA_CONDITION_ID,
|
||||||
|
receiverDocumentType: receiverDocument.receiverDocumentType,
|
||||||
|
receiverDocumentNumber: receiverDocument.receiverDocumentNumber,
|
||||||
|
receiverName: this.trimOptional(data.receiverName)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||||
|
receiverAddress: this.trimOptional(data.receiverAddress)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||||
|
receiverEmail: this.trimOptional(data.receiverEmail)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||||
|
saleCondition,
|
||||||
|
itemDescription,
|
||||||
|
itemQuantity,
|
||||||
|
itemUnit: this.trimOptional(data.itemUnit),
|
||||||
|
itemUnitPrice: data.itemUnitPrice,
|
||||||
|
itemDiscountPercent,
|
||||||
|
itemDiscountAmount,
|
||||||
|
itemSubtotal: sentSubtotal,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
@@ -233,8 +393,25 @@ class InvoicesManager implements IInvoicesManager {
|
|||||||
invoice = await this.invoices.update({
|
invoice = await this.invoices.update({
|
||||||
cashMovementId: data.cashMovementId,
|
cashMovementId: data.cashMovementId,
|
||||||
billingDate,
|
billingDate,
|
||||||
paymentMethod,
|
servicePeriodFrom,
|
||||||
description,
|
servicePeriodTo,
|
||||||
|
paymentDueDate,
|
||||||
|
activityCode,
|
||||||
|
activityDescription,
|
||||||
|
receiverIvaCondition: FINAL_CONSUMER_IVA_CONDITION_ID,
|
||||||
|
receiverDocumentType: receiverDocument.receiverDocumentType,
|
||||||
|
receiverDocumentNumber: receiverDocument.receiverDocumentNumber,
|
||||||
|
receiverName: this.trimOptional(data.receiverName)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||||
|
receiverAddress: this.trimOptional(data.receiverAddress)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||||
|
receiverEmail: this.trimOptional(data.receiverEmail)?.slice(0, RECEIVER_TEXT_MAX_LENGTH),
|
||||||
|
saleCondition,
|
||||||
|
itemDescription,
|
||||||
|
itemQuantity,
|
||||||
|
itemUnit: this.trimOptional(data.itemUnit),
|
||||||
|
itemUnitPrice: data.itemUnitPrice,
|
||||||
|
itemDiscountPercent,
|
||||||
|
itemDiscountAmount,
|
||||||
|
itemSubtotal: sentSubtotal,
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
}) || invoice;
|
}) || invoice;
|
||||||
}
|
}
|
||||||
@@ -255,6 +432,11 @@ class InvoicesManager implements IInvoicesManager {
|
|||||||
companyId,
|
companyId,
|
||||||
amount: movement.amount,
|
amount: movement.amount,
|
||||||
billingDate,
|
billingDate,
|
||||||
|
servicePeriodFrom,
|
||||||
|
servicePeriodTo,
|
||||||
|
paymentDueDate,
|
||||||
|
receiverDocumentType: receiverDocument.receiverDocumentType,
|
||||||
|
receiverDocumentNumber: receiverDocument.receiverDocumentNumber,
|
||||||
sessionUser: data.sessionUser,
|
sessionUser: data.sessionUser,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+2
@@ -34,6 +34,8 @@ export class OrganizationFiscalProfilesAdapterMongoose implements IOrganizationF
|
|||||||
enum: Object.values(ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION),
|
enum: Object.values(ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION),
|
||||||
},
|
},
|
||||||
pointOfSale: { type: Number, required: true },
|
pointOfSale: { type: Number, required: true },
|
||||||
|
activityCode: { type: String, required: false },
|
||||||
|
activityDescription: { type: String, required: false },
|
||||||
status: {
|
status: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export type UpsertOrganizationFiscalProfileParams = {
|
|||||||
legalName: string;
|
legalName: string;
|
||||||
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||||
pointOfSale: number;
|
pointOfSale: number;
|
||||||
|
activityCode?: string;
|
||||||
|
activityDescription?: string;
|
||||||
sessionUser: string;
|
sessionUser: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,6 +35,8 @@ export type CreateOrganizationFiscalProfileParams = {
|
|||||||
legalName: string;
|
legalName: string;
|
||||||
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||||
pointOfSale: number;
|
pointOfSale: number;
|
||||||
|
activityCode?: string;
|
||||||
|
activityDescription?: string;
|
||||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -43,6 +47,8 @@ export type UpdateOrganizationFiscalProfileParams = FindOrganizationFiscalProfil
|
|||||||
legalName?: string;
|
legalName?: string;
|
||||||
taxCondition?: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
taxCondition?: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||||
pointOfSale?: number;
|
pointOfSale?: number;
|
||||||
|
activityCode?: string;
|
||||||
|
activityDescription?: string;
|
||||||
status?: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
status?: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
};
|
};
|
||||||
@@ -54,6 +60,8 @@ export interface IOrganizationFiscalProfile {
|
|||||||
legalName: string;
|
legalName: string;
|
||||||
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
taxCondition: ORGANIZATION_FISCAL_PROFILE_TAX_CONDITION;
|
||||||
pointOfSale: number;
|
pointOfSale: number;
|
||||||
|
activityCode?: string;
|
||||||
|
activityDescription?: string;
|
||||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
status: ORGANIZATION_FISCAL_PROFILE_STATUS;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
|
import { validatePermissionsByCompany, validateSessionUser } from "../../helpers/check";
|
||||||
|
import { findArcaActivityByCode } from "../ArcaActivities/ArcaActivities.Catalog";
|
||||||
import { OrganizationFiscalProfilesAdapterMongoose } from "./OrganizationFiscalProfiles.Adapter.Mongoose";
|
import { OrganizationFiscalProfilesAdapterMongoose } from "./OrganizationFiscalProfiles.Adapter.Mongoose";
|
||||||
import {
|
import {
|
||||||
FindOrganizationFiscalProfilesParams,
|
FindOrganizationFiscalProfilesParams,
|
||||||
@@ -31,6 +32,31 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
|||||||
if (!data.pointOfSale || data.pointOfSale <= 0) {
|
if (!data.pointOfSale || data.pointOfSale <= 0) {
|
||||||
throw new Error("El punto de venta debe ser mayor a 0");
|
throw new Error("El punto de venta debe ser mayor a 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const activityCode = data.activityCode?.trim();
|
||||||
|
|
||||||
|
if (activityCode && !/^\d{6}$/.test(activityCode)) {
|
||||||
|
throw new Error("El código de actividad debe contener 6 dígitos");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activityCode && !findArcaActivityByCode(activityCode)) {
|
||||||
|
throw new Error("La actividad ARCA/CLAE seleccionada no existe en el catálogo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildProfileData(data: UpsertOrganizationFiscalProfileParams) {
|
||||||
|
const activityCode = data.activityCode?.trim();
|
||||||
|
const activity = activityCode ? findArcaActivityByCode(activityCode) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
cuit: data.cuit,
|
||||||
|
legalName: data.legalName.trim(),
|
||||||
|
taxCondition: data.taxCondition,
|
||||||
|
pointOfSale: data.pointOfSale,
|
||||||
|
activityCode: activity?.code || "",
|
||||||
|
activityDescription: activity?.description || "",
|
||||||
|
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async upsert(
|
public async upsert(
|
||||||
@@ -51,15 +77,12 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
|||||||
});
|
});
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const profileData = this.buildProfileData(data);
|
||||||
|
|
||||||
if (existingProfile) {
|
if (existingProfile) {
|
||||||
const updatedProfile = await this.organizationFiscalProfiles.update({
|
const updatedProfile = await this.organizationFiscalProfiles.update({
|
||||||
companyId: data.companyId,
|
companyId: data.companyId,
|
||||||
cuit: data.cuit,
|
...profileData,
|
||||||
legalName: data.legalName.trim(),
|
|
||||||
taxCondition: data.taxCondition,
|
|
||||||
pointOfSale: data.pointOfSale,
|
|
||||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -73,11 +96,7 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
|||||||
try {
|
try {
|
||||||
return await this.organizationFiscalProfiles.create({
|
return await this.organizationFiscalProfiles.create({
|
||||||
companyId: data.companyId,
|
companyId: data.companyId,
|
||||||
cuit: data.cuit,
|
...profileData,
|
||||||
legalName: data.legalName.trim(),
|
|
||||||
taxCondition: data.taxCondition,
|
|
||||||
pointOfSale: data.pointOfSale,
|
|
||||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
});
|
});
|
||||||
@@ -85,11 +104,7 @@ class OrganizationFiscalProfilesManager implements IOrganizationFiscalProfilesMa
|
|||||||
if (this.isDuplicateKeyError(error)) {
|
if (this.isDuplicateKeyError(error)) {
|
||||||
const updatedProfile = await this.organizationFiscalProfiles.update({
|
const updatedProfile = await this.organizationFiscalProfiles.update({
|
||||||
companyId: data.companyId,
|
companyId: data.companyId,
|
||||||
cuit: data.cuit,
|
...profileData,
|
||||||
legalName: data.legalName.trim(),
|
|
||||||
taxCondition: data.taxCondition,
|
|
||||||
pointOfSale: data.pointOfSale,
|
|
||||||
status: ORGANIZATION_FISCAL_PROFILE_STATUS.ACTIVE,
|
|
||||||
updatedAt: new Date(),
|
updatedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Body, Controller, Middlewares, Post, Response, Route, SuccessResponse } from "tsoa";
|
||||||
|
import { ApiValidationError } from "../../Models/Server.Error.model";
|
||||||
|
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
|
||||||
|
import { authenticateMiddleware } from "../../middleware/authentication";
|
||||||
|
import { ArcaActivitiesService, FilterArcaActivitiesParams } from "./ArcaActivities.Service";
|
||||||
|
|
||||||
|
@Route("arca-activities/filter-all")
|
||||||
|
@Middlewares(authenticateMiddleware)
|
||||||
|
export class ArcaActivitiesFilterAllController extends Controller {
|
||||||
|
@Response<ApiValidationError>(500, "Ha ocurrido un error")
|
||||||
|
@SuccessResponse(200, "Done")
|
||||||
|
@Post()
|
||||||
|
public async allArcaActivities(
|
||||||
|
@Body() requestBody: FilterArcaActivitiesParams
|
||||||
|
): Promise<TextObjectFilterResult[] | ApiValidationError> {
|
||||||
|
try {
|
||||||
|
const activities = await new ArcaActivitiesService().textObjectFilter(requestBody);
|
||||||
|
this.setStatus(200);
|
||||||
|
return activities;
|
||||||
|
} catch (e) {
|
||||||
|
const errorOccurred: Error = e as Error;
|
||||||
|
this.setStatus(500);
|
||||||
|
return new ApiValidationError(500, errorOccurred.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { TextObjectFilterResult } from "../../Models/TextObjectFilter.model";
|
||||||
|
import { filterArcaActivities } from "../../Models/ArcaActivities/ArcaActivities.Catalog";
|
||||||
|
|
||||||
|
export type FilterArcaActivitiesParams = {
|
||||||
|
name?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ArcaActivitiesService {
|
||||||
|
public async textObjectFilter(data: FilterArcaActivitiesParams): Promise<TextObjectFilterResult[]> {
|
||||||
|
return filterArcaActivities(data.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
+58
-4
@@ -29,8 +29,25 @@ export type ArcaWsfeLastVoucherResult = {
|
|||||||
export type CreateInvoiceFromCashMovementParams = {
|
export type CreateInvoiceFromCashMovementParams = {
|
||||||
cashMovementId: string;
|
cashMovementId: string;
|
||||||
billingDate: string | Date;
|
billingDate: string | Date;
|
||||||
paymentMethod: string;
|
servicePeriodFrom: string | Date;
|
||||||
description: string;
|
servicePeriodTo: string | Date;
|
||||||
|
paymentDueDate: string | Date;
|
||||||
|
activityCode: string;
|
||||||
|
activityDescription: string;
|
||||||
|
receiverIvaCondition?: 5;
|
||||||
|
receiverDocumentType?: string | number;
|
||||||
|
receiverDocumentNumber?: string | number;
|
||||||
|
receiverName?: string;
|
||||||
|
receiverAddress?: string;
|
||||||
|
receiverEmail?: string;
|
||||||
|
saleCondition: string;
|
||||||
|
itemDescription: string;
|
||||||
|
itemQuantity?: number;
|
||||||
|
itemUnit?: string;
|
||||||
|
itemUnitPrice: number;
|
||||||
|
itemDiscountPercent?: number;
|
||||||
|
itemDiscountAmount?: number;
|
||||||
|
itemSubtotal: number;
|
||||||
sessionUser: string;
|
sessionUser: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -51,8 +68,25 @@ export type Invoice = {
|
|||||||
type: "FACTURA_C";
|
type: "FACTURA_C";
|
||||||
status: InvoiceStatus;
|
status: InvoiceStatus;
|
||||||
billingDate: Date;
|
billingDate: Date;
|
||||||
paymentMethod: string;
|
servicePeriodFrom: Date;
|
||||||
description: string;
|
servicePeriodTo: Date;
|
||||||
|
paymentDueDate: Date;
|
||||||
|
activityCode: string;
|
||||||
|
activityDescription: string;
|
||||||
|
receiverIvaCondition: 5;
|
||||||
|
receiverDocumentType?: string | number;
|
||||||
|
receiverDocumentNumber?: string | number;
|
||||||
|
receiverName?: string;
|
||||||
|
receiverAddress?: string;
|
||||||
|
receiverEmail?: string;
|
||||||
|
saleCondition: string;
|
||||||
|
itemDescription: string;
|
||||||
|
itemQuantity: number;
|
||||||
|
itemUnit?: string;
|
||||||
|
itemUnitPrice: number;
|
||||||
|
itemDiscountPercent: number;
|
||||||
|
itemDiscountAmount: number;
|
||||||
|
itemSubtotal: number;
|
||||||
cae?: string;
|
cae?: string;
|
||||||
caeExpiresAt?: Date;
|
caeExpiresAt?: Date;
|
||||||
pointOfSale?: number;
|
pointOfSale?: number;
|
||||||
@@ -61,6 +95,20 @@ export type Invoice = {
|
|||||||
arcaErrorMessage?: string;
|
arcaErrorMessage?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type OrganizationFiscalProfile = {
|
||||||
|
cuit?: string;
|
||||||
|
legalName?: string;
|
||||||
|
taxCondition?: "MONOTRIBUTO" | "EXENTO";
|
||||||
|
pointOfSale?: number;
|
||||||
|
activityCode?: string;
|
||||||
|
activityDescription?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindOrganizationFiscalProfilesParams = {
|
||||||
|
companyId: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
const schemaCreateCashFlowMovement = Yup.object().shape({
|
const schemaCreateCashFlowMovement = Yup.object().shape({
|
||||||
paymentMethod: Yup.string().required("Debe introducir el tipo de movimiento."),
|
paymentMethod: Yup.string().required("Debe introducir el tipo de movimiento."),
|
||||||
companyId: Yup.string().required("La organización no es valida."),
|
companyId: Yup.string().required("La organización no es valida."),
|
||||||
@@ -136,3 +184,9 @@ export const createInvoiceFromCashMovement = async (
|
|||||||
export const findInvoices = async (data: FindInvoicesParams): Promise<Invoice[]> => {
|
export const findInvoices = async (data: FindInvoicesParams): Promise<Invoice[]> => {
|
||||||
return ApiRequest.post<Invoice[]>("invoices/find", data);
|
return ApiRequest.post<Invoice[]>("invoices/find", data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const findOrganizationFiscalProfiles = async (
|
||||||
|
data: FindOrganizationFiscalProfilesParams
|
||||||
|
): Promise<OrganizationFiscalProfile[]> => {
|
||||||
|
return ApiRequest.post<OrganizationFiscalProfile[]>("organization-fiscal-profiles/find", data);
|
||||||
|
};
|
||||||
|
|||||||
+128
-6
@@ -138,6 +138,7 @@
|
|||||||
|
|
||||||
.movementActionsSlot {
|
.movementActionsSlot {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,6 +168,17 @@
|
|||||||
padding: 7px 10px;
|
padding: 7px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movementInvoicePdfButton {
|
||||||
|
background: var(--white);
|
||||||
|
border: 1px solid rgba(22, 128, 63, 0.24);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #166534;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.emptyState {
|
.emptyState {
|
||||||
background: var(--white);
|
background: var(--white);
|
||||||
border: 1px dashed var(--gray-light);
|
border: 1px dashed var(--gray-light);
|
||||||
@@ -188,7 +200,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 14px;
|
padding: 72px 14px 14px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
@@ -197,18 +209,36 @@
|
|||||||
background: var(--white);
|
background: var(--white);
|
||||||
border-radius: 24px 24px 18px 18px;
|
border-radius: 24px 24px 18px 18px;
|
||||||
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.28);
|
box-shadow: 0 24px 70px rgba(15, 23, 42, 0.28);
|
||||||
max-height: calc(100vh - 28px);
|
display: flex;
|
||||||
overflow: auto;
|
flex-direction: column;
|
||||||
|
max-height: calc(100dvh - 86px);
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoiceModalBody {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 2px;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceModalBody::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.invoiceModalHeader {
|
.invoiceModalHeader {
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
background: var(--white);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoiceModalHeader h2 {
|
.invoiceModalHeader h2 {
|
||||||
@@ -256,6 +286,44 @@
|
|||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoiceSteps {
|
||||||
|
background: var(--white);
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 2;
|
||||||
|
scrollbar-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceSteps::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceSteps span {
|
||||||
|
background: #f1f5f9;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: var(--gray-dark);
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 7px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceSteps .invoiceStepActive {
|
||||||
|
background: rgba(22, 128, 63, 0.14);
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceGrid {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.invoiceField {
|
.invoiceField {
|
||||||
color: var(--black-light);
|
color: var(--black-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -266,6 +334,10 @@
|
|||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoiceFieldFull {
|
||||||
|
composes: invoiceField;
|
||||||
|
}
|
||||||
|
|
||||||
.invoiceField input,
|
.invoiceField input,
|
||||||
.invoiceField select,
|
.invoiceField select,
|
||||||
.invoiceField textarea {
|
.invoiceField textarea {
|
||||||
@@ -284,11 +356,49 @@
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoiceModalActions {
|
.invoiceToggle {
|
||||||
|
align-items: center;
|
||||||
|
color: var(--black-light);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceToggle input {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceSummaryBox {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid var(--gray-light);
|
||||||
|
border-radius: 18px;
|
||||||
|
color: var(--black-light);
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceSummaryBox p {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.35;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceModalActions {
|
||||||
|
background: var(--white);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-shrink: 0;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
|
padding-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceModalActions .invoiceConfirmButton {
|
||||||
|
order: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoiceCancelButton,
|
.invoiceCancelButton,
|
||||||
@@ -352,13 +462,25 @@
|
|||||||
|
|
||||||
.invoiceModal {
|
.invoiceModal {
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
max-width: 520px;
|
max-width: 680px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceGrid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.invoiceFieldFull {
|
||||||
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoiceModalActions {
|
.invoiceModalActions {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invoiceModalActions .invoiceConfirmButton {
|
||||||
|
order: initial;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 1200px) {
|
@media (min-width: 1200px) {
|
||||||
|
|||||||
+446
-87
@@ -19,7 +19,7 @@ import {
|
|||||||
PaginateCashFlowParams,
|
PaginateCashFlowParams,
|
||||||
PaginateCashFlowResults,
|
PaginateCashFlowResults,
|
||||||
} from "@core/Models/CashFlow.model";
|
} from "@core/Models/CashFlow.model";
|
||||||
import { cashFlowPaginate, createInvoiceFromCashMovement, findInvoices, Invoice } from "../Cash.Service";
|
import { cashFlowPaginate, createInvoiceFromCashMovement, findInvoices, findOrganizationFiscalProfiles, Invoice, OrganizationFiscalProfile } from "../Cash.Service";
|
||||||
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
|
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
|
||||||
import Filterable from "@core/app/components/Filterable/Filterable";
|
import Filterable from "@core/app/components/Filterable/Filterable";
|
||||||
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
|
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
|
||||||
@@ -189,32 +189,189 @@ const getMovementId = (movement: ICashFlow) => movement.id || movement._id || ""
|
|||||||
|
|
||||||
const todayInputValue = () => dayjs().format("YYYY-MM-DD");
|
const todayInputValue = () => dayjs().format("YYYY-MM-DD");
|
||||||
|
|
||||||
const invoicePaymentMethodOptions = [
|
const saleConditionOptions = [
|
||||||
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH, name: "Efectivo" },
|
{ id: "cash", name: "Contado" },
|
||||||
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.BANK_TRANSFER, name: "Transferencia bancaria" },
|
{ id: "debit_card", name: "Tarjeta de débito" },
|
||||||
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.CREDIT_CARD, name: "Otros medios de pago electrónicos" },
|
{ id: "credit_card", name: "Tarjeta de crédito" },
|
||||||
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.DEBIT_CARD, name: "Tarjeta de débito" },
|
{ id: "current_account", name: "Cuenta corriente" },
|
||||||
{ id: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER, name: "Otros" },
|
{ id: "check", name: "Cheque" },
|
||||||
|
{ id: "bank_transfer", name: "Transferencia bancaria" },
|
||||||
|
{ id: "other", name: "Otra" },
|
||||||
|
{ id: "electronic_payment", name: "Otros medios de pago electrónicos" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const isInvoicePaymentMethod = (paymentMethod?: string) => {
|
const receiverDocumentTypeOptions = [
|
||||||
return invoicePaymentMethodOptions.some((option) => option.id === paymentMethod);
|
{ id: "96", name: "DNI" },
|
||||||
|
{ id: "99", name: "Consumidor Final / sin documento" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const isSaleCondition = (saleCondition?: string) => {
|
||||||
|
return saleConditionOptions.some((option) => option.id === saleCondition);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getInvoicePaymentMethod = (paymentMethod?: string) => {
|
const getInvoiceSaleCondition = (paymentMethod?: string) => {
|
||||||
if (paymentMethod && isInvoicePaymentMethod(paymentMethod)) {
|
if (paymentMethod && isSaleCondition(paymentMethod)) {
|
||||||
return paymentMethod;
|
return paymentMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CLIENT_ACCOUNT_PAYMENT_METHODS.CASH;
|
return "cash";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getInvoiceServiceDescription = (movement: ICashFlow) => {
|
||||||
|
const description = (movement.description || "").trim();
|
||||||
|
|
||||||
|
if (!description) {
|
||||||
|
return "Servicio facturado";
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceMatch = description.match(/^[^-]+ - (.+?)(?: \([^)]+\))?$/);
|
||||||
|
|
||||||
|
return serviceMatch?.[1]?.trim() || description;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatInvoiceDate = (value?: string | Date) => value ? dayjs(value).format("DD/MM/YYYY") : "-";
|
||||||
|
|
||||||
|
const formatInvoiceNumber = (value?: number, length = 5) => String(value || 0).padStart(length, "0");
|
||||||
|
|
||||||
|
const formatInvoiceAmount = (value?: number) => new Intl.NumberFormat("es-AR", { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value || 0);
|
||||||
|
|
||||||
|
const getInvoiceSaleConditionLabel = (saleCondition?: string) => saleConditionOptions.find((option) => option.id === saleCondition)?.name || "Contado";
|
||||||
|
|
||||||
|
const getInvoiceTaxConditionLabel = (taxCondition?: string) => taxCondition === "EXENTO" ? "IVA Exento" : "Responsable Monotributo";
|
||||||
|
|
||||||
|
const buildInvoiceCopyPage = (copyLabel: string, invoice: Invoice, fiscalProfile: OrganizationFiscalProfile) => {
|
||||||
|
const legalName = fiscalProfile.legalName || "-";
|
||||||
|
const cuit = fiscalProfile.cuit || "-";
|
||||||
|
const pointOfSale = formatInvoiceNumber(invoice.pointOfSale || fiscalProfile.pointOfSale, 5);
|
||||||
|
const voucherNumber = formatInvoiceNumber(invoice.voucherNumber, 8);
|
||||||
|
const total = formatInvoiceAmount(invoice.itemSubtotal || invoice.amount);
|
||||||
|
const taxCondition = getInvoiceTaxConditionLabel(fiscalProfile.taxCondition);
|
||||||
|
const itemDescription = invoice.itemDescription || "Servicio facturado";
|
||||||
|
const itemUnit = invoice.itemUnit || "unidades";
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
table: { widths: ["*"], body: [[{ text: copyLabel, style: "copyTitle" }]] },
|
||||||
|
margin: [0, 0, 0, 0],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
widths: ["*", 54, "*"],
|
||||||
|
body: [[
|
||||||
|
{ stack: [
|
||||||
|
{ text: legalName, style: "issuerName" },
|
||||||
|
{ text: [{ text: "Razón Social: ", bold: true }, legalName], margin: [0, 24, 0, 0] },
|
||||||
|
{ text: [{ text: "Domicilio Comercial: ", bold: true }, "-"], margin: [0, 10, 0, 0] },
|
||||||
|
{ text: [{ text: "Condición frente al IVA: ", bold: true }, taxCondition], margin: [0, 10, 0, 0] },
|
||||||
|
], border: [true, true, false, true], margin: [6, 8, 6, 6] },
|
||||||
|
{ stack: [{ text: "C", style: "voucherLetter" }, { text: "COD. 011", style: "voucherCode" }], alignment: "center", border: [true, true, true, true], margin: [0, 6, 0, 4] },
|
||||||
|
{ stack: [
|
||||||
|
{ text: "FACTURA", style: "invoiceTitle" },
|
||||||
|
{ text: [{ text: "Punto de Venta: ", bold: true }, pointOfSale, { text: " Comp. Nro: ", bold: true }, voucherNumber], margin: [0, 8, 0, 0] },
|
||||||
|
{ text: [{ text: "Fecha de Emisión: ", bold: true }, formatInvoiceDate(invoice.billingDate)], margin: [0, 7, 0, 0] },
|
||||||
|
{ text: [{ text: "CUIT: ", bold: true }, cuit], margin: [0, 10, 0, 0] },
|
||||||
|
{ text: [{ text: "Ingresos Brutos: ", bold: true }, cuit], margin: [0, 2, 0, 0] },
|
||||||
|
{ text: [{ text: "Fecha de Inicio de Actividades: ", bold: true }, "-"], margin: [0, 2, 0, 0] },
|
||||||
|
], border: [false, true, true, true], margin: [18, 8, 6, 6] },
|
||||||
|
]],
|
||||||
|
},
|
||||||
|
layout: "noPadding",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: { widths: ["33%", "28%", "39%"], body: [[
|
||||||
|
{ text: [{ text: "Período Facturado Desde: ", bold: true }, formatInvoiceDate(invoice.servicePeriodFrom)] },
|
||||||
|
{ text: [{ text: "Hasta: ", bold: true }, formatInvoiceDate(invoice.servicePeriodTo)] },
|
||||||
|
{ text: [{ text: "Fecha de Vto. para el pago: ", bold: true }, formatInvoiceDate(invoice.paymentDueDate)] },
|
||||||
|
]] },
|
||||||
|
style: "smallTable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: { widths: ["34%", "33%", "33%"], body: [
|
||||||
|
[{ text: "Doc.: -", bold: true }, { text: "Apellido y Nombre / Razón Social:", bold: true }, { text: "" }],
|
||||||
|
[{ text: [{ text: "Condición frente al IVA: ", bold: true }, "Consumidor Final"] }, { text: "Domicilio:", bold: true }, { text: invoice.receiverAddress || "" }],
|
||||||
|
[{ text: [{ text: "Condición de venta: ", bold: true }, getInvoiceSaleConditionLabel(invoice.saleCondition)], colSpan: 3 }, {}, {}],
|
||||||
|
] },
|
||||||
|
margin: [0, 0, 0, 2],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
headerRows: 1,
|
||||||
|
widths: [42, "*", 58, 52, 78, 45, 70, 78],
|
||||||
|
body: [
|
||||||
|
["Código", "Producto / Servicio", "Cantidad", "U. Medida", "Precio Unit.", "% Bonif", "Imp. Bonif.", "Subtotal"].map((text) => ({ text, bold: true, fillColor: "#d7d7d7", alignment: "center" })),
|
||||||
|
["", itemDescription, formatInvoiceAmount(invoice.itemQuantity || 1), itemUnit, formatInvoiceAmount(invoice.itemUnitPrice || invoice.amount), formatInvoiceAmount(invoice.itemDiscountPercent), formatInvoiceAmount(invoice.itemDiscountAmount), total],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
fontSize: 7,
|
||||||
|
},
|
||||||
|
{ text: " ", margin: [0, 154, 0, 0] },
|
||||||
|
{
|
||||||
|
table: { widths: ["*"], body: [[{ stack: [
|
||||||
|
{ text: [{ text: "Subtotal: $ ", bold: true }, total], alignment: "right", margin: [0, 8, 0, 0] },
|
||||||
|
{ text: [{ text: "Importe Otros Tributos: $ ", bold: true }, "0,00"], alignment: "right", margin: [0, 8, 0, 0] },
|
||||||
|
{ text: [{ text: "Importe Total: $ ", bold: true }, total], alignment: "right", margin: [0, 8, 0, 8] },
|
||||||
|
] }]] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
columns: [
|
||||||
|
{ columns: [
|
||||||
|
{ qr: buildArcaQrUrl(invoice, fiscalProfile), fit: 66, width: 78, margin: [0, 13, 0, 0] },
|
||||||
|
{ stack: [{ text: "ARCA", style: "arcaLogo" }, { text: "AGENCIA DE RECAUDACIÓN\nY CONTROL ADUANERO", fontSize: 5, color: "#6b7280", margin: [0, -3, 0, 0] }, { text: "Comprobante Autorizado", bold: true, italics: true, margin: [0, 11, 0, 0] }, { text: "Esta Agencia no se responsabiliza por los datos ingresados en el detalle de la operación", fontSize: 6, italics: true, margin: [0, 8, 0, 0] }], width: "*" },
|
||||||
|
], width: "40%" },
|
||||||
|
{ text: "Pág. 1/1", bold: true, alignment: "center", width: "20%", margin: [0, 34, 0, 0] },
|
||||||
|
{ stack: [{ text: [{ text: "CAE N°: ", bold: true }, invoice.cae || "-"] }, { text: [{ text: "Fecha de Vto. de CAE: ", bold: true }, formatInvoiceDate(invoice.caeExpiresAt)], margin: [0, 8, 0, 0] }], width: "40%", alignment: "right", margin: [0, 30, 0, 0] },
|
||||||
|
],
|
||||||
|
margin: [0, 30, 0, 0],
|
||||||
|
pageBreak: copyLabel === "TRIPLICADO" ? undefined : "after",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildArcaQrUrl = (invoice: Invoice, fiscalProfile: OrganizationFiscalProfile) => {
|
||||||
|
const payload = {
|
||||||
|
ver: 1,
|
||||||
|
fecha: dayjs(invoice.billingDate).format("YYYY-MM-DD"),
|
||||||
|
cuit: Number(fiscalProfile.cuit || 0),
|
||||||
|
ptoVta: invoice.pointOfSale || fiscalProfile.pointOfSale || 0,
|
||||||
|
tipoCmp: 11,
|
||||||
|
nroCmp: invoice.voucherNumber || 0,
|
||||||
|
importe: Number(invoice.itemSubtotal || invoice.amount || 0),
|
||||||
|
moneda: "PES",
|
||||||
|
ctz: 1,
|
||||||
|
tipoDocRec: Number(invoice.receiverDocumentType || 99),
|
||||||
|
nroDocRec: Number(invoice.receiverDocumentNumber || 0),
|
||||||
|
tipoCodAut: "E",
|
||||||
|
codAut: Number(invoice.cae || 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
return `https://www.afip.gob.ar/fe/qr/?p=${btoa(JSON.stringify(payload))}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const invoiceWizardSteps = ["Datos de emisión", "Datos del receptor", "Datos de la operación", "Resumen"];
|
||||||
|
|
||||||
type InvoiceDraft = {
|
type InvoiceDraft = {
|
||||||
movement: ICashFlow;
|
movement: ICashFlow;
|
||||||
movementId: string;
|
movementId: string;
|
||||||
billingDate: string;
|
billingDate: string;
|
||||||
paymentMethod: string;
|
servicePeriodFrom: string;
|
||||||
description: string;
|
servicePeriodTo: string;
|
||||||
|
paymentDueDate: string;
|
||||||
|
activityCode: string;
|
||||||
|
activityDescription: string;
|
||||||
|
saleCondition: string;
|
||||||
|
includeReceiverData: boolean;
|
||||||
|
receiverDocumentType: string;
|
||||||
|
receiverDocumentNumber: string;
|
||||||
|
receiverName: string;
|
||||||
|
receiverAddress: string;
|
||||||
|
receiverEmail: string;
|
||||||
|
itemDescription: string;
|
||||||
|
itemQuantity: number;
|
||||||
|
itemUnit: string;
|
||||||
|
itemUnitPrice: number;
|
||||||
|
itemDiscountPercent: number;
|
||||||
|
itemDiscountAmount: number;
|
||||||
|
itemSubtotal: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPaymentMethodCardVisual = (paymentMethod?: PAYMENT_METHODS | string) => {
|
const getPaymentMethodCardVisual = (paymentMethod?: PAYMENT_METHODS | string) => {
|
||||||
@@ -304,6 +461,8 @@ export default function CashFlowMovementsList() {
|
|||||||
const [invoicingMovementId, setInvoicingMovementId] = useState<string>("");
|
const [invoicingMovementId, setInvoicingMovementId] = useState<string>("");
|
||||||
const [invoiceDraft, setInvoiceDraft] = useState<InvoiceDraft | null>(null);
|
const [invoiceDraft, setInvoiceDraft] = useState<InvoiceDraft | null>(null);
|
||||||
const [invoiceDraftsByMovementId, setInvoiceDraftsByMovementId] = useState<Record<string, InvoiceDraft>>({});
|
const [invoiceDraftsByMovementId, setInvoiceDraftsByMovementId] = useState<Record<string, InvoiceDraft>>({});
|
||||||
|
const [fiscalProfile, setFiscalProfile] = useState<OrganizationFiscalProfile>({ activityCode: "", activityDescription: "" });
|
||||||
|
const [invoiceStep, setInvoiceStep] = useState(0);
|
||||||
const movementsRef = useRef(movements);
|
const movementsRef = useRef(movements);
|
||||||
|
|
||||||
const loadMovements = (page: number = 1, pageSize: number = 30) => {
|
const loadMovements = (page: number = 1, pageSize: number = 30) => {
|
||||||
@@ -428,6 +587,19 @@ export default function CashFlowMovementsList() {
|
|||||||
loadMovements(loadPage, pageSize);
|
loadMovements(loadPage, pageSize);
|
||||||
}, [clientId, collaboratorId, methodId]);
|
}, [clientId, collaboratorId, methodId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!SessionInfo.userId) return;
|
||||||
|
|
||||||
|
findOrganizationFiscalProfiles({ companyId: id, sessionUser: SessionInfo.userId })
|
||||||
|
.then((profiles) => {
|
||||||
|
const profile = profiles[0];
|
||||||
|
setFiscalProfile(profile || { activityCode: "", activityDescription: "" });
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setFiscalProfile({ activityCode: "", activityDescription: "" });
|
||||||
|
});
|
||||||
|
}, [SessionInfo.userId]);
|
||||||
|
|
||||||
const renderMovementMeta = (movement: ICashFlow) => {
|
const renderMovementMeta = (movement: ICashFlow) => {
|
||||||
const details = [
|
const details = [
|
||||||
movement.paymentMethod ? getPaymentMethodName(movement.paymentMethod) : null,
|
movement.paymentMethod ? getPaymentMethodName(movement.paymentMethod) : null,
|
||||||
@@ -463,12 +635,32 @@ export default function CashFlowMovementsList() {
|
|||||||
|
|
||||||
const savedDraft = invoiceDraftsByMovementId[movementId];
|
const savedDraft = invoiceDraftsByMovementId[movementId];
|
||||||
|
|
||||||
|
const defaultDate = todayInputValue();
|
||||||
|
|
||||||
|
setInvoiceStep(0);
|
||||||
setInvoiceDraft(savedDraft ? { ...savedDraft, movement } : {
|
setInvoiceDraft(savedDraft ? { ...savedDraft, movement } : {
|
||||||
movement,
|
movement,
|
||||||
movementId,
|
movementId,
|
||||||
billingDate: dayjs(movement.createdAt).format("YYYY-MM-DD"),
|
billingDate: defaultDate,
|
||||||
paymentMethod: getInvoicePaymentMethod(movement.paymentMethod),
|
servicePeriodFrom: defaultDate,
|
||||||
description: movement.description || "",
|
servicePeriodTo: defaultDate,
|
||||||
|
paymentDueDate: defaultDate,
|
||||||
|
activityCode: fiscalProfile.activityCode || "",
|
||||||
|
activityDescription: fiscalProfile.activityDescription || "",
|
||||||
|
saleCondition: getInvoiceSaleCondition(movement.paymentMethod),
|
||||||
|
includeReceiverData: false,
|
||||||
|
receiverDocumentType: "",
|
||||||
|
receiverDocumentNumber: "",
|
||||||
|
receiverName: "",
|
||||||
|
receiverAddress: "",
|
||||||
|
receiverEmail: "",
|
||||||
|
itemDescription: getInvoiceServiceDescription(movement),
|
||||||
|
itemQuantity: 1,
|
||||||
|
itemUnit: "",
|
||||||
|
itemUnitPrice: movement.amount,
|
||||||
|
itemDiscountPercent: 0,
|
||||||
|
itemDiscountAmount: 0,
|
||||||
|
itemSubtotal: movement.amount,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -483,6 +675,65 @@ export default function CashFlowMovementsList() {
|
|||||||
return remaining;
|
return remaining;
|
||||||
});
|
});
|
||||||
setInvoiceDraft(null);
|
setInvoiceDraft(null);
|
||||||
|
setInvoiceStep(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateInvoiceStep = (step: number) => {
|
||||||
|
if (!invoiceDraft) return false;
|
||||||
|
|
||||||
|
if (step === 0) {
|
||||||
|
if (!invoiceDraft.billingDate || !invoiceDraft.servicePeriodFrom || !invoiceDraft.servicePeriodTo || !invoiceDraft.paymentDueDate) {
|
||||||
|
alert.showError("Debe completar las fechas de emisión, período y vencimiento.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoiceDraft.servicePeriodFrom > invoiceDraft.servicePeriodTo) {
|
||||||
|
alert.showError("El inicio del período facturado no puede ser posterior al fin.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!invoiceDraft.activityCode.trim() || !invoiceDraft.activityDescription.trim()) {
|
||||||
|
alert.showError("Debe completar el código y la descripción de actividad.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 1 && !invoiceDraft.saleCondition) {
|
||||||
|
alert.showError("Debe seleccionar la condición de venta.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 1 && invoiceDraft.includeReceiverData) {
|
||||||
|
if (!invoiceDraft.receiverDocumentType) {
|
||||||
|
alert.showError("Debe seleccionar el tipo de documento del receptor.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (invoiceDraft.receiverDocumentType === "99" && invoiceDraft.receiverDocumentNumber.trim() !== "0") {
|
||||||
|
alert.showError("El número de documento debe ser 0 para Consumidor Final sin documento.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === 2) {
|
||||||
|
if (!invoiceDraft.itemDescription.trim()) {
|
||||||
|
alert.showError("Debe ingresar la descripción del servicio facturado.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.abs(invoiceDraft.itemSubtotal - invoiceDraft.movement.amount) > 0.01) {
|
||||||
|
alert.showError("El total del ítem debe coincidir con el importe del movimiento.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const goToNextInvoiceStep = () => {
|
||||||
|
if (validateInvoiceStep(invoiceStep)) {
|
||||||
|
setInvoiceStep((current) => Math.min(current + 1, invoiceWizardSteps.length - 1));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -496,13 +747,7 @@ export default function CashFlowMovementsList() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invoiceDraft.paymentMethod) {
|
if (!invoiceWizardSteps.every((_, index) => validateInvoiceStep(index))) {
|
||||||
alert.showError("Debe seleccionar el medio de pago.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!invoiceDraft.description.trim()) {
|
|
||||||
alert.showError("Debe ingresar el detalle de la factura.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,8 +757,25 @@ export default function CashFlowMovementsList() {
|
|||||||
createInvoiceFromCashMovement({
|
createInvoiceFromCashMovement({
|
||||||
cashMovementId: invoiceDraft.movementId,
|
cashMovementId: invoiceDraft.movementId,
|
||||||
billingDate: invoiceDraft.billingDate,
|
billingDate: invoiceDraft.billingDate,
|
||||||
paymentMethod: invoiceDraft.paymentMethod,
|
servicePeriodFrom: invoiceDraft.servicePeriodFrom,
|
||||||
description: invoiceDraft.description.trim(),
|
servicePeriodTo: invoiceDraft.servicePeriodTo,
|
||||||
|
paymentDueDate: invoiceDraft.paymentDueDate,
|
||||||
|
activityCode: invoiceDraft.activityCode.trim(),
|
||||||
|
activityDescription: invoiceDraft.activityDescription.trim(),
|
||||||
|
receiverIvaCondition: 5,
|
||||||
|
receiverDocumentType: invoiceDraft.includeReceiverData ? invoiceDraft.receiverDocumentType : undefined,
|
||||||
|
receiverDocumentNumber: invoiceDraft.includeReceiverData ? invoiceDraft.receiverDocumentNumber.trim() : undefined,
|
||||||
|
receiverName: invoiceDraft.includeReceiverData ? invoiceDraft.receiverName.trim() : undefined,
|
||||||
|
receiverAddress: invoiceDraft.includeReceiverData ? invoiceDraft.receiverAddress.trim() : undefined,
|
||||||
|
receiverEmail: invoiceDraft.includeReceiverData ? invoiceDraft.receiverEmail.trim() : undefined,
|
||||||
|
saleCondition: invoiceDraft.saleCondition,
|
||||||
|
itemDescription: invoiceDraft.itemDescription.trim(),
|
||||||
|
itemQuantity: invoiceDraft.itemQuantity,
|
||||||
|
itemUnit: invoiceDraft.itemUnit.trim(),
|
||||||
|
itemUnitPrice: invoiceDraft.itemUnitPrice,
|
||||||
|
itemDiscountPercent: invoiceDraft.itemDiscountPercent,
|
||||||
|
itemDiscountAmount: invoiceDraft.itemDiscountAmount,
|
||||||
|
itemSubtotal: invoiceDraft.itemSubtotal,
|
||||||
sessionUser: SessionInfo.userId,
|
sessionUser: SessionInfo.userId,
|
||||||
})
|
})
|
||||||
.then((invoice) => {
|
.then((invoice) => {
|
||||||
@@ -550,10 +812,45 @@ export default function CashFlowMovementsList() {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setInvoicingMovementId("");
|
setInvoicingMovementId("");
|
||||||
setInvoiceDraft(null);
|
setInvoiceDraft(null);
|
||||||
|
setInvoiceStep(0);
|
||||||
eventHandler.setEventType(EVENT_TYPES.CLEAR);
|
eventHandler.setEventType(EVENT_TYPES.CLEAR);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadInvoicePdf = async (invoice: Invoice) => {
|
||||||
|
if (invoice.status !== "APPROVED") {
|
||||||
|
alert.showError("Solo se puede descargar el PDF de facturas aprobadas.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pdfMake = await import("pdfmake/build/pdfmake");
|
||||||
|
const pdfFonts = (await import("pdfmake/build/vfs_fonts")) as any;
|
||||||
|
pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs;
|
||||||
|
|
||||||
|
const content = ["ORIGINAL", "DUPLICADO", "TRIPLICADO"].flatMap((copyLabel) => buildInvoiceCopyPage(copyLabel, invoice, fiscalProfile));
|
||||||
|
const documentDefinition: any = {
|
||||||
|
pageSize: "A4",
|
||||||
|
pageMargins: [24, 14, 24, 18],
|
||||||
|
defaultStyle: { fontSize: 8, color: "#111111" },
|
||||||
|
styles: {
|
||||||
|
copyTitle: { fontSize: 15, bold: true, alignment: "center", margin: [0, 4, 0, 4] },
|
||||||
|
issuerName: { bold: true, alignment: "center", margin: [0, 14, 0, 0] },
|
||||||
|
voucherLetter: { fontSize: 27, bold: true },
|
||||||
|
voucherCode: { fontSize: 8, bold: true },
|
||||||
|
invoiceTitle: { fontSize: 22, bold: true, margin: [0, 6, 0, 0] },
|
||||||
|
smallTable: { margin: [0, 0, 0, 0] },
|
||||||
|
arcaLogo: { fontSize: 20, bold: true, color: "#4b5563", margin: [0, 13, 0, 0] },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
(pdfMake as any).createPdf({ ...documentDefinition, content }).download(`factura-c-${invoice.pointOfSale || fiscalProfile.pointOfSale || 0}-${invoice.voucherNumber || 0}.pdf`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating invoice PDF:", error);
|
||||||
|
alert.showError("No pudimos generar el PDF de la factura.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Location />
|
<Location />
|
||||||
@@ -612,9 +909,16 @@ export default function CashFlowMovementsList() {
|
|||||||
|
|
||||||
<div className={style.movementActionsSlot}>
|
<div className={style.movementActionsSlot}>
|
||||||
{isInvoiced ? (
|
{isInvoiced ? (
|
||||||
<span className={style.movementInvoiceBadge}>
|
<>
|
||||||
Factura C {invoice?.pointOfSale}-{invoice?.voucherNumber}
|
<span className={style.movementInvoiceBadge}>
|
||||||
</span>
|
Factura C {invoice?.pointOfSale}-{invoice?.voucherNumber}
|
||||||
|
</span>
|
||||||
|
{invoice && (
|
||||||
|
<button type="button" className={style.movementInvoicePdfButton} onClick={() => handleDownloadInvoicePdf(invoice)}>
|
||||||
|
PDF
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
) : isPendingInvoice ? (
|
) : isPendingInvoice ? (
|
||||||
<span className={style.movementInvoiceBadge} title={getInvoiceStatusMessage(invoice)}>
|
<span className={style.movementInvoiceBadge} title={getInvoiceStatusMessage(invoice)}>
|
||||||
Factura pendiente
|
Factura pendiente
|
||||||
@@ -671,58 +975,113 @@ export default function CashFlowMovementsList() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={style.invoiceAmountBox}>
|
<div className={style.invoiceModalBody}>
|
||||||
<span>Importe</span>
|
<div className={style.invoiceSteps}>
|
||||||
<strong>{formatPrice(invoiceDraft.movement.amount)}</strong>
|
{invoiceWizardSteps.map((step, index) => (
|
||||||
</div>
|
<span key={step} className={index === invoiceStep ? style.invoiceStepActive : ""}>
|
||||||
|
{index + 1}. {step}
|
||||||
<label className={style.invoiceField}>
|
</span>
|
||||||
<span>Fecha de comprobante</span>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
value={invoiceDraft.billingDate}
|
|
||||||
max={todayInputValue()}
|
|
||||||
onChange={(event) =>
|
|
||||||
setInvoiceDraft((current) =>
|
|
||||||
current ? { ...current, billingDate: event.target.value } : current
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={Boolean(invoicingMovementId)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className={style.invoiceField}>
|
|
||||||
<span>Medio de pago</span>
|
|
||||||
<select
|
|
||||||
value={invoiceDraft.paymentMethod}
|
|
||||||
onChange={(event) =>
|
|
||||||
setInvoiceDraft((current) =>
|
|
||||||
current ? { ...current, paymentMethod: event.target.value } : current
|
|
||||||
)
|
|
||||||
}
|
|
||||||
disabled={Boolean(invoicingMovementId)}
|
|
||||||
>
|
|
||||||
{invoicePaymentMethodOptions.map((option) => (
|
|
||||||
<option key={option.id} value={option.id}>
|
|
||||||
{option.name}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
</div>
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className={style.invoiceField}>
|
<div className={style.invoiceAmountBox}>
|
||||||
<span>Detalle</span>
|
<span>Total del comprobante</span>
|
||||||
<textarea
|
<strong>{formatPrice(invoiceDraft.itemSubtotal)}</strong>
|
||||||
value={invoiceDraft.description}
|
</div>
|
||||||
rows={4}
|
|
||||||
onChange={(event) =>
|
{invoiceStep === 0 && (
|
||||||
setInvoiceDraft((current) =>
|
<div className={style.invoiceGrid}>
|
||||||
current ? { ...current, description: event.target.value } : current
|
<label className={style.invoiceField}>
|
||||||
)
|
<span>Fecha de comprobante</span>
|
||||||
}
|
<input type="date" value={invoiceDraft.billingDate} max={todayInputValue()} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, billingDate: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} />
|
||||||
disabled={Boolean(invoicingMovementId)}
|
</label>
|
||||||
/>
|
<label className={style.invoiceField}>
|
||||||
</label>
|
<span>Concepto</span>
|
||||||
|
<input type="text" value="Servicios" disabled />
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceField}>
|
||||||
|
<span>Período facturado desde</span>
|
||||||
|
<input type="date" value={invoiceDraft.servicePeriodFrom} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, servicePeriodFrom: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} />
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceField}>
|
||||||
|
<span>Período facturado hasta</span>
|
||||||
|
<input type="date" value={invoiceDraft.servicePeriodTo} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, servicePeriodTo: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} />
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceField}>
|
||||||
|
<span>Vencimiento de pago</span>
|
||||||
|
<input type="date" value={invoiceDraft.paymentDueDate} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, paymentDueDate: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} />
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceField}>
|
||||||
|
<span>Código de actividad</span>
|
||||||
|
<input type="text" value={invoiceDraft.activityCode} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, activityCode: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} />
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceFieldFull}>
|
||||||
|
<span>Descripción de actividad</span>
|
||||||
|
<input type="text" value={invoiceDraft.activityDescription} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, activityDescription: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} placeholder="Integración futura con perfil fiscal" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invoiceStep === 1 && (
|
||||||
|
<div className={style.invoiceGrid}>
|
||||||
|
<label className={style.invoiceField}>
|
||||||
|
<span>Condición frente al IVA</span>
|
||||||
|
<input type="text" value="Consumidor Final" disabled />
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceField}>
|
||||||
|
<span>Condición de venta</span>
|
||||||
|
<select value={invoiceDraft.saleCondition} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, saleCondition: event.target.value } : current)} disabled={Boolean(invoicingMovementId)}>
|
||||||
|
{saleConditionOptions.map((option) => <option key={option.id} value={option.id}>{option.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceToggle}>
|
||||||
|
<input type="checkbox" checked={invoiceDraft.includeReceiverData} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, includeReceiverData: event.target.checked } : current)} disabled={Boolean(invoicingMovementId)} />
|
||||||
|
<span>Agregar datos del consumidor final</span>
|
||||||
|
</label>
|
||||||
|
{invoiceDraft.includeReceiverData && (
|
||||||
|
<>
|
||||||
|
<label className={style.invoiceField}>
|
||||||
|
<span>Tipo documento</span>
|
||||||
|
<select value={invoiceDraft.receiverDocumentType} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, receiverDocumentType: event.target.value, receiverDocumentNumber: event.target.value === "99" ? "0" : current.receiverDocumentNumber } : current)} disabled={Boolean(invoicingMovementId)}>
|
||||||
|
<option value="">Seleccionar</option>
|
||||||
|
{receiverDocumentTypeOptions.map((option) => <option key={option.id} value={option.id}>{option.name}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceField}><span>Nro. documento</span><input type="text" value={invoiceDraft.receiverDocumentNumber} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, receiverDocumentNumber: event.target.value } : current)} disabled={Boolean(invoicingMovementId) || invoiceDraft.receiverDocumentType === "99"} /></label>
|
||||||
|
<label className={style.invoiceField}><span>Nombre / razón social</span><input type="text" value={invoiceDraft.receiverName} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, receiverName: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} /></label>
|
||||||
|
<label className={style.invoiceField}><span>Domicilio comercial</span><input type="text" value={invoiceDraft.receiverAddress} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, receiverAddress: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} /></label>
|
||||||
|
<label className={style.invoiceFieldFull}><span>Email</span><input type="email" value={invoiceDraft.receiverEmail} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, receiverEmail: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} /></label>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invoiceStep === 2 && (
|
||||||
|
<div className={style.invoiceGrid}>
|
||||||
|
<label className={style.invoiceFieldFull}>
|
||||||
|
<span>Descripción del servicio</span>
|
||||||
|
<textarea value={invoiceDraft.itemDescription} rows={4} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, itemDescription: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} />
|
||||||
|
</label>
|
||||||
|
<label className={style.invoiceField}><span>Cantidad</span><input type="number" value={invoiceDraft.itemQuantity} disabled /></label>
|
||||||
|
<label className={style.invoiceField}><span>Unidad</span><input type="text" value={invoiceDraft.itemUnit} onChange={(event) => setInvoiceDraft((current) => current ? { ...current, itemUnit: event.target.value } : current)} disabled={Boolean(invoicingMovementId)} /></label>
|
||||||
|
<label className={style.invoiceField}><span>Precio unitario</span><input type="number" value={invoiceDraft.itemUnitPrice} disabled /></label>
|
||||||
|
<label className={style.invoiceField}><span>Subtotal</span><input type="number" value={invoiceDraft.itemSubtotal} disabled /></label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{invoiceStep === 3 && (
|
||||||
|
<div className={style.invoiceSummaryBox}>
|
||||||
|
<p><strong>Comprobante:</strong> Factura C de servicios, fecha {invoiceDraft.billingDate}</p>
|
||||||
|
<p><strong>Período:</strong> {invoiceDraft.servicePeriodFrom} al {invoiceDraft.servicePeriodTo}</p>
|
||||||
|
<p><strong>Vencimiento:</strong> {invoiceDraft.paymentDueDate}</p>
|
||||||
|
<p><strong>Actividad:</strong> {invoiceDraft.activityCode} - {invoiceDraft.activityDescription}</p>
|
||||||
|
<p><strong>Receptor:</strong> Consumidor Final{invoiceDraft.includeReceiverData && invoiceDraft.receiverName ? ` - ${invoiceDraft.receiverName}` : ""}</p>
|
||||||
|
<p><strong>Condición de venta:</strong> {saleConditionOptions.find((option) => option.id === invoiceDraft.saleCondition)?.name}</p>
|
||||||
|
<p><strong>Ítem:</strong> {invoiceDraft.itemDescription}</p>
|
||||||
|
<p><strong>Total:</strong> {formatPrice(invoiceDraft.itemSubtotal)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={style.invoiceModalActions}>
|
<div className={style.invoiceModalActions}>
|
||||||
<button
|
<button
|
||||||
@@ -733,14 +1092,14 @@ export default function CashFlowMovementsList() {
|
|||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
{invoiceStep > 0 && <button type="button" className={style.invoiceCancelButton} onClick={() => setInvoiceStep((current) => current - 1)} disabled={Boolean(invoicingMovementId)}>Atrás</button>}
|
||||||
type="button"
|
{invoiceStep < invoiceWizardSteps.length - 1 ? (
|
||||||
className={style.invoiceConfirmButton}
|
<button type="button" className={style.invoiceConfirmButton} onClick={goToNextInvoiceStep} disabled={Boolean(invoicingMovementId)}>Siguiente</button>
|
||||||
onClick={handleInvoiceMovement}
|
) : (
|
||||||
disabled={Boolean(invoicingMovementId)}
|
<button type="button" className={style.invoiceConfirmButton} onClick={handleInvoiceMovement} disabled={Boolean(invoicingMovementId)}>
|
||||||
>
|
{invoicingMovementId ? "Facturando..." : "Emitir factura"}
|
||||||
{invoicingMovementId ? "Facturando..." : "Emitir factura"}
|
</button>
|
||||||
</button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
import ApiRequest from "@services/Api.Service";
|
||||||
|
import { TextObjectFilterResult } from "@models/TextObjectFilter.model";
|
||||||
|
|
||||||
|
export type OrganizationFiscalProfileTaxCondition = "MONOTRIBUTO" | "EXENTO";
|
||||||
|
|
||||||
|
export type OrganizationFiscalProfile = {
|
||||||
|
id?: string;
|
||||||
|
companyId: string;
|
||||||
|
cuit: string;
|
||||||
|
legalName: string;
|
||||||
|
taxCondition: OrganizationFiscalProfileTaxCondition;
|
||||||
|
pointOfSale: number;
|
||||||
|
activityCode?: string;
|
||||||
|
activityDescription?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FindOrganizationFiscalProfilesParams = {
|
||||||
|
companyId: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpsertOrganizationFiscalProfileParams = {
|
||||||
|
companyId: string;
|
||||||
|
cuit: string;
|
||||||
|
legalName: string;
|
||||||
|
taxCondition: OrganizationFiscalProfileTaxCondition;
|
||||||
|
pointOfSale: number;
|
||||||
|
activityCode?: string;
|
||||||
|
activityDescription?: string;
|
||||||
|
sessionUser: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findOrganizationFiscalProfiles = async (
|
||||||
|
data: FindOrganizationFiscalProfilesParams
|
||||||
|
): Promise<OrganizationFiscalProfile[]> => {
|
||||||
|
return ApiRequest.post<OrganizationFiscalProfile[]>("organization-fiscal-profiles/find", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upsertOrganizationFiscalProfile = async (
|
||||||
|
data: UpsertOrganizationFiscalProfileParams
|
||||||
|
): Promise<OrganizationFiscalProfile> => {
|
||||||
|
return ApiRequest.post<OrganizationFiscalProfile>("organization-fiscal-profiles/upsert", data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findArcaActivities = async (): Promise<TextObjectFilterResult[]> => {
|
||||||
|
return ApiRequest.post<TextObjectFilterResult[]>("arca-activities/filter-all", {});
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Saveable from "@components/Saveable/Saveable";
|
||||||
|
|
||||||
|
export default function OrgFiscalLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
return <Saveable>{children}</Saveable>;
|
||||||
|
}
|
||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
.pageContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
margin: 12px auto 0;
|
||||||
|
max-width: 860px;
|
||||||
|
padding: 0 14px 40px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroCard {
|
||||||
|
align-items: flex-start;
|
||||||
|
background: linear-gradient(135deg, #4b6cb7 0%, #182848 100%);
|
||||||
|
border-radius: 22px;
|
||||||
|
box-shadow: 0 14px 34px rgba(24, 40, 72, 0.18);
|
||||||
|
color: var(--white);
|
||||||
|
display: flex;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroCard h2 {
|
||||||
|
font-size: 22px;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroCard p {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.45;
|
||||||
|
margin: 0;
|
||||||
|
opacity: 0.86;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroIcon {
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.14);
|
||||||
|
border-radius: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 48px;
|
||||||
|
height: 48px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroIcon svg {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection {
|
||||||
|
background: var(--white);
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 10px 28px rgba(15, 23, 42, 0.06);
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection h3 {
|
||||||
|
color: var(--black-light);
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader h3 {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectionHeader p {
|
||||||
|
color: var(--gray-dark);
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionGroup {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCard {
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1px solid rgba(100, 116, 139, 0.22);
|
||||||
|
border-radius: 18px;
|
||||||
|
color: var(--black-light);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 14px 16px;
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCard:hover {
|
||||||
|
border-color: rgba(124, 58, 237, 0.38);
|
||||||
|
box-shadow: 0 8px 20px rgba(124, 58, 237, 0.12);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCardSelected {
|
||||||
|
background: linear-gradient(135deg, rgba(124, 58, 237, 0.14) 0%, rgba(75, 108, 183, 0.1) 100%);
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 10px 24px rgba(124, 58, 237, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionTitle {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionDescription {
|
||||||
|
color: var(--gray-dark);
|
||||||
|
display: block;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.helpText {
|
||||||
|
color: var(--gray-dark);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 720px) {
|
||||||
|
.pageContainer {
|
||||||
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionGroup {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import Location from "@components/Location/Location";
|
||||||
|
import Textbox from "@components/Textbox/Textbox";
|
||||||
|
import TextObjectFilter from "@components/TextObjectFilter/TextObjectFilter";
|
||||||
|
import TextObjectFilterItem from "@components/TextObjectFilterItem/TextObjectFilterItem";
|
||||||
|
import { TextObjectFilterResult } from "@models/TextObjectFilter.model";
|
||||||
|
import { TextObjectModalContent } from "@store/TextObjectModal.Store";
|
||||||
|
import { useLocationStore } from "@store/Location.Store";
|
||||||
|
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
|
||||||
|
import { useSessionStore } from "@store/Sesion.Store";
|
||||||
|
import { useAlert } from "@store/Alert.Store";
|
||||||
|
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
|
||||||
|
import ReceiptLongOutlinedIcon from "@mui/icons-material/ReceiptLongOutlined";
|
||||||
|
import { Grid2 } from "@mui/material";
|
||||||
|
import { ApiError } from "@models/Server.Error.model";
|
||||||
|
import style from "./page.module.css";
|
||||||
|
import {
|
||||||
|
findArcaActivities,
|
||||||
|
findOrganizationFiscalProfiles,
|
||||||
|
OrganizationFiscalProfileTaxCondition,
|
||||||
|
upsertOrganizationFiscalProfile,
|
||||||
|
} from "./Fiscal.Service";
|
||||||
|
|
||||||
|
type UrlData = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function OrganizationFiscalProfileEdit() {
|
||||||
|
const data = useParams<UrlData>();
|
||||||
|
const id = data.id;
|
||||||
|
const location = useLocationStore();
|
||||||
|
const eventHandler = useEventHandlerStore();
|
||||||
|
const SessionInfo = useSessionStore();
|
||||||
|
const alert = useAlert();
|
||||||
|
|
||||||
|
const [cuit, setCuit] = useState("");
|
||||||
|
const [legalName, setLegalName] = useState("");
|
||||||
|
const [taxCondition, setTaxCondition] = useState<OrganizationFiscalProfileTaxCondition>("MONOTRIBUTO");
|
||||||
|
const [pointOfSale, setPointOfSale] = useState("1");
|
||||||
|
const [activityCode, setActivityCode] = useState("");
|
||||||
|
const [activityDescription, setActivityDescription] = useState("");
|
||||||
|
const [arcaActivities, setArcaActivities] = useState<TextObjectFilterResult[]>([]);
|
||||||
|
const [fiscalDataLoaded, setFiscalDataLoaded] = useState(false);
|
||||||
|
|
||||||
|
const [activityFilterContent] = useState<TextObjectModalContent>({
|
||||||
|
header: (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
|
||||||
|
Seleccionar actividad ARCA/CLAE
|
||||||
|
</div>
|
||||||
|
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
|
||||||
|
Buscá por código o descripción de la actividad declarada.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
bottom: <div> </div>,
|
||||||
|
renderResult: (result: TextObjectFilterResult, index: number) => (
|
||||||
|
<TextObjectFilterItem key={index} data={result} />
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const markNeedsSave = () => eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
|
|
||||||
|
const fiscalConditionOptions: Array<{
|
||||||
|
id: OrganizationFiscalProfileTaxCondition;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
id: "MONOTRIBUTO",
|
||||||
|
title: "Monotributo",
|
||||||
|
description: "Para organizaciones adheridas al régimen simplificado.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
location.clear();
|
||||||
|
|
||||||
|
location.add({
|
||||||
|
url: "/admin/org",
|
||||||
|
title: "Organizaciones",
|
||||||
|
icon: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
location.add({
|
||||||
|
url: "/admin/org/profile/" + id,
|
||||||
|
title: "Perfil",
|
||||||
|
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
|
||||||
|
});
|
||||||
|
|
||||||
|
location.add({
|
||||||
|
url: "/admin/org/profile/" + id + "/fiscal",
|
||||||
|
title: "Datos fiscales",
|
||||||
|
icon: <ReceiptLongOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!SessionInfo.userId) return;
|
||||||
|
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
findOrganizationFiscalProfiles({ companyId: id, sessionUser: SessionInfo.userId }),
|
||||||
|
findArcaActivities(),
|
||||||
|
])
|
||||||
|
.then(([profiles, activities]) => {
|
||||||
|
setArcaActivities(activities);
|
||||||
|
|
||||||
|
const profile = profiles[0];
|
||||||
|
if (!profile) return;
|
||||||
|
|
||||||
|
setCuit(profile.cuit || "");
|
||||||
|
setLegalName(profile.legalName || "");
|
||||||
|
setTaxCondition(profile.taxCondition || "MONOTRIBUTO");
|
||||||
|
setPointOfSale(profile.pointOfSale ? String(profile.pointOfSale) : "1");
|
||||||
|
const profileActivity = activities.find((activity) => activity.id === profile.activityCode);
|
||||||
|
setActivityCode(profile.activityCode || "");
|
||||||
|
setActivityDescription(profile.activityDescription || profileActivity?.name || "");
|
||||||
|
})
|
||||||
|
.catch((error: ApiError) => {
|
||||||
|
alert.showError(error.format());
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setFiscalDataLoaded(true);
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||||
|
});
|
||||||
|
}, [SessionInfo.userId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (eventHandler.eventType !== EVENT_TYPES.SAVE) return;
|
||||||
|
|
||||||
|
if (!/^\d{11}$/.test(cuit)) {
|
||||||
|
alert.showError("El CUIT debe contener 11 dígitos.");
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!legalName.trim()) {
|
||||||
|
alert.showError("La razón social es requerida.");
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number(pointOfSale) || Number(pointOfSale) <= 0) {
|
||||||
|
alert.showError("El punto de venta debe ser mayor a 0.");
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activityCode.trim() && !arcaActivities.some((activity) => activity.id === activityCode.trim())) {
|
||||||
|
alert.showError("Seleccioná una actividad ARCA/CLAE válida.");
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.LOADING);
|
||||||
|
|
||||||
|
const selectedActivity = arcaActivities.find((activity) => activity.id === activityCode.trim());
|
||||||
|
|
||||||
|
upsertOrganizationFiscalProfile({
|
||||||
|
companyId: id,
|
||||||
|
cuit: cuit.trim(),
|
||||||
|
legalName: legalName.trim(),
|
||||||
|
taxCondition,
|
||||||
|
pointOfSale: Number(pointOfSale),
|
||||||
|
activityCode: activityCode.trim() || undefined,
|
||||||
|
activityDescription: selectedActivity?.name || activityDescription.trim() || undefined,
|
||||||
|
sessionUser: SessionInfo.userId,
|
||||||
|
})
|
||||||
|
.then((profile) => {
|
||||||
|
const savedActivity = arcaActivities.find((activity) => activity.id === profile.activityCode);
|
||||||
|
setActivityCode(profile.activityCode || "");
|
||||||
|
setActivityDescription(profile.activityDescription || savedActivity?.name || "");
|
||||||
|
alert.showSuccess("Datos fiscales actualizados.");
|
||||||
|
})
|
||||||
|
.catch((error: ApiError) => {
|
||||||
|
alert.showError(error.format());
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
eventHandler.setEventType(EVENT_TYPES.SLEEP);
|
||||||
|
});
|
||||||
|
}, [eventHandler.eventType]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Location />
|
||||||
|
<div className={style.pageContainer}>
|
||||||
|
<section className={style.heroCard}>
|
||||||
|
<div className={style.heroIcon}>
|
||||||
|
<ReceiptLongOutlinedIcon />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2>Configuración fiscal</h2>
|
||||||
|
<p>
|
||||||
|
Estos datos se usan para emitir Factura C desde los movimientos de caja y para precargar el asistente ARCA.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={style.formSection}>
|
||||||
|
<h3>Datos del emisor</h3>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
<Grid2 size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Textbox
|
||||||
|
name="cuit"
|
||||||
|
placeholder="CUIT"
|
||||||
|
type="text"
|
||||||
|
value={cuit}
|
||||||
|
width="100%"
|
||||||
|
onChange={(event) => {
|
||||||
|
setCuit(event.target.value.replace(/\D/g, ""));
|
||||||
|
markNeedsSave();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
<Grid2 size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Textbox
|
||||||
|
name="legalName"
|
||||||
|
placeholder="Razón social"
|
||||||
|
type="text"
|
||||||
|
value={legalName}
|
||||||
|
width="100%"
|
||||||
|
onChange={(event) => {
|
||||||
|
setLegalName(event.target.value);
|
||||||
|
markNeedsSave();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={style.formSection}>
|
||||||
|
<div className={style.sectionHeader}>
|
||||||
|
<h3>Condición fiscal</h3>
|
||||||
|
<p>Seleccioná la condición declarada para esta organización.</p>
|
||||||
|
</div>
|
||||||
|
<div className={style.optionGroup} role="radiogroup" aria-label="Condición fiscal">
|
||||||
|
{fiscalConditionOptions.map((option) => {
|
||||||
|
const selected = taxCondition === option.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={option.id}
|
||||||
|
type="button"
|
||||||
|
className={`${style.optionCard} ${selected ? style.optionCardSelected : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setTaxCondition(option.id);
|
||||||
|
markNeedsSave();
|
||||||
|
}}
|
||||||
|
aria-pressed={selected}
|
||||||
|
>
|
||||||
|
<span className={style.optionTitle}>{option.title}</span>
|
||||||
|
<span className={style.optionDescription}>{option.description}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section className={style.formSection}>
|
||||||
|
<h3>Facturación ARCA</h3>
|
||||||
|
<Grid2 container spacing={2}>
|
||||||
|
<Grid2 size={{ xs: 12, sm: 6 }}>
|
||||||
|
<Textbox
|
||||||
|
name="pointOfSale"
|
||||||
|
placeholder="Punto de venta"
|
||||||
|
type="text"
|
||||||
|
value={pointOfSale}
|
||||||
|
width="100%"
|
||||||
|
onChange={(event) => {
|
||||||
|
setPointOfSale(event.target.value.replace(/\D/g, ""));
|
||||||
|
markNeedsSave();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid2>
|
||||||
|
</Grid2>
|
||||||
|
|
||||||
|
{fiscalDataLoaded && (
|
||||||
|
<TextObjectFilter
|
||||||
|
key={`${activityCode}-${arcaActivities.length}`}
|
||||||
|
placeholder="Actividad ARCA/CLAE"
|
||||||
|
content={activityFilterContent}
|
||||||
|
data={arcaActivities}
|
||||||
|
selectedId={activityCode}
|
||||||
|
onChange={(selectedActivityCode) => {
|
||||||
|
const selectedActivity = arcaActivities.find(
|
||||||
|
(activity) => activity.id === selectedActivityCode
|
||||||
|
);
|
||||||
|
|
||||||
|
setActivityCode(selectedActivity?.id || "");
|
||||||
|
setActivityDescription(selectedActivity?.name || "");
|
||||||
|
markNeedsSave();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className={style.helpText}>
|
||||||
|
{activityCode && activityDescription
|
||||||
|
? `${activityCode} - ${activityDescription}`
|
||||||
|
: "Seleccioná la actividad principal declarada en ARCA."}
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import PendingActionsOutlinedIcon from "@mui/icons-material/PendingActionsOutlin
|
|||||||
import TodayOutlinedIcon from "@mui/icons-material/TodayOutlined";
|
import TodayOutlinedIcon from "@mui/icons-material/TodayOutlined";
|
||||||
import PaymentsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
|
import PaymentsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
|
||||||
import CashIcon from "@mui/icons-material/AccountBalanceOutlined";
|
import CashIcon from "@mui/icons-material/AccountBalanceOutlined";
|
||||||
|
import FiscalIcon from "@mui/icons-material/ReceiptLongOutlined";
|
||||||
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
|
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
|
||||||
import TemplatesIcon from "@mui/icons-material/SpeakerNotesOutlined";
|
import TemplatesIcon from "@mui/icons-material/SpeakerNotesOutlined";
|
||||||
import AssistanceIcon from "@mui/icons-material/HeadsetMicOutlined";
|
import AssistanceIcon from "@mui/icons-material/HeadsetMicOutlined";
|
||||||
@@ -137,6 +138,11 @@ export default function OrganizationProfile() {
|
|||||||
text: "Caja",
|
text: "Caja",
|
||||||
onClick: () => goTo("/admin/org/profile/" + id + "/cash"),
|
onClick: () => goTo("/admin/org/profile/" + id + "/cash"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: <FiscalIcon />,
|
||||||
|
text: "Datos fiscales",
|
||||||
|
onClick: () => goTo("/admin/org/profile/" + id + "/fiscal"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: <DiscountsIcon />,
|
icon: <DiscountsIcon />,
|
||||||
text: "Descuentos",
|
text: "Descuentos",
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
|||||||
const [arcaLegalName, setArcaLegalName] = useState("");
|
const [arcaLegalName, setArcaLegalName] = useState("");
|
||||||
const [arcaTaxCondition, setArcaTaxCondition] = useState<"MONOTRIBUTO" | "EXENTO">("MONOTRIBUTO");
|
const [arcaTaxCondition, setArcaTaxCondition] = useState<"MONOTRIBUTO" | "EXENTO">("MONOTRIBUTO");
|
||||||
const [arcaPointOfSale, setArcaPointOfSale] = useState("1");
|
const [arcaPointOfSale, setArcaPointOfSale] = useState("1");
|
||||||
|
const [arcaActivityCode, setArcaActivityCode] = useState("");
|
||||||
|
const [arcaActivityDescription, setArcaActivityDescription] = useState("");
|
||||||
const [arcaCsrPem, setArcaCsrPem] = useState("");
|
const [arcaCsrPem, setArcaCsrPem] = useState("");
|
||||||
const [arcaCertificatePem, setArcaCertificatePem] = useState("");
|
const [arcaCertificatePem, setArcaCertificatePem] = useState("");
|
||||||
const [arcaStatusLabel, setArcaStatusLabel] = useState("");
|
const [arcaStatusLabel, setArcaStatusLabel] = useState("");
|
||||||
@@ -424,6 +426,8 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
|||||||
setArcaLegalName(profile.legalName || "");
|
setArcaLegalName(profile.legalName || "");
|
||||||
setArcaTaxCondition(profile.taxCondition || "MONOTRIBUTO");
|
setArcaTaxCondition(profile.taxCondition || "MONOTRIBUTO");
|
||||||
setArcaPointOfSale(profile.pointOfSale ? String(profile.pointOfSale) : "1");
|
setArcaPointOfSale(profile.pointOfSale ? String(profile.pointOfSale) : "1");
|
||||||
|
setArcaActivityCode(profile.activityCode || "");
|
||||||
|
setArcaActivityDescription(profile.activityDescription || "");
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
API.post<any[]>("arca-credentials/find", {
|
API.post<any[]>("arca-credentials/find", {
|
||||||
companyId: org.id || org._id,
|
companyId: org.id || org._id,
|
||||||
@@ -825,6 +829,8 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
|||||||
legalName: arcaLegalName,
|
legalName: arcaLegalName,
|
||||||
taxCondition: arcaTaxCondition,
|
taxCondition: arcaTaxCondition,
|
||||||
pointOfSale: Number(arcaPointOfSale),
|
pointOfSale: Number(arcaPointOfSale),
|
||||||
|
activityCode: arcaActivityCode.trim() || undefined,
|
||||||
|
activityDescription: arcaActivityDescription.trim() || undefined,
|
||||||
sessionUser: SessionInfo.userId
|
sessionUser: SessionInfo.userId
|
||||||
});
|
});
|
||||||
handleNext();
|
handleNext();
|
||||||
@@ -968,6 +974,10 @@ export default function StepEngine({ flowId, action, initialCompanyId, initialEm
|
|||||||
setTaxCondition={setArcaTaxCondition}
|
setTaxCondition={setArcaTaxCondition}
|
||||||
pointOfSale={arcaPointOfSale}
|
pointOfSale={arcaPointOfSale}
|
||||||
setPointOfSale={setArcaPointOfSale}
|
setPointOfSale={setArcaPointOfSale}
|
||||||
|
activityCode={arcaActivityCode}
|
||||||
|
setActivityCode={setArcaActivityCode}
|
||||||
|
activityDescription={arcaActivityDescription}
|
||||||
|
setActivityDescription={setArcaActivityDescription}
|
||||||
csrPem={arcaCsrPem}
|
csrPem={arcaCsrPem}
|
||||||
certificatePem={arcaCertificatePem}
|
certificatePem={arcaCertificatePem}
|
||||||
setCertificatePem={setArcaCertificatePem}
|
setCertificatePem={setArcaCertificatePem}
|
||||||
|
|||||||
+128
-4
@@ -1,10 +1,17 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
|
import ReceiptLongIcon from "@mui/icons-material/ReceiptLong";
|
||||||
import QuestionCard from "../../QuestionCard/QuestionCard";
|
import QuestionCard from "../../QuestionCard/QuestionCard";
|
||||||
import QuestionInput from "../../QuestionCard/QuestionInput";
|
import QuestionInput from "../../QuestionCard/QuestionInput";
|
||||||
|
import API from "@services/Api.Service";
|
||||||
|
|
||||||
type ArcaTaxCondition = "MONOTRIBUTO" | "EXENTO";
|
type ArcaTaxCondition = "MONOTRIBUTO" | "EXENTO";
|
||||||
|
|
||||||
|
type ArcaActivityOption = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentStepIndex: number;
|
currentStepIndex: number;
|
||||||
cuit: string;
|
cuit: string;
|
||||||
@@ -15,6 +22,10 @@ type Props = {
|
|||||||
setTaxCondition: (value: ArcaTaxCondition) => void;
|
setTaxCondition: (value: ArcaTaxCondition) => void;
|
||||||
pointOfSale: string;
|
pointOfSale: string;
|
||||||
setPointOfSale: (value: string) => void;
|
setPointOfSale: (value: string) => void;
|
||||||
|
activityCode: string;
|
||||||
|
setActivityCode: (value: string) => void;
|
||||||
|
activityDescription: string;
|
||||||
|
setActivityDescription: (value: string) => void;
|
||||||
csrPem: string;
|
csrPem: string;
|
||||||
certificatePem: string;
|
certificatePem: string;
|
||||||
setCertificatePem: (value: string) => void;
|
setCertificatePem: (value: string) => void;
|
||||||
@@ -32,9 +43,74 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function ArcaOnboardingFlow(props: Props) {
|
export default function ArcaOnboardingFlow(props: Props) {
|
||||||
const { currentStepIndex, cuit, setCuit, legalName, setLegalName, taxCondition, setTaxCondition, pointOfSale, setPointOfSale, csrPem, certificatePem, setCertificatePem, statusLabel, arcaLoading, arcaError, handleNext, handleSaveFiscalProfile, handleGenerateCsr, handleDownloadCsr, handleCertificateFile, handleUploadCertificate, handleEditFiscalProfile, handleFinish } = props;
|
const { currentStepIndex, cuit, setCuit, legalName, setLegalName, taxCondition, setTaxCondition, pointOfSale, setPointOfSale, activityCode, setActivityCode, activityDescription, setActivityDescription, csrPem, certificatePem, setCertificatePem, statusLabel, arcaLoading, arcaError, handleNext, handleSaveFiscalProfile, handleGenerateCsr, handleDownloadCsr, handleCertificateFile, handleUploadCertificate, handleEditFiscalProfile, handleFinish } = props;
|
||||||
const canSaveFiscalProfile = cuit.trim().length >= 11 && legalName.trim().length >= 3 && Number(pointOfSale) > 0;
|
const canSaveFiscalProfile = cuit.trim().length >= 11 && legalName.trim().length >= 3 && Number(pointOfSale) > 0 && /^\d{6}$/.test(activityCode.trim()) && activityDescription.trim().length > 0;
|
||||||
const [csrCopyStatus, setCsrCopyStatus] = useState<string | null>(null);
|
const [csrCopyStatus, setCsrCopyStatus] = useState<string | null>(null);
|
||||||
|
const [activities, setActivities] = useState<ArcaActivityOption[]>([]);
|
||||||
|
const [activitySearch, setActivitySearch] = useState("");
|
||||||
|
const [activitiesLoading, setActivitiesLoading] = useState(false);
|
||||||
|
const [activitiesLoaded, setActivitiesLoaded] = useState(false);
|
||||||
|
const [activitiesError, setActivitiesError] = useState<string | null>(null);
|
||||||
|
const [isActivityPickerOpen, setIsActivityPickerOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentStepIndex !== 1 || activitiesLoaded || activitiesLoading) return;
|
||||||
|
|
||||||
|
setActivitiesLoading(true);
|
||||||
|
setActivitiesError(null);
|
||||||
|
API.post<ArcaActivityOption[]>("arca-activities/filter-all", {})
|
||||||
|
.then((response) => {
|
||||||
|
setActivities(response || []);
|
||||||
|
setActivitiesLoaded(true);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error("Error loading ARCA activities:", error);
|
||||||
|
setActivitiesError("No pudimos cargar el catálogo de actividades. Volvé a intentar en unos segundos.");
|
||||||
|
})
|
||||||
|
.finally(() => setActivitiesLoading(false));
|
||||||
|
}, [activitiesLoaded, activitiesLoading, currentStepIndex]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activityCode && activityDescription && !activitySearch) {
|
||||||
|
setActivitySearch(`${activityCode} ${activityDescription}`);
|
||||||
|
}
|
||||||
|
}, [activityCode, activityDescription, activitySearch]);
|
||||||
|
|
||||||
|
const filteredActivities = useMemo(() => {
|
||||||
|
const normalizedSearch = normalizeActivityText(activitySearch);
|
||||||
|
const selectedCode = activityCode.trim();
|
||||||
|
|
||||||
|
if (!normalizedSearch) {
|
||||||
|
return activities.slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedActivity = activities.find((activity) => activity.id === selectedCode);
|
||||||
|
const matches = activities.filter((activity) => {
|
||||||
|
const searchableText = normalizeActivityText(`${activity.id} ${activity.name} ${activity.description || ""}`);
|
||||||
|
return normalizedSearch.split(" ").every((term) => searchableText.includes(term));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selectedActivity && !matches.some((activity) => activity.id === selectedActivity.id)) {
|
||||||
|
return [selectedActivity, ...matches].slice(0, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches.slice(0, 8);
|
||||||
|
}, [activities, activityCode, activitySearch]);
|
||||||
|
|
||||||
|
const handleActivitySearchChange = (value: string) => {
|
||||||
|
setActivitySearch(value);
|
||||||
|
if (activityCode || activityDescription) {
|
||||||
|
setActivityCode("");
|
||||||
|
setActivityDescription("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActivitySelect = (activity: ArcaActivityOption) => {
|
||||||
|
setActivityCode(activity.id);
|
||||||
|
setActivityDescription(activity.name);
|
||||||
|
setActivitySearch(`${activity.id} ${activity.name}`);
|
||||||
|
setIsActivityPickerOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleCopyCsr = async () => {
|
const handleCopyCsr = async () => {
|
||||||
if (!csrPem) return;
|
if (!csrPem) return;
|
||||||
@@ -59,9 +135,9 @@ export default function ArcaOnboardingFlow(props: Props) {
|
|||||||
<QuestionInput label="Razón social" value={legalName} onChange={(event) => setLegalName(event.target.value)} placeholder="Nombre fiscal o razón social" />
|
<QuestionInput label="Razón social" value={legalName} onChange={(event) => setLegalName(event.target.value)} placeholder="Nombre fiscal o razón social" />
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: "10px" }}>
|
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", gap: "10px" }}>
|
||||||
<OptionButton active={taxCondition === "MONOTRIBUTO"} onClick={() => setTaxCondition("MONOTRIBUTO")}>Monotributo</OptionButton>
|
<OptionButton active={taxCondition === "MONOTRIBUTO"} onClick={() => setTaxCondition("MONOTRIBUTO")}>Monotributo</OptionButton>
|
||||||
<OptionButton active={taxCondition === "EXENTO"} onClick={() => setTaxCondition("EXENTO")}>Exento</OptionButton>
|
|
||||||
</div>
|
</div>
|
||||||
<QuestionInput label="Punto de venta" value={pointOfSale} onChange={(event) => setPointOfSale(event.target.value.replace(/\D/g, ""))} placeholder="Ejemplo: 1" inputMode="numeric" />
|
<QuestionInput label="Punto de venta" value={pointOfSale} onChange={(event) => setPointOfSale(event.target.value.replace(/\D/g, ""))} placeholder="Ejemplo: 1" inputMode="numeric" />
|
||||||
|
<ActivityPicker search={activitySearch} selectedCode={activityCode} selectedDescription={activityDescription} activities={filteredActivities} loading={activitiesLoading} error={activitiesError} isOpen={isActivityPickerOpen} onOpen={() => setIsActivityPickerOpen(true)} onClose={() => setIsActivityPickerOpen(false)} onSearchChange={handleActivitySearchChange} onSelect={handleActivitySelect} />
|
||||||
<StatusMessage error={arcaError} />
|
<StatusMessage error={arcaError} />
|
||||||
</div>
|
</div>
|
||||||
</QuestionCard>
|
</QuestionCard>
|
||||||
@@ -122,6 +198,45 @@ function SecondaryButton({ onClick, children }: { onClick: () => void; children:
|
|||||||
return <button type="button" onClick={onClick} style={{ padding: "12px 16px", borderRadius: "10px", background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.16)", color: "white", cursor: "pointer", fontWeight: 700, fontFamily: "inherit" }}>{children}</button>;
|
return <button type="button" onClick={onClick} style={{ padding: "12px 16px", borderRadius: "10px", background: "rgba(255,255,255,0.06)", border: "1px solid rgba(255,255,255,0.16)", color: "white", cursor: "pointer", fontWeight: 700, fontFamily: "inherit" }}>{children}</button>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ActivityPicker({ search, selectedCode, selectedDescription, activities, loading, error, isOpen, onOpen, onClose, onSearchChange, onSelect }: { search: string; selectedCode: string; selectedDescription: string; activities: ArcaActivityOption[]; loading: boolean; error: string | null; isOpen: boolean; onOpen: () => void; onClose: () => void; onSearchChange: (value: string) => void; onSelect: (activity: ArcaActivityOption) => void; }) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
|
||||||
|
<label style={{ color: "rgba(255,255,255,0.82)", fontSize: "14px", fontWeight: 700 }}>Actividad ARCA/CLAE</label>
|
||||||
|
<button type="button" onClick={onOpen} style={{ width: "100%", textAlign: "left", padding: "12px 16px", borderRadius: "10px", border: selectedCode ? "1px solid var(--wine-red)" : "1px solid rgba(255,255,255,0.16)", background: selectedCode ? "rgba(255,42,127,0.16)" : "rgba(255,255,255,0.05)", color: selectedCode ? "white" : "rgba(255,255,255,0.58)", cursor: "pointer", fontWeight: 700, fontFamily: "inherit", lineHeight: 1.35 }}>
|
||||||
|
{selectedCode ? selectedDescription : "Seleccionar actividad"}
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div role="dialog" aria-modal="true" aria-label="Buscar actividad ARCA/CLAE" style={{ position: "fixed", inset: 0, zIndex: 3000, display: "flex", alignItems: "center", justifyContent: "center", padding: "18px", background: "rgba(0,0,0,0.62)" }} onClick={onClose}>
|
||||||
|
<div style={{ width: "min(620px, 100%)", maxHeight: "min(760px, 88vh)", display: "flex", flexDirection: "column", padding: "18px", borderRadius: "12px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", boxShadow: "0 28px 80px rgba(0,0,0,0.45)", backdropFilter: "blur(12px)" }} onClick={(event) => event.stopPropagation()}>
|
||||||
|
<div style={{ display: "flex", justifyContent: "space-between", gap: "14px", alignItems: "flex-start", marginBottom: "14px" }}>
|
||||||
|
<div>
|
||||||
|
<h3 style={{ margin: 0, color: "white", fontSize: "20px" }}>Buscar actividad ARCA/CLAE</h3>
|
||||||
|
<p style={{ margin: "6px 0 0", color: "rgba(255,255,255,0.62)", fontSize: "13px", lineHeight: 1.4 }}>Buscá por código o nombre y seleccioná la actividad fiscal de la organización.</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={onClose} aria-label="Cerrar buscador" style={{ width: "36px", height: "36px", borderRadius: "10px", border: "1px solid rgba(255,255,255,0.16)", background: "rgba(255,255,255,0.05)", color: "white", cursor: "pointer", fontSize: "18px" }}>×</button>
|
||||||
|
</div>
|
||||||
|
<input autoFocus value={search} onChange={(event) => onSearchChange(event.target.value)} placeholder="Ejemplo: 855000 o educación" style={{ width: "100%", boxSizing: "border-box", borderRadius: "12px", border: "1px solid rgba(255,255,255,0.16)", background: "rgba(255,255,255,0.05)", color: "white", padding: "14px 16px", fontFamily: "inherit", outline: "none" }} />
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: "8px", marginTop: "14px", overflowY: "auto", paddingRight: "2px" }}>
|
||||||
|
{loading && <InfoPanel>Cargando actividades ARCA...</InfoPanel>}
|
||||||
|
{error && <StatusMessage error={error} />}
|
||||||
|
{!loading && !error && activities.length === 0 && <InfoPanel>No encontramos actividades para esa búsqueda.</InfoPanel>}
|
||||||
|
{!loading && !error && activities.map((activity) => {
|
||||||
|
const isSelected = activity.id === selectedCode;
|
||||||
|
return (
|
||||||
|
<button key={activity.id} type="button" onClick={() => onSelect(activity)} style={{ display: "grid", gridTemplateColumns: "78px 1fr", gap: "12px", textAlign: "left", padding: "13px", borderRadius: "12px", border: isSelected ? "1px solid var(--wine-red)" : "1px solid rgba(255,255,255,0.12)", background: isSelected ? "rgba(255,42,127,0.16)" : "rgba(255,255,255,0.05)", color: "white", cursor: "pointer", fontFamily: "inherit" }}>
|
||||||
|
<strong style={{ fontSize: "13px" }}>{activity.id}</strong>
|
||||||
|
<span style={{ color: "rgba(255,255,255,0.78)", fontSize: "13px", lineHeight: 1.35 }}>{activity.name}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function StepItem({ number, children }: { number: string; children: React.ReactNode }) {
|
function StepItem({ number, children }: { number: string; children: React.ReactNode }) {
|
||||||
return <div style={{ display: "flex", gap: "12px", alignItems: "flex-start", padding: "14px", borderRadius: "12px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.78)", lineHeight: 1.5 }}><strong style={{ color: "white" }}>{number}.</strong><span>{children}</span></div>;
|
return <div style={{ display: "flex", gap: "12px", alignItems: "flex-start", padding: "14px", borderRadius: "12px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.12)", color: "rgba(255,255,255,0.78)", lineHeight: 1.5 }}><strong style={{ color: "white" }}>{number}.</strong><span>{children}</span></div>;
|
||||||
}
|
}
|
||||||
@@ -130,3 +245,12 @@ function StatusMessage({ error }: { error: string | null }) {
|
|||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
return <div style={{ padding: "12px 14px", borderRadius: "10px", background: "rgba(255,60,60,0.1)", border: "1px solid rgba(255,60,60,0.3)", color: "#ff9999", fontSize: "13px", lineHeight: 1.4 }}>{error}</div>;
|
return <div style={{ padding: "12px 14px", borderRadius: "10px", background: "rgba(255,60,60,0.1)", border: "1px solid rgba(255,60,60,0.3)", color: "#ff9999", fontSize: "13px", lineHeight: 1.4 }}>{error}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeActivityText(value: string) {
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.normalize("NFD")
|
||||||
|
.replace(/[\u0300-\u036f]/g, "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user