feat(web): copiar de / a entre documentos de ventas y compras
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cafe4bc035
commit
29e80181fe
8 files changed
+383
-6
No files matched your search
@@ -11,7 +11,9 @@ import { DateField, LookupField, NumberField, TextField, TextareaField } from '.
|
||||
import type { GridLine } from '../../shared/grid/grid.types';
|
||||
import { LinesGrid } from '../../shared/grid/lines-grid';
|
||||
import type { VhLookup } from '../../shared/value-help/value-help.types';
|
||||
import { Icon, SboWindow, Toolbar, type StatusMessage, type WindowBadge } from '../../shared/window';
|
||||
import { Icon, MenuButton, type MenuItem, SboWindow, Toolbar, type StatusMessage, type WindowBadge } from '../../shared/window';
|
||||
import { ValueHelp } from '../../shared/value-help/value-help';
|
||||
import type { ListRow } from '../../shared/list/list.types';
|
||||
import {
|
||||
type DocRecord,
|
||||
type NavTarget,
|
||||
@@ -23,7 +25,8 @@ import {
|
||||
updatePayload,
|
||||
validateDocument,
|
||||
} from './document-model';
|
||||
import { type DocumentScreen, documentStatus } from './document-screens';
|
||||
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). */
|
||||
@@ -51,7 +54,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, NumberField, RouterLink, SboWindow, TextField, TextareaField, Toolbar],
|
||||
imports: [DateField, Icon, LinesGrid, LookupField, MenuButton, NumberField, RouterLink, SboWindow, TextField, TextareaField, Toolbar, ValueHelp],
|
||||
templateUrl: './document-form.page.html',
|
||||
styles: `
|
||||
h1 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
|
||||
@@ -149,6 +152,80 @@ export class DocumentFormPage {
|
||||
);
|
||||
protected readonly fmt = (n: number): string => formatNumber(n, 2);
|
||||
|
||||
// --- copiar de / a ----------------------------------------------------------------------------
|
||||
|
||||
private readonly copyOrigin = (this.router.currentNavigation()?.extras.state as { copyFrom?: CopyOrigin } | undefined)?.copyFrom;
|
||||
protected readonly copyListFor = signal<string | null>(null);
|
||||
|
||||
/** "Copiar a": solo desde un documento guardado, abierto y sin cambios pendientes. */
|
||||
protected readonly copyToItems = computed<MenuItem[]>(() => {
|
||||
const blocked = this.isNew() || this.readOnly() || this.dirty();
|
||||
return copyTargets(this.screen.path).map((p) => ({ id: p, label: menuLabel(p), disabled: blocked }));
|
||||
});
|
||||
protected readonly copyFromItems = computed<MenuItem[]>(() =>
|
||||
copySources(this.screen.path).map((p) => ({ id: p, label: menuLabel(p), disabled: !this.isNew() })),
|
||||
);
|
||||
protected readonly copyListScreen = computed(() => DOCUMENT_SCREENS.find((s) => s.path === this.copyListFor()));
|
||||
/** Documentos abiertos del origen elegido, del mismo socio si ya se indicó. */
|
||||
protected readonly copyListFilter = computed(() => {
|
||||
const card = text(this.header()['CardCode']);
|
||||
return [`DocumentStatus eq 'bost_Open'`, `Cancelled eq 'tNO'`, ...(card ? [`CardCode eq '${card.replace(/'/g, "''")}'`] : [])].join(' and ');
|
||||
});
|
||||
protected readonly copyListColumns = [
|
||||
docNum,
|
||||
{ key: 'CardCode', title: this.partner?.label ?? 'Socio', width: '120px', mono: true },
|
||||
{ key: 'CardName', title: 'Nombre' },
|
||||
{ key: 'DocDate', title: 'Fecha', type: 'date', align: 'center', width: '105px' },
|
||||
] as const;
|
||||
|
||||
protected copyTo(path: string): void {
|
||||
const docEntry = this.docEntry();
|
||||
if (docEntry === null) return;
|
||||
const origin: CopyOrigin = { path: this.screen.path, docEntry };
|
||||
void this.router.navigateByUrl(`${path}/nuevo`, { state: { copyFrom: origin } });
|
||||
}
|
||||
|
||||
protected copyFrom(path: string): void {
|
||||
this.copyListFor.set(path);
|
||||
}
|
||||
|
||||
protected async copyPicked(rows: ListRow[]): Promise<void> {
|
||||
const path = this.copyListFor();
|
||||
this.copyListFor.set(null);
|
||||
const docEntry = rows[0]?.['DocEntry'];
|
||||
if (path && typeof docEntry === 'number') await this.loadCopy({ path, docEntry });
|
||||
}
|
||||
|
||||
/** Trae el documento origen y vuelca sus datos y líneas abiertas en el formulario. */
|
||||
private async loadCopy(origin: CopyOrigin): Promise<void> {
|
||||
const source = DOCUMENT_SCREENS.find((s) => s.path === origin.path);
|
||||
if (!source) return;
|
||||
try {
|
||||
const doc = await this.api.get(source.resource, origin.docEntry);
|
||||
const copied = copyFromDocument(doc, origin.path, menuLabel(origin.path));
|
||||
const card = text(this.header()['CardCode']);
|
||||
if (card && card !== text(copied.header['CardCode'])) {
|
||||
this.message.set({ kind: 'warn', text: `El documento es de ${text(copied.header['CardCode'])}; este es de ${card}.` });
|
||||
return;
|
||||
}
|
||||
if (!copied.lines.length) {
|
||||
this.message.set({ kind: 'warn', text: 'El documento no tiene líneas abiertas para copiar.' });
|
||||
return;
|
||||
}
|
||||
if (!card) {
|
||||
this.header.update((h) => ({ ...h, ...copied.header }));
|
||||
this.partnerName.set(text(doc['CardName']));
|
||||
}
|
||||
this.lines.update((ls) => [...ls.filter((l) => !isBlankLine(l)), ...copied.lines]);
|
||||
this.message.set({
|
||||
kind: 'ok',
|
||||
text: `Copiado de ${menuLabel(origin.path).toLowerCase()} ${text(doc['DocNum'])}${copied.skipped ? ` (${copied.skipped} línea(s) cerradas no se copian)` : ''}. Revise y pulse Crear.`,
|
||||
});
|
||||
} catch (e) {
|
||||
this.message.set({ kind: 'err', text: apiErrorMessage(e) });
|
||||
}
|
||||
}
|
||||
|
||||
// --- configuración de la vista --------------------------------------------------------------
|
||||
|
||||
protected readonly columns = lineColumnsFor(this.partner?.taxResource ?? 'SalesTaxCodes');
|
||||
@@ -168,6 +245,10 @@ export class DocumentFormPage {
|
||||
|
||||
protected str = text;
|
||||
|
||||
constructor() {
|
||||
if (this.copyOrigin) void this.loadCopy(this.copyOrigin);
|
||||
}
|
||||
|
||||
protected setHeader(key: string, value: unknown): void {
|
||||
this.header.update((h) => ({ ...h, [key]: value }));
|
||||
this.message.set(null);
|
||||
|
||||
Reference in new issue
Block a user