@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.
- package/CHANGELOG.md +6 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +16 -4
- package/dist/business/document-lines.d.ts +30 -0
- package/dist/business/document-lines.d.ts.map +1 -0
- package/dist/business/document-lines.js +65 -0
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +1 -0
- package/dist/business/line-items-editor.d.ts +28 -4
- package/dist/business/line-items-editor.d.ts.map +1 -1
- package/dist/business/line-items-editor.js +139 -14
- package/dist/business/line-items.d.ts +24 -2
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +50 -5
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/business/product-search.js +5 -1
- package/dist/dynamic-form-schema.d.ts +9 -0
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +61 -0
- package/dist/dynamic-line-items.d.ts.map +1 -1
- package/dist/dynamic-line-items.js +18 -8
- package/dist/dynamic-select-field.d.ts +3 -1
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +2 -1
- package/dist/use-options-resolver.d.ts +5 -5
- package/dist/use-options-resolver.d.ts.map +1 -1
- package/dist/use-options-resolver.js +11 -0
- package/package.json +1 -1
- package/src/__tests__/document-lines-grid.test.tsx +79 -0
- package/src/__tests__/line-item-formulas.test.ts +18 -1
- package/src/__tests__/use-options-resolver.test.ts +5 -0
- package/src/business/business.test.ts +43 -0
- package/src/business/catalog.ts +16 -4
- package/src/business/document-lines.ts +91 -0
- package/src/business/index.ts +1 -0
- package/src/business/line-items-editor.tsx +365 -46
- package/src/business/line-items.ts +65 -7
- package/src/business/product-search.ts +9 -1
- package/src/dynamic-form-schema.ts +60 -0
- package/src/dynamic-line-items.tsx +23 -3
- package/src/dynamic-select-field.tsx +4 -0
- 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', () => {
|
package/src/business/catalog.ts
CHANGED
|
@@ -34,12 +34,24 @@ export const BUSINESS_COMPONENTS: readonly BusinessComponentSpec[] = [
|
|
|
34
34
|
helpers: ['parseProductQuery', 'parseTireSize', 'availableStock', 'productToLine'],
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
|
-
name: '
|
|
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: [
|
|
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
|
+
}
|
package/src/business/index.ts
CHANGED