Proceso de paso de proforma a factura
This commit is contained in:
parent
2c84dc26bd
commit
c8eff4e9fc
@ -1,8 +1,8 @@
|
||||
import { ITransactionManager } from "@erp/core/api";
|
||||
import { UniqueID, UtcDate } from "@repo/rdx-ddd";
|
||||
import { Maybe, Result } from "@repo/rdx-utils";
|
||||
import { InvalidProformaStatusError } from "../../domain";
|
||||
import { StatusInvoiceIsApprovedSpecification } from "../../domain/specs";
|
||||
import { Result } from "@repo/rdx-utils";
|
||||
import { ProformaCannotBeConvertedToInvoiceError } from "../../domain";
|
||||
import { ProformaCanTranstionToIssuedSpecification } from "../../domain/specs";
|
||||
import { CustomerInvoiceApplicationService } from "../customer-invoice-application.service";
|
||||
|
||||
type IssueCustomerInvoiceUseCaseInput = {
|
||||
@ -52,9 +52,9 @@ export class IssueCustomerInvoiceUseCase {
|
||||
const proforma = proformaResult.data;
|
||||
|
||||
/** 2. Comprobamos que la proforma origen está aprovada para generar la factura */
|
||||
const isApprovedSpec = new StatusInvoiceIsApprovedSpecification();
|
||||
if (!(await isApprovedSpec.isSatisfiedBy(proforma))) {
|
||||
return Result.fail(new InvalidProformaStatusError(proformaId.toString()));
|
||||
const isOk = new ProformaCanTranstionToIssuedSpecification();
|
||||
if (!(await isOk.isSatisfiedBy(proforma))) {
|
||||
return Result.fail(new ProformaCannotBeConvertedToInvoiceError(proformaId.toString()));
|
||||
}
|
||||
|
||||
/** 3. Generar nueva factura */
|
||||
@ -71,7 +71,7 @@ export class IssueCustomerInvoiceUseCase {
|
||||
|
||||
// props base obtenidas del agregado proforma
|
||||
const issuedInvoiceOrError = this.service.buildIssueInvoiceInCompany(companyId, proforma, {
|
||||
invoiceNumber: Maybe.some(newIssueNumber),
|
||||
invoiceNumber: newIssueNumber,
|
||||
invoiceDate: UtcDate.today(),
|
||||
});
|
||||
|
||||
|
||||
@ -1 +1 @@
|
||||
export * from "./status-invoice-is-approved.specification";
|
||||
export * from "./proforma-can-transtion-to-issued.specification";
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import { CompositeSpecification } from "@repo/rdx-ddd";
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
import { INVOICE_STATUS } from "../value-objects";
|
||||
|
||||
export class ProformaCanTranstionToIssuedSpecification extends CompositeSpecification<CustomerInvoice> {
|
||||
public async isSatisfiedBy(proforma: CustomerInvoice): Promise<boolean> {
|
||||
return proforma.isProforma && proforma.canTransitionTo(INVOICE_STATUS.ISSUED);
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { CompositeSpecification } from "@repo/rdx-ddd";
|
||||
import { CustomerInvoice } from "../aggregates";
|
||||
|
||||
export class StatusInvoiceIsApprovedSpecification extends CompositeSpecification<CustomerInvoice> {
|
||||
public async isSatisfiedBy(invoice: CustomerInvoice): Promise<boolean> {
|
||||
return invoice.status.isApproved();
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,8 @@ export enum INVOICE_STATUS {
|
||||
APPROVED = "approved", // <- Proforma
|
||||
REJECTED = "rejected", // <- Proforma
|
||||
|
||||
// status === issued <- (si is_proforma === true) => Es una proforma (histórica)
|
||||
// status === issued <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
||||
|
||||
// status === "issued" <- (si is_proforma === true) => Es una proforma (histórica)
|
||||
// status === "issued" <- (si is_proforma === false) => Factura y enviará/enviada a Veri*Factu
|
||||
ISSUED = "issued",
|
||||
}
|
||||
export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusProps> {
|
||||
@ -93,15 +92,6 @@ export class CustomerInvoiceStatus extends ValueObject<ICustomerInvoiceStatusPro
|
||||
return CustomerInvoiceStatus.TRANSITIONS[this.props.value].includes(nextStatus);
|
||||
}
|
||||
|
||||
transitionTo(nextStatus: string): Result<CustomerInvoiceStatus, Error> {
|
||||
if (!this.canTransitionTo(nextStatus)) {
|
||||
return Result.fail(
|
||||
new Error(`Transición no permitida de ${this.props.value} a ${nextStatus}`)
|
||||
);
|
||||
}
|
||||
return CustomerInvoiceStatus.create(nextStatus);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return String(this.props.value);
|
||||
}
|
||||
|
||||
@ -1,20 +1,19 @@
|
||||
import { DataTableColumnHeader } from '@repo/rdx-ui/components';
|
||||
import { DataTableColumnHeader } from "@repo/rdx-ui/components";
|
||||
import { InputGroup, InputGroupTextarea } from "@repo/shadcn-ui/components";
|
||||
import { cn } from "@repo/shadcn-ui/lib/utils";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import * as React from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import { useInvoiceContext } from '../../../context';
|
||||
import { CustomerInvoiceTaxesMultiSelect } from '../../customer-invoice-taxes-multi-select';
|
||||
import { AmountInputField } from './amount-input-field';
|
||||
import { HoverCardTotalsSummary } from './hover-card-total-summary';
|
||||
import { ItemDataTableRowActions } from './items-data-table-row-actions';
|
||||
import { PercentageInputField } from './percentage-input-field';
|
||||
import { QuantityInputField } from './quantity-input-field';
|
||||
|
||||
import { useInvoiceContext } from "../../../context";
|
||||
import { CustomerInvoiceTaxesMultiSelect } from "../../customer-invoice-taxes-multi-select";
|
||||
import { AmountInputField } from "./amount-input-field";
|
||||
import { HoverCardTotalsSummary } from "./hover-card-total-summary";
|
||||
import { ItemDataTableRowActions } from "./items-data-table-row-actions";
|
||||
import { PercentageInputField } from "./percentage-input-field";
|
||||
import { QuantityInputField } from "./quantity-input-field";
|
||||
|
||||
export interface InvoiceItemFormData {
|
||||
id: string; // ← mapea RHF field.id aquí
|
||||
id: string; // ← mapea RHF field.id aquí
|
||||
description: string;
|
||||
quantity: number | "";
|
||||
unit_amount: number | "";
|
||||
@ -22,53 +21,62 @@ export interface InvoiceItemFormData {
|
||||
tax_codes: string[];
|
||||
total_amount: number | ""; // readonly calculado
|
||||
}
|
||||
export interface InvoiceFormData { items: InvoiceItemFormData[] }
|
||||
export interface InvoiceFormData {
|
||||
items: InvoiceItemFormData[];
|
||||
}
|
||||
|
||||
export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
||||
const { t, readOnly, currency_code, language_code } = useInvoiceContext();
|
||||
const { control } = useFormContext<InvoiceFormData>();
|
||||
|
||||
// Atención: Memoizar siempre para evitar reconstrucciones y resets de estado de tabla
|
||||
return React.useMemo<ColumnDef<InvoiceItemFormData>[]>(() => [
|
||||
{
|
||||
id: 'position',
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={"#"} className='text-center' />
|
||||
),
|
||||
cell: ({ row }) => row.index + 1,
|
||||
enableSorting: false,
|
||||
size: 32,
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("form_fields.item.description.label")} className='text-left' />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${row.index}.description`}
|
||||
render={({ field }) => (
|
||||
<InputGroup>
|
||||
<InputGroupTextarea {...field}
|
||||
id={`desc-${row.original.id}`} // ← estable
|
||||
rows={1}
|
||||
aria-label={t("form_fields.item.description.label")}
|
||||
spellCheck
|
||||
readOnly={readOnly}
|
||||
// auto-grow simple
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
}}
|
||||
className={cn(
|
||||
"min-w-[12rem] max-w-[46rem] w-full resize-none bg-transparent border-dashed transition",
|
||||
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:border-solid",
|
||||
"focus:resize-y"
|
||||
)}
|
||||
data-cell-focus />
|
||||
{/*<InputGroupAddon align="block-end">
|
||||
return React.useMemo<ColumnDef<InvoiceItemFormData>[]>(
|
||||
() => [
|
||||
{
|
||||
id: "position",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={"#"} className='text-center' />
|
||||
),
|
||||
cell: ({ row }) => row.index + 1,
|
||||
enableSorting: false,
|
||||
size: 32,
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("form_fields.item.description.label")}
|
||||
className='text-left'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${row.index}.description`}
|
||||
render={({ field }) => (
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
{...field}
|
||||
id={`desc-${row.original.id}`} // ← estable
|
||||
rows={1}
|
||||
aria-label={t("form_fields.item.description.label")}
|
||||
spellCheck
|
||||
readOnly={readOnly}
|
||||
// auto-grow simple
|
||||
onInput={(e) => {
|
||||
const el = e.currentTarget;
|
||||
el.style.height = "auto";
|
||||
el.style.height = `${el.scrollHeight}px`;
|
||||
}}
|
||||
className={cn(
|
||||
"min-w-[12rem] max-w-[46rem] w-full resize-none bg-transparent border-dashed transition",
|
||||
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:bg-background focus-visible:border-solid",
|
||||
"focus:resize-y"
|
||||
)}
|
||||
data-cell-focus
|
||||
/>
|
||||
{/*<InputGroupAddon align="block-end">
|
||||
<InputGroupText>Line 1, Column 1</InputGroupText>
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
@ -80,129 +88,186 @@ export function useItemsColumns(): ColumnDef<InvoiceItemFormData>[] {
|
||||
<span className="sr-only">Send</span>
|
||||
</InputGroupButton>
|
||||
</InputGroupAddon>*/}
|
||||
</InputGroup>
|
||||
|
||||
)}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 480, minSize: 240, maxSize: 768,
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("form_fields.item.quantity.label")} className='text-right' />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<QuantityInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.quantity`}
|
||||
readOnly={readOnly}
|
||||
inputId={`qty-${row.original.id}`}
|
||||
emptyMode="blank"
|
||||
data-row-index={row.index}
|
||||
data-col-index={4}
|
||||
data-cell-focus
|
||||
className="font-base"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 52, minSize: 48, maxSize: 64,
|
||||
},
|
||||
{
|
||||
accessorKey: "unit_amount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("form_fields.item.unit_amount.label")} className='text-right' />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<AmountInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.unit_amount`}
|
||||
readOnly={readOnly}
|
||||
inputId={`unit-${row.original.id}`}
|
||||
scale={4}
|
||||
currencyCode={currency_code}
|
||||
languageCode={language_code}
|
||||
data-row-index={row.index}
|
||||
data-col-index={5}
|
||||
data-cell-focus
|
||||
className="font-base"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120, minSize: 100, maxSize: 160,
|
||||
},
|
||||
{
|
||||
accessorKey: "discount_percentage",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("form_fields.item.discount_percentage.label")} className='text-right' />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<PercentageInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.discount_percentage`}
|
||||
readOnly={readOnly}
|
||||
inputId={`disc-${row.original.id}`}
|
||||
scale={4}
|
||||
data-row-index={row.index}
|
||||
data-col-index={6}
|
||||
data-cell-focus
|
||||
className="font-base"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 40, minSize: 40
|
||||
},
|
||||
{
|
||||
accessorKey: "tax_codes",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("form_fields.item.tax_codes.label")} />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${row.index}.tax_codes`}
|
||||
render={({ field }) => (
|
||||
<CustomerInvoiceTaxesMultiSelect
|
||||
{...field}
|
||||
inputId={`tax-${row.original.id}`}
|
||||
data-row-index={row.index}
|
||||
data-col-index={7}
|
||||
data-cell-focus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 240, minSize: 232, maxSize: 320,
|
||||
},
|
||||
{
|
||||
accessorKey: "total_amount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("form_fields.item.total_amount.label")} className='text-right' />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<HoverCardTotalsSummary rowIndex={row.index}>
|
||||
</InputGroup>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 480,
|
||||
minSize: 240,
|
||||
maxSize: 768,
|
||||
},
|
||||
{
|
||||
accessorKey: "quantity",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("form_fields.item.quantity.label")}
|
||||
className='text-right'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<QuantityInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.quantity`}
|
||||
readOnly={readOnly}
|
||||
inputId={`qty-${row.original.id}`}
|
||||
emptyMode='blank'
|
||||
data-row-index={row.index}
|
||||
data-col-index={4}
|
||||
data-cell-focus
|
||||
className='font-base'
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 52,
|
||||
minSize: 48,
|
||||
maxSize: 64,
|
||||
},
|
||||
{
|
||||
accessorKey: "unit_amount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("form_fields.item.unit_amount.label")}
|
||||
className='text-right'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<AmountInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.total_amount`}
|
||||
readOnly
|
||||
inputId={`total-${row.original.id}`}
|
||||
name={`items.${row.index}.unit_amount`}
|
||||
readOnly={readOnly}
|
||||
inputId={`unit-${row.original.id}`}
|
||||
scale={4}
|
||||
currencyCode={currency_code}
|
||||
languageCode={language_code}
|
||||
className="font-semibold"
|
||||
data-row-index={row.index}
|
||||
data-col-index={5}
|
||||
data-cell-focus
|
||||
className='font-base'
|
||||
/>
|
||||
</HoverCardTotalsSummary>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120, minSize: 100, maxSize: 160,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("components.datatable.actions")} />
|
||||
),
|
||||
cell: ({ row, table }) => <ItemDataTableRowActions row={row} table={table} />,
|
||||
},
|
||||
], [t, readOnly, control, currency_code, language_code,]);
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
maxSize: 160,
|
||||
},
|
||||
{
|
||||
accessorKey: "discount_percentage",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("form_fields.item.discount_percentage.label")}
|
||||
className='text-right'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<PercentageInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.discount_percentage`}
|
||||
readOnly={readOnly}
|
||||
inputId={`disc-${row.original.id}`}
|
||||
scale={4}
|
||||
data-row-index={row.index}
|
||||
data-col-index={6}
|
||||
data-cell-focus
|
||||
className='font-base'
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 40,
|
||||
minSize: 40,
|
||||
},
|
||||
{
|
||||
accessorKey: "taxable_amount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("form_fields.item.taxable_amount.label")}
|
||||
className='text-right'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<AmountInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.taxable_amount`}
|
||||
readOnly={readOnly}
|
||||
inputId={`unit-${row.original.id}`}
|
||||
scale={4}
|
||||
currencyCode={currency_code}
|
||||
languageCode={language_code}
|
||||
data-row-index={row.index}
|
||||
data-col-index={5}
|
||||
data-cell-focus
|
||||
className='font-base'
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
maxSize: 160,
|
||||
},
|
||||
{
|
||||
accessorKey: "tax_codes",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("form_fields.item.tax_codes.label")} />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`items.${row.index}.tax_codes`}
|
||||
render={({ field }) => (
|
||||
<CustomerInvoiceTaxesMultiSelect
|
||||
{...field}
|
||||
inputId={`tax-${row.original.id}`}
|
||||
data-row-index={row.index}
|
||||
data-col-index={7}
|
||||
data-cell-focus
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 130,
|
||||
maxSize: 180,
|
||||
},
|
||||
{
|
||||
accessorKey: "total_amount",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader
|
||||
column={column}
|
||||
title={t("form_fields.item.total_amount.label")}
|
||||
className='text-right'
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<HoverCardTotalsSummary rowIndex={row.index}>
|
||||
<AmountInputField
|
||||
control={control}
|
||||
name={`items.${row.index}.total_amount`}
|
||||
readOnly
|
||||
inputId={`total-${row.original.id}`}
|
||||
currencyCode={currency_code}
|
||||
languageCode={language_code}
|
||||
className='font-semibold'
|
||||
/>
|
||||
</HoverCardTotalsSummary>
|
||||
),
|
||||
enableSorting: false,
|
||||
size: 120,
|
||||
minSize: 100,
|
||||
maxSize: 160,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: ({ column }) => (
|
||||
<DataTableColumnHeader column={column} title={t("components.datatable.actions")} />
|
||||
),
|
||||
cell: ({ row, table }) => <ItemDataTableRowActions row={row} table={table} />,
|
||||
},
|
||||
],
|
||||
[t, readOnly, control, currency_code, language_code]
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user