feat: enhance issued invoice detail sections with new components and data structures
- Updated GetIssuedInvoiceByIdResult to include verifactu as nullable. - Introduced new fiscal classification and verifactu policy interfaces in issued-invoice.entity.ts. - Added new UI components for displaying issued invoice details, including sections for document, fiscal, items, payment, recipient, summary, taxes, totals, and verifactu. - Implemented skeleton loading state for issued invoice detail page. - Created tests for issued invoice detail page to ensure proper rendering and error handling. - Updated pnpm workspace configuration to disallow builds for specific packages.
This commit is contained in:
parent
7becfd31c0
commit
9c1f98df0c
@ -796,48 +796,108 @@
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"summary": "Summary",
|
||||
"customer": "Customer",
|
||||
"document": "Document details",
|
||||
"commercial": "Commercial terms",
|
||||
"fiscal": "Tax",
|
||||
"items": "Line items",
|
||||
"taxes": "Taxes",
|
||||
"totals": "Totals",
|
||||
"verifactu": "VeriFactu",
|
||||
"linked_proforma": "Linked proforma",
|
||||
"content": "Content"
|
||||
},
|
||||
"section_descriptions": {
|
||||
"summary": "Identity and primary data of the issued document.",
|
||||
"customer": "Historical recipient snapshot materialized in the invoice.",
|
||||
"document": "Document metadata and navigation to the source proforma when available.",
|
||||
"commercial": "Payment method, payment term and due rules preserved in the snapshot.",
|
||||
"fiscal": "Historical tax data available in the issued invoice.",
|
||||
"items": "Materialized line items from the document, without frontend recalculation.",
|
||||
"taxes": "Materialized tax breakdown returned by the API.",
|
||||
"totals": "Historical totals of the issued invoice.",
|
||||
"verifactu": "Current VeriFactu state from the singular block available today."
|
||||
},
|
||||
"fields": {
|
||||
"invoice_reference": "Invoice number",
|
||||
"invoice_number": "Sequential number",
|
||||
"series": "Series",
|
||||
"status": "Status",
|
||||
"invoice_date": "Invoice date",
|
||||
"operation_date": "Operation date",
|
||||
"line_count": "Lines",
|
||||
"total_units": "Units",
|
||||
"currency": "Currency",
|
||||
"total_amount": "Total amount",
|
||||
"customer_name": "Customer",
|
||||
"customer_tin": "TIN",
|
||||
"address": "Address",
|
||||
"payment_method": "Payment method",
|
||||
"payment_method_description": "Payment method description",
|
||||
"payment_term": "Payment term",
|
||||
"payment_term_description": "Payment term description",
|
||||
"payment_term_due_rules": "Due rules",
|
||||
"language": "Language",
|
||||
"internal_reference": "Internal reference",
|
||||
"tax_regime": "Tax regime",
|
||||
"fiscal_tax_regime": "Historical tax regime",
|
||||
"operation_qualification": "Operation qualification",
|
||||
"exemption_reason": "Exemption reason",
|
||||
"verifactu_profile_status": "VeriFactu status",
|
||||
"verifactu_policy": "VeriFactu policy",
|
||||
"verifactu_effective_at": "VeriFactu effective date",
|
||||
"subtotal_amount": "Subtotal",
|
||||
"items_discount_amount": "Item discount",
|
||||
"global_discount_amount": "Global discount",
|
||||
"discount_amount": "Discount",
|
||||
"taxable_amount": "Taxable amount",
|
||||
"tax_amount": "Tax amount",
|
||||
"taxes_amount": "Total taxes",
|
||||
"rec_amount": "Equivalence surcharge",
|
||||
"retention_amount": "Withholding",
|
||||
"tax_breakdown": "Tax breakdown",
|
||||
"verifactu_status": "VeriFactu status",
|
||||
"verifactu_url": "VeriFactu URL",
|
||||
"verifactu_qr": "VeriFactu QR",
|
||||
"linked_proforma": "Linked proforma",
|
||||
"description": "Description",
|
||||
"notes": "Notes"
|
||||
"notes": "Notes",
|
||||
"quantity": "Quantity",
|
||||
"unit_amount": "Unit price",
|
||||
"item_discount": "Line discount",
|
||||
"item_taxes": "Line taxes"
|
||||
},
|
||||
"empty": {
|
||||
"not_available": "—",
|
||||
"no_address": "No address recorded.",
|
||||
"no_description": "No description.",
|
||||
"no_taxes": "No taxes reported."
|
||||
},
|
||||
"actions": {
|
||||
"back": "Back",
|
||||
"open_linked_proforma": "Open linked proforma",
|
||||
"operation_in_progress": "The operation is in progress."
|
||||
},
|
||||
"payment_term": {
|
||||
"due_days": "{{days}} days"
|
||||
},
|
||||
"items": {
|
||||
"position": "Line {{position}}"
|
||||
},
|
||||
"verifactu": {
|
||||
"empty": "This invoice has no VeriFactu block available in the current detail view.",
|
||||
"qr_alt": "VeriFactu QR code",
|
||||
"available": "Available",
|
||||
"uses_verifactu": "Uses VeriFactu",
|
||||
"does_not_use_verifactu": "Does not use VeriFactu",
|
||||
"policy_status": {
|
||||
"not_configured": "Not configured",
|
||||
"exempt": "Exempt or not applicable",
|
||||
"obligated_without_verifactu": "Obligated without VeriFactu",
|
||||
"obligated_with_verifactu": "Obligated with VeriFactu"
|
||||
}
|
||||
},
|
||||
"fiscal": {
|
||||
"unknown_tax_code": "Tax"
|
||||
}
|
||||
|
||||
@ -707,13 +707,13 @@
|
||||
}
|
||||
},
|
||||
"grid_columns": {
|
||||
"series_invoice_number": "#",
|
||||
"invoice_reference": "Num.",
|
||||
"verifactu_status": "Estado",
|
||||
"verifactu_qr_code": "QR",
|
||||
"verifactu_url": "URL",
|
||||
"invoice_number": "Nº factura",
|
||||
"series": "Serie",
|
||||
"reference": "Reference",
|
||||
"reference": "Referencia",
|
||||
"invoice_date": "Fecha de factura",
|
||||
"operation_date": "Fecha de operación",
|
||||
"recipient": "Cliente",
|
||||
@ -797,48 +797,108 @@
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"summary": "Resumen",
|
||||
"customer": "Cliente",
|
||||
"document": "Datos del documento",
|
||||
"commercial": "Condiciones comerciales",
|
||||
"fiscal": "Fiscalidad",
|
||||
"items": "Líneas",
|
||||
"taxes": "Impuestos",
|
||||
"totals": "Totales",
|
||||
"verifactu": "VeriFactu",
|
||||
"linked_proforma": "Proforma vinculada",
|
||||
"content": "Contenido"
|
||||
},
|
||||
"section_descriptions": {
|
||||
"summary": "Identidad y datos principales del documento emitido.",
|
||||
"customer": "Snapshot histórico del destinatario materializado en la factura.",
|
||||
"document": "Metadatos del documento y navegación a la proforma origen cuando existe.",
|
||||
"commercial": "Forma, plazo y reglas de pago preservadas en el snapshot.",
|
||||
"fiscal": "Datos fiscales históricos disponibles en la factura emitida.",
|
||||
"items": "Líneas materializadas del documento, sin recálculo en frontend.",
|
||||
"taxes": "Desglose fiscal materializado recibido desde la API.",
|
||||
"totals": "Totales históricos de la factura emitida.",
|
||||
"verifactu": "Estado actual de VeriFactu según el bloque singular disponible hoy."
|
||||
},
|
||||
"fields": {
|
||||
"invoice_reference": "Número de factura",
|
||||
"invoice_number": "Número correlativo",
|
||||
"series": "Serie",
|
||||
"status": "Estado",
|
||||
"invoice_date": "Fecha de factura",
|
||||
"operation_date": "Fecha de operación",
|
||||
"line_count": "Líneas",
|
||||
"total_units": "Unidades",
|
||||
"currency": "Moneda",
|
||||
"total_amount": "Importe total",
|
||||
"customer_name": "Cliente",
|
||||
"customer_tin": "NIF",
|
||||
"address": "Dirección",
|
||||
"payment_method": "Método de pago",
|
||||
"payment_method_description": "Descripción del método de pago",
|
||||
"payment_term": "Plazo de pago",
|
||||
"payment_term_description": "Descripción del plazo de pago",
|
||||
"payment_term_due_rules": "Reglas de vencimiento",
|
||||
"language": "Idioma",
|
||||
"internal_reference": "Referencia interna",
|
||||
"tax_regime": "Régimen fiscal",
|
||||
"fiscal_tax_regime": "Régimen fiscal histórico",
|
||||
"operation_qualification": "Calificación de operación",
|
||||
"exemption_reason": "Motivo de exención",
|
||||
"verifactu_profile_status": "Situación VeriFactu",
|
||||
"verifactu_policy": "Política VeriFactu",
|
||||
"verifactu_effective_at": "Fecha efectiva VeriFactu",
|
||||
"items_discount_amount": "Descuento de líneas",
|
||||
"global_discount_amount": "Descuento global",
|
||||
"subtotal_amount": "Subtotal",
|
||||
"discount_amount": "Descuento",
|
||||
"taxable_amount": "Base imponible",
|
||||
"tax_amount": "Impuestos",
|
||||
"taxes_amount": "Total impuestos",
|
||||
"rec_amount": "Recargo de equivalencia",
|
||||
"retention_amount": "Retención",
|
||||
"tax_breakdown": "Desglose fiscal",
|
||||
"verifactu_status": "Estado VeriFactu",
|
||||
"verifactu_url": "URL VeriFactu",
|
||||
"verifactu_qr": "QR VeriFactu",
|
||||
"linked_proforma": "Proforma vinculada",
|
||||
"description": "Descripción",
|
||||
"notes": "Notas"
|
||||
"notes": "Notas",
|
||||
"quantity": "Cantidad",
|
||||
"unit_amount": "Precio unitario",
|
||||
"item_discount": "Descuento de línea",
|
||||
"item_taxes": "Impuestos de línea"
|
||||
},
|
||||
"empty": {
|
||||
"not_available": "—",
|
||||
"no_address": "Sin dirección registrada.",
|
||||
"no_description": "Sin descripción.",
|
||||
"no_taxes": "Sin impuestos informados."
|
||||
},
|
||||
"actions": {
|
||||
"back": "Volver",
|
||||
"open_linked_proforma": "Abrir proforma vinculada",
|
||||
"operation_in_progress": "La operación está en curso."
|
||||
},
|
||||
"payment_term": {
|
||||
"due_days": "{{days}} días"
|
||||
},
|
||||
"items": {
|
||||
"position": "Línea {{position}}"
|
||||
},
|
||||
"verifactu": {
|
||||
"empty": "Esta factura no tiene un bloque VeriFactu disponible en el detalle actual.",
|
||||
"qr_alt": "Código QR de VeriFactu",
|
||||
"available": "Disponible",
|
||||
"uses_verifactu": "Usa VeriFactu",
|
||||
"does_not_use_verifactu": "No usa VeriFactu",
|
||||
"policy_status": {
|
||||
"not_configured": "No configurado",
|
||||
"exempt": "Exenta o no aplicable",
|
||||
"obligated_without_verifactu": "Obligada sin VeriFactu",
|
||||
"obligated_with_verifactu": "Obligada con VeriFactu"
|
||||
}
|
||||
},
|
||||
"fiscal": {
|
||||
"unknown_tax_code": "Impuesto"
|
||||
}
|
||||
@ -1135,4 +1195,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,6 +50,7 @@ export interface IssuedInvoiceDetailSummaryViewModel {
|
||||
totalLabel: string;
|
||||
documentReference: string;
|
||||
seriesLabel: string;
|
||||
invoiceNumberLabel: string;
|
||||
invoiceDateLabel: string;
|
||||
operationDateLabel: string | null;
|
||||
statusLabel: string;
|
||||
@ -64,12 +65,22 @@ export interface IssuedInvoiceDetailCustomerViewModel {
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailCommercialViewModel {
|
||||
paymentMethodLabel: string | null;
|
||||
paymentTermLabel: string | null;
|
||||
paymentMethodName: string | null;
|
||||
paymentMethodDescription: string | null;
|
||||
paymentTermName: string | null;
|
||||
paymentTermDescription: string | null;
|
||||
paymentTermDueRules: IssuedInvoiceDetailPaymentTermDueRuleViewModel[];
|
||||
languageLabel: string;
|
||||
currencyCode: string;
|
||||
internalReferenceLabel: string | null;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailPaymentTermDueRuleViewModel {
|
||||
key: string;
|
||||
dueDaysLabel: string;
|
||||
percentageLabel: string;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailFiscalBreakdownItemViewModel {
|
||||
key: string;
|
||||
taxLabel: string;
|
||||
@ -83,10 +94,15 @@ export interface IssuedInvoiceDetailFiscalBreakdownItemViewModel {
|
||||
|
||||
export interface IssuedInvoiceDetailFiscalViewModel {
|
||||
taxRegimeLabel: string;
|
||||
fiscalClassification: IssuedInvoiceDetailFiscalFieldViewModel[];
|
||||
verifactuPolicy: IssuedInvoiceDetailFiscalFieldViewModel[];
|
||||
subtotalLabel: string;
|
||||
itemsDiscountLabel: string | null;
|
||||
globalDiscountLabel: string | null;
|
||||
discountLabel: string;
|
||||
taxableAmountLabel: string;
|
||||
taxAmountLabel: string;
|
||||
taxesAmountLabel: string;
|
||||
recAmountLabel: string | null;
|
||||
retentionAmountLabel: string | null;
|
||||
totalAmountLabel: string;
|
||||
@ -95,15 +111,24 @@ export interface IssuedInvoiceDetailFiscalViewModel {
|
||||
usesRetention: boolean;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailFiscalFieldViewModel {
|
||||
key: string;
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailLinkedProformaViewModel {
|
||||
linkedProformaId: string | null;
|
||||
canOpenLinkedProforma: boolean;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailVerifactuViewModel {
|
||||
status: VerifactuRecordStatus;
|
||||
status: VerifactuRecordStatus | null;
|
||||
hasRecord: boolean;
|
||||
url: string | null;
|
||||
qrCode: string | null;
|
||||
qrValue: string | null;
|
||||
qrMode: "image" | "text" | "generated" | null;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailContentViewModel {
|
||||
@ -111,11 +136,54 @@ export interface IssuedInvoiceDetailContentViewModel {
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailItemViewModel {
|
||||
key: string;
|
||||
positionLabel: string;
|
||||
description: string;
|
||||
quantityLabel: string | null;
|
||||
unitAmountLabel: string | null;
|
||||
subtotalLabel: string;
|
||||
discountLabel: string | null;
|
||||
taxableAmountLabel: string;
|
||||
taxesLabel: string;
|
||||
totalAmountLabel: string;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailTaxBreakdownViewModel {
|
||||
key: string;
|
||||
taxLabel: string;
|
||||
taxableAmountLabel: string;
|
||||
taxAmountLabel: string;
|
||||
taxesAmountLabel: string;
|
||||
recLabel: string | null;
|
||||
recAmountLabel: string | null;
|
||||
retentionLabel: string | null;
|
||||
retentionAmountLabel: string | null;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailTotalsViewModel {
|
||||
subtotalLabel: string;
|
||||
itemsDiscountLabel: string | null;
|
||||
globalDiscountLabel: string | null;
|
||||
discountLabel: string;
|
||||
taxableAmountLabel: string;
|
||||
taxAmountLabel: string;
|
||||
taxesAmountLabel: string;
|
||||
recAmountLabel: string | null;
|
||||
retentionAmountLabel: string | null;
|
||||
totalAmountLabel: string;
|
||||
usesRec: boolean;
|
||||
usesRetention: boolean;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceDetailGeneralViewModel {
|
||||
summary: IssuedInvoiceDetailSummaryViewModel;
|
||||
customer: IssuedInvoiceDetailCustomerViewModel;
|
||||
commercial: IssuedInvoiceDetailCommercialViewModel;
|
||||
fiscal: IssuedInvoiceDetailFiscalViewModel;
|
||||
items: IssuedInvoiceDetailItemViewModel[];
|
||||
taxes: IssuedInvoiceDetailTaxBreakdownViewModel[];
|
||||
totals: IssuedInvoiceDetailTotalsViewModel;
|
||||
linkedProforma: IssuedInvoiceDetailLinkedProformaViewModel;
|
||||
verifactu: IssuedInvoiceDetailVerifactuViewModel;
|
||||
content: IssuedInvoiceDetailContentViewModel;
|
||||
|
||||
@ -2,8 +2,9 @@ import { Button } from "@repo/shadcn-ui/components";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { ProformaDetailField } from "../../../../proformas/detail/ui/components";
|
||||
import { VerifactuStatusBadge } from "../../../list/ui/components";
|
||||
import type { IssuedInvoiceDetailGeneralViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField } from "../components";
|
||||
|
||||
export interface IssuedInvoiceGeneralPanelProps {
|
||||
general: IssuedInvoiceDetailGeneralViewModel;
|
||||
@ -12,14 +13,69 @@ export interface IssuedInvoiceGeneralPanelProps {
|
||||
|
||||
const EMPTY_VALUE = "\u2014";
|
||||
|
||||
const buildPaymentTermLabel = (general: IssuedInvoiceDetailGeneralViewModel) => {
|
||||
const dueRulesSummary = general.commercial.paymentTermDueRules
|
||||
.map((rule) => `${rule.percentageLabel} · ${rule.dueDaysLabel}`)
|
||||
.join(" / ");
|
||||
|
||||
return [
|
||||
general.commercial.paymentTermName,
|
||||
general.commercial.paymentTermDescription,
|
||||
dueRulesSummary || null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" — ");
|
||||
};
|
||||
|
||||
export const IssuedInvoiceGeneralPanel = ({
|
||||
general,
|
||||
onOpenLinkedProforma,
|
||||
}: IssuedInvoiceGeneralPanelProps) => {
|
||||
const { t } = useTranslation();
|
||||
const paymentTermLabel = buildPaymentTermLabel(general);
|
||||
|
||||
return (
|
||||
<div className="space-y-5 px-4 py-4 sm:px-5">
|
||||
<div className="space-y-2 border rounded-lg bg-muted/10 px-4 py-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("pages.issued_invoices.detail.sections.verifactu")}
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_status")}
|
||||
value={
|
||||
general.verifactu.hasRecord && general.verifactu.status ? (
|
||||
<VerifactuStatusBadge className="sm:ml-auto" status={general.verifactu.status} />
|
||||
) : (
|
||||
EMPTY_VALUE
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_url")}
|
||||
value={
|
||||
general.verifactu.url ? (
|
||||
<a
|
||||
className="break-all text-primary underline underline-offset-2"
|
||||
href={general.verifactu.url}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
{general.verifactu.url}
|
||||
</a>
|
||||
) : (
|
||||
EMPTY_VALUE
|
||||
)
|
||||
}
|
||||
valueClassName="sm:text-left"
|
||||
/>
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_qr")}
|
||||
value={general.verifactu.qrValue ? t("pages.issued_invoices.detail.verifactu.available") : EMPTY_VALUE}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-3xl font-semibold tracking-tight text-foreground tabular-nums">
|
||||
{general.summary.totalLabel}
|
||||
@ -32,42 +88,35 @@ export const IssuedInvoiceGeneralPanel = ({
|
||||
<div className="border-t" />
|
||||
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.invoice_reference")}
|
||||
value={general.summary.documentReference}
|
||||
valueClassName="font-mono font-medium text-foreground sm:text-right"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.status")}
|
||||
value={general.summary.statusLabel}
|
||||
valueClassName="sm:text-right"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.invoice_date")}
|
||||
value={general.summary.invoiceDateLabel}
|
||||
valueClassName="sm:text-right"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.operation_date")}
|
||||
value={general.summary.operationDateLabel ?? EMPTY_VALUE}
|
||||
valueClassName="sm:text-right"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.internal_reference")}
|
||||
value={general.commercial.internalReferenceLabel ?? EMPTY_VALUE}
|
||||
valueClassName="sm:text-right"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_method")}
|
||||
value={general.commercial.paymentMethodLabel ?? EMPTY_VALUE}
|
||||
valueClassName="sm:text-right"
|
||||
value={general.commercial.paymentMethodName ?? EMPTY_VALUE}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_term")}
|
||||
value={general.commercial.paymentTermLabel ?? EMPTY_VALUE}
|
||||
valueClassName="sm:text-right"
|
||||
value={paymentTermLabel || EMPTY_VALUE}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
<ProformaDetailField
|
||||
label={t("pages.issued_invoices.detail.sections.linked_proforma")}
|
||||
value={
|
||||
general.linkedProforma.canOpenLinkedProforma ? (
|
||||
|
||||
@ -1 +1,3 @@
|
||||
export * from './issued-invoice-detail-field';
|
||||
export * from "./issued-invoice-detail-field";
|
||||
export * from "./issued-invoice-detail-section";
|
||||
export * from "./issued-invoice-detail-skeleton";
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
import { Separator } from "@repo/shadcn-ui/components";
|
||||
import type { ReactNode } from "react";
|
||||
import { useId } from "react";
|
||||
|
||||
export interface IssuedInvoiceDetailSectionProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
action?: ReactNode;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const IssuedInvoiceDetailSection = ({
|
||||
title,
|
||||
description,
|
||||
action,
|
||||
children,
|
||||
}: IssuedInvoiceDetailSectionProps) => {
|
||||
const headingId = useId();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold tracking-tight text-foreground" id={headingId}>
|
||||
{title}
|
||||
</h3>
|
||||
{description ? (
|
||||
<p className="text-sm leading-5 text-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{action ? <div className="shrink-0">{action}</div> : null}
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">{children}</div>
|
||||
<Separator />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { Skeleton } from "@repo/shadcn-ui/components";
|
||||
|
||||
export const IssuedInvoiceDetailSkeleton = () => {
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col overflow-hidden bg-muted/30">
|
||||
<div className="shrink-0 border-b bg-background px-4 py-3 sm:px-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<Skeleton className="size-10 rounded-md" />
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-4 w-56" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-10 w-28" />
|
||||
<Skeleton className="h-10 w-28" />
|
||||
<Skeleton className="size-10 rounded-md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
<Skeleton className="h-7 w-24 rounded-full" />
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-4 w-40" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-1 overflow-hidden lg:grid-cols-[minmax(0,1fr)_340px] xl:grid-cols-[minmax(0,1fr)_380px]">
|
||||
<div className="border-b bg-muted/30 p-4 sm:p-6 lg:min-h-0 lg:border-r lg:border-b-0">
|
||||
<Skeleton className="h-full min-h-[32rem] w-full rounded-xl" />
|
||||
</div>
|
||||
|
||||
<div className="bg-background p-4 sm:p-6">
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-5 w-24" />
|
||||
<Skeleton className="h-px w-full" />
|
||||
<Skeleton className="h-28 w-full rounded-xl" />
|
||||
<Skeleton className="h-[42rem] w-full rounded-xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,374 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mock, test } from "node:test";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
const buildController = () => ({
|
||||
state: {
|
||||
isInitialLoading: false,
|
||||
isRefetching: false,
|
||||
pageError: null,
|
||||
},
|
||||
documentState: {
|
||||
isInitialLoading: false,
|
||||
isFetching: false,
|
||||
isError: false,
|
||||
errorMessage: null,
|
||||
},
|
||||
header: {
|
||||
title: "Factura A-12",
|
||||
documentReference: "A-12",
|
||||
customerName: "Acme Labs",
|
||||
status: "issued" as const,
|
||||
statusLabel: "Emitida",
|
||||
invoiceDateLabel: "10/08/2026",
|
||||
},
|
||||
document: {
|
||||
title: "Documento",
|
||||
description: "PDF",
|
||||
src: "blob:invoice",
|
||||
mode: "pdf" as const,
|
||||
capabilities: {
|
||||
canOpen: true,
|
||||
canDownload: true,
|
||||
canPrint: true,
|
||||
},
|
||||
},
|
||||
general: {
|
||||
summary: {
|
||||
totalLabel: "121,00 €",
|
||||
documentReference: "A-12",
|
||||
seriesLabel: "A",
|
||||
invoiceNumberLabel: "12",
|
||||
invoiceDateLabel: "10/08/2026",
|
||||
operationDateLabel: "09/08/2026",
|
||||
statusLabel: "Emitida",
|
||||
lineCount: 1,
|
||||
totalUnitsLabel: "2",
|
||||
},
|
||||
customer: {
|
||||
name: "Acme Labs",
|
||||
tin: "B12345678",
|
||||
addressLines: ["Calle Mayor 1", "28001 Madrid", "ES"],
|
||||
},
|
||||
commercial: {
|
||||
paymentMethodName: "Transferencia",
|
||||
paymentMethodDescription: "Transferencia bancaria",
|
||||
paymentTermName: "30 días",
|
||||
paymentTermDescription: "Pago estándar",
|
||||
paymentTermDueRules: [],
|
||||
languageLabel: "español",
|
||||
currencyCode: "EUR",
|
||||
internalReferenceLabel: "REF-CLI-12",
|
||||
},
|
||||
fiscal: {
|
||||
taxRegimeLabel: "Régimen general",
|
||||
fiscalClassification: [],
|
||||
verifactuPolicy: [],
|
||||
subtotalLabel: "100,00 €",
|
||||
itemsDiscountLabel: null,
|
||||
globalDiscountLabel: null,
|
||||
discountLabel: "0,00 €",
|
||||
taxableAmountLabel: "100,00 €",
|
||||
taxAmountLabel: "21,00 €",
|
||||
taxesAmountLabel: "21,00 €",
|
||||
recAmountLabel: null,
|
||||
retentionAmountLabel: null,
|
||||
totalAmountLabel: "121,00 €",
|
||||
breakdown: [],
|
||||
usesRec: false,
|
||||
usesRetention: false,
|
||||
},
|
||||
items: [],
|
||||
taxes: [],
|
||||
totals: {
|
||||
subtotalLabel: "100,00 €",
|
||||
itemsDiscountLabel: null,
|
||||
globalDiscountLabel: null,
|
||||
discountLabel: "0,00 €",
|
||||
taxableAmountLabel: "100,00 €",
|
||||
taxAmountLabel: "21,00 €",
|
||||
taxesAmountLabel: "21,00 €",
|
||||
recAmountLabel: null,
|
||||
retentionAmountLabel: null,
|
||||
totalAmountLabel: "121,00 €",
|
||||
usesRec: false,
|
||||
usesRetention: false,
|
||||
},
|
||||
linkedProforma: {
|
||||
linkedProformaId: "proforma-1",
|
||||
canOpenLinkedProforma: true,
|
||||
},
|
||||
verifactu: {
|
||||
hasRecord: false,
|
||||
status: null,
|
||||
url: null,
|
||||
qrCode: null,
|
||||
qrValue: null,
|
||||
qrMode: null as const,
|
||||
},
|
||||
content: {
|
||||
description: "Factura de prueba",
|
||||
notes: null,
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
items: [],
|
||||
},
|
||||
navigation: {
|
||||
navigateBack: () => undefined,
|
||||
openLinkedProforma: () => undefined,
|
||||
},
|
||||
handlers: {
|
||||
executeAction: () => undefined,
|
||||
},
|
||||
viewer: {
|
||||
handleDownload: async () => undefined,
|
||||
handleOpenPreviewPdf: async () => undefined,
|
||||
handlePrintPreview: () => undefined,
|
||||
handleRetryDocument: async () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
test("muestra el skeleton mientras la pantalla está cargando", async () => {
|
||||
let skeletonRendered = false;
|
||||
|
||||
mock.module("../../../../i18n", {
|
||||
namedExports: {
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../../controllers", {
|
||||
namedExports: {
|
||||
useIssuedInvoiceDetailPageController: () => ({
|
||||
...buildController(),
|
||||
state: {
|
||||
isInitialLoading: true,
|
||||
isRefetching: false,
|
||||
pageError: null,
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../blocks", {
|
||||
namedExports: {
|
||||
IssuedInvoiceDetailContent: () => null,
|
||||
IssuedInvoiceDetailHeader: () => null,
|
||||
IssuedInvoiceDetailInspector: () => null,
|
||||
IssuedInvoiceDocumentPane: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../components", {
|
||||
namedExports: {
|
||||
IssuedInvoiceDetailSkeleton: () => {
|
||||
skeletonRendered = true;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("@erp/core/components", {
|
||||
namedExports: {
|
||||
PageErrorState: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const { IssuedInvoiceDetailPage } = await import("./issued-invoice-detail-page.tsx");
|
||||
renderToStaticMarkup(<IssuedInvoiceDetailPage />);
|
||||
|
||||
assert.equal(skeletonRendered, true);
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
test("usa PageErrorState para el error inicial bloqueante", async () => {
|
||||
let capturedErrorProps:
|
||||
| {
|
||||
title?: string;
|
||||
variant?: string;
|
||||
reference?: string;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
mock.module("../../../../i18n", {
|
||||
namedExports: {
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../../controllers", {
|
||||
namedExports: {
|
||||
useIssuedInvoiceDetailPageController: () => ({
|
||||
...buildController(),
|
||||
state: {
|
||||
isInitialLoading: false,
|
||||
isRefetching: false,
|
||||
pageError: {
|
||||
variant: "server",
|
||||
title: "No se pudo cargar la factura",
|
||||
description: "Error",
|
||||
primaryAction: undefined,
|
||||
secondaryAction: undefined,
|
||||
reference: "corr-1",
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("@erp/core/components", {
|
||||
namedExports: {
|
||||
PageErrorState: (props: {
|
||||
title?: string;
|
||||
variant?: string;
|
||||
reference?: string;
|
||||
}) => {
|
||||
capturedErrorProps = props;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../blocks", {
|
||||
namedExports: {
|
||||
IssuedInvoiceDetailContent: () => null,
|
||||
IssuedInvoiceDetailHeader: () => null,
|
||||
IssuedInvoiceDetailInspector: () => null,
|
||||
IssuedInvoiceDocumentPane: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../components", {
|
||||
namedExports: {
|
||||
IssuedInvoiceDetailSkeleton: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
const { IssuedInvoiceDetailPage } = await import("./issued-invoice-detail-page.tsx");
|
||||
renderToStaticMarkup(<IssuedInvoiceDetailPage />);
|
||||
|
||||
assert.equal(capturedErrorProps?.title, "No se pudo cargar la factura");
|
||||
assert.equal(capturedErrorProps?.variant, "server");
|
||||
assert.equal(capturedErrorProps?.reference, "corr-1");
|
||||
mock.restoreAll();
|
||||
});
|
||||
|
||||
test("compone header, visor e inspector con el detalle emitido y la navegación a proforma", async () => {
|
||||
let capturedInspectorProps:
|
||||
| {
|
||||
general?: {
|
||||
linkedProforma?: {
|
||||
linkedProformaId?: string | null;
|
||||
};
|
||||
verifactu?: {
|
||||
hasRecord?: boolean;
|
||||
};
|
||||
};
|
||||
onOpenLinkedProforma?: () => void;
|
||||
}
|
||||
| undefined;
|
||||
let capturedHeaderProps:
|
||||
| {
|
||||
header?: {
|
||||
documentReference?: string;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
let capturedDocumentPaneProps:
|
||||
| {
|
||||
document?: {
|
||||
src?: string | null;
|
||||
};
|
||||
}
|
||||
| undefined;
|
||||
|
||||
mock.module("../../../../i18n", {
|
||||
namedExports: {
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../../controllers", {
|
||||
namedExports: {
|
||||
useIssuedInvoiceDetailPageController: () => buildController(),
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("@erp/core/components", {
|
||||
namedExports: {
|
||||
PageErrorState: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../components", {
|
||||
namedExports: {
|
||||
IssuedInvoiceDetailSkeleton: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
mock.module("../blocks", {
|
||||
namedExports: {
|
||||
IssuedInvoiceDetailHeader: (props: {
|
||||
header?: {
|
||||
documentReference?: string;
|
||||
};
|
||||
}) => {
|
||||
capturedHeaderProps = props;
|
||||
return null;
|
||||
},
|
||||
IssuedInvoiceDetailContent: ({
|
||||
documentPane,
|
||||
inspector,
|
||||
}: {
|
||||
documentPane: ReactNode;
|
||||
inspector: ReactNode;
|
||||
}) => (
|
||||
<>
|
||||
{documentPane}
|
||||
{inspector}
|
||||
</>
|
||||
),
|
||||
IssuedInvoiceDocumentPane: (props: {
|
||||
document?: {
|
||||
src?: string | null;
|
||||
};
|
||||
}) => {
|
||||
capturedDocumentPaneProps = props;
|
||||
return null;
|
||||
},
|
||||
IssuedInvoiceDetailInspector: (props: {
|
||||
general?: {
|
||||
linkedProforma?: {
|
||||
linkedProformaId?: string | null;
|
||||
};
|
||||
verifactu?: {
|
||||
hasRecord?: boolean;
|
||||
};
|
||||
};
|
||||
onOpenLinkedProforma?: () => void;
|
||||
}) => {
|
||||
capturedInspectorProps = props;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { IssuedInvoiceDetailPage } = await import("./issued-invoice-detail-page.tsx");
|
||||
renderToStaticMarkup(<IssuedInvoiceDetailPage />);
|
||||
|
||||
assert.equal(capturedHeaderProps?.header?.documentReference, "A-12");
|
||||
assert.equal(capturedDocumentPaneProps?.document?.src, "blob:invoice");
|
||||
assert.equal(capturedInspectorProps?.general?.linkedProforma?.linkedProformaId, "proforma-1");
|
||||
assert.equal(typeof capturedInspectorProps?.onOpenLinkedProforma, "function");
|
||||
assert.equal(capturedInspectorProps?.general?.verifactu?.hasRecord, false);
|
||||
mock.restoreAll();
|
||||
});
|
||||
@ -1,5 +1,4 @@
|
||||
import { PageErrorState } from "@erp/core/components";
|
||||
import { Spinner } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { useIssuedInvoiceDetailPageController } from "../../controllers";
|
||||
@ -9,20 +8,14 @@ import {
|
||||
IssuedInvoiceDetailInspector,
|
||||
IssuedInvoiceDocumentPane,
|
||||
} from "../blocks";
|
||||
import { IssuedInvoiceDetailSkeleton } from "../components";
|
||||
|
||||
export const IssuedInvoiceDetailPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const ctrl = useIssuedInvoiceDetailPageController();
|
||||
|
||||
if (ctrl.state.isInitialLoading) {
|
||||
return (
|
||||
<div className="flex h-full min-h-[24rem] items-center justify-center p-6">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<Spinner className="size-4" />
|
||||
<span>{t("pages.issued_invoices.detail.document.loading")}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
return <IssuedInvoiceDetailSkeleton />;
|
||||
}
|
||||
|
||||
if (ctrl.state.pageError) {
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
export * from "./issued-invoice-document-section";
|
||||
export * from "./issued-invoice-fiscal-section";
|
||||
export * from "./issued-invoice-items-section";
|
||||
export * from "./issued-invoice-payment-section";
|
||||
export * from "./issued-invoice-recipient-section";
|
||||
export * from "./issued-invoice-summary-section";
|
||||
export * from "./issued-invoice-taxes-section";
|
||||
export * from "./issued-invoice-totals-section";
|
||||
export * from "./issued-invoice-verifactu-section";
|
||||
@ -0,0 +1,71 @@
|
||||
import { Button } from "@repo/shadcn-ui/components";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type {
|
||||
IssuedInvoiceDetailCommercialViewModel,
|
||||
IssuedInvoiceDetailLinkedProformaViewModel,
|
||||
IssuedInvoiceDetailSummaryViewModel,
|
||||
} from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceDocumentSectionProps = {
|
||||
summary: IssuedInvoiceDetailSummaryViewModel;
|
||||
commercial: IssuedInvoiceDetailCommercialViewModel;
|
||||
linkedProforma: IssuedInvoiceDetailLinkedProformaViewModel;
|
||||
onOpenLinkedProforma: () => void;
|
||||
};
|
||||
|
||||
export const IssuedInvoiceDocumentSection = ({
|
||||
summary,
|
||||
commercial,
|
||||
linkedProforma,
|
||||
onOpenLinkedProforma,
|
||||
}: IssuedInvoiceDocumentSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.document")}
|
||||
title={t("pages.issued_invoices.detail.sections.document")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.series")}
|
||||
value={summary.seriesLabel}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.language")}
|
||||
value={commercial.languageLabel}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.currency")}
|
||||
value={commercial.currencyCode}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.internal_reference")}
|
||||
value={commercial.internalReferenceLabel ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.linked_proforma")}
|
||||
value={
|
||||
linkedProforma.canOpenLinkedProforma ? (
|
||||
<Button
|
||||
className="h-auto px-0 py-0 font-medium"
|
||||
onClick={onOpenLinkedProforma}
|
||||
type="button"
|
||||
variant="link"
|
||||
>
|
||||
<ExternalLinkIcon aria-hidden="true" className="mr-1 size-4" />
|
||||
{t("pages.issued_invoices.detail.actions.open_linked_proforma")}
|
||||
</Button>
|
||||
) : (
|
||||
t("pages.issued_invoices.detail.empty.not_available")
|
||||
)
|
||||
}
|
||||
valueClassName="sm:text-right"
|
||||
/>
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceDetailFiscalViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceFiscalSectionProps = {
|
||||
fiscal: IssuedInvoiceDetailFiscalViewModel;
|
||||
};
|
||||
|
||||
export const IssuedInvoiceFiscalSection = ({
|
||||
fiscal,
|
||||
}: IssuedInvoiceFiscalSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.fiscal")}
|
||||
title={t("pages.issued_invoices.detail.sections.fiscal")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.tax_regime")}
|
||||
value={fiscal.taxRegimeLabel}
|
||||
/>
|
||||
{fiscal.fiscalClassification.map((field) => (
|
||||
<IssuedInvoiceDetailField key={field.key} label={field.label} value={field.value} />
|
||||
))}
|
||||
{fiscal.verifactuPolicy.map((field) => (
|
||||
<IssuedInvoiceDetailField key={field.key} label={field.label} value={field.value} />
|
||||
))}
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,73 @@
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceDetailItemViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceItemsSectionProps = {
|
||||
items: IssuedInvoiceDetailItemViewModel[];
|
||||
};
|
||||
|
||||
export const IssuedInvoiceItemsSection = ({ items }: IssuedInvoiceItemsSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.items")}
|
||||
title={t("pages.issued_invoices.detail.sections.items")}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{items.map((item) => (
|
||||
<div className="space-y-3 rounded-lg border bg-muted/10 p-4" key={item.key}>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<p className="text-sm font-semibold text-foreground">
|
||||
{t("pages.issued_invoices.detail.items.position", {
|
||||
position: item.positionLabel,
|
||||
})}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">{item.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.quantity")}
|
||||
value={item.quantityLabel ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.unit_amount")}
|
||||
value={item.unitAmountLabel ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.subtotal_amount")}
|
||||
value={item.subtotalLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
{item.discountLabel ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.item_discount")}
|
||||
value={item.discountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.taxable_amount")}
|
||||
value={item.taxableAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.item_taxes")}
|
||||
value={item.taxesLabel}
|
||||
valueClassName="sm:text-left"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.total_amount")}
|
||||
value={item.totalAmountLabel}
|
||||
valueClassName="tabular-nums text-base"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,63 @@
|
||||
import { Badge } from "@repo/shadcn-ui/components";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceDetailCommercialViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoicePaymentSectionProps = {
|
||||
commercial: IssuedInvoiceDetailCommercialViewModel;
|
||||
};
|
||||
|
||||
export const IssuedInvoicePaymentSection = ({
|
||||
commercial,
|
||||
}: IssuedInvoicePaymentSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.commercial")}
|
||||
title={t("pages.issued_invoices.detail.sections.commercial")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_method")}
|
||||
value={commercial.paymentMethodName ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
/>
|
||||
{commercial.paymentMethodDescription ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_method_description")}
|
||||
value={commercial.paymentMethodDescription}
|
||||
valueClassName="sm:text-left"
|
||||
/>
|
||||
) : null}
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_term")}
|
||||
value={commercial.paymentTermName ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
/>
|
||||
{commercial.paymentTermDescription ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_term_description")}
|
||||
value={commercial.paymentTermDescription}
|
||||
valueClassName="sm:text-left"
|
||||
/>
|
||||
) : null}
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.payment_term_due_rules")}
|
||||
value={
|
||||
commercial.paymentTermDueRules.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-2 sm:justify-end">
|
||||
{commercial.paymentTermDueRules.map((rule) => (
|
||||
<Badge className="text-xs" key={rule.key} variant="secondary">
|
||||
{rule.percentageLabel} · {rule.dueDaysLabel}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
t("pages.issued_invoices.detail.empty.not_available")
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceDetailCustomerViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceRecipientSectionProps = {
|
||||
customer: IssuedInvoiceDetailCustomerViewModel;
|
||||
};
|
||||
|
||||
export const IssuedInvoiceRecipientSection = ({
|
||||
customer,
|
||||
}: IssuedInvoiceRecipientSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.customer")}
|
||||
title={t("pages.issued_invoices.detail.sections.customer")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.customer_name")}
|
||||
value={customer.name}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.customer_tin")}
|
||||
value={customer.tin ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.address")}
|
||||
value={
|
||||
customer.addressLines.length > 0 ? (
|
||||
<div className="space-y-1 whitespace-pre-wrap">
|
||||
{customer.addressLines.map((line) => (
|
||||
<p key={line}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
t("pages.issued_invoices.detail.empty.no_address")
|
||||
)
|
||||
}
|
||||
valueClassName="sm:text-left"
|
||||
/>
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,82 @@
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type {
|
||||
IssuedInvoiceDetailContentViewModel,
|
||||
IssuedInvoiceDetailSummaryViewModel,
|
||||
} from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceSummarySectionProps = {
|
||||
summary: IssuedInvoiceDetailSummaryViewModel;
|
||||
content: IssuedInvoiceDetailContentViewModel;
|
||||
};
|
||||
|
||||
export const IssuedInvoiceSummarySection = ({
|
||||
summary,
|
||||
content,
|
||||
}: IssuedInvoiceSummarySectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.summary")}
|
||||
title={t("pages.issued_invoices.detail.sections.summary")}
|
||||
>
|
||||
<div className="rounded-lg border bg-muted/20 px-4 py-4">
|
||||
<p className="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">
|
||||
{t("pages.issued_invoices.detail.fields.total_amount")}
|
||||
</p>
|
||||
<p className="mt-2 text-3xl font-semibold tracking-tight text-foreground tabular-nums">
|
||||
{summary.totalLabel}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.invoice_reference")}
|
||||
value={summary.documentReference}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.invoice_number")}
|
||||
value={summary.invoiceNumberLabel}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.status")}
|
||||
value={summary.statusLabel}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.invoice_date")}
|
||||
value={summary.invoiceDateLabel}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.operation_date")}
|
||||
value={summary.operationDateLabel ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.line_count")}
|
||||
value={String(summary.lineCount)}
|
||||
/>
|
||||
{summary.totalUnitsLabel ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.total_units")}
|
||||
value={summary.totalUnitsLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
{content.description ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.description")}
|
||||
value={content.description}
|
||||
valueClassName="whitespace-pre-wrap sm:text-left"
|
||||
/>
|
||||
) : null}
|
||||
{content.notes ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.notes")}
|
||||
value={content.notes}
|
||||
valueClassName="whitespace-pre-wrap sm:text-left"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceDetailTaxBreakdownViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceTaxesSectionProps = {
|
||||
taxes: IssuedInvoiceDetailTaxBreakdownViewModel[];
|
||||
};
|
||||
|
||||
export const IssuedInvoiceTaxesSection = ({ taxes }: IssuedInvoiceTaxesSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.taxes")}
|
||||
title={t("pages.issued_invoices.detail.sections.taxes")}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{taxes.map((item) => (
|
||||
<div className="space-y-3 rounded-lg border bg-muted/10 p-4" key={item.key}>
|
||||
<p className="text-sm font-semibold text-foreground">{item.taxLabel}</p>
|
||||
<div className="space-y-2">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.taxable_amount")}
|
||||
value={item.taxableAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.tax_amount")}
|
||||
value={item.taxAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.taxes_amount")}
|
||||
value={item.taxesAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
{item.recLabel && item.recAmountLabel ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={item.recLabel}
|
||||
value={item.recAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
{item.retentionLabel && item.retentionAmountLabel ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={item.retentionLabel}
|
||||
value={item.retentionAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,79 @@
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { IssuedInvoiceDetailTotalsViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceTotalsSectionProps = {
|
||||
totals: IssuedInvoiceDetailTotalsViewModel;
|
||||
};
|
||||
|
||||
export const IssuedInvoiceTotalsSection = ({ totals }: IssuedInvoiceTotalsSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.totals")}
|
||||
title={t("pages.issued_invoices.detail.sections.totals")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.subtotal_amount")}
|
||||
value={totals.subtotalLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
{totals.itemsDiscountLabel ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.items_discount_amount")}
|
||||
value={totals.itemsDiscountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
{totals.globalDiscountLabel ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.global_discount_amount")}
|
||||
value={totals.globalDiscountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.discount_amount")}
|
||||
value={totals.discountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.taxable_amount")}
|
||||
value={totals.taxableAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.tax_amount")}
|
||||
value={totals.taxAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.taxes_amount")}
|
||||
value={totals.taxesAmountLabel}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
{totals.usesRec ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.rec_amount")}
|
||||
value={totals.recAmountLabel ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
{totals.usesRetention ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.retention_amount")}
|
||||
value={totals.retentionAmountLabel ?? t("pages.issued_invoices.detail.empty.not_available")}
|
||||
valueClassName="tabular-nums"
|
||||
/>
|
||||
) : null}
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.total_amount")}
|
||||
value={totals.totalAmountLabel}
|
||||
valueClassName="tabular-nums text-base font-semibold"
|
||||
/>
|
||||
</div>
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,99 @@
|
||||
import { ReactQRCode } from "@lglab/react-qr-code";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import { VerifactuStatusBadge } from "../../../list/ui/components";
|
||||
import type { IssuedInvoiceDetailVerifactuViewModel } from "../../entities";
|
||||
import { IssuedInvoiceDetailField, IssuedInvoiceDetailSection } from "../components";
|
||||
|
||||
type IssuedInvoiceVerifactuSectionProps = {
|
||||
verifactu: IssuedInvoiceDetailVerifactuViewModel;
|
||||
};
|
||||
|
||||
export const IssuedInvoiceVerifactuSection = ({
|
||||
verifactu,
|
||||
}: IssuedInvoiceVerifactuSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IssuedInvoiceDetailSection
|
||||
description={t("pages.issued_invoices.detail.section_descriptions.verifactu")}
|
||||
title={t("pages.issued_invoices.detail.sections.verifactu")}
|
||||
>
|
||||
{!verifactu.hasRecord ? (
|
||||
<p className="rounded-lg border border-dashed bg-muted/10 px-4 py-3 text-sm text-muted-foreground">
|
||||
{t("pages.issued_invoices.detail.verifactu.empty")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_status")}
|
||||
value={
|
||||
verifactu.status ? (
|
||||
<VerifactuStatusBadge className="sm:ml-auto" status={verifactu.status} />
|
||||
) : (
|
||||
t("pages.issued_invoices.detail.empty.not_available")
|
||||
)
|
||||
}
|
||||
/>
|
||||
{verifactu.url ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_url")}
|
||||
value={
|
||||
<a
|
||||
className="break-all text-primary underline underline-offset-2"
|
||||
href={verifactu.url}
|
||||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
{verifactu.url}
|
||||
</a>
|
||||
}
|
||||
valueClassName="sm:text-left"
|
||||
/>
|
||||
) : null}
|
||||
{verifactu.qrMode === "image" && verifactu.qrValue ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_qr")}
|
||||
value={
|
||||
<img
|
||||
alt={t("pages.issued_invoices.detail.verifactu.qr_alt")}
|
||||
className="max-w-40 rounded border bg-white p-2 sm:ml-auto"
|
||||
src={verifactu.qrValue}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{verifactu.qrMode === "generated" && verifactu.qrValue ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_qr")}
|
||||
value={
|
||||
<div className="inline-flex rounded border bg-white p-2 sm:ml-auto">
|
||||
<ReactQRCode
|
||||
background="#fff"
|
||||
dataModulesSettings={{ color: "#000" }}
|
||||
finderPatternInnerSettings={{ color: "#000" }}
|
||||
finderPatternOuterSettings={{ color: "#000" }}
|
||||
marginSize={6}
|
||||
size={132}
|
||||
value={verifactu.qrValue}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{verifactu.qrMode === "text" && verifactu.qrValue ? (
|
||||
<IssuedInvoiceDetailField
|
||||
label={t("pages.issued_invoices.detail.fields.verifactu_qr")}
|
||||
value={
|
||||
<code className="block whitespace-pre-wrap break-all rounded border bg-muted/20 px-3 py-2 text-xs sm:text-left">
|
||||
{verifactu.qrValue}
|
||||
</code>
|
||||
}
|
||||
valueClassName="sm:text-left"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</IssuedInvoiceDetailSection>
|
||||
);
|
||||
};
|
||||
@ -111,6 +111,19 @@ const buildInvoice = (): IssuedInvoice => ({
|
||||
},
|
||||
taxRegimeCode: "general",
|
||||
taxRegimeDescription: null,
|
||||
fiscalClassification: {
|
||||
taxRegimeCode: "general",
|
||||
taxRegimeLabel: "Régimen general",
|
||||
operationQualificationCode: "S1",
|
||||
operationQualificationLabel: "Sujeta",
|
||||
exemptionReasonCode: null,
|
||||
exemptionReasonLabel: null,
|
||||
},
|
||||
verifactuPolicy: {
|
||||
profileStatus: "obligated_with_verifactu",
|
||||
usesVerifactu: true,
|
||||
effectiveAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
subtotalAmount: 150,
|
||||
itemsDiscountAmount: 0,
|
||||
globalDiscountPercentage: 0,
|
||||
@ -187,22 +200,42 @@ test("construye el view model general con resumen fiscal, unidades y enlace a pr
|
||||
});
|
||||
|
||||
assert.equal(viewModel.summary.documentReference, "A-12");
|
||||
assert.equal(viewModel.summary.invoiceNumberLabel, "12");
|
||||
assert.equal(viewModel.summary.statusLabel, "Emitida");
|
||||
assert.equal(viewModel.summary.totalUnitsLabel, "3.5");
|
||||
assert.equal(viewModel.customer.addressLines.length, 3);
|
||||
assert.equal(viewModel.commercial.paymentMethodLabel, null);
|
||||
assert.equal(
|
||||
viewModel.commercial.paymentTermLabel,
|
||||
"30 días — Pago estándar — 100.00% a 30 días"
|
||||
);
|
||||
assert.equal(viewModel.commercial.paymentMethodName, null);
|
||||
assert.equal(viewModel.commercial.paymentTermName, "30 días");
|
||||
assert.equal(viewModel.commercial.paymentTermDueRules[0]?.dueDaysLabel, "30 días");
|
||||
assert.equal(viewModel.fiscal.taxRegimeLabel, "general");
|
||||
assert.equal(viewModel.fiscal.fiscalClassification[0]?.value, "Régimen general");
|
||||
assert.equal(viewModel.fiscal.verifactuPolicy[0]?.value, "obligated_with_verifactu");
|
||||
assert.equal(viewModel.fiscal.usesRec, true);
|
||||
assert.equal(viewModel.fiscal.usesRetention, true);
|
||||
assert.equal(viewModel.fiscal.breakdown[1]?.taxLabel, "Impuesto");
|
||||
assert.equal(viewModel.fiscal.breakdown[1]?.recLabel, "RE · 5.2%");
|
||||
assert.equal(viewModel.fiscal.breakdown[1]?.retentionLabel, "IRPF · 15%");
|
||||
assert.equal(viewModel.items[0]?.positionLabel, "1");
|
||||
assert.equal(viewModel.totals.taxesAmountLabel, "16,10 €");
|
||||
assert.equal(viewModel.linkedProforma.canOpenLinkedProforma, true);
|
||||
assert.equal(viewModel.verifactu.url, "https://example.com/verifactu/A-12");
|
||||
assert.equal(viewModel.verifactu.qrMode, "generated");
|
||||
});
|
||||
|
||||
test("mantiene verifactu a null sin fabricar estado ni qr", () => {
|
||||
const viewModel = buildIssuedInvoiceDetailGeneralViewModel(
|
||||
{
|
||||
...buildInvoice(),
|
||||
verifactu: null,
|
||||
},
|
||||
{
|
||||
translate,
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(viewModel.verifactu.hasRecord, false);
|
||||
assert.equal(viewModel.verifactu.status, null);
|
||||
assert.equal(viewModel.verifactu.qrMode, null);
|
||||
});
|
||||
|
||||
test("mapea errores desconocidos del detalle al variant unknown sin exponer detalles técnicos", () => {
|
||||
|
||||
@ -8,12 +8,16 @@ import type {
|
||||
IssuedInvoiceDetailContentViewModel,
|
||||
IssuedInvoiceDetailCustomerViewModel,
|
||||
IssuedInvoiceDetailDocumentViewModel,
|
||||
IssuedInvoiceDetailFiscalFieldViewModel,
|
||||
IssuedInvoiceDetailFiscalBreakdownItemViewModel,
|
||||
IssuedInvoiceDetailFiscalViewModel,
|
||||
IssuedInvoiceDetailGeneralViewModel,
|
||||
IssuedInvoiceDetailHeaderViewModel,
|
||||
IssuedInvoiceDetailItemViewModel,
|
||||
IssuedInvoiceDetailPageErrorViewModel,
|
||||
IssuedInvoiceDetailSummaryViewModel,
|
||||
IssuedInvoiceDetailTaxBreakdownViewModel,
|
||||
IssuedInvoiceDetailTotalsViewModel,
|
||||
IssuedInvoiceDetailVerifactuViewModel,
|
||||
} from "../entities";
|
||||
|
||||
@ -97,32 +101,55 @@ const buildTaxLabel = (
|
||||
return percentage ? `${code} · ${percentage}` : code;
|
||||
};
|
||||
|
||||
const buildPaymentTermLabel = (issuedInvoice: IssuedInvoice): string | null => {
|
||||
if (issuedInvoice.paymentTerm == null) {
|
||||
const buildPercentageLabel = (value: number | null, scale = 2) => {
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dueRulesSummary = issuedInvoice.paymentTerm.dueRules
|
||||
.map((rule) => {
|
||||
const percentageLabel = NumberHelper.formatNumber(
|
||||
PercentageDTOHelper.toNumber({
|
||||
value: rule.percentage.value,
|
||||
scale: String(rule.percentage.scale),
|
||||
}),
|
||||
rule.percentage.scale
|
||||
);
|
||||
return `${NumberHelper.formatNumber(value, scale)}%`;
|
||||
};
|
||||
|
||||
return `${percentageLabel}% a ${rule.dueDays} días`;
|
||||
})
|
||||
.join(" / ");
|
||||
const buildPolicyStatusLabel = (
|
||||
profileStatus: NonNullable<IssuedInvoice["verifactuPolicy"]>["profileStatus"],
|
||||
translate: IssuedInvoiceDetailTranslate
|
||||
) => {
|
||||
return translate(`pages.issued_invoices.detail.verifactu.policy_status.${profileStatus}`, {
|
||||
defaultValue: profileStatus,
|
||||
});
|
||||
};
|
||||
|
||||
const segments = [
|
||||
issuedInvoice.paymentTerm.name,
|
||||
issuedInvoice.paymentTerm.description,
|
||||
dueRulesSummary.length > 0 ? dueRulesSummary : null,
|
||||
const buildItemTaxesLabel = (
|
||||
item: IssuedInvoice["items"][number],
|
||||
translate: IssuedInvoiceDetailTranslate
|
||||
) => {
|
||||
const taxPercentageLabel = buildPercentageLabel(item.taxPercentage);
|
||||
const recPercentageLabel = buildPercentageLabel(item.recPercentage);
|
||||
const retentionPercentageLabel = buildPercentageLabel(item.retentionPercentage);
|
||||
const parts = [
|
||||
item.taxCode ? (taxPercentageLabel ? `${item.taxCode} · ${taxPercentageLabel}` : item.taxCode) : null,
|
||||
item.recCode ? (recPercentageLabel ? `${item.recCode} · ${recPercentageLabel}` : item.recCode) : null,
|
||||
item.retentionCode
|
||||
? retentionPercentageLabel
|
||||
? `${item.retentionCode} · ${retentionPercentageLabel}`
|
||||
: item.retentionCode
|
||||
: null,
|
||||
].filter(Boolean);
|
||||
|
||||
return segments.length > 0 ? segments.join(" — ") : null;
|
||||
return parts.length > 0
|
||||
? parts.join(" / ")
|
||||
: translate("pages.issued_invoices.detail.empty.no_taxes");
|
||||
};
|
||||
|
||||
const buildFiscalField = (
|
||||
key: string,
|
||||
label: string,
|
||||
value: string | null
|
||||
): IssuedInvoiceDetailFiscalFieldViewModel | null => {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { key, label, value };
|
||||
};
|
||||
|
||||
const buildPageErrorCopy = (
|
||||
@ -241,6 +268,14 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
|
||||
): IssuedInvoiceDetailGeneralViewModel => {
|
||||
const usesRec = issuedInvoice.recAmount > 0;
|
||||
const usesRetention = issuedInvoice.retentionAmount > 0;
|
||||
const itemsDiscountLabel =
|
||||
issuedInvoice.itemsDiscountAmount > 0
|
||||
? formatMoney(issuedInvoice, issuedInvoice.itemsDiscountAmount)
|
||||
: null;
|
||||
const globalDiscountLabel =
|
||||
issuedInvoice.globalDiscountAmount > 0
|
||||
? `${formatMoney(issuedInvoice, issuedInvoice.globalDiscountAmount)} (${NumberHelper.formatNumber(issuedInvoice.globalDiscountPercentage, 2)}%)`
|
||||
: null;
|
||||
|
||||
const breakdown: IssuedInvoiceDetailFiscalBreakdownItemViewModel[] = issuedInvoice.taxes.map(
|
||||
(tax, index) => ({
|
||||
@ -248,6 +283,7 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
|
||||
taxLabel: buildTaxLabel(tax, context.translate),
|
||||
taxableAmountLabel: formatMoney(issuedInvoice, tax.taxableAmount),
|
||||
taxAmountLabel: formatMoney(issuedInvoice, tax.ivaAmount),
|
||||
taxesAmountLabel: formatMoney(issuedInvoice, tax.taxesAmount),
|
||||
recLabel:
|
||||
tax.recPercentage === null
|
||||
? null
|
||||
@ -266,6 +302,7 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
|
||||
totalLabel: formatMoney(issuedInvoice, issuedInvoice.totalAmount),
|
||||
documentReference: issuedInvoice.invoiceReference,
|
||||
seriesLabel: issuedInvoice.series,
|
||||
invoiceNumberLabel: issuedInvoice.invoiceNumber,
|
||||
invoiceDateLabel: DateHelper.format(issuedInvoice.invoiceDate),
|
||||
operationDateLabel: formatDate(issuedInvoice.operationDate),
|
||||
statusLabel: context.translate(
|
||||
@ -285,18 +322,86 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
|
||||
};
|
||||
|
||||
const commercial: IssuedInvoiceDetailCommercialViewModel = {
|
||||
paymentMethodLabel: issuedInvoice.paymentMethodName,
|
||||
paymentTermLabel: buildPaymentTermLabel(issuedInvoice),
|
||||
paymentMethodName: issuedInvoice.paymentMethodName,
|
||||
paymentMethodDescription: issuedInvoice.paymentMethodDescription,
|
||||
paymentTermName: issuedInvoice.paymentTerm?.name ?? null,
|
||||
paymentTermDescription: issuedInvoice.paymentTerm?.description ?? null,
|
||||
paymentTermDueRules:
|
||||
issuedInvoice.paymentTerm?.dueRules.map((rule, index) => ({
|
||||
key: `${rule.dueDays}-${rule.percentage.value}-${index}`,
|
||||
dueDaysLabel: context.translate("pages.issued_invoices.detail.payment_term.due_days", {
|
||||
days: rule.dueDays,
|
||||
}),
|
||||
percentageLabel: `${NumberHelper.formatNumber(
|
||||
PercentageDTOHelper.toNumber({
|
||||
value: rule.percentage.value,
|
||||
scale: String(rule.percentage.scale),
|
||||
}),
|
||||
rule.percentage.scale
|
||||
)}%`,
|
||||
})) ?? [],
|
||||
languageLabel: buildLanguageLabel(issuedInvoice.languageCode),
|
||||
currencyCode: issuedInvoice.currencyCode,
|
||||
internalReferenceLabel: issuedInvoice.reference,
|
||||
};
|
||||
|
||||
const fiscalClassification = [
|
||||
buildFiscalField(
|
||||
"tax-regime",
|
||||
context.translate("pages.issued_invoices.detail.fields.fiscal_tax_regime"),
|
||||
issuedInvoice.fiscalClassification?.taxRegimeLabel ??
|
||||
issuedInvoice.fiscalClassification?.taxRegimeCode ??
|
||||
null
|
||||
),
|
||||
buildFiscalField(
|
||||
"operation-qualification",
|
||||
context.translate("pages.issued_invoices.detail.fields.operation_qualification"),
|
||||
issuedInvoice.fiscalClassification?.operationQualificationLabel ??
|
||||
issuedInvoice.fiscalClassification?.operationQualificationCode ??
|
||||
null
|
||||
),
|
||||
buildFiscalField(
|
||||
"exemption-reason",
|
||||
context.translate("pages.issued_invoices.detail.fields.exemption_reason"),
|
||||
issuedInvoice.fiscalClassification?.exemptionReasonLabel ??
|
||||
issuedInvoice.fiscalClassification?.exemptionReasonCode ??
|
||||
null
|
||||
),
|
||||
].filter((item): item is IssuedInvoiceDetailFiscalFieldViewModel => item !== null);
|
||||
|
||||
const verifactuPolicy = issuedInvoice.verifactuPolicy
|
||||
? [
|
||||
buildFiscalField(
|
||||
"profile-status",
|
||||
context.translate("pages.issued_invoices.detail.fields.verifactu_profile_status"),
|
||||
buildPolicyStatusLabel(issuedInvoice.verifactuPolicy.profileStatus, context.translate)
|
||||
),
|
||||
buildFiscalField(
|
||||
"uses-verifactu",
|
||||
context.translate("pages.issued_invoices.detail.fields.verifactu_policy"),
|
||||
issuedInvoice.verifactuPolicy.usesVerifactu
|
||||
? context.translate("pages.issued_invoices.detail.verifactu.uses_verifactu")
|
||||
: context.translate("pages.issued_invoices.detail.verifactu.does_not_use_verifactu")
|
||||
),
|
||||
buildFiscalField(
|
||||
"effective-at",
|
||||
context.translate("pages.issued_invoices.detail.fields.verifactu_effective_at"),
|
||||
formatDate(issuedInvoice.verifactuPolicy.effectiveAt)
|
||||
),
|
||||
].filter((item): item is IssuedInvoiceDetailFiscalFieldViewModel => item !== null)
|
||||
: [];
|
||||
|
||||
const fiscal: IssuedInvoiceDetailFiscalViewModel = {
|
||||
taxRegimeLabel: issuedInvoice.taxRegimeDescription ?? issuedInvoice.taxRegimeCode,
|
||||
fiscalClassification,
|
||||
verifactuPolicy,
|
||||
subtotalLabel: formatMoney(issuedInvoice, issuedInvoice.subtotalAmount),
|
||||
itemsDiscountLabel,
|
||||
globalDiscountLabel,
|
||||
discountLabel: formatMoney(issuedInvoice, issuedInvoice.totalDiscountAmount),
|
||||
taxableAmountLabel: formatMoney(issuedInvoice, issuedInvoice.taxableAmount),
|
||||
taxAmountLabel: formatMoney(issuedInvoice, issuedInvoice.ivaAmount),
|
||||
taxesAmountLabel: formatMoney(issuedInvoice, issuedInvoice.taxesAmount),
|
||||
recAmountLabel: usesRec ? formatMoney(issuedInvoice, issuedInvoice.recAmount) : null,
|
||||
retentionAmountLabel: usesRetention
|
||||
? formatMoney(issuedInvoice, issuedInvoice.retentionAmount)
|
||||
@ -308,9 +413,63 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
|
||||
};
|
||||
|
||||
const verifactu: IssuedInvoiceDetailVerifactuViewModel = {
|
||||
status: issuedInvoice.verifactu.status,
|
||||
url: issuedInvoice.verifactu.url,
|
||||
qrCode: issuedInvoice.verifactu.qrCode,
|
||||
hasRecord: issuedInvoice.verifactu !== null,
|
||||
status: issuedInvoice.verifactu?.status ?? null,
|
||||
url: issuedInvoice.verifactu?.url ?? null,
|
||||
qrCode: issuedInvoice.verifactu?.qrCode ?? null,
|
||||
qrValue: issuedInvoice.verifactu?.qrCode ?? issuedInvoice.verifactu?.url ?? null,
|
||||
qrMode: issuedInvoice.verifactu?.qrCode
|
||||
? issuedInvoice.verifactu.qrCode.startsWith("data:image/")
|
||||
? "image"
|
||||
: "text"
|
||||
: issuedInvoice.verifactu?.url
|
||||
? "generated"
|
||||
: null,
|
||||
};
|
||||
|
||||
const items: IssuedInvoiceDetailItemViewModel[] = issuedInvoice.items.map((item) => ({
|
||||
key: item.id,
|
||||
positionLabel: String(item.position),
|
||||
description:
|
||||
item.description || context.translate("pages.issued_invoices.detail.empty.no_description"),
|
||||
quantityLabel:
|
||||
item.quantity === null
|
||||
? null
|
||||
: NumberHelper.formatNumber(item.quantity, Number.isInteger(item.quantity) ? 0 : 2),
|
||||
unitAmountLabel: item.unitAmount === null ? null : formatMoney(issuedInvoice, item.unitAmount),
|
||||
subtotalLabel: formatMoney(issuedInvoice, item.subtotalAmount),
|
||||
discountLabel:
|
||||
item.itemDiscountAmount > 0
|
||||
? `${formatMoney(issuedInvoice, item.itemDiscountAmount)}${
|
||||
item.itemDiscountPercentage !== null
|
||||
? ` (${NumberHelper.formatNumber(item.itemDiscountPercentage, 2)}%)`
|
||||
: ""
|
||||
}`
|
||||
: null,
|
||||
taxableAmountLabel: formatMoney(issuedInvoice, item.taxableAmount),
|
||||
taxesLabel: buildItemTaxesLabel(item, context.translate),
|
||||
totalAmountLabel: formatMoney(issuedInvoice, item.totalAmount),
|
||||
}));
|
||||
|
||||
const taxes: IssuedInvoiceDetailTaxBreakdownViewModel[] = breakdown.map((item) => ({
|
||||
...item,
|
||||
}));
|
||||
|
||||
const totals: IssuedInvoiceDetailTotalsViewModel = {
|
||||
subtotalLabel: formatMoney(issuedInvoice, issuedInvoice.subtotalAmount),
|
||||
itemsDiscountLabel,
|
||||
globalDiscountLabel,
|
||||
discountLabel: formatMoney(issuedInvoice, issuedInvoice.totalDiscountAmount),
|
||||
taxableAmountLabel: formatMoney(issuedInvoice, issuedInvoice.taxableAmount),
|
||||
taxAmountLabel: formatMoney(issuedInvoice, issuedInvoice.ivaAmount),
|
||||
taxesAmountLabel: formatMoney(issuedInvoice, issuedInvoice.taxesAmount),
|
||||
recAmountLabel: usesRec ? formatMoney(issuedInvoice, issuedInvoice.recAmount) : null,
|
||||
retentionAmountLabel: usesRetention
|
||||
? formatMoney(issuedInvoice, issuedInvoice.retentionAmount)
|
||||
: null,
|
||||
totalAmountLabel: formatMoney(issuedInvoice, issuedInvoice.totalAmount),
|
||||
usesRec,
|
||||
usesRetention,
|
||||
};
|
||||
|
||||
const content: IssuedInvoiceDetailContentViewModel = {
|
||||
@ -323,6 +482,9 @@ export const buildIssuedInvoiceDetailGeneralViewModel = (
|
||||
customer,
|
||||
commercial,
|
||||
fiscal,
|
||||
items,
|
||||
taxes,
|
||||
totals,
|
||||
linkedProforma: {
|
||||
linkedProformaId: issuedInvoice.linkedProformaId,
|
||||
canOpenLinkedProforma: Boolean(issuedInvoice.linkedProformaId),
|
||||
|
||||
@ -33,27 +33,28 @@ export function useIssuedInvoicesGridColumns(
|
||||
() => [
|
||||
// Nº
|
||||
{
|
||||
id: "series_invoice_number",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
className="text-right tabular-nums justify-end"
|
||||
column={column}
|
||||
title={t("pages.issued_invoices.list.grid_columns.series_invoice_number")}
|
||||
title={t("pages.issued_invoices.list.grid_columns.invoice_number")}
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
accessorKey: "invoiceReference",
|
||||
/*cell: ({ row }) => (
|
||||
<div className="text-right tabular-nums">
|
||||
{row.original.series}
|
||||
{row.original.invoiceNumber}
|
||||
</div>
|
||||
),
|
||||
),*/
|
||||
enableHiding: false,
|
||||
enableSorting: false,
|
||||
maxSize: 96,
|
||||
enableSorting: true,
|
||||
/*maxSize: 96,
|
||||
size: 96,
|
||||
minSize: 96,
|
||||
minSize: 96,*/
|
||||
meta: {
|
||||
title: t("pages.issued_invoices.list.grid_columns.series_invoice_number"),
|
||||
title: t("pages.issued_invoices.list.grid_columns.invoice_reference"),
|
||||
cellClassName: "font-mono text-sm font-medium",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@ -70,6 +70,19 @@ const buildDto = (): GetIssuedInvoiceByIdResponseDTO => ({
|
||||
code: "general",
|
||||
description: "Régimen general",
|
||||
},
|
||||
fiscal_classification: {
|
||||
tax_regime_code: "general",
|
||||
tax_regime_label: "Régimen general",
|
||||
operation_qualification_code: "S1",
|
||||
operation_qualification_label: "Sujeta",
|
||||
exemption_reason_code: null,
|
||||
exemption_reason_label: null,
|
||||
},
|
||||
verifactu_policy: {
|
||||
profile_status: "obligated_with_verifactu",
|
||||
uses_verifactu: true,
|
||||
effective_at: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
subtotal_amount: MoneyDTOHelper.fromNumber(100, "EUR"),
|
||||
items_discount_amount: MoneyDTOHelper.fromNumber(5, "EUR"),
|
||||
global_discount_percentage: PercentageDTOHelper.fromNumber(10),
|
||||
@ -125,6 +138,8 @@ test("adapta el DTO de detalle a una entidad con nullables y valores numéricos
|
||||
assert.equal(invoice.paymentMethodName, "Transferencia");
|
||||
assert.equal(invoice.paymentTerm?.name, "30 días");
|
||||
assert.equal(invoice.paymentTerm?.dueRules[0]?.dueDays, "30");
|
||||
assert.equal(invoice.fiscalClassification?.operationQualificationCode, "S1");
|
||||
assert.equal(invoice.verifactuPolicy?.usesVerifactu, true);
|
||||
assert.equal(invoice.globalDiscountPercentage, 10);
|
||||
assert.equal(invoice.taxableAmount, 85);
|
||||
assert.equal(invoice.totalAmount, 91);
|
||||
@ -133,3 +148,12 @@ test("adapta el DTO de detalle a una entidad con nullables y valores numéricos
|
||||
assert.equal(invoice.items[0]?.unitAmount, 50);
|
||||
assert.equal(invoice.items[0]?.retentionPercentage, 15);
|
||||
});
|
||||
|
||||
test("acepta facturas sin bloque verifactu sin inventar estado desde el frontend", () => {
|
||||
const invoice = GetIssuedInvoiceByIdAdapter.fromDto({
|
||||
...buildDto(),
|
||||
verifactu: null,
|
||||
});
|
||||
|
||||
assert.equal(invoice.verifactu, null);
|
||||
});
|
||||
|
||||
@ -4,11 +4,13 @@ import type { GetIssuedInvoiceByIdResponseDTO } from "../../../../common";
|
||||
import type { GetIssuedInvoiceByIdResult } from "../api";
|
||||
import type {
|
||||
IssuedInvoice,
|
||||
IssuedInvoiceFiscalClassification,
|
||||
IssuedInvoiceItem,
|
||||
IssuedInvoicePaymentTermDetail,
|
||||
IssuedInvoiceRecipientDetail,
|
||||
IssuedInvoiceStatus,
|
||||
IssuedInvoiceTaxSummary,
|
||||
IssuedInvoiceVerifactuPolicy,
|
||||
IssuedInvoiceVerifactuRecord,
|
||||
VerifactuRecordStatus,
|
||||
} from "../entities";
|
||||
@ -41,6 +43,8 @@ export const GetIssuedInvoiceByIdAdapter = {
|
||||
paymentTerm: mapPaymentTerm(dto.payment_term),
|
||||
taxRegimeCode: dto.tax_regime.code,
|
||||
taxRegimeDescription: dto.tax_regime.description,
|
||||
fiscalClassification: mapFiscalClassification(dto.fiscal_classification),
|
||||
verifactuPolicy: mapVerifactuPolicy(dto.verifactu_policy),
|
||||
subtotalAmount: MoneyDTOHelper.toNumber(dto.subtotal_amount),
|
||||
itemsDiscountAmount: MoneyDTOHelper.toNumber(dto.items_discount_amount),
|
||||
globalDiscountPercentage: PercentageDTOHelper.toNumber(dto.global_discount_percentage),
|
||||
@ -52,7 +56,7 @@ export const GetIssuedInvoiceByIdAdapter = {
|
||||
retentionAmount: MoneyDTOHelper.toNumber(dto.retention_amount),
|
||||
taxesAmount: MoneyDTOHelper.toNumber(dto.taxes_amount),
|
||||
totalAmount: MoneyDTOHelper.toNumber(dto.total_amount),
|
||||
verifactu: mapVerifactu(dto.verifactu),
|
||||
verifactu: mapVerifactu(dto.verifactu ?? null),
|
||||
items: dto.items.map(mapItem),
|
||||
};
|
||||
},
|
||||
@ -140,11 +144,46 @@ const mapPaymentTerm = (
|
||||
};
|
||||
|
||||
const mapVerifactu = (
|
||||
dto: GetIssuedInvoiceByIdResponseDTO["verifactu"]
|
||||
): IssuedInvoiceVerifactuRecord => {
|
||||
dto: GetIssuedInvoiceByIdResponseDTO["verifactu"] | null
|
||||
): IssuedInvoiceVerifactuRecord | null => {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
status: dto.status as VerifactuRecordStatus,
|
||||
url: dto.url,
|
||||
qrCode: dto.qr_code,
|
||||
};
|
||||
};
|
||||
|
||||
const mapFiscalClassification = (
|
||||
dto: GetIssuedInvoiceByIdResponseDTO["fiscal_classification"]
|
||||
): IssuedInvoiceFiscalClassification | null => {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
taxRegimeCode: dto.tax_regime_code,
|
||||
taxRegimeLabel: dto.tax_regime_label,
|
||||
operationQualificationCode: dto.operation_qualification_code,
|
||||
operationQualificationLabel: dto.operation_qualification_label,
|
||||
exemptionReasonCode: dto.exemption_reason_code,
|
||||
exemptionReasonLabel: dto.exemption_reason_label,
|
||||
};
|
||||
};
|
||||
|
||||
const mapVerifactuPolicy = (
|
||||
dto: GetIssuedInvoiceByIdResponseDTO["verifactu_policy"]
|
||||
): IssuedInvoiceVerifactuPolicy | null => {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
profileStatus: dto.profile_status,
|
||||
usesVerifactu: dto.uses_verifactu,
|
||||
effectiveAt: dto.effective_at,
|
||||
};
|
||||
};
|
||||
|
||||
@ -7,7 +7,9 @@ export interface GetIssuedInvoiceByIdParams {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export type GetIssuedInvoiceByIdResult = GetIssuedInvoiceByIdResponseDTO;
|
||||
export type GetIssuedInvoiceByIdResult = Omit<GetIssuedInvoiceByIdResponseDTO, "verifactu"> & {
|
||||
verifactu: GetIssuedInvoiceByIdResponseDTO["verifactu"] | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Recupera el detalle completo de una factura emitida a partir de su identificador.
|
||||
|
||||
@ -59,6 +59,25 @@ export interface IssuedInvoiceVerifactuRecord {
|
||||
qrCode: string | null;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceFiscalClassification {
|
||||
taxRegimeCode: string;
|
||||
taxRegimeLabel: string | null;
|
||||
operationQualificationCode: string | null;
|
||||
operationQualificationLabel: string | null;
|
||||
exemptionReasonCode: string | null;
|
||||
exemptionReasonLabel: string | null;
|
||||
}
|
||||
|
||||
export interface IssuedInvoiceVerifactuPolicy {
|
||||
profileStatus:
|
||||
| "not_configured"
|
||||
| "exempt"
|
||||
| "obligated_without_verifactu"
|
||||
| "obligated_with_verifactu";
|
||||
usesVerifactu: boolean;
|
||||
effectiveAt: string | null;
|
||||
}
|
||||
|
||||
export interface IssuedInvoicePaymentTermDueRule {
|
||||
dueDays: string;
|
||||
percentage: {
|
||||
@ -103,6 +122,8 @@ export interface IssuedInvoice {
|
||||
paymentTerm: IssuedInvoicePaymentTermDetail | null;
|
||||
taxRegimeCode: string;
|
||||
taxRegimeDescription: string | null;
|
||||
fiscalClassification: IssuedInvoiceFiscalClassification | null;
|
||||
verifactuPolicy: IssuedInvoiceVerifactuPolicy | null;
|
||||
subtotalAmount: number;
|
||||
itemsDiscountAmount: number;
|
||||
globalDiscountPercentage: number;
|
||||
@ -114,6 +135,6 @@ export interface IssuedInvoice {
|
||||
retentionAmount: number;
|
||||
taxesAmount: number;
|
||||
totalAmount: number;
|
||||
verifactu: IssuedInvoiceVerifactuRecord;
|
||||
verifactu: IssuedInvoiceVerifactuRecord | null;
|
||||
items: IssuedInvoiceItem[];
|
||||
}
|
||||
|
||||
@ -3,6 +3,12 @@ packages:
|
||||
- modules/*
|
||||
- apps/*
|
||||
|
||||
allowBuilds:
|
||||
'@parcel/watcher': false
|
||||
bcrypt: false
|
||||
esbuild: false
|
||||
msw: false
|
||||
|
||||
ignoredBuiltDependencies:
|
||||
- esbuild
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user