feat(web): dirección del documento desde AddressExtension (texto y corrección con «…») y campos de lista que muestran el código con formato (cuenta asociada)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-19 14:58:30 -05:00
1 parent c1f2cea1e1
commit 896fa25f2f
9 files changed
+342 -14

No files matched your search

@@ -9,7 +9,7 @@ import { API_URL, apiErrorMessage } from '../../core/api';
import { formatNumber, todayIso } from '../../core/format';
import { CompanySettingsService } from '../../core/company/company-settings';
import { SlDocumentApi } from '../../core/sl/sl-document';
import { CheckboxField, CheckboxInput, DateField, LookupField, NumberField, NumberInput, SboField, SelectField, SelectInput, TextField, TextInput, TextareaField } from '../../shared/form';
import { CheckboxField, CheckboxInput, DateField, LookupField, NumberField, NumberInput, SboField, SelectField, SelectInput, TextField, TextInput, TextareaField, TextareaInput } 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 {
} from './document-model';
import { SERVICE_DOC_TYPE } from './document-layouts';
import { type ComputedSource, type GroupCell, type HeaderField, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasCurrency, hasExpenses, hasFolio, hasPartnerData, hasSeries, headerKeysOf, keysOfFields, requiredHeaderOf } from './document-layouts';
import { type AddressSide, type AddressValues, addressFromExtension, addressFromPartner, composeAddress, toExtension } from './address-fields';
import { AddressDialog } from './address-dialog';
import { type CopyOrigin, copyFromDocument, copySources, copyTargets } from './document-copy';
import { DOCUMENT_SCREENS, type DocumentScreen, docNum, documentStatus, menuLabel } from './document-screens';
import { OBJECT_TYPES } from './document-objects';
@@ -72,7 +74,7 @@ const folioAsNumber = (h: DocRecord): DocRecord => {
@Component({
selector: 'sbo-document-form-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [Tab, Tabs, CheckboxField, CheckboxInput, DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, NumberInput, RouterLink, RowsTable, SboField, SelectField, SelectInput, SboWindow, TextField, TextInput, TextareaField, Toolbar, ValueHelp],
imports: [AddressDialog, Tab, Tabs, CheckboxField, CheckboxInput, DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, NumberInput, RouterLink, RowsTable, SboField, SelectField, SelectInput, SboWindow, TextField, TextInput, TextareaField, TextareaInput, Toolbar, ValueHelp],
templateUrl: './document-form.page.html',
styles: `
h1 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
@@ -296,9 +298,9 @@ export class DocumentFormPage {
protected readonly dirty = computed(() => {
const d = this.doc();
if (!d) return text(this.header()['CardCode']) !== '' || this.lines().some((l) => !isBlankLine(l, this.serviceMode())) || expensesPayload(this.expenses()).length > 0;
if (!d) return text(this.header()['CardCode']) !== '' || this.lines().some((l) => !isBlankLine(l, this.serviceMode())) || expensesPayload(this.expenses()).length > 0 || Object.keys(this.addressEdits()).length > 0;
const { body } = updatePayload(this.pick(d), this.header(), this.docLines(d), this.lines(), this.spec());
return Object.keys(body).length > 0 || (this.hasExpenses && expensesChanged(d, this.expenses()));
return Object.keys(body).length > 0 || (this.hasExpenses && expensesChanged(d, this.expenses())) || Object.keys(this.addressEdits()).length > 0;
});
// --- totales -------------------------------------------------------------------------------
@@ -501,6 +503,11 @@ export class DocumentFormPage {
if (key === 'DocType' && this.layout.service) return this.onDocType(String(value));
this.header.update((h) => ({ ...h, [key]: value }));
this.message.set(null);
// otra dirección elegida: las correcciones hechas a la anterior ya no valen
if (key === 'ShipToCode' || key === 'PayToCode') {
const side = key === 'ShipToCode' ? 'ShipTo' : 'BillTo';
this.addressEdits.update((e) => Object.fromEntries(Object.entries(e).filter(([k]) => !k.startsWith(side))));
}
// otra fecha del documento, otro tipo de cambio
if (key === 'DocDate' && this.isNew() && !this.isLocalCurrency()) void this.fetchRate(this.currencyValue());
}
@@ -538,13 +545,49 @@ export class DocumentFormPage {
protected readonly shipOptions = computed(() => this.addressOptions('ship'));
protected readonly billOptions = computed(() => this.addressOptions('bill'));
/** El texto de la dirección elegida: el que SAP guardó en el documento o, al elegir otra, el armado con los datos de la dirección del socio. */
protected addressText(f: { key: string; side: 'ship' | 'bill' }): string {
/** Correcciones del usuario a la dirección de este documento (`AddressExtension`, campos con prefijo `ShipTo`/`BillTo`). */
protected readonly addressEdits = linkedSignal<Record<string, string>>(() => (this.doc(), {}));
private sideOf(f: { side: 'ship' | 'bill' }): AddressSide {
return f.side === 'ship' ? 'ShipTo' : 'BillTo';
}
/** Las partes de la dirección sin correcciones: las del documento guardado o, si se eligió otra, las de la dirección del socio. */
private baseAddress(f: { key: string; side: 'ship' | 'bill' }): AddressValues {
const code = text(this.header()[f.key]);
const found = this.addresses().find((a) => text(a['AddressName']) === code && a['AddressType'] === (f.side === 'ship' ? 'bo_ShipTo' : 'bo_BillTo'));
if (found) return [found['Street'], found['Block'], found['ZipCode'], found['City'], found['County'], found['State']].map(text).filter((p) => p !== '').join(', ');
const saved = this.doc();
if (saved && code === text(saved[f.key])) return addressFromExtension(saved['AddressExtension'] as Record<string, unknown> | undefined, this.sideOf(f));
const type = f.side === 'ship' ? 'bo_ShipTo' : 'bo_BillTo';
return addressFromPartner(this.addresses().find((a) => text(a['AddressName']) === code && a['AddressType'] === type));
}
/** Las partes de la dirección con las correcciones del usuario encima. */
protected addressValues(f: { key: string; side: 'ship' | 'bill' }): AddressValues {
const side = this.sideOf(f);
const values = this.baseAddress(f);
for (const [key, value] of Object.entries(this.addressEdits())) if (key.startsWith(side)) values[key.slice(side.length)] = value;
return values;
}
/** El texto de la dirección elegida; si no hay datos de ella, el que SAP guardó en el documento. */
protected addressText(f: { key: string; side: 'ship' | 'bill' }): string {
const composed = composeAddress(this.addressValues(f));
if (composed) return composed;
const saved = this.doc()?.[f.side === 'ship' ? 'Address' : 'Address2'];
return code === text(this.doc()?.[f.key]) ? text(saved).replace(/\r/g, '\n') : '';
return text(this.header()[f.key]) === text(this.doc()?.[f.key]) ? text(saved).replace(/\r/g, '\n') : '';
}
/** La ventana de corrección de la dirección abierta. */
protected readonly addressDialog = signal<{ key: string; side: 'ship' | 'bill'; label: string } | null>(null);
/** Guarda lo corregido (solo lo que cambió respecto de la dirección elegida). */
protected saveAddress(f: { key: string; side: 'ship' | 'bill' }, values: AddressValues): void {
const base = this.baseAddress(f);
const side = this.sideOf(f);
const changed = Object.fromEntries(Object.entries(toExtension(values, side)).filter(([key, v]) => v !== (base[key.slice(side.length)] ?? '')));
this.addressEdits.update((e) => ({ ...Object.fromEntries(Object.entries(e).filter(([k]) => !k.startsWith(side))), ...changed }));
this.addressDialog.set(null);
this.message.set(null);
}
/** Un código numérico de SAP (lista de precios, empleado) que se escribe o elige como texto se guarda como número. */
@@ -659,6 +702,7 @@ export class DocumentFormPage {
}
const body = createPayload(header, this.lines(), this.spec());
if (this.hasExpenses && expensesPayload(this.expenses()).length) body[EXPENSES_COLLECTION] = expensesPayload(this.expenses());
if (Object.keys(this.addressEdits()).length) body['AddressExtension'] = this.addressEdits();
const created = await this.api.create(this.screen.resource, body);
await this.router.navigate(['..', created['DocEntry']], {
relativeTo: this.route,
@@ -669,6 +713,7 @@ export class DocumentFormPage {
const update = updatePayload(this.pick(d), this.header(), this.docLines(d), this.lines(), this.spec());
const body = folioAsNumber(update.body);
let replaceLines = update.replaceLines;
if (Object.keys(this.addressEdits()).length) body['AddressExtension'] = this.addressEdits();
if (this.hasExpenses && expensesChanged(d, this.expenses())) {
body[EXPENSES_COLLECTION] = expensesPayload(this.expenses());
replaceLines = true; // SAP reemplaza la colección entera