feat(web): folio y RUT en los formularios de facturas, notas de crédito, entregas y recepciones; RUT en todos los de socio

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-14 14:48:50 -05:00
1 parent 49408de082
commit c734dcbd0a
6 files changed
+186 -8

No files matched your search

@@ -8,7 +8,7 @@ import { map } from 'rxjs';
import { API_URL, apiErrorMessage } from '../../core/api';
import { formatNumber } from '../../core/format';
import { SlDocumentApi } from '../../core/sl/sl-document';
import { DateField, LookupField, NumberField, TextField, TextareaField } from '../../shared/form';
import { DateField, LookupField, NumberField, SboField, TextField, TextInput, TextareaField } from '../../shared/form';
import type { GridLine, GridMultiAdd } from '../../shared/grid/grid.types';
import { LinesGrid } from '../../shared/grid/lines-grid';
import type { VhLookup } from '../../shared/value-help/value-help.types';
@@ -35,6 +35,8 @@ import { ITEMS_MULTI_ADD, lineColumnsFor, newDocumentLine, recalcLine } from './
/** 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;
const today = (): string => new Date().toISOString().slice(0, 10);
const DATE_KEYS = new Set(['DocDate', 'DocDueDate', 'RequriedDate']);
@@ -43,10 +45,19 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
const pickHeader = (doc: DocRecord | undefined, keys: readonly string[], blank: DocRecord): DocRecord => {
if (!doc) return blank;
return Object.fromEntries(
keys.map((k) => [k, DATE_KEYS.has(k) ? dateOnly(doc[k]) : (doc[k] ?? (k === 'DiscountPercent' ? 0 : ''))]),
keys.map((k) => [
k,
DATE_KEYS.has(k) ? dateOnly(doc[k]) : k === 'FolioNumber' ? text(doc[k]) : (doc[k] ?? (k === 'DiscountPercent' ? 0 : '')),
]),
);
};
const text = (v: unknown): string => (v === null || v === undefined ? '' : String(v));
/** El folio se edita como texto; SAP lo espera como número. */
const folioAsNumber = (h: DocRecord): DocRecord => {
const folio = h['FolioNumber'];
if (typeof folio !== 'string') return h;
return { ...h, FolioNumber: folio.trim() === '' ? null : Number(folio) };
};
/**
* Formulario de un documento de ventas o compras: `nuevo` (vacío) o `:docEntry` (ver / editar).
@@ -56,7 +67,7 @@ const text = (v: unknown): string => (v === null || v === undefined ? '' : Strin
@Component({
selector: 'sbo-document-form-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, RouterLink, SboWindow, TextField, TextareaField, Toolbar, ValueHelp],
imports: [DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, RouterLink, SboField, SboWindow, TextField, TextInput, TextareaField, Toolbar, ValueHelp],
templateUrl: './document-form.page.html',
styles: `
h1 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
@@ -76,7 +87,12 @@ export class DocumentFormPage {
/** Formulario de inventario o de solicitud de compra (sin socio ni totales). */
protected readonly layout = this.screen.layout;
private readonly lineSpec = this.layout?.lines ?? MARKETING_LINES;
private readonly headerKeys: readonly string[] = this.layout ? headerKeysOf(this.layout) : HEADER_KEYS;
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 };
@@ -115,6 +131,8 @@ export class DocumentFormPage {
protected readonly header = linkedSignal<DocRecord>(() => this.pick(this.doc()));
protected readonly lines = linkedSignal<readonly GridLine[]>(() => this.docLines(this.doc()).map((l) => ({ ...l })));
protected readonly partnerName = linkedSignal(() => text(this.doc()?.['CardName']));
/** RUT del socio: lo completa SAP desde el maestro; aquí solo se muestra. */
protected readonly partnerTaxId = linkedSignal(() => text(this.doc()?.['FederalTaxID']));
// --- estado --------------------------------------------------------------------------------
@@ -233,6 +251,7 @@ export class DocumentFormPage {
if (!card) {
this.header.update((h) => ({ ...h, ...copied.header }));
this.partnerName.set(text(doc['CardName']));
this.partnerTaxId.set(text(doc['FederalTaxID']));
}
this.lines.update((ls) => [...ls.filter((l) => !isBlankLine(l)), ...copied.lines]);
this.message.set({
@@ -262,6 +281,7 @@ export class DocumentFormPage {
{ key: 'CardCode', title: 'Código', width: '130px', mono: true },
{ key: 'CardName', title: 'Nombre' },
],
extraFields: ['FederalTaxID'],
};
protected str = text;
@@ -280,6 +300,7 @@ export class DocumentFormPage {
if (code === text(this.header()['CardCode'])) return;
this.setHeader('CardCode', code);
this.partnerName.set('');
this.partnerTaxId.set('');
}
protected notify(textMessage: string): void {
@@ -288,6 +309,7 @@ export class DocumentFormPage {
protected onPartner(row: Record<string, unknown>): void {
this.partnerName.set(text(row['CardName']));
this.partnerTaxId.set(text(row['FederalTaxID']));
}
protected setLines(lines: readonly GridLine[]): void {
@@ -321,6 +343,7 @@ export class DocumentFormPage {
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.');
if (errors.length) {
this.message.set({ kind: 'err', text: errors[0] });
return;
@@ -330,7 +353,7 @@ export class DocumentFormPage {
try {
const d = this.doc();
if (!d) {
const header = 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,
@@ -338,7 +361,9 @@ export class DocumentFormPage {
});
return;
}
const { body, replaceLines } = updatePayload(this.pick(d), this.header(), this.docLines(d), this.lines(), this.lineSpec);
const update = updatePayload(this.pick(d), this.header(), this.docLines(d), this.lines(), this.lineSpec);
const body = folioAsNumber(update.body);
const replaceLines = update.replaceLines;
if (!Object.keys(body).length) {
this.message.set({ kind: 'info', text: 'No hay cambios que guardar.' });
return;