2026-10-13 22:00:54 -05:00
import { HttpErrorResponse , httpResource } from '@angular/common/http' ;
2026-10-14 14:37:01 -05:00
import { NgTemplateOutlet } from '@angular/common' ;
2026-10-13 22:00:54 -05:00
import { ChangeDetectionStrategy , Component , computed , inject , linkedSignal , signal } from '@angular/core' ;
import { toSignal } from '@angular/core/rxjs-interop' ;
import { ActivatedRoute , Router , RouterLink } from '@angular/router' ;
import type { SlListResponse } from '@basic-erp/sap-types' ;
import { map } from 'rxjs' ;
import { API_URL , apiErrorMessage } from '../../core/api' ;
import { formatNumber } from '../../core/format' ;
import { SlDocumentApi } from '../../core/sl/sl-document' ;
2026-10-14 16:09:06 -05:00
import { DateField , LookupField , NumberField , NumberInput , SboField , SelectField , SelectInput , TextField , TextInput , TextareaField } from '../../shared/form' ;
2026-10-14 14:37:01 -05:00
import type { GridLine , GridMultiAdd } from '../../shared/grid/grid.types' ;
2026-10-13 22:00:54 -05:00
import { LinesGrid } from '../../shared/grid/lines-grid' ;
import type { VhLookup } from '../../shared/value-help/value-help.types' ;
2026-10-14 11:59:07 -05:00
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' ;
2026-10-13 22:00:54 -05:00
import {
type DocRecord ,
type NavTarget ,
computeTotals ,
2026-10-14 16:09:06 -05:00
freightOf ,
freightPayload ,
2026-10-13 22:00:54 -05:00
createPayload ,
isBlankLine ,
isDocumentReadOnly ,
taxRateMap ,
updatePayload ,
2026-10-14 14:37:01 -05:00
validateLayout ,
2026-10-13 22:00:54 -05:00
} from './document-model' ;
2026-10-16 11:07:38 -05:00
import { type ComputedSource , PARTNER_COLUMNS , WAREHOUSE_LOOKUP , hasFolio , hasFreight , hasSeries , headerKeysOf , requiredHeaderOf } from './document-layouts' ;
2026-10-14 11:59:07 -05:00
import { type CopyOrigin , copyFromDocument , copySources , copyTargets } from './document-copy' ;
import { DOCUMENT_SCREENS , type DocumentScreen , docNum , documentStatus , menuLabel } from './document-screens' ;
2026-10-16 11:07:38 -05:00
import { OBJECT_TYPES } from './document-objects' ;
2026-10-14 15:10:40 -05:00
import { ITEMS_MULTI_ADD } from './line-columns' ;
2026-10-13 22:00:54 -05:00
const today = () : string => new Date (). toISOString (). slice ( 0 , 10 );
2026-10-14 14:37:01 -05:00
const DATE_KEYS = new Set ([ 'DocDate' , 'DocDueDate' , 'RequriedDate' ]);
2026-10-14 09:13:53 -05:00
/** SAP devuelve las fechas como `2026-01-05T00:00:00Z`; el campo de fecha y el guardado usan `2026-01-05`. */
const dateOnly = ( v : unknown ) : string => ( typeof v === 'string' ? v . slice ( 0 , 10 ) : '' );
2026-10-14 14:37:01 -05:00
const pickHeader = ( doc : DocRecord | undefined , keys : readonly string [], blank : DocRecord ) : DocRecord => {
if ( ! doc ) return blank ;
2026-10-14 09:13:53 -05:00
return Object . fromEntries (
2026-10-14 14:48:50 -05:00
keys . map (( k ) => [
k ,
DATE_KEYS . has ( k ) ? dateOnly ( doc [ k ]) : k === 'FolioNumber' ? text ( doc [ k ]) : ( doc [ k ] ?? ( k === 'DiscountPercent' ? 0 : '' )),
]),
2026-10-14 09:13:53 -05:00
);
2026-10-13 22:00:54 -05:00
};
const text = ( v : unknown ) : string => ( v === null || v === undefined ? '' : String ( v ));
2026-10-14 14:48:50 -05:00
/** 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 ) };
};
2026-10-13 22:00:54 -05:00
/**
* Formulario de un documento de ventas o compras: `nuevo` (vacío) o `:docEntry` (ver / editar).
* Los cerrados y cancelados quedan en solo lectura. Al modificar se envía el `odata.etag` leído
* (`If-Match`): si otro usuario cambió el documento, SAP responde 412 y se avisa sin pisarlo.
*/
2026-10-13 20:01:34 -05:00
@Component ({
selector : 'sbo-document-form-page' ,
changeDetection : ChangeDetectionStrategy.OnPush ,
2026-10-14 16:09:06 -05:00
imports : [ DateField , Icon , LinesGrid , LookupField , MenuButton , NgTemplateOutlet , NumberField , NumberInput , RouterLink , SboField , SelectField , SelectInput , SboWindow , TextField , TextInput , TextareaField , Toolbar , ValueHelp ],
2026-10-13 22:00:54 -05:00
templateUrl : './document-form.page.html' ,
2026-10-13 20:01:34 -05:00
styles : `
h1 { margin: 0 0 6px; font-size: 18px; font-weight: 600; }
p { margin: 0; color: var(--quiet); }
a { color: var(--focus); }
2026-10-13 22:00:54 -05:00
.totals { display: grid; gap: 2px; }
2026-10-13 20:01:34 -05:00
` ,
})
export class DocumentFormPage {
private readonly route = inject ( ActivatedRoute );
2026-10-13 22:00:54 -05:00
private readonly router = inject ( Router );
private readonly api = inject ( SlDocumentApi );
2026-10-13 20:01:34 -05:00
protected readonly title = this . route . snapshot . data [ 'title' ] as string ;
protected readonly screen = this . route . snapshot . data [ 'screen' ] as DocumentScreen ;
2026-10-13 22:00:54 -05:00
protected readonly partner = this . screen . partner ;
2026-10-14 15:10:40 -05:00
/** Qué campos y columnas lleva este documento (lo declara su pantalla); el formulario solo lo dibuja. */
2026-10-14 14:37:01 -05:00
protected readonly layout = this . screen . layout ;
2026-10-14 15:10:40 -05:00
private readonly lineSpec = this . layout . lines ;
private readonly headerKeys : readonly string [] = headerKeysOf ( this . layout );
private readonly hasFolio = hasFolio ( this . layout );
private readonly blankHeader : DocRecord = this . layout . newHeader ();
2026-10-14 16:09:06 -05:00
private readonly hasFreight = hasFreight ( this . layout );
2026-10-14 14:37:01 -05:00
private pick ( doc : DocRecord | undefined ) : DocRecord {
2026-10-14 16:09:06 -05:00
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 };
2026-10-14 14:37:01 -05:00
}
private docLines ( doc : DocRecord | undefined ) : GridLine [] {
return ( doc ? .[ this . lineSpec . key ] as GridLine [] | undefined ) ?? [];
}
2026-10-13 22:00:54 -05:00
protected readonly docEntry = toSignal (
this . route . paramMap . pipe ( map (( p ) => ( p . has ( 'docEntry' ) ? Number ( p . get ( 'docEntry' )) : null ))),
{ initialValue : this.route.snapshot.paramMap.has ( 'docEntry' ) ? Number ( this . route . snapshot . paramMap . get ( 'docEntry' )) : null },
);
protected readonly isNew = computed (() => this . docEntry () === null );
// --- datos ---------------------------------------------------------------------------------
private readonly docResource = httpResource < DocRecord >(() => {
const id = this . docEntry ();
return id === null ? undefined : ` ${ API_URL } /sl/ ${ this . screen . resource } / ${ id } ` ;
});
private readonly taxResource = httpResource < SlListResponse < DocRecord >>(() =>
this . partner
? { url : ` ${ API_URL } /sl/ ${ this . partner . taxResource } ` , params : { $select : 'Code,Rate' , $top : 500 } }
: undefined ,
);
2026-10-14 16:09:06 -05:00
/** 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 );
2026-10-16 11:07:38 -05:00
/** Series de numeración de este tipo de documento (acción `SeriesService_GetDocumentSeries` de SAP). */
private readonly hasSeries = hasSeries ( this . layout );
private readonly objectType = OBJECT_TYPES [ this . screen . path ];
private readonly seriesResource = httpResource < SlListResponse < DocRecord >>(() =>
this . hasSeries && this . objectType
? {
url : ` ${ API_URL } /sl/SeriesService_GetDocumentSeries` ,
method : 'POST' ,
body : { DocumentTypeParams : { Document : String ( this . objectType ) } },
}
: undefined ,
);
protected readonly seriesValue = computed < number | null >(() => Number ( this . header ()[ 'Series' ]) || null );
/** Series elegibles: del subtipo de la pantalla y sin bloquear; la del documento abierto se incluye siempre. */
protected readonly seriesOptions = computed (() => {
const subType = this . layout . seriesSubType ?? '--' ;
const rows = this . seriesResource . hasValue () ? this . seriesResource . value (). value : [];
const options = rows
. filter (( r ) => r [ 'DocumentSubType' ] === subType && r [ 'Locked' ] !== 'tYES' )
. map (( r ) => ({ value : Number ( r [ 'Series' ]), label : text ( r [ 'Name' ]) || String ( r [ 'Series' ]) }));
const current = Number ( this . doc () ? .[ 'Series' ]);
if ( current && ! options . some (( o ) => o . value === current )) {
const known = rows . find (( r ) => Number ( r [ 'Series' ]) === current );
options . unshift ({ value : current , label : text ( known ? .[ 'Name' ]) || `Serie ${ current } ` });
}
return options ;
});
2026-10-13 22:00:54 -05:00
protected readonly doc = computed (() => ( this . docResource . hasValue () ? this . docResource . value () : undefined ));
protected readonly loading = computed (() => this . docResource . isLoading ());
protected readonly loadError = computed (() => {
const err = this . docResource . error ();
return err ? ( err instanceof HttpErrorResponse && err . status === 404 ? 'El documento no existe.' : apiErrorMessage ( err )) : '' ;
});
/** Valores editables; se reinician al cargar o recargar el documento. */
2026-10-14 14:37:01 -05:00
protected readonly header = linkedSignal < DocRecord >(() => this . pick ( this . doc ()));
protected readonly lines = linkedSignal < readonly GridLine [] >(() => this . docLines ( this . doc ()). map (( l ) => ({ ... l })));
2026-10-13 22:00:54 -05:00
protected readonly partnerName = linkedSignal (() => text ( this . doc () ? .[ 'CardName' ]));
2026-10-14 14:48:50 -05:00
/** RUT del socio: lo completa SAP desde el maestro; aquí solo se muestra. */
protected readonly partnerTaxId = linkedSignal (() => text ( this . doc () ? .[ 'FederalTaxID' ]));
2026-10-13 22:00:54 -05:00
// --- estado --------------------------------------------------------------------------------
2026-10-14 15:10:40 -05:00
protected readonly readOnly = computed (() => isDocumentReadOnly ( this . doc ()) || ( !! this . layout . immutable && ! this . isNew ()));
2026-10-13 22:00:54 -05:00
protected readonly saving = signal ( false );
private readonly message = signal < StatusMessage | null >(
(() : StatusMessage | null => {
const created = ( this . router . currentNavigation () ? . extras . state as { created? : string } | undefined ) ? . created ;
return created ? { kind : 'ok' , text : `Documento ${ created } creado.` } : null ;
})(),
);
protected readonly conflict = signal ( false );
protected readonly status = computed < StatusMessage >(() => {
if ( this . loading ()) return { kind : 'info' , text : 'Cargando…' };
if ( this . loadError ()) return { kind : 'err' , text : this.loadError () };
const message = this . message ();
if ( message ) return message ;
2026-10-14 14:37:01 -05:00
if ( this . readOnly ()) {
2026-10-14 15:10:40 -05:00
const posted = this . layout . immutable && ! isDocumentReadOnly ( this . doc ());
2026-10-14 14:37:01 -05:00
return { kind : 'info' , text : posted ? 'Documento contabilizado: solo lectura.' : 'Documento cerrado o cancelado: solo lectura.' };
}
if ( this . isNew ()) {
2026-10-14 15:10:40 -05:00
return { kind : 'info' , text : this.partner ? `Indique el ${ this . partner . label . toLowerCase () } y las líneas.` : 'Complete la cabecera y las líneas.' };
2026-10-14 14:37:01 -05:00
}
2026-10-13 22:00:54 -05:00
return { kind : 'info' , text : this.dirty () ? 'Hay cambios sin guardar.' : '' };
});
protected readonly badge = computed < WindowBadge | undefined >(() => {
if ( this . isNew ()) return { label : 'Nueva' , tone : 'info' };
const d = this . doc ();
return d ? documentStatus ( d ) : undefined ;
});
protected readonly windowTitle = computed (() => {
const num = this . doc () ? .[ 'DocNum' ];
return num === undefined ? this . title : ` ${ this . title } · ${ text ( num ) } ` ;
});
protected readonly dirty = computed (() => {
const d = this . doc ();
if ( ! d ) return text ( this . header ()[ 'CardCode' ]) !== '' || this . lines (). some (( l ) => ! isBlankLine ( l ));
2026-10-14 14:37:01 -05:00
const { body } = updatePayload ( this . pick ( d ), this . header (), this . docLines ( d ), this . lines (), this . lineSpec );
2026-10-13 22:00:54 -05:00
return Object . keys ( body ). length > 0 ;
});
// --- totales -------------------------------------------------------------------------------
private readonly rates = computed (() =>
taxRateMap ( this . taxResource . hasValue () ? this . taxResource . value (). value : []),
);
protected readonly totals = computed (() =>
2026-10-14 16:09:06 -05:00
computeTotals ( this . lines (), this . rates (), Number ( this . header ()[ 'DiscountPercent' ] ?? 0 ), Number ( this . header ()[ 'FreightAmount' ] ?? 0 )),
2026-10-13 22:00:54 -05:00
);
protected readonly fmt = ( n : number ) : string => formatNumber ( n , 2 );
2026-10-14 11:59:07 -05:00
// --- 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 () })),
);
2026-10-14 12:00:58 -05:00
protected readonly copyListTitle = computed (() => menuLabel ( this . copyListFor () ?? '' ));
2026-10-14 11:59:07 -05:00
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' ]));
2026-10-14 14:48:50 -05:00
this . partnerTaxId . set ( text ( doc [ 'FederalTaxID' ]));
2026-10-14 11:59:07 -05:00
}
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 ) });
}
}
2026-10-13 22:00:54 -05:00
// --- configuración de la vista --------------------------------------------------------------
2026-10-14 15:10:40 -05:00
protected readonly columns = this . layout . columns ;
2026-10-14 15:39:53 -05:00
private readonly itemColumn = this . layout . columns . find (( c ) => c . key === 'ItemCode' ) ?? this . layout . columns [ 0 ];
2026-10-14 15:10:40 -05:00
protected readonly newLine = () => this . layout . newLine ( this . header ());
2026-10-16 11:07:38 -05:00
/** Recalcula la línea con la tasa de cada impuesto (precio con y sin impuesto, total). */
protected readonly recalc = this . layout . recalc ? ( line : GridLine , key : string ) => this . layout . recalc ! ( line , key , this . rates ()) : undefined ;
2026-10-14 15:10:40 -05:00
protected readonly multi : GridMultiAdd = {
... ITEMS_MULTI_ADD ,
2026-10-14 15:39:53 -05:00
lookup : this.itemColumn.lookup ! . config ,
toLine : ( item ) => this . itemColumn . lookup ! . apply ( item , this . layout . newLine ( this . header ())),
};
protected readonly warehouseLookup : VhLookup = {
... WAREHOUSE_LOOKUP ,
columns : this.layout.lookups?.warehouse ?? WAREHOUSE_LOOKUP . columns ,
2026-10-14 15:10:40 -05:00
};
2026-10-13 22:00:54 -05:00
protected readonly partnerLookup : VhLookup = {
title : this.partner?.type === 'cSupplier' ? 'Proveedores' : 'Clientes' ,
resource : 'BusinessPartners' ,
baseFilter : `CardType eq ' ${ this . partner ? . type ?? 'cCustomer' } '` ,
orderby : 'CardCode' ,
2026-10-14 15:39:53 -05:00
columns : this.layout.lookups?.partner ?? PARTNER_COLUMNS ,
2026-10-14 14:48:50 -05:00
extraFields : [ 'FederalTaxID' ],
2026-10-13 22:00:54 -05:00
};
protected str = text ;
2026-10-14 15:10:40 -05:00
/** Valor de una fila de solo lectura (número del documento, datos del socio, totales). */
protected computedValue ( source : ComputedSource ) : string {
switch ( source ) {
case 'docNum' :
return text ( this . doc () ? .[ 'DocNum' ]);
case 'partnerName' :
return this . partnerName ();
case 'partnerTaxId' :
return this . partnerTaxId ();
case 'subtotal' :
return this . fmt ( this . totals (). subtotal );
case 'tax' :
return this . fmt ( this . totals (). tax );
case 'total' :
return this . fmt ( this . totals (). total );
}
}
2026-10-14 11:59:07 -05:00
constructor () {
if ( this . copyOrigin ) void this . loadCopy ( this . copyOrigin );
}
2026-10-13 22:00:54 -05:00
protected setHeader ( key : string , value : unknown ) : void {
this . header . update (( h ) => ({ ... h , [ key ] : value }));
this . message . set ( null );
}
2026-10-14 11:42:51 -05:00
/** Al escribir otro código el nombre anterior ya no vale; se completa al salir del campo o al elegir de la lista. */
protected onPartnerText ( code : string ) : void {
if ( code === text ( this . header ()[ 'CardCode' ])) return ;
this . setHeader ( 'CardCode' , code );
this . partnerName . set ( '' );
2026-10-14 14:48:50 -05:00
this . partnerTaxId . set ( '' );
2026-10-14 11:42:51 -05:00
}
protected notify ( textMessage : string ) : void {
this . message . set ({ kind : 'warn' , text : textMessage });
}
2026-10-13 22:00:54 -05:00
protected onPartner ( row : Record < string , unknown >) : void {
this . partnerName . set ( text ( row [ 'CardName' ]));
2026-10-14 14:48:50 -05:00
this . partnerTaxId . set ( text ( row [ 'FederalTaxID' ]));
2026-10-13 22:00:54 -05:00
}
protected setLines ( lines : readonly GridLine []) : void {
this . lines . set ( lines );
this . message . set ( null );
}
// --- acciones ------------------------------------------------------------------------------
protected async go ( target : NavTarget ) : Promise < void > {
try {
2026-10-15 17:13:33 -05:00
const entry = await this . api . neighbour ( this . screen . resource , target , this . docEntry (), this . screen . baseFilter );
2026-10-13 22:00:54 -05:00
if ( entry === null ) {
this . message . set ({ kind : 'warn' , text : 'No hay más documentos en esa dirección.' });
return ;
}
this . message . set ( null );
await this . router . navigate ([ '..' , entry ], { relativeTo : this.route });
} catch ( e ) {
this . message . set ({ kind : 'err' , text : apiErrorMessage ( e ) });
}
}
protected async save () : Promise < void > {
if ( this . saving () || this . readOnly ()) return ;
2026-10-14 15:10:40 -05:00
const errors = validateLayout ( this . header (), this . lines (), requiredHeaderOf ( this . layout ), this . layout . requiredLine );
if ( this . hasFolio && ! /^\d*$/ . test ( text ( this . header ()[ 'FolioNumber' ]). trim ())) errors . push ( 'El número de folio debe ser numérico.' );
2026-10-13 22:00:54 -05:00
if ( errors . length ) {
this . message . set ({ kind : 'err' , text : errors [ 0 ] });
return ;
}
this . saving . set ( true );
this . conflict . set ( false );
try {
const d = this . doc ();
if ( ! d ) {
2026-10-14 15:10:40 -05:00
const header = folioAsNumber ( this . layout . beforeCreate ? .( this . header ()) ?? this . header ());
2026-10-14 16:09:06 -05:00
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 );
2026-10-13 22:00:54 -05:00
await this . router . navigate ([ '..' , created [ 'DocEntry' ]], {
relativeTo : this.route ,
state : { created : text ( created [ 'DocNum' ]) },
});
return ;
}
2026-10-14 14:48:50 -05:00
const update = updatePayload ( this . pick ( d ), this . header (), this . docLines ( d ), this . lines (), this . lineSpec );
2026-10-14 16:09:06 -05:00
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 ;
}
2026-10-13 22:00:54 -05:00
if ( ! Object . keys ( body ). length ) {
this . message . set ({ kind : 'info' , text : 'No hay cambios que guardar.' });
return ;
}
await this . api . update ( this . screen . resource , this . docEntry () ! , body , text ( d [ 'odata.etag' ]) || undefined , replaceLines );
this . message . set ({ kind : 'ok' , text : 'Documento actualizado.' });
this . docResource . reload ();
} catch ( e ) {
if ( e instanceof HttpErrorResponse && e . status === 412 ) {
this . conflict . set ( true );
this . message . set ({
kind : 'warn' ,
text : 'Otro usuario modificó este documento. Recárguelo para ver sus cambios; los suyos no se guardaron.' ,
});
} else {
this . message . set ({ kind : 'err' , text : apiErrorMessage ( e ) });
}
} finally {
this . saving . set ( false );
}
}
protected reload () : void {
this . conflict . set ( false );
this . message . set ( null );
this . docResource . reload ();
}
2026-10-13 20:01:34 -05:00
}