feat(web): más campos en los maestros (vendedor, contabilidad, unidades, proveedor principal, valoración)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
GeorgeandClaude Sonnet 5.5 committed 2026-10-16 12:26:49 -05:00
1 parent abc1cc4ac6
commit e853b1b76c
5 files changed
+120 -2

No files matched your search

@@ -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,
}),
@@ -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<HTMLInputElement>('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' });
@@ -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> = {}): 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',
@@ -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']);
});
});
@@ -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 },
},
};