feat(web): fila del flete (gastos adicionales) y toolbar de ancho completo; la configuración de pantallas se carga en diferido

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-14 16:09:06 -05:00
1 parent d3ab97cd2e
commit d6c127627f
14 files changed
+360 -27

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, SboField, SelectField, TextField, TextInput, TextareaField } from '../../shared/form';
import { DateField, LookupField, NumberField, NumberInput, SboField, SelectField, SelectInput, 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';
@@ -19,6 +19,8 @@ import {
type DocRecord,
type NavTarget,
computeTotals,
freightOf,
freightPayload,
createPayload,
isBlankLine,
isDocumentReadOnly,
@@ -26,7 +28,7 @@ import {
updatePayload,
validateLayout,
} from './document-model';
import { type ComputedSource, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasFolio, headerKeysOf, requiredHeaderOf } from './document-layouts';
import { type ComputedSource, PARTNER_COLUMNS, WAREHOUSE_LOOKUP, hasFolio, hasFreight, 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 } from './line-columns';
@@ -60,7 +62,7 @@ const folioAsNumber = (h: DocRecord): DocRecord => {
@Component({
selector: 'sbo-document-form-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, RouterLink, SboField, SelectField, SboWindow, TextField, TextInput, TextareaField, Toolbar, ValueHelp],
imports: [DateField, Icon, LinesGrid, LookupField, MenuButton, NgTemplateOutlet, NumberField, NumberInput, RouterLink, SboField, SelectField, SelectInput, SboWindow, TextField, TextInput, TextareaField, Toolbar, ValueHelp],
templateUrl: './document-form.page.html',
styles: `
h1 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
@@ -83,8 +85,12 @@ export class DocumentFormPage {
private readonly headerKeys: readonly string[] = headerKeysOf(this.layout);
private readonly hasFolio = hasFolio(this.layout);
private readonly blankHeader: DocRecord = this.layout.newHeader();
private readonly hasFreight = hasFreight(this.layout);
private pick(doc: DocRecord | undefined): DocRecord {
return pickHeader(doc, this.headerKeys, this.blankHeader);
const header = pickHeader(doc, this.headerKeys, this.blankHeader);
if (!doc || !this.hasFreight) return header;
const freight = freightOf(doc);
return { ...header, FreightExpense: freight.expense, FreightAmount: freight.amount };
}
private docLines(doc: DocRecord | undefined): GridLine[] {
return (doc?.[this.lineSpec.key] as GridLine[] | undefined) ?? [];
@@ -107,6 +113,23 @@ export class DocumentFormPage {
: undefined,
);
/** Gastos adicionales de SAP (el flete se registra con uno de ellos). */
private readonly expensesResource = httpResource<SlListResponse<DocRecord>>(() =>
this.hasFreight ? { url: `${API_URL}/sl/AdditionalExpenses`, params: { $select: 'ExpensCode,Name', $top: 100 } } : undefined,
);
protected readonly expenseOptions = computed(() =>
(this.expensesResource.hasValue() ? this.expensesResource.value().value : []).map((e) => ({
value: String(e['ExpensCode']),
label: text(e['Name']) || String(e['ExpensCode']),
})),
);
/** Gasto que se usa si el usuario no elige: el que se llame "flete" o, si no hay, el primero. */
protected readonly defaultExpense = computed(() => {
const options = this.expenseOptions();
return (options.find((o) => /flete|freight/i.test(o.label)) ?? options[0])?.value ?? '';
});
protected readonly freightEditable = computed(() => freightOf(this.doc()).editable);
protected readonly doc = computed(() => (this.docResource.hasValue() ? this.docResource.value() : undefined));
protected readonly loading = computed(() => this.docResource.isLoading());
protected readonly loadError = computed(() => {
@@ -170,7 +193,7 @@ export class DocumentFormPage {
taxRateMap(this.taxResource.hasValue() ? this.taxResource.value().value : []),
);
protected readonly totals = computed(() =>
computeTotals(this.lines(), this.rates(), Number(this.header()['DiscountPercent'] ?? 0)),
computeTotals(this.lines(), this.rates(), Number(this.header()['DiscountPercent'] ?? 0), Number(this.header()['FreightAmount'] ?? 0)),
);
protected readonly fmt = (n: number): string => formatNumber(n, 2);
@@ -355,7 +378,10 @@ export class DocumentFormPage {
const d = this.doc();
if (!d) {
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 body = this.hasFreight
? freightPayload(createPayload(header, this.lines(), this.lineSpec), this.header(), this.defaultExpense(), 'create').body
: createPayload(header, this.lines(), this.lineSpec);
const created = await this.api.create(this.screen.resource, body);
await this.router.navigate(['..', created['DocEntry']], {
relativeTo: this.route,
state: { created: text(created['DocNum']) },
@@ -363,8 +389,13 @@ export class DocumentFormPage {
return;
}
const update = updatePayload(this.pick(d), this.header(), this.docLines(d), this.lines(), this.lineSpec);
const body = folioAsNumber(update.body);
const replaceLines = update.replaceLines;
let body = folioAsNumber(update.body);
let replaceLines = update.replaceLines;
if (this.hasFreight) {
const freight = freightPayload(body, this.header(), this.defaultExpense(), 'update', freightOf(d).amount > 0);
body = freight.body;
replaceLines = replaceLines || freight.replace;
}
if (!Object.keys(body).length) {
this.message.set({ kind: 'info', text: 'No hay cambios que guardar.' });
return;