@asteby/metacore-runtime-react 46.2.3 → 46.3.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/business/catalog.d.ts.map +1 -1
  3. package/dist/business/catalog.js +16 -4
  4. package/dist/business/document-lines.d.ts +30 -0
  5. package/dist/business/document-lines.d.ts.map +1 -0
  6. package/dist/business/document-lines.js +65 -0
  7. package/dist/business/index.d.ts +1 -0
  8. package/dist/business/index.d.ts.map +1 -1
  9. package/dist/business/index.js +1 -0
  10. package/dist/business/line-items-editor.d.ts +28 -4
  11. package/dist/business/line-items-editor.d.ts.map +1 -1
  12. package/dist/business/line-items-editor.js +139 -14
  13. package/dist/business/line-items.d.ts +24 -2
  14. package/dist/business/line-items.d.ts.map +1 -1
  15. package/dist/business/line-items.js +50 -5
  16. package/dist/business/product-search.d.ts +4 -0
  17. package/dist/business/product-search.d.ts.map +1 -1
  18. package/dist/business/product-search.js +5 -1
  19. package/dist/dynamic-form-schema.d.ts +9 -0
  20. package/dist/dynamic-form-schema.d.ts.map +1 -1
  21. package/dist/dynamic-form-schema.js +61 -0
  22. package/dist/dynamic-line-items.d.ts.map +1 -1
  23. package/dist/dynamic-line-items.js +18 -8
  24. package/dist/dynamic-select-field.d.ts +3 -1
  25. package/dist/dynamic-select-field.d.ts.map +1 -1
  26. package/dist/dynamic-select-field.js +2 -1
  27. package/dist/use-options-resolver.d.ts +5 -5
  28. package/dist/use-options-resolver.d.ts.map +1 -1
  29. package/dist/use-options-resolver.js +11 -0
  30. package/package.json +1 -1
  31. package/src/__tests__/document-lines-grid.test.tsx +79 -0
  32. package/src/__tests__/line-item-formulas.test.ts +18 -1
  33. package/src/__tests__/use-options-resolver.test.ts +5 -0
  34. package/src/business/business.test.ts +43 -0
  35. package/src/business/catalog.ts +16 -4
  36. package/src/business/document-lines.ts +91 -0
  37. package/src/business/index.ts +1 -0
  38. package/src/business/line-items-editor.tsx +365 -46
  39. package/src/business/line-items.ts +65 -7
  40. package/src/business/product-search.ts +9 -1
  41. package/src/dynamic-form-schema.ts +60 -0
  42. package/src/dynamic-line-items.tsx +23 -3
  43. package/src/dynamic-select-field.tsx +4 -0
  44. package/src/use-options-resolver.ts +15 -0
@@ -2,12 +2,15 @@ import { describe, expect, it } from 'vitest'
2
2
  import { createFormatter, formatDate, formatDateTime, formatMoney, roundMoney, toAmount } from './format'
3
3
  import { mapApiError } from './field-errors'
4
4
  import {
5
+ computeLine,
5
6
  computeTotals,
6
7
  makeLine,
7
8
  parseLineItems,
8
9
  serializeLineItems,
10
+ taxBreakdown,
9
11
  validateLineItems,
10
12
  } from './line-items'
13
+ import { addProductLine, lineGridKeyCommand, priceFromProduct } from './document-lines'
11
14
  import { newTender, serializePayment, summarizePayment, validatePayment, type PaymentMethodOption } from './payment'
12
15
  import { singleDestination, validateRefund } from './refund'
13
16
  import { parseProductQuery, parseTireSize, productToLine } from './product-search'
@@ -94,8 +97,48 @@ describe('line items', () => {
94
97
  expect(out[0]).toEqual({ position: 1, kind: 'item', description: 'Llanta', quantity: 2, unit_price: 0, discount: 0, tax_rate: 0, lot: 'L1' })
95
98
  expect(out[1]).toEqual({ position: 2, kind: 'note', description: 'Nota' })
96
99
  expect(out[0]).not.toHaveProperty('key')
100
+ expect(out[0]).not.toHaveProperty('catalog_price')
101
+ expect(out[0]).not.toHaveProperty('discount_kind')
102
+ expect(out[0]).not.toHaveProperty('unit')
97
103
  expect(parseLineItems(out)).toHaveLength(2)
98
104
  })
