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:
1 parent
49408de082
commit
c734dcbd0a
6 files changed
+186
-8
No files matched your search
@@ -56,7 +56,7 @@ Cada menú abre primero una **lista de los últimos documentos** (barra de filtr
|
|||||||
Hecho (commits en `main`): monorepo · API (config, capa SAP con sesión compartida y cola, login, permisos SQLite, proxy `/api/sl`) · web (temas, login, layout con menú por permisos, `sbo-value-help`, campos y filas compuestas, listas con filtros, ventana/toolbar/pestañas/menús, grilla de líneas, `/dev/componentes`).
|
Hecho (commits en `main`): monorepo · API (config, capa SAP con sesión compartida y cola, login, permisos SQLite, proxy `/api/sl`) · web (temas, login, layout con menú por permisos, `sbo-value-help`, campos y filas compuestas, listas con filtros, ventana/toolbar/pestañas/menús, grilla de líneas, `/dev/componentes`).
|
||||||
|
|
||||||
Pendiente, en orden:
|
Pendiente, en orden:
|
||||||
1. **Paso 9 — formulario de documentos:** hecho para ventas y compras (`document-form.page`, `document-model.ts`: cargar, crear, actualizar con `If-Match`/412, navegación, totales con `SalesTaxCodes` (también en compras), solo lectura), probado con SAP simulado y contra `SBODemoCL` (leer, crear, actualizar, líneas, 412). Un código escrito a mano (socio, artículo, almacén, impuesto) se resuelve al salir del campo con `SlLookupApi` (mismo recurso y filtro base que la lista "…"); si no existe se avisa. "Copiar de/a" hecho (`document-copy.ts`: flujos estándar y `BaseType/BaseEntry/BaseLine`; probado: orden de compra 2883 → recepción 299, orden de venta 315 → entrega 580, ambas órdenes quedaron cerradas). Formularios sin socio ni totales **hechos** (`document-layouts.ts`, configuración declarativa: cabecera de 2 columnas, columnas de línea, colección `DocumentLines` o `StockTransferLines`): entradas y salidas de inventario, transferencias de stock, solicitudes de traslado y solicitudes de compra. Entradas, salidas y transferencias contabilizadas son de solo lectura (SAP no las modifica); solicitudes de traslado y de compra se crean y modifican. Probado contra `SBODemoCL`: solicitud de compra 2 (la 1 es una prueba por API), solicitud de traslado 472 (cantidad modificada) y entrada de inventario 155 (1 unidad de `3MG30080714 M 10.5` en el almacén 8803). Limitaciones conocidas: los artículos por **lote o serie** no se pueden contabilizar (SAP pide `BatchNumbers`/`SerialNumbers`); las líneas de compra/inventario llevan `UoMEntry` solo si el artículo tiene grupo de unidades (`uomEntryOf`; sin él SAP responde "specify a UoM code"); los formularios de ventas y compras (`marketing`) aún no lo envían, y los de compra fallan con artículos sin unidad de compra por defecto. Falta: botón PDF, fila del flete y toolbar de ancho completo.
|
1. **Paso 9 — formulario de documentos:** hecho para ventas y compras (`document-form.page`, `document-model.ts`: cargar, crear, actualizar con `If-Match`/412, navegación, totales con `SalesTaxCodes` (también en compras), solo lectura), probado con SAP simulado y contra `SBODemoCL` (leer, crear, actualizar, líneas, 412). Un código escrito a mano (socio, artículo, almacén, impuesto) se resuelve al salir del campo con `SlLookupApi` (mismo recurso y filtro base que la lista "…"); si no existe se avisa. "Copiar de/a" hecho (`document-copy.ts`: flujos estándar y `BaseType/BaseEntry/BaseLine`; probado: orden de compra 2883 → recepción 299, orden de venta 315 → entrega 580, ambas órdenes quedaron cerradas). Formularios sin socio ni totales **hechos** (`document-layouts.ts`, configuración declarativa: cabecera de 2 columnas, columnas de línea, colección `DocumentLines` o `StockTransferLines`): entradas y salidas de inventario, transferencias de stock, solicitudes de traslado y solicitudes de compra. Entradas, salidas y transferencias contabilizadas son de solo lectura (SAP no las modifica); solicitudes de traslado y de compra se crean y modifican. Probado contra `SBODemoCL`: solicitud de compra 2 (la 1 es una prueba por API), solicitud de traslado 472 (cantidad modificada) y entrada de inventario 155 (1 unidad de `3MG30080714 M 10.5` en el almacén 8803). Limitaciones conocidas: los artículos por **lote o serie** no se pueden contabilizar (SAP pide `BatchNumbers`/`SerialNumbers`); las líneas de compra/inventario llevan `UoMEntry` solo si el artículo tiene grupo de unidades (`uomEntryOf`; sin él SAP responde "specify a UoM code"); los formularios de ventas y compras (`marketing`) aún no lo envían, y los de compra fallan con artículos sin unidad de compra por defecto. Los formularios con socio muestran `NumAtCard` y el RUT (`FederalTaxID`, solo lectura: lo completa SAP desde el socio); facturas, notas de crédito, entregas y recepciones (`FOLIO_PATHS` en `document-screens.ts`) añaden el folio tributario, que en el Service Layer es `FolioPrefixString` + `FolioNumber` (se edita como texto y se envía como número); las listas de facturas también los muestran. Falta: botón PDF, fila del flete y toolbar de ancho completo.
|
||||||
2. Pantallas de maestros (`Items`, `BusinessPartners`: Clientes/Proveedores, Artículos), hoy "no disponible".
|
2. Pantallas de maestros (`Items`, `BusinessPartners`: Clientes/Proveedores, Artículos), hoy "no disponible".
|
||||||
3. ~~Pantalla web de permisos~~ — hecha (`/admin/permisos`).
|
3. ~~Pantalla web de permisos~~ — hecha (`/admin/permisos`).
|
||||||
4. Auditoría (`audit_log` en SQLite; hoy solo líneas `AUDIT` en el log) y `nestjs-cls` para el usuario en los logs.
|
4. Auditoría (`audit_log` en SQLite; hoy solo líneas `AUDIT` en el log) y `nestjs-cls` para el usuario en los logs.
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { provideHttpClient } from '@angular/common/http';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { type ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, Router, convertToParamMap, provideRouter } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { DocumentFormPage } from './document-form.page';
|
||||||
|
import { DOCUMENT_SCREENS } from './document-screens';
|
||||||
|
|
||||||
|
const invoice = (over: Record<string, unknown> = {}) => ({
|
||||||
|
'odata.etag': 'W/"5"',
|
||||||
|
DocEntry: 8,
|
||||||
|
DocNum: 108,
|
||||||
|
CardCode: 'P1',
|
||||||
|
CardName: 'Proveedor Uno',
|
||||||
|
FederalTaxID: '76543210-K',
|
||||||
|
NumAtCard: 'REF-1',
|
||||||
|
FolioPrefixString: 'F001',
|
||||||
|
FolioNumber: 198837,
|
||||||
|
DocDate: '2026-09-11T00:00:00Z',
|
||||||
|
DocDueDate: '2026-09-20T00:00:00Z',
|
||||||
|
DiscountPercent: 0,
|
||||||
|
Comments: '',
|
||||||
|
DocumentStatus: 'bost_Open',
|
||||||
|
Cancelled: 'tNO',
|
||||||
|
DocumentLines: [{ LineNum: 0, ItemCode: 'A1', ItemDescription: 'Pieza', Quantity: 1, Price: 10, DiscountPercent: 0, TaxCode: 'IVA', LineTotal: 10, WarehouseCode: '01' }],
|
||||||
|
...over,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('folio y RUT en los formularios', () => {
|
||||||
|
let fixture: ComponentFixture<DocumentFormPage>;
|
||||||
|
let ctrl: HttpTestingController;
|
||||||
|
const el = () => fixture.nativeElement as HTMLElement;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const page = () => fixture.componentInstance as any;
|
||||||
|
const tick = async () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
await new Promise((r) => setTimeout(r));
|
||||||
|
fixture.detectChanges();
|
||||||
|
};
|
||||||
|
const labels = () => [...el().querySelectorAll('label')].map((l) => l.textContent?.trim());
|
||||||
|
|
||||||
|
async function open(resource: string, doc: Record<string, unknown> | null) {
|
||||||
|
const screen = DOCUMENT_SCREENS.find((s) => s.resource === resource)!;
|
||||||
|
const params = doc ? { docEntry: '8' } : {};
|
||||||
|
const route = {
|
||||||
|
snapshot: { data: { screen, title: 'Documento' }, paramMap: convertToParamMap(params) },
|
||||||
|
paramMap: of(convertToParamMap(params)),
|
||||||
|
};
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DocumentFormPage],
|
||||||
|
providers: [provideRouter([]), provideHttpClient(), provideHttpClientTesting(), { provide: ActivatedRoute, useValue: route }],
|
||||||
|
}).compileComponents();
|
||||||
|
ctrl = TestBed.inject(HttpTestingController);
|
||||||
|
vi.spyOn(TestBed.inject(Router), 'navigate').mockResolvedValue(true);
|
||||||
|
fixture = TestBed.createComponent(DocumentFormPage);
|
||||||
|
await tick();
|
||||||
|
ctrl.expectOne((r) => r.url === '/api/sl/SalesTaxCodes').flush({ value: [{ Code: 'IVA', Rate: 19 }] });
|
||||||
|
if (doc) ctrl.expectOne(`/api/sl/${resource}/8`).flush(doc);
|
||||||
|
await tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => TestBed.resetTestingModule());
|
||||||
|
|
||||||
|
it('lo llevan facturas, notas de crédito, entregas y recepciones (ventas y compras)', () => {
|
||||||
|
const withFolio = DOCUMENT_SCREENS.filter((s) => s.folio).map((s) => s.resource);
|
||||||
|
expect(withFolio.sort()).toEqual(
|
||||||
|
['CreditNotes', 'DeliveryNotes', 'Invoices', 'PurchaseCreditNotes', 'PurchaseDeliveryNotes', 'PurchaseInvoices'].sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('una factura muestra folio, número de folio, referencia y RUT', async () => {
|
||||||
|
await open('PurchaseInvoices', invoice());
|
||||||
|
expect(labels()).toEqual(expect.arrayContaining(['Folio', 'Nº referencia', 'RUT']));
|
||||||
|
const inputs = [...el().querySelectorAll<HTMLInputElement>('input')].map((i) => i.value);
|
||||||
|
expect(inputs).toEqual(expect.arrayContaining(['F001', '198837', 'REF-1', '76543210-K']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('una orden muestra referencia y RUT pero no el folio', async () => {
|
||||||
|
await open('Orders', invoice());
|
||||||
|
expect(labels()).toEqual(expect.arrayContaining(['Nº referencia', 'RUT']));
|
||||||
|
expect(labels()).not.toContain('Folio');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cambiar el folio envía el número como número', async () => {
|
||||||
|
await open('Invoices', invoice());
|
||||||
|
page().setHeader('FolioPrefixString', 'F002');
|
||||||
|
page().setHeader('FolioNumber', '200100');
|
||||||
|
await tick();
|
||||||
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||||||
|
await tick();
|
||||||
|
const req = ctrl.expectOne((r) => r.method === 'PATCH');
|
||||||
|
expect(req.request.body).toEqual({ FolioPrefixString: 'F002', FolioNumber: 200100 });
|
||||||
|
req.flush(null);
|
||||||
|
await tick();
|
||||||
|
ctrl.expectOne('/api/sl/Invoices/8').flush(invoice());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('un folio que no es numérico no se envía', async () => {
|
||||||
|
await open('Invoices', invoice());
|
||||||
|
page().setHeader('FolioNumber', '12ab');
|
||||||
|
await tick();
|
||||||
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||||||
|
await tick();
|
||||||
|
ctrl.expectNone((r) => r.method === 'PATCH');
|
||||||
|
expect(el().querySelector('.status')?.textContent).toContain('folio debe ser numérico');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('un documento nuevo sin folio no lo envía', async () => {
|
||||||
|
await open('Invoices', null);
|
||||||
|
page().setHeader('CardCode', 'C1');
|
||||||
|
page().setLines([{ ItemCode: 'A1', Quantity: 1, Price: 50, LineTotal: 50 }]);
|
||||||
|
await tick();
|
||||||
|
(el().querySelector('.actions .btn.main') as HTMLElement).click();
|
||||||
|
await tick();
|
||||||
|
const body = ctrl.expectOne((r) => r.method === 'POST').request.body as Record<string, unknown>;
|
||||||
|
expect(body).not.toHaveProperty('FolioNumber');
|
||||||
|
expect(body).not.toHaveProperty('FolioPrefixString');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
(picked)="onPartner($event)"
|
(picked)="onPartner($event)"
|
||||||
/>
|
/>
|
||||||
<sbo-text-field label="Nombre" readonly [value]="partnerName()" />
|
<sbo-text-field label="Nombre" readonly [value]="partnerName()" />
|
||||||
|
<sbo-text-field label="RUT" readonly [value]="partnerTaxId()" />
|
||||||
<sbo-text-field
|
<sbo-text-field
|
||||||
label="Nº referencia"
|
label="Nº referencia"
|
||||||
[readonly]="readOnly()"
|
[readonly]="readOnly()"
|
||||||
@@ -45,6 +46,23 @@
|
|||||||
[value]="str(header()['DocDueDate'])"
|
[value]="str(header()['DocDueDate'])"
|
||||||
(valueChange)="setHeader('DocDueDate', $event)"
|
(valueChange)="setHeader('DocDueDate', $event)"
|
||||||
/>
|
/>
|
||||||
|
@if (folio) {
|
||||||
|
<sbo-field label="Folio">
|
||||||
|
<sbo-input
|
||||||
|
width="90px"
|
||||||
|
[maxlength]="10"
|
||||||
|
[readonly]="readOnly()"
|
||||||
|
[value]="str(header()['FolioPrefixString'])"
|
||||||
|
(valueChange)="setHeader('FolioPrefixString', $event)"
|
||||||
|
/>
|
||||||
|
<sbo-input
|
||||||
|
ariaLabel="Número de folio"
|
||||||
|
[readonly]="readOnly()"
|
||||||
|
[value]="str(header()['FolioNumber'])"
|
||||||
|
(valueChange)="setHeader('FolioNumber', $event)"
|
||||||
|
/>
|
||||||
|
</sbo-field>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { map } from 'rxjs';
|
|||||||
import { API_URL, apiErrorMessage } from '../../core/api';
|
import { API_URL, apiErrorMessage } from '../../core/api';
|
||||||
import { formatNumber } from '../../core/format';
|
import { formatNumber } from '../../core/format';
|
||||||
import { SlDocumentApi } from '../../core/sl/sl-document';
|
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 type { GridLine, GridMultiAdd } from '../../shared/grid/grid.types';
|
||||||
import { LinesGrid } from '../../shared/grid/lines-grid';
|
import { LinesGrid } from '../../shared/grid/lines-grid';
|
||||||
import type { VhLookup } from '../../shared/value-help/value-help.types';
|
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). */
|
/** 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;
|
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 today = (): string => new Date().toISOString().slice(0, 10);
|
||||||
const DATE_KEYS = new Set(['DocDate', 'DocDueDate', 'RequriedDate']);
|
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 => {
|
const pickHeader = (doc: DocRecord | undefined, keys: readonly string[], blank: DocRecord): DocRecord => {
|
||||||
if (!doc) return blank;
|
if (!doc) return blank;
|
||||||
return Object.fromEntries(
|
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));
|
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).
|
* 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({
|
@Component({
|
||||||
selector: 'sbo-document-form-page',
|
selector: 'sbo-document-form-page',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
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',
|
templateUrl: './document-form.page.html',
|
||||||
styles: `
|
styles: `
|
||||||
h1 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
|
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). */
|
/** Formulario de inventario o de solicitud de compra (sin socio ni totales). */
|
||||||
protected readonly layout = this.screen.layout;
|
protected readonly layout = this.screen.layout;
|
||||||
private readonly lineSpec = this.layout?.lines ?? MARKETING_LINES;
|
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
|
private readonly blankHeader: DocRecord = this.layout
|
||||||
? this.layout.newHeader()
|
? this.layout.newHeader()
|
||||||
: { DocDate: today(), DocDueDate: today(), DiscountPercent: 0 };
|
: { DocDate: today(), DocDueDate: today(), DiscountPercent: 0 };
|
||||||
@@ -115,6 +131,8 @@ export class DocumentFormPage {
|
|||||||
protected readonly header = linkedSignal<DocRecord>(() => this.pick(this.doc()));
|
protected readonly header = linkedSignal<DocRecord>(() => this.pick(this.doc()));
|
||||||
protected readonly lines = linkedSignal<readonly GridLine[]>(() => this.docLines(this.doc()).map((l) => ({ ...l })));
|
protected readonly lines = linkedSignal<readonly GridLine[]>(() => this.docLines(this.doc()).map((l) => ({ ...l })));
|
||||||
protected readonly partnerName = linkedSignal(() => text(this.doc()?.['CardName']));
|
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 --------------------------------------------------------------------------------
|
// --- estado --------------------------------------------------------------------------------
|
||||||
|
|
||||||
@@ -233,6 +251,7 @@ export class DocumentFormPage {
|
|||||||
if (!card) {
|
if (!card) {
|
||||||
this.header.update((h) => ({ ...h, ...copied.header }));
|
this.header.update((h) => ({ ...h, ...copied.header }));
|
||||||
this.partnerName.set(text(doc['CardName']));
|
this.partnerName.set(text(doc['CardName']));
|
||||||
|
this.partnerTaxId.set(text(doc['FederalTaxID']));
|
||||||
}
|
}
|
||||||
this.lines.update((ls) => [...ls.filter((l) => !isBlankLine(l)), ...copied.lines]);
|
this.lines.update((ls) => [...ls.filter((l) => !isBlankLine(l)), ...copied.lines]);
|
||||||
this.message.set({
|
this.message.set({
|
||||||
@@ -262,6 +281,7 @@ export class DocumentFormPage {
|
|||||||
{ key: 'CardCode', title: 'Código', width: '130px', mono: true },
|
{ key: 'CardCode', title: 'Código', width: '130px', mono: true },
|
||||||
{ key: 'CardName', title: 'Nombre' },
|
{ key: 'CardName', title: 'Nombre' },
|
||||||
],
|
],
|
||||||
|
extraFields: ['FederalTaxID'],
|
||||||
};
|
};
|
||||||
|
|
||||||
protected str = text;
|
protected str = text;
|
||||||
@@ -280,6 +300,7 @@ export class DocumentFormPage {
|
|||||||
if (code === text(this.header()['CardCode'])) return;
|
if (code === text(this.header()['CardCode'])) return;
|
||||||
this.setHeader('CardCode', code);
|
this.setHeader('CardCode', code);
|
||||||
this.partnerName.set('');
|
this.partnerName.set('');
|
||||||
|
this.partnerTaxId.set('');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected notify(textMessage: string): void {
|
protected notify(textMessage: string): void {
|
||||||
@@ -288,6 +309,7 @@ export class DocumentFormPage {
|
|||||||
|
|
||||||
protected onPartner(row: Record<string, unknown>): void {
|
protected onPartner(row: Record<string, unknown>): void {
|
||||||
this.partnerName.set(text(row['CardName']));
|
this.partnerName.set(text(row['CardName']));
|
||||||
|
this.partnerTaxId.set(text(row['FederalTaxID']));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected setLines(lines: readonly GridLine[]): void {
|
protected setLines(lines: readonly GridLine[]): void {
|
||||||
@@ -321,6 +343,7 @@ export class DocumentFormPage {
|
|||||||
this.layout.requiredLine,
|
this.layout.requiredLine,
|
||||||
)
|
)
|
||||||
: validateDocument(this.header(), this.lines(), this.partner?.label ?? 'Socio de negocio');
|
: 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) {
|
if (errors.length) {
|
||||||
this.message.set({ kind: 'err', text: errors[0] });
|
this.message.set({ kind: 'err', text: errors[0] });
|
||||||
return;
|
return;
|
||||||
@@ -330,7 +353,7 @@ export class DocumentFormPage {
|
|||||||
try {
|
try {
|
||||||
const d = this.doc();
|
const d = this.doc();
|
||||||
if (!d) {
|
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));
|
const created = await this.api.create(this.screen.resource, createPayload(header, this.lines(), this.lineSpec));
|
||||||
await this.router.navigate(['..', created['DocEntry']], {
|
await this.router.navigate(['..', created['DocEntry']], {
|
||||||
relativeTo: this.route,
|
relativeTo: this.route,
|
||||||
@@ -338,7 +361,9 @@ export class DocumentFormPage {
|
|||||||
});
|
});
|
||||||
return;
|
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) {
|
if (!Object.keys(body).length) {
|
||||||
this.message.set({ kind: 'info', text: 'No hay cambios que guardar.' });
|
this.message.set({ kind: 'info', text: 'No hay cambios que guardar.' });
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ export interface DocumentScreen {
|
|||||||
/** Maestro de impuestos (el Service Layer no tiene uno propio de compras: en Chile ambos usan `SalesTaxCodes`). */
|
/** Maestro de impuestos (el Service Layer no tiene uno propio de compras: en Chile ambos usan `SalesTaxCodes`). */
|
||||||
taxResource: string;
|
taxResource: string;
|
||||||
};
|
};
|
||||||
|
/** El formulario muestra el folio tributario (`FolioPrefixString`, `FolioNumber`) y el RUT del socio. */
|
||||||
|
folio?: boolean;
|
||||||
/** Formulario de los documentos sin socio ni totales (inventario y solicitudes de compra). */
|
/** Formulario de los documentos sin socio ni totales (inventario y solicitudes de compra). */
|
||||||
layout?: DocumentLayout;
|
layout?: DocumentLayout;
|
||||||
}
|
}
|
||||||
@@ -165,7 +167,17 @@ const inventory = (
|
|||||||
filters: withStatus ? [statusFilter, dateFilter] : [dateFilter],
|
filters: withStatus ? [statusFilter, dateFilter] : [dateFilter],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DOCUMENT_SCREENS: readonly DocumentScreen[] = [
|
/** Documentos tributarios: llevan folio y RUT. */
|
||||||
|
const FOLIO_PATHS: ReadonlySet<string> = new Set([
|
||||||
|
'/ventas/entrega',
|
||||||
|
'/ventas/facturas',
|
||||||
|
'/ventas/notas-credito',
|
||||||
|
'/compras/recepcion',
|
||||||
|
'/compras/facturas',
|
||||||
|
'/compras/notas-credito',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const SCREENS: readonly DocumentScreen[] = [
|
||||||
// Ventas
|
// Ventas
|
||||||
marketing('/ventas/ofertas', 'Quotations', 'Nueva oferta', 'Cliente'),
|
marketing('/ventas/ofertas', 'Quotations', 'Nueva oferta', 'Cliente'),
|
||||||
marketing('/ventas/ordenes', 'Orders', 'Nueva orden', 'Cliente'),
|
marketing('/ventas/ordenes', 'Orders', 'Nueva orden', 'Cliente'),
|
||||||
@@ -202,3 +214,5 @@ export const DOCUMENT_SCREENS: readonly DocumentScreen[] = [
|
|||||||
inventory('/inventario/transferencias', 'StockTransfers', 'Nueva transferencia', false, LAYOUTS.transfer),
|
inventory('/inventario/transferencias', 'StockTransfers', 'Nueva transferencia', false, LAYOUTS.transfer),
|
||||||
inventory('/inventario/solicitudes-traslado', 'InventoryTransferRequests', 'Nueva solicitud', true, LAYOUTS.transferRequest),
|
inventory('/inventario/solicitudes-traslado', 'InventoryTransferRequests', 'Nueva solicitud', true, LAYOUTS.transferRequest),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const DOCUMENT_SCREENS: readonly DocumentScreen[] = SCREENS.map((s) => (FOLIO_PATHS.has(s.path) ? { ...s, folio: true } : s));
|
||||||
@@ -62,6 +62,7 @@ import { SboField } from './sbo-field';
|
|||||||
[columns]="lookup().columns"
|
[columns]="lookup().columns"
|
||||||
[baseFilter]="lookup().baseFilter"
|
[baseFilter]="lookup().baseFilter"
|
||||||
[orderby]="lookup().orderby"
|
[orderby]="lookup().orderby"
|
||||||
|
[extraFields]="lookup().extraFields ?? []"
|
||||||
(selected)="onPicked($event)"
|
(selected)="onPicked($event)"
|
||||||
(closed)="closeList()"
|
(closed)="closeList()"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in new issue
Block a user