@asteby/metacore-runtime-react 49.6.0 → 49.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/business/document-editor-model.d.ts +27 -2
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +53 -3
- package/dist/business/document-editor.d.ts +7 -2
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +116 -21
- package/dist/business/document-lines.d.ts +21 -1
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +69 -7
- package/dist/business/line-items-editor.d.ts +21 -1
- package/dist/business/line-items-editor.d.ts.map +1 -1
- package/dist/business/line-items-editor.js +74 -15
- package/dist/business/line-items.d.ts +28 -0
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +1 -0
- package/dist/business/line-product-cell.d.ts +6 -0
- package/dist/business/line-product-cell.d.ts.map +1 -1
- package/dist/business/line-product-cell.js +9 -3
- package/dist/business/product-options.js +1 -1
- package/dist/dialogs/dynamic-record.js +1 -1
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +22 -4
- package/dist/dynamic-select-field.d.ts +2 -0
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +42 -15
- package/dist/field-grid.d.ts +4 -3
- package/dist/field-grid.d.ts.map +1 -1
- package/dist/field-grid.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +4 -0
- package/dist/record-picker.d.ts +2 -0
- package/dist/record-picker.d.ts.map +1 -1
- package/dist/record-picker.js +11 -4
- package/dist/use-option-ids.d.ts +34 -0
- package/dist/use-option-ids.d.ts.map +1 -0
- package/dist/use-option-ids.js +138 -0
- package/dist/use-options-resolver.d.ts +4 -0
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +10 -1
- package/package.json +1 -1
- package/src/__tests__/document-editor-inline-selects.test.tsx +11 -4
- package/src/__tests__/edit-field-polish.test.tsx +6 -3
- package/src/__tests__/line-row-create-edit.test.tsx +312 -0
- package/src/__tests__/option-ids.test.tsx +181 -0
- package/src/business/document-editor-model.ts +60 -2
- package/src/business/document-editor.tsx +132 -17
- package/src/business/document-lines.ts +79 -7
- package/src/business/line-items-editor.tsx +158 -30
- package/src/business/line-items.ts +31 -0
- package/src/business/line-product-cell.tsx +32 -5
- package/src/business/product-options.tsx +1 -1
- package/src/dialogs/dynamic-record.tsx +1 -1
- package/src/dynamic-multi-select-field.tsx +20 -8
- package/src/dynamic-select-field.tsx +51 -17
- package/src/field-grid.tsx +5 -4
- package/src/index.ts +8 -0
- package/src/query-batch.ts +3 -0
- package/src/record-picker.tsx +16 -3
- package/src/use-option-ids.ts +168 -0
- package/src/use-options-resolver.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 49.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 13e4aaf: DocumentEditor: crear y editar el producto desde la celda del renglón. «Crear producto «texto»» y «+» abren el alta del modelo de catálogo (metacore:create-record) prellenada con lo buscado y el producto nuevo llena ESE renglón con el mismo mapeo que al elegirlo; el lápiz (hover/foco, accesible por teclado) abre la edición (metacore:edit-record) y al guardar el renglón se actualiza salvo los campos que el usuario sobrescribió (dirty por campo contra la foto del catálogo), con aviso «Precio de catálogo cambió a $X — aplicar». Los renglones de un documento origen no cambian cantidad, tope ni precios. Tarjeta de la contraparte con etiquetas de la metadata (clave corta si la larga no resuelve) y valores de catálogo «G03 · Gastos en general», sin vacíos. Placeholder corto «Buscar producto…» con la ayuda como tooltip y en el estado vacío; el buscador del renglón vacío usa el ancho de las columnas que aún no llena; la unidad bajo el SKU sale con su etiqueta (nunca la clave cruda). RecordPicker: la lista se acota al alto disponible (no se sale del viewport al abrirse hacia arriba), `maxListWidth` y nombres a 2 líneas. FieldLabel y el modal genérico usan sentence case por defecto.
|
|
8
|
+
|
|
9
|
+
## 49.7.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 468bd2b: RecordPicker: al editar un registro con una relación ya guardada, `DynamicSelectField` y `DynamicMultiSelectField` resuelven la etiqueta del valor con UNA petición `GET /options/<ref>?field=id&ids=…` sin abrir el popover (antes mostraban el UUID crudo si no había semilla). Nuevo `useResolveOptionIds`: react-query por id (usa el `QueryClient` del host o uno propio), agrupa en una sola petición los ids pedidos por todos los pickers de la misma ref en pantalla (trozos de 100), no dispara con semilla ni con el valor ya cargado, y se invalida con `invalidateOptionsCache`. Mientras carga muestra «Cargando…»; un id que ya no existe se ve como «(registro eliminado)». Compatible con hosts sin `ids`: si la respuesta trae otros registros se filtra por id y, si no está, se muestra el valor como antes. Las lecturas con `ids` no pasan por el batch `/q`. El `DynamicSelectField` de solo lectura ya no carga la primera página para resolver su etiqueta. Requiere metacore-kernel con `?ids=` (y en ops, el handler de lookup que lo reenvía) para el modo completo.
|
|
14
|
+
|
|
3
15
|
## 49.6.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -61,8 +61,33 @@ export declare function gatedOptionFixes(fields: readonly ActionFieldDef[], valu
|
|
|
61
61
|
* Solo llena lo que está vacío: nunca pisa lo que el usuario ya capturó.
|
|
62
62
|
*/
|
|
63
63
|
export declare function partyDefaults(party: Record<string, any> | null, documentFieldKeys: readonly string[], current: Record<string, unknown>): Record<string, unknown>;
|
|
64
|
-
/**
|
|
65
|
-
export
|
|
64
|
+
/** Etiqueta y opciones de catálogo de un campo (de la metadata del modelo o del propio tipo). */
|
|
65
|
+
export interface FieldDisplayMeta {
|
|
66
|
+
label?: string;
|
|
67
|
+
options?: ReadonlyArray<{
|
|
68
|
+
value: unknown;
|
|
69
|
+
label?: unknown;
|
|
70
|
+
}>;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Metadata de pantalla por clave a partir de listas de campos/columnas
|
|
74
|
+
* (`/metadata/modal` → fields, `/metadata/table` → columns, campos del tipo).
|
|
75
|
+
* La primera lista que trae etiqueta u opciones para una clave gana.
|
|
76
|
+
*/
|
|
77
|
+
export declare function fieldDisplayMeta(lists: ReadonlyArray<ReadonlyArray<unknown> | null | undefined>, translate?: (s: string) => string): Record<string, FieldDisplayMeta>;
|
|
78
|
+
/** `tax_id` → «Tax id»: último recurso cuando ningún metadata etiqueta la columna. */
|
|
79
|
+
export declare function humanizeKey(key: string): string;
|
|
80
|
+
/**
|
|
81
|
+
* Valor de catálogo legible: `G03` con la opción «Gastos en general» →
|
|
82
|
+
* «G03 · Gastos en general» (si la etiqueta ya trae el código, tal cual).
|
|
83
|
+
*/
|
|
84
|
+
export declare function formatOptionValue(value: unknown, options?: FieldDisplayMeta['options']): string;
|
|
85
|
+
/**
|
|
86
|
+
* Filas de la tarjeta de contraparte: resumen declarado + extensiones (máximo
|
|
87
|
+
* `limit`). Etiqueta del campo (metadata / labels declarados, ya traducidos),
|
|
88
|
+
* valores de catálogo con su texto y nunca claves crudas ni vacíos.
|
|
89
|
+
*/
|
|
90
|
+
export declare function partySummaryRows(party: Record<string, any> | null, summary?: readonly string[], labels?: Record<string, string>, limit?: number, meta?: Record<string, FieldDisplayMeta>): Array<{
|
|
66
91
|
key: string;
|
|
67
92
|
label: string;
|
|
68
93
|
value: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-editor-model.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-model.ts"],"names":[],"mappings":"AAMA,OAAO,EAA2B,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAIrE,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACzF,OAAO,KAAK,EACR,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,yBAAyB,EACzB,qBAAqB,EACrB,gBAAgB,EACnB,MAAM,UAAU,CAAA;AAEjB,eAAO,MAAM,gBAAgB,iBAAiB,CAAA;AAK9C,MAAM,MAAM,iBAAiB,GAAG,iBAAiB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAErE,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,gBAAgB,EAAE,QAAQ,GAAG,SAAS,CAAC,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAI7G;AAED,qEAAqE;AACrE,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,IAAI,CAAC,qBAAqB,EAAE,aAAa,CAAC,GAAG,iBAAiB,GAAG,SAAS,CAK1I;AAeD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,cAAc,EAAE,aAAa,GAAE,WAAW,CAAC,MAAM,CAAa,GAAG,OAAO,CAM1G;AAED,MAAM,WAAW,iBAAiB;IAC9B,iFAAiF;IACjF,KAAK,CAAC,EAAE,cAAc,CAAA;IACtB,8CAA8C;IAC9C,SAAS,EAAE,cAAc,EAAE,CAAA;IAC3B,uCAAuC;IACvC,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,yCAAyC;IACzC,KAAK,EAAE,cAAc,EAAE,CAAA;CAC1B;AAED,gFAAgF;AAChF,wBAAgB,iBAAiB,CAC7B,MAAM,EAAE,SAAS,cAAc,EAAE,EACjC,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAAC,eAAe,CAAC,EAAE,SAAS,cAAc,EAAE,CAAA;CAAO,GACnH,iBAAiB,CAgBnB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGzD;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,cAAc,GAAG,cAAc,CAGrE;AAOD,mFAAmF;AACnF,wBAAgB,YAAY,CAAC,CAAC,EAAE,cAAc,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,cAAc,CAG/F;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAc5H;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CACzB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,EACjC,iBAAiB,EAAE,SAAS,MAAM,EAAE,EACpC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACjC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAYzB;AAED,
|
|
1
|
+
{"version":3,"file":"document-editor-model.d.ts","sourceRoot":"","sources":["../../src/business/document-editor-model.ts"],"names":[],"mappings":"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"}
|
|
@@ -152,8 +152,55 @@ export function partyDefaults(party, documentFieldKeys, current) {
|
|
|
152
152
|
}
|
|
153
153
|
return out;
|
|
154
154
|
}
|
|
155
|
-
/**
|
|
156
|
-
|
|
155
|
+
/**
|
|
156
|
+
* Metadata de pantalla por clave a partir de listas de campos/columnas
|
|
157
|
+
* (`/metadata/modal` → fields, `/metadata/table` → columns, campos del tipo).
|
|
158
|
+
* La primera lista que trae etiqueta u opciones para una clave gana.
|
|
159
|
+
*/
|
|
160
|
+
export function fieldDisplayMeta(lists, translate = (s) => s) {
|
|
161
|
+
const out = {};
|
|
162
|
+
for (const list of lists) {
|
|
163
|
+
for (const raw of list ?? []) {
|
|
164
|
+
const f = raw;
|
|
165
|
+
const key = typeof f?.key === 'string' ? f.key : typeof f?.name === 'string' ? f.name : undefined;
|
|
166
|
+
if (!key)
|
|
167
|
+
continue;
|
|
168
|
+
const cur = (out[key] ??= {});
|
|
169
|
+
if (!cur.label && typeof f.label === 'string' && f.label)
|
|
170
|
+
cur.label = translate(f.label);
|
|
171
|
+
if (!cur.options && Array.isArray(f.options) && f.options.length > 0) {
|
|
172
|
+
cur.options = f.options.map((o) => ({ value: o?.value, label: typeof o?.label === 'string' ? translate(o.label) : o?.label }));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/** `tax_id` → «Tax id»: último recurso cuando ningún metadata etiqueta la columna. */
|
|
179
|
+
export function humanizeKey(key) {
|
|
180
|
+
const base = key.replace(EXTENSION_PREFIX, '').replace(/[_.]+/g, ' ').trim();
|
|
181
|
+
return base ? base.charAt(0).toUpperCase() + base.slice(1) : key;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Valor de catálogo legible: `G03` con la opción «Gastos en general» →
|
|
185
|
+
* «G03 · Gastos en general» (si la etiqueta ya trae el código, tal cual).
|
|
186
|
+
*/
|
|
187
|
+
export function formatOptionValue(value, options) {
|
|
188
|
+
const raw = String(value);
|
|
189
|
+
const opt = options?.find((o) => String(o.value) === raw);
|
|
190
|
+
const label = opt && typeof opt.label === 'string' ? opt.label.trim() : '';
|
|
191
|
+
if (!label || label === raw)
|
|
192
|
+
return raw;
|
|
193
|
+
if (label.includes(raw))
|
|
194
|
+
return label;
|
|
195
|
+
const codeLike = /^[A-Z0-9]{1,5}$/.test(raw) && /\d/.test(raw);
|
|
196
|
+
return codeLike ? `${raw} · ${label}` : label;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Filas de la tarjeta de contraparte: resumen declarado + extensiones (máximo
|
|
200
|
+
* `limit`). Etiqueta del campo (metadata / labels declarados, ya traducidos),
|
|
201
|
+
* valores de catálogo con su texto y nunca claves crudas ni vacíos.
|
|
202
|
+
*/
|
|
203
|
+
export function partySummaryRows(party, summary = [], labels = {}, limit = 6, meta = {}) {
|
|
157
204
|
if (!party)
|
|
158
205
|
return [];
|
|
159
206
|
const keys = new Set(summary);
|
|
@@ -166,7 +213,10 @@ export function partySummaryRows(party, summary = [], labels = {}, limit = 6) {
|
|
|
166
213
|
const v = readPath(party, key);
|
|
167
214
|
if (!hasValue(v) || typeof v === 'object')
|
|
168
215
|
continue;
|
|
169
|
-
|
|
216
|
+
if (typeof v === 'string' && !v.trim())
|
|
217
|
+
continue;
|
|
218
|
+
const m = meta[key];
|
|
219
|
+
rows.push({ key, label: labels[key] ?? m?.label ?? humanizeKey(key), value: formatOptionValue(v, m?.options) });
|
|
170
220
|
}
|
|
171
221
|
return rows.slice(0, limit);
|
|
172
222
|
}
|
|
@@ -16,8 +16,13 @@ export interface DocumentEditorProps {
|
|
|
16
16
|
onSaved?: (record?: unknown) => void;
|
|
17
17
|
onCancel: () => void;
|
|
18
18
|
searchProducts?: DocumentLinesGridProps['search'];
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Modelo de catálogo: buscador por defecto y alta/edición del producto
|
|
21
|
+
* desde la celda del renglón. Default `products.Product`.
|
|
22
|
+
*/
|
|
20
23
|
productModel?: string;
|
|
24
|
+
/** Columna del modelo de catálogo que recibe lo buscado al crear. Default `name`. */
|
|
25
|
+
productLabelField?: string;
|
|
21
26
|
defaultTaxRate?: number;
|
|
22
27
|
currency?: string;
|
|
23
28
|
/** Hoy (días de atraso del reparto; inyectable en tests). */
|
|
@@ -29,5 +34,5 @@ export interface DocumentEditorProps {
|
|
|
29
34
|
*/
|
|
30
35
|
fullscreen?: boolean;
|
|
31
36
|
}
|
|
32
|
-
export declare function DocumentEditor({ model, endpoint, forms, type, record, initialSource, onSaved, onCancel, searchProducts, productModel, defaultTaxRate, currency, today, fullscreen, }: DocumentEditorProps): import("react").JSX.Element;
|
|
37
|
+
export declare function DocumentEditor({ model, endpoint, forms, type, record, initialSource, onSaved, onCancel, searchProducts, productModel, productLabelField, defaultTaxRate, currency, today, fullscreen, }: DocumentEditorProps): import("react").JSX.Element;
|
|
33
38
|
//# sourceMappingURL=document-editor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-editor.d.ts","sourceRoot":"","sources":["../../src/business/document-editor.tsx"],"names":[],"mappings":"AA4BA,OAAO,EAAqB,KAAK,sBAAsB,EAAwB,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"document-editor.d.ts","sourceRoot":"","sources":["../../src/business/document-editor.tsx"],"names":[],"mappings":"AA4BA,OAAO,EAAqB,KAAK,sBAAsB,EAAwB,MAAM,qBAAqB,CAAA;AAsD1G,OAAO,KAAK,EAAsC,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAA;AAE3G,MAAM,WAAW,mBAAmB;IAChC,KAAK,EAAE,MAAM,CAAA;IACb,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,EAAE,qBAAqB,CAAA;IAC5B,IAAI,EAAE,gBAAgB,CAAA;IACtB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC5B,oEAAoE;IACpE,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAA;IAC3C,OAAO,CAAC,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACpC,QAAQ,EAAE,MAAM,IAAI,CAAA;IACpB,cAAc,CAAC,EAAE,sBAAsB,CAAC,QAAQ,CAAC,CAAA;IACjD;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,qFAAqF;IACrF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6DAA6D;IAC7D,KAAK,CAAC,EAAE,IAAI,CAAA;IACZ;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;CACvB;AAID,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,IAAI,EACJ,MAAM,EACN,aAAa,EACb,OAAO,EACP,QAAQ,EACR,cAAc,EACd,YAAY,EACZ,iBAA0B,EAC1B,cAAc,EACd,QAAQ,EACR,KAAK,EACL,UAAkB,GACrB,EAAE,mBAAmB,+BAo2BrB"}
|
|
@@ -30,8 +30,9 @@ import { FormErrorBanner } from './feedback';
|
|
|
30
30
|
import { DocumentLinesGrid } from './line-items-editor';
|
|
31
31
|
import { computeTotals, serializeLineItems, taxBreakdown } from './line-items';
|
|
32
32
|
import { roundMoney, toAmount, useFormatter } from './format';
|
|
33
|
-
import { createCatalogProductSearch } from './catalog-product-search';
|
|
34
|
-
import {
|
|
33
|
+
import { catalogRecordToProduct, createCatalogProductSearch } from './catalog-product-search';
|
|
34
|
+
import { requestRecordCreate, requestRecordEdit, withSearchPrefill } from '../record-picker-actions';
|
|
35
|
+
import { allocationAmountField, allocationIssueMessages, allocationPayload, creditStatus, editorLinesConfig, fieldDisplayMeta, linesFromSource, localIssues, sourceLoadSummary, sourceTracksRemaining, partyDefaults, partySummaryRows, recordLabel, seedRecord, sourceHeaderSeeds, splitEditorFields, toOpenDocuments, friendlyOptionLabel, gatedOptionFixes, gatedOptions, withFriendlyOptions, } from './document-editor-model';
|
|
35
36
|
import { CollapsibleSection, DraftPreview, EditorSection, PartyCard, PreviewPanel, TotalsPanel, ValidationChecklist, } from './document-editor-panels';
|
|
36
37
|
import { PaymentAllocator } from './payment-allocator';
|
|
37
38
|
import { actionErrorBannerMessage, buildFieldDefaults, prefillFromFieldRelationRequest, renderField } from '../action-modal-dispatcher';
|
|
@@ -43,7 +44,7 @@ import { evaluateVisibleWhen, getVisibleWhen } from '../dynamic-form-schema';
|
|
|
43
44
|
import { useInstalledAddons } from '../installed-addons-context';
|
|
44
45
|
import { allocatePayment, validateAllocation } from '../primitives/allocation';
|
|
45
46
|
import { useDocumentContributions } from '../primitives/use-contributions';
|
|
46
|
-
export function DocumentEditor({ model, endpoint, forms, type, record, initialSource, onSaved, onCancel, searchProducts, productModel, defaultTaxRate, currency, today, fullscreen = false, }) {
|
|
47
|
+
export function DocumentEditor({ model, endpoint, forms, type, record, initialSource, onSaved, onCancel, searchProducts, productModel, productLabelField = 'name', defaultTaxRate, currency, today, fullscreen = false, }) {
|
|
47
48
|
const { t, i18n } = useTranslation();
|
|
48
49
|
const lang = i18n?.language;
|
|
49
50
|
const api = useApi();
|
|
@@ -64,7 +65,8 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
64
65
|
const [linesFromSourceDoc, setLinesFromSourceDoc] = useState(false);
|
|
65
66
|
const [extFields, setExtFields] = useState([]);
|
|
66
67
|
const [party, setParty] = useState(null);
|
|
67
|
-
const [
|
|
68
|
+
const [partyFieldLists, setPartyFieldLists] = useState([]);
|
|
69
|
+
const [catalogMeta, setCatalogMeta] = useState(null);
|
|
68
70
|
// Etiquetas de los selectores que el editor llena por código (contraparte y
|
|
69
71
|
// documento origen de «Cargar desde…»): sin ellas el selector cerrado pinta el UUID.
|
|
70
72
|
const [seeds, setSeeds] = useState({});
|
|
@@ -156,28 +158,86 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
156
158
|
cancelled = true;
|
|
157
159
|
};
|
|
158
160
|
}, [api, type.party, partyId]);
|
|
159
|
-
// Etiquetas de la tarjeta (RFC, régimen…)
|
|
160
|
-
//
|
|
161
|
+
// Etiquetas de la tarjeta (RFC, régimen…) y texto de sus catálogos: la
|
|
162
|
+
// metadata del modelo de la contraparte (formulario, con extensiones, y
|
|
163
|
+
// tabla); sin ellas se veía la columna cruda (`tax_id`).
|
|
161
164
|
const partyModel = type.party?.model;
|
|
162
165
|
useEffect(() => {
|
|
163
166
|
if (!partyModel)
|
|
164
167
|
return;
|
|
165
168
|
let cancelled = false;
|
|
166
|
-
api.
|
|
167
|
-
.then((res) => {
|
|
168
|
-
const fields = res?.data?.data?.fields ?? res?.data?.fields ?? [];
|
|
169
|
-
const out = {};
|
|
170
|
-
for (const f of fields)
|
|
171
|
-
if (typeof f?.key === 'string' && typeof f?.label === 'string' && f.label)
|
|
172
|
-
out[f.key] = f.label;
|
|
169
|
+
void loadModelFieldLists(api, partyModel).then((lists) => {
|
|
173
170
|
if (!cancelled)
|
|
174
|
-
|
|
175
|
-
})
|
|
176
|
-
.catch(() => { });
|
|
171
|
+
setPartyFieldLists(lists);
|
|
172
|
+
});
|
|
177
173
|
return () => {
|
|
178
174
|
cancelled = true;
|
|
179
175
|
};
|
|
180
176
|
}, [api, partyModel]);
|
|
177
|
+
// Modelo de catálogo del renglón: alta/edición desde la celda y etiquetas
|
|
178
|
+
// de sus catálogos (unidad «unit» → «Pieza»).
|
|
179
|
+
const catalogModel = productModel ?? 'products.Product';
|
|
180
|
+
// Perezoso: solo cuando ya hay un renglón con producto (no compite con la
|
|
181
|
+
// carga inicial del editor).
|
|
182
|
+
const hasCatalogLine = lines.some((l) => l.kind === 'item' && !!l.product_id);
|
|
183
|
+
const wantsCatalogMeta = !!lineCfg && !isAllocation && hasCatalogLine;
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
if (!wantsCatalogMeta)
|
|
186
|
+
return;
|
|
187
|
+
let cancelled = false;
|
|
188
|
+
void loadModelFieldLists(api, catalogModel).then((lists) => {
|
|
189
|
+
if (!cancelled)
|
|
190
|
+
setCatalogMeta(fieldDisplayMeta(lists, tl));
|
|
191
|
+
});
|
|
192
|
+
return () => {
|
|
193
|
+
cancelled = true;
|
|
194
|
+
};
|
|
195
|
+
}, [api, catalogModel, tl, wantsCatalogMeta]);
|
|
196
|
+
/** Lee el producto guardado con la misma proyección del buscador (catalogRecordToProduct). */
|
|
197
|
+
const loadCatalogProduct = useCallback(async (id, saved) => {
|
|
198
|
+
try {
|
|
199
|
+
const res = await api.get(`/data/${catalogModel}`, { params: { f_id: `eq:${id}`, per_page: 1 } });
|
|
200
|
+
const rows = res?.data?.data;
|
|
201
|
+
const row = Array.isArray(rows) ? rows[0] : null;
|
|
202
|
+
if (row)
|
|
203
|
+
return catalogRecordToProduct({ ...(saved ?? {}), ...row }, taxRate);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Sin lectura, lo que devolvió el guardado.
|
|
207
|
+
}
|
|
208
|
+
return saved && saved.id != null ? catalogRecordToProduct(saved, taxRate) : null;
|
|
209
|
+
}, [api, catalogModel, taxRate]);
|
|
210
|
+
const createProduct = useCallback((query) => new Promise((resolve) => {
|
|
211
|
+
requestRecordCreate({
|
|
212
|
+
model: catalogModel,
|
|
213
|
+
defaults: withSearchPrefill(query, productLabelField),
|
|
214
|
+
onCreated: (rec) => {
|
|
215
|
+
if (rec?.id == null)
|
|
216
|
+
return resolve(null);
|
|
217
|
+
void loadCatalogProduct(String(rec.id), rec).then(resolve);
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
}), [catalogModel, productLabelField, loadCatalogProduct]);
|
|
221
|
+
const editProduct = useCallback((line) => new Promise((resolve) => {
|
|
222
|
+
const id = line.catalog?.product_ref ?? line.product_id;
|
|
223
|
+
if (!id)
|
|
224
|
+
return resolve(null);
|
|
225
|
+
requestRecordEdit({
|
|
226
|
+
model: catalogModel,
|
|
227
|
+
recordId: id,
|
|
228
|
+
onSaved: (rec) => void loadCatalogProduct(id, rec && typeof rec === 'object' ? { id, ...rec } : undefined).then(resolve),
|
|
229
|
+
});
|
|
230
|
+
}), [catalogModel, loadCatalogProduct]);
|
|
231
|
+
/** Unidad visible: la opción del catálogo; sin opciones declaradas, el valor tal cual. */
|
|
232
|
+
const unitLabel = useCallback((u) => {
|
|
233
|
+
if (!catalogMeta)
|
|
234
|
+
return undefined;
|
|
235
|
+
const opts = catalogMeta.unit_of_measure?.options ?? catalogMeta.unit?.options;
|
|
236
|
+
if (!opts)
|
|
237
|
+
return u;
|
|
238
|
+
const o = opts.find((x) => String(x.value) === u);
|
|
239
|
+
return typeof o?.label === 'string' && o.label && o.label !== u ? o.label : undefined;
|
|
240
|
+
}, [catalogMeta]);
|
|
181
241
|
// Cobro: documentos abiertos de la contraparte.
|
|
182
242
|
const openCfg = lineCfg?.open_documents;
|
|
183
243
|
const allocPartyId = openCfg ? header[openCfg.party_field] ?? partyId : undefined;
|
|
@@ -483,10 +543,10 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
483
543
|
};
|
|
484
544
|
const fieldRecord = useMemo(() => seedRecord(seeds, record), [seeds, record]);
|
|
485
545
|
const fieldCell = (field, fullWidth = false, className) => visible(field) ? (_jsxs(FieldCell, { fullWidth: fullWidth, className: className, children: [_jsx(FieldLabel, { htmlFor: field.key, required: field.required, tone: "sentence", children: tl(field.label) }), renderField(withFriendlyOptions(gatedOptions(field, header)), header[field.key], (v) => updateField(field.key, v), header, fieldRecord, fieldErrors), fieldErrors[field.key] && _jsx("p", { className: "mt-1 text-xs text-destructive", children: fieldErrors[field.key] })] }, field.key)) : null;
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
});
|
|
546
|
+
// Etiquetas y catálogos: la metadata de la contraparte y, para las
|
|
547
|
+
// extensiones compartidas (`fiscal_data.uso_cfdi`), los campos del propio documento.
|
|
548
|
+
const summaryMeta = useMemo(() => fieldDisplayMeta([...partyFieldLists, allFields], tl), [partyFieldLists, allFields, tl]);
|
|
549
|
+
const summaryRows = partySummaryRows(party, type.party?.summary, {}, 6, summaryMeta);
|
|
490
550
|
const advancedSummary = groups.advanced
|
|
491
551
|
.filter(visible)
|
|
492
552
|
.map((f) => {
|
|
@@ -546,7 +606,7 @@ export function DocumentEditor({ model, endpoint, forms, type, record, initialSo
|
|
|
546
606
|
const linesSection = lineCfg && (_jsxs(EditorSection, { slot: isAllocation ? 'editor-allocation' : 'editor-lines', title: linesTitle, hint: isAllocation && openBalance > 0 && amountKey ? (_jsx("button", { type: "button", className: "underline-offset-2 hover:underline", onClick: () => updateField(amountKey, openBalance), children: t('documentEditor.pay_all', { defaultValue: 'Cobrar todo ({{amount}})', amount: fmt.money(openBalance) }) })) : undefined, children: [isAllocation ? (allocPartyId ? (_jsx(PaymentAllocator, { documents: openDocs, result: allocation, value: alloc, onChange: setAlloc, currency: currency, loading: docsLoading, today: today })) : (_jsx("p", { className: "text-sm text-muted-foreground", children: t('documentEditor.pick_party_first', { defaultValue: 'Elige el cliente para ver sus documentos con saldo.' }) }))) : (_jsx(DocumentLinesGrid, { value: lines, onChange: (next) => {
|
|
547
607
|
setLines(next);
|
|
548
608
|
setServerIssues([]);
|
|
549
|
-
}, columns: lineCfg.columns ?? ['discount', 'tax'], priceSource: lineCfg.price_source ?? (kind === 'purchase' ? 'cost' : 'sale'), discountMode: lineCfg.discount_mode, mode: linesFromSourceDoc ? 'from_source' : 'free', policy: kind === 'credit' ? { allowZeroQuantity: linesFromSourceDoc } : undefined, search: search, currency: currency, showTotals: false })), !fullscreen && totalsPanel] }));
|
|
609
|
+
}, columns: lineCfg.columns ?? ['discount', 'tax'], priceSource: lineCfg.price_source ?? (kind === 'purchase' ? 'cost' : 'sale'), discountMode: lineCfg.discount_mode, mode: linesFromSourceDoc ? 'from_source' : 'free', policy: kind === 'credit' ? { allowZeroQuantity: linesFromSourceDoc } : undefined, search: search, currency: currency, showTotals: false, onCreateProduct: kind === 'credit' ? undefined : createProduct, onEditProduct: editProduct, unitLabel: unitLabel })), !fullscreen && totalsPanel] }));
|
|
550
610
|
const advanced = (groups.advanced.length > 0 || headerContribs.length > 0) && (_jsxs(CollapsibleSection, { slot: "advanced-options", title: t('documentEditor.advanced', { defaultValue: 'Opciones fiscales' }), summary: advancedSummary || t('documentEditor.advanced_defaults', { defaultValue: 'Valores sugeridos' }), open: advancedOpen, onOpenChange: setAdvancedOpen, children: [groups.advanced.length > 0 && _jsx(FieldGrid, { children: groups.advanced.map((f) => fieldCell(f)) }), headerContribs.map(({ id, component: C }) => (_jsx(C, { ...contribProps }, id)))] }));
|
|
551
611
|
const contributedSections = [...bodyContribs, ...sideContribs].map(({ id, component: C }) => (_jsx("section", { "data-contribution": id, children: _jsx(C, { ...contribProps }) }, id)));
|
|
552
612
|
const notes = groups.notes.length > 0 && _jsx(FieldGrid, { children: groups.notes.map((f) => fieldCell(f, true)) });
|
|
@@ -580,3 +640,38 @@ function SourcePicker({ source, value, seed, header, label, onPick, }) {
|
|
|
580
640
|
}), [source, label]);
|
|
581
641
|
return (_jsx("div", { className: "min-w-60 flex-1", "data-source": source.key, children: renderField(field, value, (v) => onPick(v == null || v === '' ? '' : String(v)), header, seed ? { id: seed.value, label: seed.label } : undefined) }));
|
|
582
642
|
}
|
|
643
|
+
/**
|
|
644
|
+
* Campos y columnas de un modelo para etiquetar sus valores: formulario
|
|
645
|
+
* (`/metadata/modal`, con extensiones) y tabla (`/metadata/table`). Si la
|
|
646
|
+
* clave con módulo (`customers.Customer`) no resuelve en el host, prueba la
|
|
647
|
+
* corta (`Customer`), como el puente de alta del host. Nunca lanza.
|
|
648
|
+
*/
|
|
649
|
+
async function loadModelFieldLists(api, model) {
|
|
650
|
+
const short = model.includes('.') ? model.slice(model.lastIndexOf('.') + 1) : undefined;
|
|
651
|
+
// `undefined` = la petición falló (el host no conoce esa clave); `[]` = respondió sin campos.
|
|
652
|
+
const read = async (url, key) => {
|
|
653
|
+
try {
|
|
654
|
+
const res = await api.get(url);
|
|
655
|
+
const list = res?.data?.data?.[key] ?? res?.data?.[key];
|
|
656
|
+
return Array.isArray(list) ? list : [];
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
return undefined;
|
|
660
|
+
}
|
|
661
|
+
};
|
|
662
|
+
// En serie y solo lo necesario (una lectura en el caso normal): el
|
|
663
|
+
// formulario; la clave corta solo si la larga falló; la tabla solo si
|
|
664
|
+
// ningún formulario respondió con campos.
|
|
665
|
+
const candidates = short ? [model, short] : [model];
|
|
666
|
+
for (const kind of ['modal', 'table']) {
|
|
667
|
+
for (const m of candidates) {
|
|
668
|
+
const list = await read(`/metadata/${kind}/${m}`, kind === 'modal' ? 'fields' : 'columns');
|
|
669
|
+
if (list === undefined)
|
|
670
|
+
continue;
|
|
671
|
+
if (list.length > 0)
|
|
672
|
+
return [list];
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
return [];
|
|
677
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type LineItem } from './line-items';
|
|
1
|
+
import { type CatalogLineField, type LineItem } from './line-items';
|
|
2
2
|
import { type ProductResult, type ProductVariant } from './product-search';
|
|
3
3
|
export type PriceSource = 'sale' | 'cost' | 'origin';
|
|
4
4
|
export type DocumentLinesMode = 'free' | 'from_source';
|
|
@@ -10,6 +10,26 @@ export declare function applyProductToLine(line: LineItem, product: ProductResul
|
|
|
10
10
|
warehouseId?: string;
|
|
11
11
|
quantity?: number;
|
|
12
12
|
}): LineItem;
|
|
13
|
+
/**
|
|
14
|
+
* Campos que el usuario cambió a mano respecto de lo que puso el catálogo
|
|
15
|
+
* («dirty» por campo). Sin foto (renglón libre, o cargado de un documento
|
|
16
|
+
* origen con sus propios precios) todo cuenta como capturado: nada se pisa.
|
|
17
|
+
*/
|
|
18
|
+
export declare function lineOverrides(line: LineItem): Set<CatalogLineField>;
|
|
19
|
+
/**
|
|
20
|
+
* El producto del renglón cambió en el catálogo (se editó desde la celda):
|
|
21
|
+
* actualiza SOLO los campos que siguen igual a lo que el catálogo había puesto;
|
|
22
|
+
* lo que el usuario sobrescribió se respeta y el valor nuevo queda en
|
|
23
|
+
* `catalog_pending` para ofrecer «aplicar». Nunca toca la cantidad, el tope de
|
|
24
|
+
* lo pendiente ni el vínculo con el documento origen. Siempre refresca lo que
|
|
25
|
+
* no se captura en el renglón (precio de catálogo, costo, extensiones, existencia).
|
|
26
|
+
*/
|
|
27
|
+
export declare function refreshLineFromProduct(line: LineItem, product: ProductResult, opts?: {
|
|
28
|
+
priceSource?: PriceSource;
|
|
29
|
+
warehouseId?: string;
|
|
30
|
+
}): LineItem;
|
|
31
|
+
/** «Aplicar» el aviso del catálogo: los valores pendientes entran al renglón. */
|
|
32
|
+
export declare function applyCatalogPending(line: LineItem): LineItem;
|
|
13
33
|
/**
|
|
14
34
|
* Agrega el producto como renglón. Por defecto, un segundo clic del mismo
|
|
15
35
|
* producto incrementa la cantidad (como el catálogo de Compras) y respeta `max_quantity`.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-lines.d.ts","sourceRoot":"","sources":["../../src/business/document-lines.ts"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"document-lines.d.ts","sourceRoot":"","sources":["../../src/business/document-lines.ts"],"names":[],"mappings":"AAGA,OAAO,EAAiC,KAAK,gBAAgB,EAA4B,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAA;AAC5H,OAAO,EAAkB,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE1F,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,CAAA;AACpD,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,aAAa,CAAA;AAEtD,8DAA8D;AAC9D,wBAAgB,gBAAgB,CAC5B,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,cAAc,GAAG,SAAS,EACnC,MAAM,EAAE,WAAW,GACpB,MAAM,CAKR;AAED,uGAAuG;AACvG,wBAAgB,kBAAkB,CAC9B,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,QAAQ,CAsBV;AAwBD;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAInE;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAClC,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,aAAa,EACtB,IAAI,GAAE;IAAE,WAAW,CAAC,EAAE,WAAW,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/D,QAAQ,CAmBV;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAI5D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,KAAK,EAAE,QAAQ,EAAE,EACjB,OAAO,EAAE,aAAa,EACtB,OAAO,CAAC,EAAE,cAAc,EACxB,IAAI,GAAE;IACF,WAAW,CAAC,EAAE,WAAW,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CACxB,GACP,QAAQ,EAAE,CAeZ;AAED,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAA;AAEhE,2GAA2G;AAC3G,wBAAgB,kBAAkB,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,iBAAiB,CAAA;CAAO,GACxF,eAAe,CAKjB"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Helpers del editor único de renglones (DocumentLinesGrid / LineItemsEditor).
|
|
2
2
|
// Puros: el componente solo pinta. `serializeLineItems` sigue siendo la puerta al backend.
|
|
3
3
|
import { toAmount } from './format';
|
|
4
|
-
import { makeLine } from './line-items';
|
|
4
|
+
import { CATALOG_LINE_FIELDS, makeLine } from './line-items';
|
|
5
5
|
import { availableStock } from './product-search';
|
|
6
6
|
/** Precio que se escribe en el renglón según el documento. */
|
|
7
7
|
export function priceFromProduct(product, variant, source) {
|
|
@@ -20,22 +20,84 @@ export function applyProductToLine(line, product, variant, opts = {}) {
|
|
|
20
20
|
const catalog = source === 'cost' ? (cost ?? list) : list;
|
|
21
21
|
// Nunca cantidad 0: un renglón vacío (o con la celda borrada) entra con 1.
|
|
22
22
|
const qty = opts.quantity ?? toAmount(line.quantity);
|
|
23
|
+
const values = catalogLineValues(line, product, variant, source);
|
|
24
|
+
const { catalog_pending: _pending, ...rest } = line;
|
|
23
25
|
return {
|
|
24
|
-
...
|
|
26
|
+
...rest,
|
|
25
27
|
kind: 'item',
|
|
26
28
|
product_id: variant?.id ?? product.id,
|
|
27
|
-
|
|
28
|
-
description: variant ? `${product.name} · ${variant.label}` : product.name,
|
|
29
|
+
...values,
|
|
29
30
|
quantity: qty !== 0 ? qty : 1,
|
|
30
|
-
unit_price: priceFromProduct(product, variant, source),
|
|
31
31
|
catalog_price: catalog,
|
|
32
32
|
cost,
|
|
33
|
-
tax_rate: product.tax_rate ?? line.tax_rate ?? 0,
|
|
34
|
-
unit: product.unit ?? line.unit,
|
|
35
33
|
extensions: product.extensions ? { ...(line.extensions ?? {}), ...product.extensions } : line.extensions,
|
|
36
34
|
available: availableStock(src, opts.warehouseId) ?? availableStock(product, opts.warehouseId),
|
|
35
|
+
catalog: { ...values, product_ref: product.id, variant },
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Lo que el producto escribe en los campos editables del renglón. Es EL mapeo
|
|
40
|
+
* producto → renglón: lo usan elegir, crear y editar desde la celda.
|
|
41
|
+
*/
|
|
42
|
+
function catalogLineValues(line, product, variant, source) {
|
|
43
|
+
return {
|
|
44
|
+
description: variant ? `${product.name} · ${variant.label}` : product.name,
|
|
45
|
+
sku: variant?.sku ?? product.sku,
|
|
46
|
+
unit_price: priceFromProduct(product, variant, source),
|
|
47
|
+
tax_rate: product.tax_rate ?? line.tax_rate ?? 0,
|
|
48
|
+
unit: product.unit ?? line.unit,
|
|
37
49
|
};
|
|
38
50
|
}
|
|
51
|
+
const sameValue = (a, b) => typeof a === 'number' || typeof b === 'number' ? toAmount(a) === toAmount(b) : (a ?? '') === (b ?? '');
|
|
52
|
+
/**
|
|
53
|
+
* Campos que el usuario cambió a mano respecto de lo que puso el catálogo
|
|
54
|
+
* («dirty» por campo). Sin foto (renglón libre, o cargado de un documento
|
|
55
|
+
* origen con sus propios precios) todo cuenta como capturado: nada se pisa.
|
|
56
|
+
*/
|
|
57
|
+
export function lineOverrides(line) {
|
|
58
|
+
const snap = line.catalog;
|
|
59
|
+
if (!snap)
|
|
60
|
+
return new Set(CATALOG_LINE_FIELDS);
|
|
61
|
+
return new Set(CATALOG_LINE_FIELDS.filter((f) => !sameValue(line[f], snap[f])));
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* El producto del renglón cambió en el catálogo (se editó desde la celda):
|
|
65
|
+
* actualiza SOLO los campos que siguen igual a lo que el catálogo había puesto;
|
|
66
|
+
* lo que el usuario sobrescribió se respeta y el valor nuevo queda en
|
|
67
|
+
* `catalog_pending` para ofrecer «aplicar». Nunca toca la cantidad, el tope de
|
|
68
|
+
* lo pendiente ni el vínculo con el documento origen. Siempre refresca lo que
|
|
69
|
+
* no se captura en el renglón (precio de catálogo, costo, extensiones, existencia).
|
|
70
|
+
*/
|
|
71
|
+
export function refreshLineFromProduct(line, product, opts = {}) {
|
|
72
|
+
const source = opts.priceSource ?? 'sale';
|
|
73
|
+
const variant = line.catalog?.variant;
|
|
74
|
+
const fresh = applyProductToLine(line, product, variant, { ...opts, quantity: toAmount(line.quantity) });
|
|
75
|
+
const overrides = lineOverrides(line);
|
|
76
|
+
const next = { ...fresh, product_id: line.product_id ?? fresh.product_id, quantity: line.quantity, max_quantity: line.max_quantity, source_line_id: line.source_line_id };
|
|
77
|
+
const pending = {};
|
|
78
|
+
const values = catalogLineValues(line, product, variant, source);
|
|
79
|
+
for (const f of CATALOG_LINE_FIELDS) {
|
|
80
|
+
if (!overrides.has(f))
|
|
81
|
+
continue;
|
|
82
|
+
next[f] = line[f];
|
|
83
|
+
if (!sameValue(values[f], line[f]))
|
|
84
|
+
pending[f] = values[f];
|
|
85
|
+
}
|
|
86
|
+
// La foto avanza a los valores nuevos del catálogo: el override sigue siéndolo.
|
|
87
|
+
next.catalog = { ...values, product_ref: line.catalog?.product_ref ?? product.id, variant };
|
|
88
|
+
if (Object.keys(pending).length > 0)
|
|
89
|
+
next.catalog_pending = pending;
|
|
90
|
+
else
|
|
91
|
+
delete next.catalog_pending;
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
/** «Aplicar» el aviso del catálogo: los valores pendientes entran al renglón. */
|
|
95
|
+
export function applyCatalogPending(line) {
|
|
96
|
+
if (!line.catalog_pending)
|
|
97
|
+
return line;
|
|
98
|
+
const { catalog_pending: pending, ...rest } = line;
|
|
99
|
+
return { ...rest, ...pending };
|
|
100
|
+
}
|
|
39
101
|
/**
|
|
40
102
|
* Agrega el producto como renglón. Por defecto, un segundo clic del mismo
|
|
41
103
|
* producto incrementa la cantidad (como el catálogo de Compras) y respeta `max_quantity`.
|
|
@@ -45,10 +45,30 @@ export interface DocumentLinesGridProps {
|
|
|
45
45
|
* ya muestra sus propios totales (DocumentEditor) lo apaga.
|
|
46
46
|
*/
|
|
47
47
|
showTotals?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Alta del producto desde la celda («+» y «Crear producto «texto»»). Resuelve
|
|
50
|
+
* con el producto guardado (o nada si se canceló): el renglón se llena con
|
|
51
|
+
* el mismo mapeo que al elegir uno existente.
|
|
52
|
+
*/
|
|
53
|
+
onCreateProduct?: (query: string) => Promise<ProductResult | null | undefined>;
|
|
54
|
+
/**
|
|
55
|
+
* Edición del producto del renglón (lápiz en la celda). Resuelve con el
|
|
56
|
+
* producto ya guardado: el renglón se actualiza salvo los campos que el
|
|
57
|
+
* usuario sobrescribió (ver `refreshLineFromProduct`).
|
|
58
|
+
*/
|
|
59
|
+
onEditProduct?: (line: LineItem) => Promise<ProductResult | null | undefined>;
|
|
60
|
+
/** Nombre del modelo de catálogo para «Crear …» / «Editar …» (default «producto»). */
|
|
61
|
+
productLabel?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Etiqueta visible de la unidad (`unit` → «pza»). Si devuelve vacío, la
|
|
64
|
+
* unidad no se muestra bajo el SKU (nunca la clave cruda). Sin esta prop se
|
|
65
|
+
* muestra el valor tal cual.
|
|
66
|
+
*/
|
|
67
|
+
unitLabel?: (unit: string) => string | undefined;
|
|
48
68
|
}
|
|
49
69
|
/** @deprecated Usa `DocumentLinesGridProps`. Es el mismo contrato. */
|
|
50
70
|
export type LineItemsEditorProps = DocumentLinesGridProps;
|
|
51
|
-
export declare function DocumentLinesGrid({ value, onChange, columns, policy, serverErrors, onValidate, onRequestProduct, search, priceSource, mode, discountMode, quantityStepper, mergeSameProduct, warehouseId, allowSections, readOnly, editPermission, currency, showTotals, }: DocumentLinesGridProps): import("react").JSX.Element;
|
|
71
|
+
export declare function DocumentLinesGrid({ value, onChange, columns, policy, serverErrors, onValidate, onRequestProduct, search, priceSource, mode, discountMode, quantityStepper, mergeSameProduct, warehouseId, allowSections, readOnly, editPermission, currency, showTotals, onCreateProduct, onEditProduct, productLabel, unitLabel, }: DocumentLinesGridProps): import("react").JSX.Element;
|
|
52
72
|
/** Mismo componente. El nombre anterior sigue exportado. */
|
|
53
73
|
export declare const LineItemsEditor: typeof DocumentLinesGrid;
|
|
54
74
|
//# sourceMappingURL=line-items-editor.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"line-items-editor.d.ts","sourceRoot":"","sources":["../../src/business/line-items-editor.tsx"],"names":[],"mappings":"AASA,OAAO,
|
|
1
|
+
{"version":3,"file":"line-items-editor.d.ts","sourceRoot":"","sources":["../../src/business/line-items-editor.tsx"],"names":[],"mappings":"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"}
|