From e853b1b76c237d6d028c93566a2ed436acb26bdb Mon Sep 17 00:00:00 2001 From: George Date: Fri, 16 Oct 2026 12:26:49 -0500 Subject: [PATCH] =?UTF-8?q?feat(web):=20m=C3=A1s=20campos=20en=20los=20mae?= =?UTF-8?q?stros=20(vendedor,=20contabilidad,=20unidades,=20proveedor=20pr?= =?UTF-8?q?incipal,=20valoraci=C3=B3n)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../masters/articulos/articulos.screens.ts | 40 +++++++++++++++++++ .../features/masters/master-form.page.spec.ts | 3 +- .../app/features/masters/master-lookups.ts | 19 +++++++++ .../app/features/masters/master-model.spec.ts | 37 +++++++++++++++++ .../masters/socios-negocio/socios.screens.ts | 23 ++++++++++- 5 files changed, 120 insertions(+), 2 deletions(-) diff --git a/apps/web/src/app/features/masters/articulos/articulos.screens.ts b/apps/web/src/app/features/masters/articulos/articulos.screens.ts index 6026ba5..39fca03 100644 --- a/apps/web/src/app/features/masters/articulos/articulos.screens.ts +++ b/apps/web/src/app/features/masters/articulos/articulos.screens.ts @@ -64,6 +64,7 @@ export const ARTICULOS_SCREENS: readonly MasterScreen[] = [ field({ type: 'checkbox', key: 'InventoryItem', label: 'Artículo de inventario' }), field({ type: 'checkbox', key: 'SalesItem', label: 'Artículo de venta' }), field({ type: 'checkbox', key: 'PurchaseItem', label: 'Artículo de compra' }), + field({ type: 'checkbox', key: 'VatLiable', label: 'Sujeto a impuesto' }), ], }, { @@ -73,6 +74,27 @@ export const ARTICULOS_SCREENS: readonly MasterScreen[] = [ field({ type: 'warehouse', key: 'DefaultWarehouse', label: 'Almacén por defecto' }), field({ type: 'checkbox', key: 'ManageBatchNumbers', label: 'Gestionar por lotes' }), field({ type: 'checkbox', key: 'ManageSerialNumbers', label: 'Gestionar por series' }), + field({ + type: 'select', + key: 'CostAccountingMethod', + label: 'Método de valoración', + options: [ + { value: 'bis_MovingAverage', label: 'Promedio móvil' }, + { value: 'bis_Standard', label: 'Estándar' }, + { value: 'bis_FIFO', label: 'FIFO' }, + { value: 'bis_SNB', label: 'Por serie o lote' }, + ], + }), + field({ + type: 'select', + key: 'GLMethod', + label: 'Cuentas contables por', + options: [ + { value: 'glm_WH', label: 'Almacén' }, + { value: 'glm_ItemClass', label: 'Grupo de artículos' }, + { value: 'glm_ItemLevel', label: 'Artículo' }, + ], + }), ], right: [ field({ type: 'number', key: 'QuantityOnStock', label: 'En stock', readonly: true }), @@ -80,6 +102,21 @@ export const ARTICULOS_SCREENS: readonly MasterScreen[] = [ field({ type: 'number', key: 'MaxInventory', label: 'Stock máximo' }), ], }, + { + id: 'ventas', + label: 'Ventas', + left: [field({ type: 'text', key: 'SalesUnit', label: 'Unidad de medida de venta' })], + right: [], + }, + { + id: 'compras', + label: 'Compras', + left: [ + field({ type: 'lookup', key: 'Mainsupplier', label: 'Proveedor principal', lookup: MASTER_LOOKUPS.supplier }), + field({ type: 'text', key: 'PurchaseUnit', label: 'Unidad de medida de compra' }), + ], + right: [], + }, { id: 'observaciones', label: 'Observaciones', left: [field({ type: 'textarea', key: 'User_Text', label: 'Observaciones' })], right: [] }, ], newHeader: () => ({ @@ -89,6 +126,9 @@ export const ARTICULOS_SCREENS: readonly MasterScreen[] = [ PurchaseItem: 'tYES', ManageBatchNumbers: 'tNO', ManageSerialNumbers: 'tNO', + VatLiable: 'tYES', + CostAccountingMethod: 'bis_MovingAverage', + GLMethod: 'glm_ItemClass', MinInventory: 0, MaxInventory: 0, }), diff --git a/apps/web/src/app/features/masters/master-form.page.spec.ts b/apps/web/src/app/features/masters/master-form.page.spec.ts index 9b40da4..cd9a7ed 100644 --- a/apps/web/src/app/features/masters/master-form.page.spec.ts +++ b/apps/web/src/app/features/masters/master-form.page.spec.ts @@ -74,7 +74,7 @@ describe('MasterFormPage', () => { it('muestra el código y el nombre, el estado y las pestañas con sus campos', () => { expect(el().querySelector('.title')?.textContent).toContain('Cliente · C100'); expect(el().querySelector('.badge.tone-ok')?.textContent?.trim()).toBe('Activo'); - expect([...el().querySelectorAll('[role=tab]')].map((t) => t.textContent?.trim())).toEqual(['General', 'Condiciones de pago', 'Observaciones']); + expect([...el().querySelectorAll('[role=tab]')].map((t) => t.textContent?.trim())).toEqual(['General', 'Condiciones de pago', 'Contabilidad', 'Observaciones']); expect(labels()).toEqual(expect.arrayContaining(['Código', 'Nombre', 'RUT', 'Teléfono 1', 'Correo electrónico', 'Condición de pago', 'Límite de crédito'])); const values = [...el().querySelectorAll('input')].map((i) => i.value); expect(values).toEqual(expect.arrayContaining(['C100', 'ACME Ltda', '76111222-3', 'ventas@acme.cl'])); @@ -213,6 +213,7 @@ describe('MasterFormPage', () => { Frozen: 'tNO', CreditLimit: 0, DiscountPercent: 0, + MaxCommitment: 0, CardType: 'cCustomer', }); req.flush({ CardCode: 'C300' }); diff --git a/apps/web/src/app/features/masters/master-lookups.ts b/apps/web/src/app/features/masters/master-lookups.ts index b78c48d..c3ab9f1 100644 --- a/apps/web/src/app/features/masters/master-lookups.ts +++ b/apps/web/src/app/features/masters/master-lookups.ts @@ -1,5 +1,6 @@ import type { ListColumn } from '../../shared/list/list.types'; import type { VhLookup } from '../../shared/value-help/value-help.types'; +import { ACCOUNT_LOOKUP } from '../documents/dimension-lookups'; const col = (key: string, title: string, extra: Partial = {}): ListColumn => ({ key, title, ...extra }); @@ -34,6 +35,24 @@ export const MASTER_LOOKUPS = { valueType: 'number', columns: [col('PriceListNo', 'Código', { width: '90px', mono: true }), col('PriceListName', 'Nombre')], } satisfies VhLookup, + salesPerson: { + title: 'Empleados de ventas', + resource: 'SalesPersons', + orderby: 'SalesEmployeeCode', + valueType: 'number', + baseFilter: `Active eq 'tYES'`, + columns: [col('SalesEmployeeCode', 'Código', { width: '90px', mono: true }), col('SalesEmployeeName', 'Nombre')], + } satisfies VhLookup, + /** Cuenta contable del socio de negocio (cuentas por cobrar o por pagar). */ + account: ACCOUNT_LOOKUP, + /** Proveedor principal de un artículo. */ + supplier: { + title: 'Proveedores', + resource: 'BusinessPartners', + orderby: 'CardCode', + baseFilter: `CardType eq 'cSupplier'`, + columns: [col('CardCode', 'Código', { width: '130px', mono: true }), col('CardName', 'Nombre')], + } satisfies VhLookup, itemGroup: { title: 'Grupos de artículos', resource: 'ItemGroups', diff --git a/apps/web/src/app/features/masters/master-model.spec.ts b/apps/web/src/app/features/masters/master-model.spec.ts index 2ae0b0f..f1168a9 100644 --- a/apps/web/src/app/features/masters/master-model.spec.ts +++ b/apps/web/src/app/features/masters/master-model.spec.ts @@ -111,3 +111,40 @@ describe('pantallas de maestros', () => { } }); }); + +describe('más campos de los maestros', () => { + it('el cliente suma vendedor, tipo de empresa, compromiso máximo y los datos contables', () => { + const keys = masterKeysOf(by('/socios/clientes').layout); + expect(keys).toEqual(expect.arrayContaining(['SalesPersonCode', 'CompanyPrivate', 'MaxCommitment', 'DebitorAccount', 'AdditionalID', 'UnifiedFederalTaxID'])); + }); + + it('la cuenta del socio se elige del plan de cuentas', () => { + const f = by('/socios/proveedores').layout.tabs.find((t) => t.id === 'contabilidad')!.left.find((x) => x.type === 'lookup' && x.key === 'DebitorAccount'); + expect(f && f.type === 'lookup' && f.lookup.resource).toBe('ChartOfAccounts'); + expect(f && f.type === 'lookup' && f.label).toBe('Cuenta de proveedor'); + }); + + it('el vendedor es un código numérico elegido de los empleados de ventas activos', () => { + const f = by('/socios/clientes').layout.tabs[0].right.find((x) => x.type === 'lookup' && x.key === 'SalesPersonCode'); + expect(f && f.type === 'lookup' && f.lookup.valueType).toBe('number'); + expect(f && f.type === 'lookup' && f.lookup.resource).toBe('SalesPersons'); + }); + + it('el artículo suma unidades, proveedor principal, método de valoración y si es sujeto a impuesto', () => { + const a = by('/inventario/articulos').layout; + expect(masterKeysOf(a)).toEqual(expect.arrayContaining(['SalesUnit', 'PurchaseUnit', 'Mainsupplier', 'CostAccountingMethod', 'GLMethod', 'VatLiable'])); + expect(a.tabs.map((t) => t.id)).toEqual(['general', 'inventario', 'ventas', 'compras', 'observaciones']); + expect(a.newHeader()).toMatchObject({ VatLiable: 'tYES', CostAccountingMethod: 'bis_MovingAverage', GLMethod: 'glm_ItemClass' }); + }); + + it('el método de valoración y el tipo de empresa ofrecen los valores del enumerado de SAP', () => { + const select = (path: string, key: string) => + by(path).layout.tabs.flatMap((t) => [...t.left, ...t.right]).find((x) => x.type === 'select' && x.key === key); + const cost = select('/inventario/articulos', 'CostAccountingMethod'); + expect(cost && cost.type === 'select' && cost.options.map((o) => o.value)).toEqual(['bis_MovingAverage', 'bis_Standard', 'bis_FIFO', 'bis_SNB']); + const gl = select('/inventario/articulos', 'GLMethod'); + expect(gl && gl.type === 'select' && gl.options.map((o) => o.value)).toEqual(['glm_WH', 'glm_ItemClass', 'glm_ItemLevel']); + const type = select('/socios/clientes', 'CompanyPrivate'); + expect(type && type.type === 'select' && type.options.map((o) => o.value)).toEqual(['cCompany', 'cPrivate', 'cGovernment']); + }); +}); diff --git a/apps/web/src/app/features/masters/socios-negocio/socios.screens.ts b/apps/web/src/app/features/masters/socios-negocio/socios.screens.ts index 6a1ee8e..a3698b9 100644 --- a/apps/web/src/app/features/masters/socios-negocio/socios.screens.ts +++ b/apps/web/src/app/features/masters/socios-negocio/socios.screens.ts @@ -66,6 +66,16 @@ const partner = (opts: { path: string; type: 'cCustomer' | 'cSupplier'; singular field({ type: 'text', key: 'CardForeignName', label: 'Nombre extranjero' }), field({ type: 'lookup', key: 'GroupCode', label: 'Grupo', lookup: MASTER_LOOKUPS.partnerGroup(opts.type) }), field({ type: 'lookup', key: 'Currency', label: 'Moneda', lookup: MASTER_LOOKUPS.currency }), + field({ + type: 'select', + key: 'CompanyPrivate', + label: 'Tipo', + options: [ + { value: 'cCompany', label: 'Empresa' }, + { value: 'cPrivate', label: 'Persona natural' }, + { value: 'cGovernment', label: 'Gobierno' }, + ], + }), ], right: [ field({ type: 'text', key: 'Phone1', label: 'Teléfono 1' }), @@ -74,6 +84,7 @@ const partner = (opts: { path: string; type: 'cCustomer' | 'cSupplier'; singular field({ type: 'text', key: 'EmailAddress', label: 'Correo electrónico' }), field({ type: 'text', key: 'Website', label: 'Sitio web' }), field({ type: 'text', key: 'ContactPerson', label: 'Persona de contacto' }), + field({ type: 'lookup', key: 'SalesPersonCode', label: 'Empleado de ventas', lookup: MASTER_LOOKUPS.salesPerson }), ], }, { @@ -85,11 +96,21 @@ const partner = (opts: { path: string; type: 'cCustomer' | 'cSupplier'; singular field({ type: 'number', key: 'CreditLimit', label: 'Límite de crédito', decimals: 2 }), field({ type: 'number', key: 'DiscountPercent', label: 'Descuento %', decimals: 2 }), ], + right: [field({ type: 'number', key: 'MaxCommitment', label: 'Compromiso máximo', decimals: 2 })], + }, + { + id: 'contabilidad', + label: 'Contabilidad', + left: [ + field({ type: 'lookup', key: 'DebitorAccount', label: opts.type === 'cSupplier' ? 'Cuenta de proveedor' : 'Cuenta de cliente', lookup: MASTER_LOOKUPS.account }), + field({ type: 'text', key: 'AdditionalID', label: 'Identificación adicional' }), + field({ type: 'text', key: 'UnifiedFederalTaxID', label: 'RUT unificado' }), + ], right: [], }, { id: 'observaciones', label: 'Observaciones', left: [field({ type: 'textarea', key: 'Notes', label: 'Observaciones' })], right: [] }, ], - newHeader: () => ({ Frozen: 'tNO', CreditLimit: 0, DiscountPercent: 0 }), + newHeader: () => ({ Frozen: 'tNO', CreditLimit: 0, DiscountPercent: 0, MaxCommitment: 0 }), fixed: { CardType: opts.type }, }, };