105
+ it('2 × 800 es 1600 neto; el IVA 16% suma 256 y el total 1856', () => {
106
+ const line = makeLine({ quantity: 2, unit_price: 800, tax_rate: 0.16 })
107
+ expect(computeLine(line)).toMatchObject({ gross: 1600, net: 1600, tax: 256, total: 1856 })
108
+ expect(computeTotals([line])).toMatchObject({ subtotal: 1600, tax: 256, total: 1856 })
109
+ expect(taxBreakdown([line])).toEqual([{ rate: 0.16, base: 1600, tax: 256 }])
110
+ })
111
+ it('descuento en importe y tope de cantidad', () => {
112
+ const line = makeLine({ description: 'x', quantity: 1, unit_price: 100, discount: 15, discount_kind: 'amount' })
113
+ expect(computeLine(line)).toMatchObject({ discount: 15, net: 85, total: 85 })
114
+ expect(validateLineItems([makeLine({ description: 'x', quantity: 3, max_quantity: 2 })]).errors['0.quantity']).toMatch(/máximo/)
115
+ expect(validateLineItems([makeLine({ description: 'x', quantity: 0 })], { allowZeroQuantity: true }).valid).toBe(true)
116
+ const out = serializeLineItems([makeLine({ description: 'A', unit: 'pza', catalog_price: 800, discount_kind: 'amount', discount: 10 })])
117
+ expect(out[0]).toMatchObject({ unit: 'pza', catalog_price: 800, discount_kind: 'amount', discount: 10 })
118
+ expect(out[0]).not.toHaveProperty('cost')
119
+ expect(out[0]).not.toHaveProperty('max_quantity')
120
+ })
121
+ })
122
+
123
+ describe('document lines', () => {
124
+ const tire = { id: 'p1', name: 'Llanta X', sku: 'LX', price: 1200, cost: 800, tax_rate: 0.16 }
125
+ it('compras usa el costo y un segundo clic suma cantidad', () => {
126
+ expect(priceFromProduct(tire, undefined, 'cost')).toBe(800)
127
+ expect(priceFromProduct(tire, undefined, 'sale')).toBe(1200)
128
+ const once = addProductLine([], tire, undefined, { priceSource: 'cost' })
129
+ expect(once[0]).toMatchObject({ quantity: 1, unit_price: 800, catalog_price: 800, sku: 'LX', description: 'Llanta X' })
130
+ const twice = addProductLine(once, tire, undefined, { priceSource: 'cost' })
131
+ expect(twice).toHaveLength(1)
132
+ expect(twice[0]!.quantity).toBe(2)
133
+ expect(computeLine(twice[0]!)).toMatchObject({ net: 1600, tax: 256, total: 1856 })
134
+ })
135
+ it('teclado: Enter agrega, Supr en celda vacía borra, no con sugerencias abiertas', () => {
136
+ expect(lineGridKeyCommand('Enter', { mode: 'free' })).toBe('add')
137
+ expect(lineGridKeyCommand('Enter', { suggestionsOpen: true })).toBe('none')
138
+ expect(lineGridKeyCommand('Enter', { mode: 'from_source' })).toBe('none')
139
+ expect(lineGridKeyCommand('Backspace', { cellEmpty: true })).toBe('delete')
140
+ expect(lineGridKeyCommand('Tab')).toBe('next')
141
+ })
99
142
  })
100
143
 
