refactor(web): el formulario base recibe de cada pantalla los campos de cabecera, pie y línea
Todas las pantallas declaran su DocumentLayout (catálogo FIELDS): ventas y compras dejan de tener la cabecera escrita en el HTML; el folio solo lo llevan los documentos tributarios. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c734dcbd0a
commit
8b0b8afc87
9 files changed
+320
-257
No files matched your search
@@ -19,24 +19,17 @@ import {
|
||||
type DocRecord,
|
||||
type NavTarget,
|
||||
computeTotals,
|
||||
MARKETING_LINES,
|
||||
createPayload,
|
||||
isBlankLine,
|
||||
isDocumentReadOnly,
|
||||
taxRateMap,
|
||||
updatePayload,
|
||||
validateDocument,
|
||||
validateLayout,
|
||||
} from './document-model';
|
||||
import { WAREHOUSE_LOOKUP, headerKeysOf } from './document-layouts';
|
||||
import { type ComputedSource, WAREHOUSE_LOOKUP, hasFolio, headerKeysOf, requiredHeaderOf } from './document-layouts';
|
||||
import { type CopyOrigin, copyFromDocument, copySources, copyTargets } from './document-copy';
|
||||
import { DOCUMENT_SCREENS, type DocumentScreen, docNum, documentStatus, menuLabel } from './document-screens';
|
||||
import { ITEMS_MULTI_ADD, lineColumnsFor, newDocumentLine, recalcLine } from './line-columns';
|
||||
|
||||
/** Campos de cabecera que se editan en el formulario (el resto lo calcula SAP). */
|
||||
const HEADER_KEYS = ['CardCode', 'NumAtCard', 'DocDate', 'DocDueDate', 'DiscountPercent', 'Comments'] as const;
|
||||
/** Folio tributario de los documentos que lo llevan. */
|
||||
const FOLIO_KEYS = ['FolioPrefixString', 'FolioNumber'] as const;
|
||||
import { ITEMS_MULTI_ADD } from './line-columns';
|
||||
|
||||
const today = (): string => new Date().toISOString().slice(0, 10);
|
||||
const DATE_KEYS = new Set(['DocDate', 'DocDueDate', 'RequriedDate']);
|
||||
@@ -84,18 +77,12 @@ export class DocumentFormPage {
|
||||
protected readonly title = this.route.snapshot.data['title'] as string;
|
||||
protected readonly screen = this.route.snapshot.data['screen'] as DocumentScreen;
|
||||
protected readonly partner = this.screen.partner;
|
||||
/** Formulario de inventario o de solicitud de compra (sin socio ni totales). */
|
||||
/** Qué campos y columnas lleva este documento (lo declara su pantalla); el formulario solo lo dibuja. */
|
||||
protected readonly layout = this.screen.layout;
|
||||
private readonly lineSpec = this.layout?.lines ?? MARKETING_LINES;
|
||||
private readonly headerKeys: readonly string[] = this.layout
|
||||
? headerKeysOf(this.layout)
|
||||
: this.screen.folio
|
||||
? [...HEADER_KEYS, ...FOLIO_KEYS]
|
||||
: HEADER_KEYS;
|
||||
protected readonly folio = !!this.screen.folio;
|
||||
private readonly blankHeader: DocRecord = this.layout
|
||||
? this.layout.newHeader()
|
||||
: { DocDate: today(), DocDueDate: today(), DiscountPercent: 0 };
|
||||
private readonly lineSpec = this.layout.lines;
|
||||
private readonly headerKeys: readonly string[] = headerKeysOf(this.layout);
|
||||
private readonly hasFolio = hasFolio(this.layout);
|
||||
private readonly blankHeader: DocRecord = this.layout.newHeader();
|
||||
private pick(doc: DocRecord | undefined): DocRecord {
|
||||
return pickHeader(doc, this.headerKeys, this.blankHeader);
|
||||
}
|
||||
@@ -136,7 +123,7 @@ export class DocumentFormPage {
|
||||
|
||||
// --- estado --------------------------------------------------------------------------------
|
||||
|
||||
protected readonly readOnly = computed(() => isDocumentReadOnly(this.doc()) || (!!this.layout?.immutable && !this.isNew()));
|
||||
protected readonly readOnly = computed(() => isDocumentReadOnly(this.doc()) || (!!this.layout.immutable && !this.isNew()));
|
||||
protected readonly saving = signal(false);
|
||||
private readonly message = signal<StatusMessage | null>(
|
||||
((): StatusMessage | null => {
|
||||
@@ -152,11 +139,11 @@ export class DocumentFormPage {
|
||||
const message = this.message();
|
||||
if (message) return message;
|
||||
if (this.readOnly()) {
|
||||
const posted = this.layout?.immutable && !isDocumentReadOnly(this.doc());
|
||||
const posted = this.layout.immutable && !isDocumentReadOnly(this.doc());
|
||||
return { kind: 'info', text: posted ? 'Documento contabilizado: solo lectura.' : 'Documento cerrado o cancelado: solo lectura.' };
|
||||
}
|
||||
if (this.isNew()) {
|
||||
return { kind: 'info', text: this.layout ? 'Complete la cabecera y las líneas.' : `Indique el ${this.partner?.label.toLowerCase() ?? 'socio'} y las líneas.` };
|
||||
return { kind: 'info', text: this.partner ? `Indique el ${this.partner.label.toLowerCase()} y las líneas.` : 'Complete la cabecera y las líneas.' };
|
||||
}
|
||||
return { kind: 'info', text: this.dirty() ? 'Hay cambios sin guardar.' : '' };
|
||||
});
|
||||
@@ -265,12 +252,13 @@ export class DocumentFormPage {
|
||||
|
||||
// --- configuración de la vista --------------------------------------------------------------
|
||||
|
||||
protected readonly columns = this.layout?.columns ?? lineColumnsFor(this.partner?.taxResource ?? 'SalesTaxCodes');
|
||||
protected readonly newLine = this.layout ? () => this.layout!.newLine(this.header()) : newDocumentLine;
|
||||
protected readonly recalc = this.layout ? undefined : recalcLine;
|
||||
protected readonly multi: GridMultiAdd = this.layout
|
||||
? { ...ITEMS_MULTI_ADD, toLine: (item) => this.layout!.columns[0].lookup!.apply(item, this.layout!.newLine(this.header())) }
|
||||
: ITEMS_MULTI_ADD;
|
||||
protected readonly columns = this.layout.columns;
|
||||
protected readonly newLine = () => this.layout.newLine(this.header());
|
||||
protected readonly recalc = this.layout.recalc;
|
||||
protected readonly multi: GridMultiAdd = {
|
||||
...ITEMS_MULTI_ADD,
|
||||
toLine: (item) => this.layout.columns[0].lookup!.apply(item, this.layout.newLine(this.header())),
|
||||
};
|
||||
protected readonly warehouseLookup = WAREHOUSE_LOOKUP;
|
||||
protected readonly partnerLookup: VhLookup = {
|
||||
title: this.partner?.type === 'cSupplier' ? 'Proveedores' : 'Clientes',
|
||||
@@ -286,6 +274,24 @@ export class DocumentFormPage {
|
||||
|
||||
protected str = text;
|
||||
|
||||
/** Valor de una fila de solo lectura (número del documento, datos del socio, totales). */
|
||||
protected computedValue(source: ComputedSource): string {
|
||||
switch (source) {
|
||||
case 'docNum':
|
||||
return text(this.doc()?.['DocNum']);
|
||||
case 'partnerName':
|
||||
return this.partnerName();
|
||||
case 'partnerTaxId':
|
||||
return this.partnerTaxId();
|
||||
case 'subtotal':
|
||||
return this.fmt(this.totals().subtotal);
|
||||
case 'tax':
|
||||
return this.fmt(this.totals().tax);
|
||||
case 'total':
|
||||
return this.fmt(this.totals().total);
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
if (this.copyOrigin) void this.loadCopy(this.copyOrigin);
|
||||
}
|
||||
@@ -335,15 +341,8 @@ export class DocumentFormPage {
|
||||
|
||||
protected async save(): Promise<void> {
|
||||
if (this.saving() || this.readOnly()) return;
|
||||
const errors = this.layout
|
||||
? validateLayout(
|
||||
this.header(),
|
||||
this.lines(),
|
||||
[...this.layout.left, ...this.layout.right].filter((f) => f.required),
|
||||
this.layout.requiredLine,
|
||||
)
|
||||
: validateDocument(this.header(), this.lines(), this.partner?.label ?? 'Socio de negocio');
|
||||
if (this.folio && !/^\d*$/.test(text(this.header()['FolioNumber']).trim())) errors.push('El número de folio debe ser numérico.');
|
||||
const errors = validateLayout(this.header(), this.lines(), requiredHeaderOf(this.layout), this.layout.requiredLine);
|
||||
if (this.hasFolio && !/^\d*$/.test(text(this.header()['FolioNumber']).trim())) errors.push('El número de folio debe ser numérico.');
|
||||
if (errors.length) {
|
||||
this.message.set({ kind: 'err', text: errors[0] });
|
||||
return;
|
||||
@@ -353,7 +352,7 @@ export class DocumentFormPage {
|
||||
try {
|
||||
const d = this.doc();
|
||||
if (!d) {
|
||||
const header = folioAsNumber(this.layout?.beforeCreate?.(this.header()) ?? this.header());
|
||||
const header = folioAsNumber(this.layout.beforeCreate?.(this.header()) ?? this.header());
|
||||
const created = await this.api.create(this.screen.resource, createPayload(header, this.lines(), this.lineSpec));
|
||||
await this.router.navigate(['..', created['DocEntry']], {
|
||||
relativeTo: this.route,
|
||||
|
||||
Reference in new issue
Block a user