feat(web): asiento con indicador, folio, referencias 1 a 3 (cabecera y línea), cuenta/socio y vencimiento por línea

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-16 14:44:15 -05:00
1 parent 8bbd05d07b
commit eb84d73719
4 files changed
+60 -6

No files matched your search

@@ -26,10 +26,16 @@
</div>
<div class="sbo-hdr">
<div class="sbo-col">
<sbo-text-field label="Referencia 1" [value]="reference()" (valueChange)="set('reference', $event)" [readonly]="saved()" />
<sbo-lookup-field label="Indicador" [lookup]="indicatorLookup" [value]="indicator()" (valueChange)="indicator.set($event)" [readonly]="saved()" />
<sbo-field label="Folio">
<sbo-input width="90px" [maxlength]="10" ariaLabel="Prefijo del folio" [readonly]="saved()" [value]="folioPrefix()" (valueChange)="set('folioPrefix', $event)" />
<sbo-input ariaLabel="Número de folio" [readonly]="saved()" [value]="folioNumber()" (valueChange)="set('folioNumber', $event)" />
</sbo-field>
</div>
<div class="sbo-col">
<sbo-text-field label="Referencia 1" [value]="reference()" (valueChange)="set('reference', $event)" [readonly]="saved()" />
<sbo-text-field label="Referencia 2" [value]="reference2()" (valueChange)="set('reference2', $event)" [readonly]="saved()" />
<sbo-text-field label="Referencia 3" [value]="reference3()" (valueChange)="set('reference3', $event)" [readonly]="saved()" />
</div>
</div>
@@ -7,11 +7,11 @@ 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, SelectField, TextField } from '../../shared/form';
import { DateField, LookupField, SboField, SelectField, TextField, TextInput } from '../../shared/form';
import type { DetailColumn } from '../../shared/grid/grid.types';
import { RowsTable } from '../../shared/grid/rows-table';
import { Icon, SboWindow, Toolbar, type StatusMessage, type WindowBadge } from '../../shared/window';
import { ACCOUNT_LOOKUP, PROJECT_LOOKUP, dimensionLookup } from '../documents/dimension-lookups';
import { ACCOUNT_LOOKUP, INDICATOR_LOOKUP, PROJECT_LOOKUP, dimensionLookup } from '../documents/dimension-lookups';
import type { NavTarget } from '../documents/document-model';
import { isManualJournal, journalOrigin, journalPayload, journalTotals, validateJournal } from './journal-model';
@@ -32,7 +32,7 @@ const dateOnly = (v: unknown): string => (typeof v === 'string' ? v.slice(0, 10)
@Component({
selector: 'sbo-journal-form-page',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DateField, Icon, RouterLink, RowsTable, SboWindow, SelectField, TextField, Toolbar],
imports: [DateField, Icon, LookupField, RouterLink, RowsTable, SboField, SboWindow, SelectField, TextField, TextInput, Toolbar],
templateUrl: './journal-form.page.html',
styleUrl: './journal-form.page.css',
})
@@ -71,6 +71,11 @@ export class JournalFormPage {
protected readonly memo = linkedSignal(() => text(this.doc()?.['Memo']));
protected readonly reference = linkedSignal(() => text(this.doc()?.['Reference']));
protected readonly reference2 = linkedSignal(() => text(this.doc()?.['Reference2']));
protected readonly reference3 = linkedSignal(() => text(this.doc()?.['Reference3']));
protected readonly indicator = linkedSignal(() => text(this.doc()?.['Indicator']));
protected readonly folioPrefix = linkedSignal(() => text(this.doc()?.['FolioPrefixString']));
protected readonly folioNumber = linkedSignal(() => text(this.doc()?.['FolioNumber']));
protected readonly indicatorLookup = INDICATOR_LOOKUP;
protected readonly lines = linkedSignal<Row[]>(() => {
const rows = ((this.doc()?.['JournalEntryLines'] as Row[] | undefined) ?? []).map((l) => ({ ...l }));
return this.doc() ? rows : [{}, {}];
@@ -102,8 +107,13 @@ export class JournalFormPage {
protected readonly columns: readonly DetailColumn[] = [
{ key: 'AccountCode', title: 'Cuenta', type: 'lookup', required: true, lookup: ACCOUNT_LOOKUP, width: '170px' },
{ key: 'ShortName', title: 'Cuenta / socio', type: 'text', width: '150px' },
{ key: 'Debit', title: 'Debe', type: 'number', decimals: 2, width: '120px' },
{ key: 'Credit', title: 'Haber', type: 'number', decimals: 2, width: '120px' },
{ key: 'DueDate', title: 'Vencimiento', type: 'date', width: '120px' },
{ key: 'Reference1', title: 'Ref. 1', type: 'text', width: '110px' },
{ key: 'Reference2', title: 'Ref. 2', type: 'text', width: '110px' },
{ key: 'AdditionalReference', title: 'Ref. 3', type: 'text', width: '110px' },
{ key: 'LineMemo', title: 'Comentarios', type: 'text', width: '220px' },
...([1, 2, 3, 4, 5] as const).map((n): DetailColumn => ({
key: n === 1 ? 'CostingCode' : `CostingCode${n}`,
@@ -163,7 +173,7 @@ export class JournalFormPage {
this.setLines(rows);
}
protected set(key: 'memo' | 'reference' | 'reference2', value: string): void {
protected set(key: 'memo' | 'reference' | 'reference2' | 'reference3' | 'folioPrefix' | 'folioNumber', value: string): void {
this[key].set(value);
this.message.set(null);
}
@@ -192,6 +202,10 @@ export class JournalFormPage {
memo: this.memo(),
reference: this.reference(),
reference2: this.reference2(),
reference3: this.reference3(),
indicator: this.indicator(),
folioPrefix: this.folioPrefix(),
folioNumber: this.folioNumber(),
lines: this.lines(),
};
const errors = validateJournal(draft);
@@ -9,6 +9,10 @@ const draft = (over: Partial<JournalDraft> = {}): JournalDraft => ({
memo: '',
reference: '',
reference2: '',
reference3: '',
indicator: '',
folioPrefix: '',
folioNumber: '',
lines: [
{ AccountCode: '1-1-010-10-001', Debit: 1000 },
{ AccountCode: '6-1-010-40-000', Credit: 1000 },
@@ -60,6 +64,26 @@ describe('asiento contable', () => {
});
});
it('indicador, folio y referencias de cabecera y de línea viajan si se indicaron', () => {
const body = journalPayload(
draft({
indicator: '33',
folioPrefix: 'FA',
folioNumber: '123',
reference: 'R1',
reference2: 'R2',
reference3: 'R3',
lines: [
{ AccountCode: 'A', ShortName: 'C1', Debit: 5, DueDate: '2026-11-16', Reference1: 'L1', Reference2: 'L2', AdditionalReference: 'L3' },
{ AccountCode: 'B', Credit: 5 },
],
}),
);
expect(body).toMatchObject({ Indicator: '33', FolioPrefixString: 'FA', FolioNumber: 123, Reference: 'R1', Reference2: 'R2', Reference3: 'R3' });
expect((body['JournalEntryLines'] as unknown[])[0]).toEqual({ AccountCode: 'A', ShortName: 'C1', Debit: 5, DueDate: '2026-11-16', Reference1: 'L1', Reference2: 'L2', AdditionalReference: 'L3' });
expect(validateJournal(draft({ folioNumber: '12a' }))).toEqual(['El número de folio debe ser numérico.']);
});
it('solo los asientos manuales se cancelan; el origen se muestra en español', () => {
expect(isManualJournal({ OriginalJournal: 'ttJournalEntry' })).toBe(true);
expect(isManualJournal({ OriginalJournal: 'ttARInvoice' })).toBe(false);
@@ -5,7 +5,7 @@ const round2 = (n: number): number => Math.round((n + Number.EPSILON) * 100) / 1
const has = (v: unknown): boolean => !(v === null || v === undefined || (typeof v === 'string' && v.trim() === ''));
/** Campos de la línea que se envían a SAP además de la cuenta y los importes. */
export const JOURNAL_LINE_TEXT_FIELDS = ['LineMemo', 'CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode'] as const;
export const JOURNAL_LINE_TEXT_FIELDS = ['ShortName', 'DueDate', 'Reference1', 'Reference2', 'AdditionalReference', 'LineMemo', 'CostingCode', 'CostingCode2', 'CostingCode3', 'CostingCode4', 'CostingCode5', 'ProjectCode'] as const;
export interface JournalDraft {
referenceDate: string;
@@ -15,6 +15,11 @@ export interface JournalDraft {
memo: string;
reference: string;
reference2: string;
reference3: string;
indicator: string;
folioPrefix: string;
/** Se edita como texto; SAP lo espera como número. */
folioNumber: string;
lines: Row[];
}
@@ -38,6 +43,7 @@ export function journalTotals(lines: readonly Row[]): JournalTotals {
export function validateJournal(d: JournalDraft): string[] {
const errors: string[] = [];
if (!has(d.referenceDate)) errors.push('Indique la fecha de contabilización.');
if (!/^\d*$/.test(d.folioNumber.trim())) errors.push('El número de folio debe ser numérico.');
const lines = d.lines.filter((l) => !isBlank(l));
if (lines.length < 2) errors.push('Un asiento necesita al menos dos líneas.');
lines.forEach((l, i) => {
@@ -62,6 +68,10 @@ export function journalPayload(d: JournalDraft): Row {
if (has(d.memo)) body['Memo'] = d.memo;
if (has(d.reference)) body['Reference'] = d.reference;
if (has(d.reference2)) body['Reference2'] = d.reference2;
if (has(d.reference3)) body['Reference3'] = d.reference3;
if (has(d.indicator)) body['Indicator'] = d.indicator;
if (has(d.folioPrefix)) body['FolioPrefixString'] = d.folioPrefix.trim();
if (has(d.folioNumber)) body['FolioNumber'] = Number(d.folioNumber);
body['JournalEntryLines'] = d.lines
.filter((l) => !isBlank(l))
.map((l) => {