101
144
  describe('payment', () => {
@@ -34,12 +34,24 @@ export const BUSINESS_COMPONENTS: readonly BusinessComponentSpec[] = [
34
34
  helpers: ['parseProductQuery', 'parseTireSize', 'availableStock', 'productToLine'],
35
35
  },
36
36
  {
37
- name: 'LineItemsEditor',
38
- value: 'value: LineItem[]',
37
+ name: 'DocumentLinesGrid',
38
+ value: 'value: LineItem[] (LineItemsEditor es el mismo componente)',
39
39
  events: ['onChange', 'onValidate', 'onRequestProduct'],
40
40
  permissions: ['editPermission (prop, opcional)'],
41
- usedBy: ['Cotización', 'Pedido', 'Factura', 'OC', 'Recepción', 'OT', 'RMA', 'NC'],
42
- helpers: ['makeLine', 'computeLine', 'computeTotals', 'validateLineItems', 'serializeLineItems', 'parseLineItems'],
41
+ usedBy: ['Cotización', 'Pedido', 'Factura', 'OC', 'Recepción', 'OT', 'RMA', 'NC', 'Devolución'],
42
+ helpers: [
43
+ 'makeLine',
44
+ 'computeLine',
45
+ 'computeTotals',
46
+ 'taxBreakdown',
47
+ 'validateLineItems',
48
+ 'serializeLineItems',
49
+ 'parseLineItems',
50
+ 'priceFromProduct',
51
+ 'applyProductToLine',
52
+ 'addProductLine',
53
+ 'lineGridKeyCommand',
54
+ ],
43
55
  },
44
56
  {
45
57
  name: 'PaymentCapture',
@@ -0,0 +1,91 @@
1
+ // Helpers del editor único de renglones (DocumentLinesGrid / LineItemsEditor).
2
+ // Puros: el componente solo pinta. `serializeLineItems` sigue siendo la puerta al backend.
3
+ import { toAmount } from './format'
4
+ import { makeLine, type LineItem } from './line-items'
5
+ import { availableStock, type ProductResult, type ProductVariant } from './product-search'
6
+
7
+ export type PriceSource = 'sale' | 'cost' | 'origin'
8
+ export type DocumentLinesMode = 'free' | 'from_source'
9
+
10
+ /** Precio que se escribe en el renglón según el documento. */
11
+ export function priceFromProduct(
12
+ product: ProductResult,
13
+ variant: ProductVariant | undefined,
14
+ source: PriceSource,
15
+ ): number {
16
+ const list = variant?.price ?? product.price
17
+ const cost = variant?.cost ?? product.cost
18
+ if (source === 'cost') return cost ?? list ?? 0
19
+ return list ?? 0
20
+ }
21
+
22
+ /** Rellena un renglón con el producto elegido (nombre, SKU, precio, costo de catálogo, existencia). */
23
+ export function applyProductToLine(
24
+ line: LineItem,
25
+ product: ProductResult,
26
+ variant?: ProductVariant,
27
+ opts: { priceSource?: PriceSource; warehouseId?: string; quantity?: number } = {},
28
+ ): LineItem {
29
+ const source = opts.priceSource ?? 'sale'
30
+ const src = variant ?? product
31
+ const list = variant?.price ?? product.price
32
+ const cost = variant?.cost ?? product.cost
33
+ const catalog = source === 'cost' ? (cost ?? list) : list
34
+ return {
35
+ ...line,
36
+ kind: 'item',
37
+ product_id: variant?.id ?? product.id,
38
+ sku: variant?.sku ?? product.sku,
39
+ description: variant ? `${product.name} · ${variant.label}` : product.name,
40
+ quantity: opts.quantity ?? (toAmount(line.quantity) || 1),
41
+ unit_price: priceFromProduct(product, variant, source),
42
+ catalog_price: catalog,
43
+ cost,
44
+ tax_rate: product.tax_rate ?? line.tax_rate ?? 0,
45
+ available: availableStock(src, opts.warehouseId) ?? availableStock(product, opts.warehouseId),
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Agrega el producto como renglón. Por defecto, un segundo clic del mismo
51
+ * producto incrementa la cantidad (como el catálogo de Compras) y respeta `max_quantity`.
52
+ */
53
+ export function addProductLine(
54
+ lines: LineItem[],
55
+ product: ProductResult,
56
+ variant?: ProductVariant,
57
+ opts: {
58
+ priceSource?: PriceSource
59
+ warehouseId?: string
60
+ quantity?: number
61
+ mergeSameProduct?: boolean
62
+ } = {},
63
+ ): LineItem[] {
64
+ const id = variant?.id ?? product.id
65
+ const addQty = opts.quantity ?? 1
66
+ if (opts.mergeSameProduct !== false) {
67
+ const idx = lines.findIndex((l) => l.kind === 'item' && l.product_id === id)
68
+ if (idx >= 0) {
69
+ return lines.map((l, i) => {
70
+ if (i !== idx) return l
71
+ const next = toAmount(l.quantity) + addQty
72
+ const capped = l.max_quantity != null ? Math.min(next, toAmount(l.max_quantity)) : next
73
+ return { ...l, quantity: capped }
74
+ })
75
+ }
76
+ }
77
+ return [...lines, applyProductToLine(makeLine(), product, variant, { ...opts, quantity: addQty })]
78
+ }
79
+
80
+ export type LineGridCommand = 'add' | 'next' | 'delete' | 'none'
81
+
82
+ /** Enter agrega, Tab avanza, Supr/Backspace en celda vacía borra. No actúa si hay sugerencias abiertas. */
83
+ export function lineGridKeyCommand(
84
+ key: string,
85
+ opts: { suggestionsOpen?: boolean; cellEmpty?: boolean; mode?: DocumentLinesMode } = {},
86
+ ): LineGridCommand {
87
+ if (key === 'Tab') return 'next'
88
+ if (key === 'Enter' && !opts.suggestionsOpen && opts.mode !== 'from_source') return 'add'
89
+ if ((key === 'Delete' || key === 'Backspace') && opts.cellEmpty && opts.mode !== 'from_source') return 'delete'
90
+ return 'none'
91
+ }
@@ -2,6 +2,7 @@ export * from './format'
2
2
  export * from './field-errors'
3
3
  export * from './feedback'
4
4
  export * from './line-items'
5
+ export * from './document-lines'
5
6
  export * from './line-items-editor'
6
7
  export * from './payment'
7
8
  export * from './payment-capture'