@apptimate/ui 4.5.0 → 4.7.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.
@@ -1,10 +1,12 @@
1
1
  'use client';
2
2
 
3
3
  import React, { useState, useCallback, useMemo, useEffect } from 'react';
4
- import { cn } from '@apptimate/core-lib';
4
+ import { cn, PriceCalculationService, evaluatePriceLists, checkHasActivePriceLists } from '@apptimate/core-lib';
5
5
  import toast from 'react-hot-toast';
6
6
  import { ProductSelectionPanel } from './ProductSelectionPanel';
7
7
  import { MetadataPanel } from './MetadataPanel';
8
+ import { Modal, ModalFooter } from '../../base-components/Modal';
9
+ import { Button } from '../../base-components/Button';
8
10
  import type {
9
11
  ProductTransactionScreenProps, TransactionItem, TransactionLineItem,
10
12
  TransactionPayload, PaymentEntry, PartyLookup,
@@ -42,6 +44,60 @@ export function ProductTransactionScreen({
42
44
  const [notes, setNotes] = useState('');
43
45
  const [payments, setPayments] = useState<PaymentEntry[]>([]);
44
46
  const [isSubmitting, setIsSubmitting] = useState(false);
47
+ const [hasActivePriceLists, setHasActivePriceLists] = useState<boolean>(false);
48
+ const [priceEvaluationModal, setPriceEvaluationModal] = useState<{
49
+ isOpen: boolean;
50
+ type: 'confirm' | 'force';
51
+ evaluatedRules: Record<number, any[]>;
52
+ pendingPartyId: number | null;
53
+ pendingParty: any;
54
+ } | null>(null);
55
+
56
+ const applyEvaluatedPrices = useCallback((evaluatedRules: Record<number, any[]>) => {
57
+ setLineItems(prev => prev.map(l => {
58
+ if (l.is_free_qty || !l.variant_id || !evaluatedRules[l.variant_id]) return l;
59
+ const rules = evaluatedRules[l.variant_id];
60
+
61
+ const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
62
+ const actualVariant = l.item.variants?.find((v: any) => v.id === l.variant_id);
63
+ const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
64
+ const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
65
+ const basePrice = (batchPrice !== undefined && batchPrice !== null)
66
+ ? Number(batchPrice)
67
+ : (actualVariant
68
+ ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
69
+ : Number(l.item[config.priceField] || 0));
70
+
71
+ const uoms = l.item.uom?.uom_group?.uoms || [];
72
+ const currentUom = uoms.find((u: any) => u.id === (l.uom_id || l.item.uom_id));
73
+ const factor = Number(currentUom?.conversion_factor) || 1;
74
+ const effectiveQty = l.quantity * factor;
75
+
76
+ const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, rules, l.batches?.[0]?.batch_id);
77
+
78
+ return {
79
+ ...l,
80
+ pricing_rules: rules,
81
+ unit_price: priceDetails.unitPrice * factor,
82
+ discount_percent: priceDetails.discountPercent,
83
+ discount_amount: priceDetails.discountAmount * factor,
84
+ applied_price_rule: priceDetails.appliedRule,
85
+ line_total: (priceDetails.unitPrice * factor * l.quantity) - (priceDetails.discountAmount * factor)
86
+ };
87
+ }));
88
+ }, [config.priceField]);
89
+
90
+ useEffect(() => {
91
+ if (['sales', 'pos', 'quotation'].includes(config.type)) {
92
+ checkHasActivePriceLists().then(res => {
93
+ if (res.is_success) {
94
+ setHasActivePriceLists(res.result as boolean);
95
+ }
96
+ }).catch(() => {
97
+ // Silently ignore
98
+ });
99
+ }
100
+ }, [config.type]);
45
101
 
46
102
  const submitting = externalSubmitting ?? isSubmitting;
47
103
 
@@ -53,24 +109,25 @@ export function ProductTransactionScreen({
53
109
  }
54
110
  if (initialData.warehouse_id) {
55
111
  setWarehouseId(initialData.warehouse_id);
112
+ if (initialData.warehouse?.name) setWarehouseName(initialData.warehouse.name);
56
113
  }
57
114
  if (initialData.lines && Array.isArray(initialData.lines)) {
58
115
  setLineItems(
59
116
  initialData.lines.map((l: any) => {
60
- const qty = l.quantity || 1;
61
- const unitPrice = l.unit_price || 0;
117
+ const qty = l.quantity !== undefined ? Number(l.quantity) : 1;
118
+ const unitPrice = l.unit_price !== undefined ? Number(l.unit_price) : 0;
62
119
  return {
63
120
  uid: l.uid || Math.random().toString(36).slice(2, 7),
64
121
  item: l.item,
65
122
  variant_id: l.variant_id || null,
66
- batches: [],
67
- serial_numbers: [],
123
+ batches: l.batches || [],
124
+ serial_numbers: l.serial_numbers || [],
68
125
  quantity: qty,
69
126
  unit_price: unitPrice,
70
- discount_percent: 0,
71
- discount_amount: 0,
72
- line_total: unitPrice * qty,
73
- expanded: false,
127
+ discount_percent: l.discount_percent !== undefined ? Number(l.discount_percent) : 0,
128
+ discount_amount: l.discount_amount !== undefined ? Number(l.discount_amount) : 0,
129
+ line_total: l.line_total !== undefined ? Number(l.line_total) : (unitPrice * qty - (l.discount_amount || 0)),
130
+ expanded: l.expanded || false,
74
131
  is_free_qty: l.is_free_qty,
75
132
  parent_line_uids: l.parent_line_uids,
76
133
  uom_id: l.uom_id,
@@ -78,6 +135,25 @@ export function ProductTransactionScreen({
78
135
  };
79
136
  })
80
137
  );
138
+
139
+ // Populate pricing rules for existing items if party is selected
140
+ if (initialData.party_id && ['sales', 'pos', 'quotation'].includes(config.type)) {
141
+ evaluatePriceLists({
142
+ party_id: initialData.party_id,
143
+ transaction_type: config.type,
144
+ items: initialData.lines.filter((l: any) => l.variant_id && !l.is_free_qty).map((l: any) => ({ variant_id: l.variant_id }))
145
+ }).then(res => {
146
+ if (res.is_success && res.result) {
147
+ const evaluatedRules = res.result as Record<number, any[]>;
148
+ setLineItems(prev => prev.map(line => {
149
+ if (!line.is_free_qty && line.variant_id && evaluatedRules[line.variant_id]) {
150
+ return { ...line, pricing_rules: evaluatedRules[line.variant_id] };
151
+ }
152
+ return line;
153
+ }));
154
+ }
155
+ }).catch(() => {});
156
+ }
81
157
  }
82
158
  }
83
159
  }, [initialData]);
@@ -104,9 +180,32 @@ export function ProductTransactionScreen({
104
180
  }
105
181
 
106
182
  const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
107
- const unitPrice = actualVariant ? Number(actualVariant[variantPriceField] || 0) : 0;
183
+ const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
184
+ const batchPrice = ['sales', 'quotation'].includes(config.type) && extra?.batches?.[0] ? (extra.batches[0] as any)[batchPriceField] : undefined;
185
+ const basePrice = (batchPrice !== undefined && batchPrice !== null)
186
+ ? Number(batchPrice)
187
+ : (actualVariant ? Number(actualVariant[variantPriceField] || itemWithVariants[config.priceField] || 0) : Number(itemWithVariants[config.priceField] || 0));
108
188
  const qty = extra?.quantity || 1;
109
- const discountAmt = 0; // Initial discount
189
+
190
+ const uomId = itemWithVariants.uom_id;
191
+ const uoms = itemWithVariants.uom?.uom_group?.uoms || [];
192
+ const currentUom = uoms.find((u: any) => u.id === uomId);
193
+ const factor = Number(currentUom?.conversion_factor) || 1;
194
+
195
+ let unitPrice = basePrice * factor;
196
+ let discountPercent = 0;
197
+ let discountAmt = 0;
198
+
199
+ let appliedRule = undefined;
200
+
201
+ if (actualVariant && actualVariant.pricing_rules) {
202
+ const effectiveQty = qty * factor;
203
+ const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, actualVariant.pricing_rules, extra?.batches?.[0]?.batch_id);
204
+ unitPrice = priceDetails.unitPrice * factor;
205
+ discountPercent = priceDetails.discountPercent;
206
+ discountAmt = priceDetails.discountAmount * factor;
207
+ appliedRule = priceDetails.appliedRule;
208
+ }
110
209
 
111
210
  const newLine: TransactionLineItem = {
112
211
  uid: `${itemWithVariants.id}-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
@@ -116,11 +215,13 @@ export function ProductTransactionScreen({
116
215
  serial_numbers: extra?.serial_numbers || [],
117
216
  quantity: qty,
118
217
  unit_price: unitPrice,
119
- discount_percent: 0,
218
+ discount_percent: discountPercent,
120
219
  discount_amount: discountAmt,
121
- line_total: unitPrice * qty - discountAmt,
220
+ line_total: (unitPrice * qty) - discountAmt,
122
221
  expanded: false,
123
222
  uom_id: itemWithVariants.uom_id,
223
+ pricing_rules: actualVariant?.pricing_rules,
224
+ applied_price_rule: appliedRule,
124
225
  };
125
226
  return [...prevLineItems, newLine];
126
227
  });
@@ -131,13 +232,76 @@ export function ProductTransactionScreen({
131
232
  // ── Update line ──
132
233
  const handleUpdateLine = useCallback((uid: string, updates: Partial<TransactionLineItem>) => {
133
234
  setLineItems((prev) =>
134
- prev.map((l) => (l.uid === uid ? { ...l, ...updates } : l))
235
+ prev.map((l) => {
236
+ if (l.uid !== uid) return l;
237
+
238
+ const updatedLine = { ...l, ...updates };
239
+
240
+ const isQtyChanged = 'quantity' in updates && updates.quantity !== l.quantity;
241
+ const isUomChanged = 'uom_id' in updates && updates.uom_id !== l.uom_id;
242
+ const isPriceChanged = 'unit_price' in updates && updates.unit_price !== l.unit_price;
243
+ const isDiscountChanged = ('discount_amount' in updates && updates.discount_amount !== l.discount_amount) || ('discount_percent' in updates && updates.discount_percent !== l.discount_percent);
244
+ const isBatchChanged = 'batches' in updates;
245
+ const isFreeQtyChanged = 'is_free_qty' in updates && updates.is_free_qty !== l.is_free_qty;
246
+
247
+ const isQtyOrUomOrBatchChanged = isQtyChanged || isUomChanged || isBatchChanged || (isFreeQtyChanged && !updates.is_free_qty);
248
+
249
+ if (isQtyOrUomOrBatchChanged && updatedLine.pricing_rules && updatedLine.pricing_rules.length > 0 && !updatedLine.is_free_qty) {
250
+ const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
251
+ const actualVariant = updatedLine.item.variants?.find((v: any) => v.id === updatedLine.variant_id);
252
+ const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
253
+ const batchPrice = ['sales', 'quotation'].includes(config.type) && updatedLine.batches?.[0] ? (updatedLine.batches[0] as any)[batchPriceField] : undefined;
254
+ const basePrice = (batchPrice !== undefined && batchPrice !== null)
255
+ ? Number(batchPrice)
256
+ : (actualVariant
257
+ ? Number(actualVariant[variantPriceField] || updatedLine.item[config.priceField] || 0)
258
+ : Number(updatedLine.item[config.priceField] || 0));
259
+
260
+ const uoms = updatedLine.item.uom?.uom_group?.uoms || [];
261
+ const currentUom = uoms.find((u: any) => u.id === (updatedLine.uom_id || updatedLine.item.uom_id));
262
+ const factor = Number(currentUom?.conversion_factor) || 1;
263
+
264
+ const effectiveQty = updatedLine.quantity * factor;
265
+
266
+ console.log("DEBUG: handleUpdateLine rules -> ", {
267
+ basePrice, effectiveQty, rules: updatedLine.pricing_rules
268
+ });
269
+
270
+ const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, updatedLine.pricing_rules, updatedLine.batches?.[0]?.batch_id);
271
+ updatedLine.unit_price = priceDetails.unitPrice * factor;
272
+ updatedLine.discount_percent = priceDetails.discountPercent;
273
+ updatedLine.discount_amount = priceDetails.discountAmount * factor;
274
+ updatedLine.applied_price_rule = priceDetails.appliedRule;
275
+ updatedLine.line_total = (updatedLine.unit_price * updatedLine.quantity) - updatedLine.discount_amount;
276
+ } else if (isQtyChanged || isUomChanged || isPriceChanged || isDiscountChanged) {
277
+ updatedLine.line_total = (updatedLine.unit_price * updatedLine.quantity) - updatedLine.discount_amount;
278
+
279
+ const isManualPriceOverride = isPriceChanged && !isUomChanged;
280
+ if (isManualPriceOverride) {
281
+ updatedLine.applied_price_rule = undefined;
282
+ }
283
+ }
284
+
285
+ if (isQtyChanged && updatedLine.batches && updatedLine.batches.length === 1) {
286
+ updatedLine.batches[0].quantity = updatedLine.quantity;
287
+ }
288
+
289
+ return updatedLine;
290
+ })
135
291
  );
136
- }, []);
292
+ }, [config.priceField]);
137
293
 
138
294
  // ── Remove line ──
139
295
  const handleRemoveLine = useCallback((uid: string) => {
140
- setLineItems((prev) => prev.filter((l) => l.uid !== uid));
296
+ setLineItems((prev) => prev
297
+ .filter((l) => l.uid !== uid)
298
+ .map((l) => {
299
+ if (l.parent_line_uids && l.parent_line_uids.includes(uid)) {
300
+ return { ...l, parent_line_uids: l.parent_line_uids.filter((pUid) => pUid !== uid) };
301
+ }
302
+ return l;
303
+ })
304
+ );
141
305
  }, []);
142
306
 
143
307
  // ── Toggle expand ──
@@ -148,8 +312,8 @@ export function ProductTransactionScreen({
148
312
  }, []);
149
313
 
150
314
  // ── Calculations ──
151
- const subtotal = useMemo(() => lineItems.reduce((s, l) => s + l.unit_price * l.quantity, 0), [lineItems]);
152
- const discountTotal = useMemo(() => lineItems.reduce((s, l) => s + l.discount_amount, 0), [lineItems]);
315
+ const subtotal = useMemo(() => lineItems.reduce((s, l) => s + (Number(l.unit_price) || 0) * (Number(l.quantity) || 1), 0), [lineItems]);
316
+ const discountTotal = useMemo(() => lineItems.reduce((s, l) => s + (Number(l.discount_amount) || 0), 0), [lineItems]);
153
317
  const grandTotal = useMemo(() => subtotal - discountTotal, [subtotal, discountTotal]);
154
318
 
155
319
  // ── Submit handler ──
@@ -164,16 +328,25 @@ export function ProductTransactionScreen({
164
328
  }
165
329
  }
166
330
 
167
- if (!config.disableTrackingSelection) {
168
- for (const line of lineItems) {
169
- if (line.item.tracking_type === 'batch') {
331
+ if (config.showWarehouse && !warehouseId) {
332
+ toast.error('Please select a warehouse.');
333
+ return;
334
+ }
335
+
336
+ for (const line of lineItems) {
337
+ if (line.item.tracking_type === 'batch') {
338
+ const requiresBatch = !config.disableTrackingSelection || ['sales', 'quotation'].includes(config.type);
339
+ if (requiresBatch) {
170
340
  const selectedTotal = line.batches?.reduce((sum, b) => sum + b.quantity, 0) || 0;
171
341
  if (selectedTotal !== line.quantity) {
172
342
  toast.error(`Please select batches for ${line.item.name} to match the total quantity.`);
173
343
  return;
174
344
  }
175
345
  }
176
- if (line.item.tracking_type === 'serial') {
346
+ }
347
+ if (line.item.tracking_type === 'serial') {
348
+ const requiresSerial = !config.disableTrackingSelection;
349
+ if (requiresSerial) {
177
350
  const selectedCount = line.serial_numbers?.length || 0;
178
351
  if (selectedCount !== line.quantity) {
179
352
  toast.error(`Please select exactly ${line.quantity} serial numbers for ${line.item.name}.`);
@@ -190,6 +363,7 @@ export function ProductTransactionScreen({
190
363
  transaction_date: transactionDate,
191
364
  expected_delivery_date: expectedDeliveryDate || undefined,
192
365
  notes: notes || undefined,
366
+ revised_from_id: initialData?.revised_from_id || undefined,
193
367
  lines: lineItems.flatMap((l): TransactionPayload['lines'] => {
194
368
  if (l.batches && l.batches.length > 0) {
195
369
  return l.batches.map(b => ({
@@ -210,7 +384,7 @@ export function ProductTransactionScreen({
210
384
  purchase_order_line_id: l.purchase_order_line_id,
211
385
  unit_price: l.unit_price,
212
386
  discount_percent: l.discount_percent,
213
- discount_amount: (l.discount_amount / l.quantity) * b.quantity,
387
+ unit_discount: l.discount_amount / l.quantity,
214
388
  line_total: (l.line_total / l.quantity) * b.quantity,
215
389
  }));
216
390
  }
@@ -232,7 +406,7 @@ export function ProductTransactionScreen({
232
406
  purchase_order_line_id: l.purchase_order_line_id,
233
407
  unit_price: l.unit_price,
234
408
  discount_percent: l.discount_percent,
235
- discount_amount: l.discount_amount,
409
+ unit_discount: l.discount_amount / l.quantity,
236
410
  line_total: l.line_total,
237
411
  }];
238
412
  }),
@@ -260,7 +434,7 @@ export function ProductTransactionScreen({
260
434
  } finally {
261
435
  setIsSubmitting(false);
262
436
  }
263
- }, [lineItems, config, partyId, warehouseId, transactionDate, expectedDeliveryDate, notes, payments, subtotal, discountTotal, grandTotal, onSubmit, defaultWarehouseId]);
437
+ }, [lineItems, config, partyId, warehouseId, transactionDate, expectedDeliveryDate, notes, payments, subtotal, discountTotal, grandTotal, onSubmit, defaultWarehouseId, initialData]);
264
438
 
265
439
  return (
266
440
  <div className="flex flex-col h-full">
@@ -270,6 +444,7 @@ export function ProductTransactionScreen({
270
444
  <div className="flex-[3] min-w-0 flex flex-col">
271
445
  <ProductSelectionPanel
272
446
  config={config}
447
+ partyId={partyId}
273
448
  warehouseId={warehouseId ?? undefined}
274
449
  lineItems={lineItems}
275
450
  onAddItem={handleAddItem}
@@ -295,7 +470,81 @@ export function ProductTransactionScreen({
295
470
  expectedDeliveryDate={expectedDeliveryDate}
296
471
  notes={notes}
297
472
  payments={payments}
298
- onPartyChange={(id, party) => { setPartyId(id); setPartyName(party?.name ?? null); }}
473
+ onPartyChange={async (id, party) => {
474
+ if (partyId !== id && lineItems.length > 0 && ['sales', 'pos', 'quotation'].includes(config.type)) {
475
+ if (hasActivePriceLists) {
476
+ toast.loading('Checking price changes...', { id: 'price-calc' });
477
+ try {
478
+ const res = await evaluatePriceLists({
479
+ party_id: id || null,
480
+ transaction_type: config.type,
481
+ items: lineItems.filter(l => l.variant_id && !l.is_free_qty).map(l => ({ variant_id: l.variant_id! }))
482
+ });
483
+
484
+ toast.dismiss('price-calc');
485
+
486
+ if (res.is_success && res.result) {
487
+ const evaluatedRules = res.result as Record<number, any[]>;
488
+
489
+ let hasActualPriceChanges = false;
490
+ for (const l of lineItems) {
491
+ if (!l.is_free_qty && l.variant_id && evaluatedRules[l.variant_id]) {
492
+ const rules = evaluatedRules[l.variant_id];
493
+ const variantPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'sale_price';
494
+ const actualVariant = l.item.variants?.find((v: any) => v.id === l.variant_id);
495
+ const batchPriceField = config.priceField === 'default_cost_price' ? 'cost_price' : 'selling_price';
496
+ const batchPrice = ['sales', 'quotation'].includes(config.type) && l.batches?.[0] ? (l.batches[0] as any)[batchPriceField] : undefined;
497
+ const basePrice = (batchPrice !== undefined && batchPrice !== null)
498
+ ? Number(batchPrice)
499
+ : (actualVariant
500
+ ? Number(actualVariant[variantPriceField] || l.item[config.priceField] || 0)
501
+ : Number(l.item[config.priceField] || 0));
502
+
503
+ const uoms = l.item.uom?.uom_group?.uoms || [];
504
+ const currentUom = uoms.find((u: any) => u.id === (l.uom_id || l.item.uom_id));
505
+ const factor = Number(currentUom?.conversion_factor) || 1;
506
+ const effectiveQty = l.quantity * factor;
507
+
508
+ const priceDetails = PriceCalculationService.calculatePriceDetails(basePrice, effectiveQty, rules, l.batches?.[0]?.batch_id);
509
+
510
+ if (Math.abs((priceDetails.unitPrice * factor) - l.unit_price) > 0.001) {
511
+ hasActualPriceChanges = true;
512
+ break;
513
+ }
514
+ }
515
+ }
516
+
517
+ if (hasActualPriceChanges) {
518
+ const previousRulesApplied = lineItems.some(l => l.applied_price_rule);
519
+ setPriceEvaluationModal({
520
+ isOpen: true,
521
+ type: (partyId && previousRulesApplied) ? 'force' : 'confirm',
522
+ evaluatedRules,
523
+ pendingPartyId: id,
524
+ pendingParty: party
525
+ });
526
+ return;
527
+ } else {
528
+ // No immediate price changes for current quantities,
529
+ // but we still need to update the rules in the background
530
+ // so that if they change quantity later, it uses the new rules!
531
+ setLineItems(prev => prev.map(l => {
532
+ if (!l.is_free_qty && l.variant_id && evaluatedRules[l.variant_id]) {
533
+ return { ...l, pricing_rules: evaluatedRules[l.variant_id] };
534
+ }
535
+ return l;
536
+ }));
537
+ }
538
+ }
539
+ } catch (e) {
540
+ toast.dismiss('price-calc');
541
+ toast.error('Failed to check prices');
542
+ }
543
+ }
544
+ }
545
+ setPartyId(id);
546
+ setPartyName(party?.name ?? null);
547
+ }}
299
548
  onWarehouseChange={(id, name) => { setWarehouseId(id); setWarehouseName(name ?? null); }}
300
549
  onDateChange={setTransactionDate}
301
550
  onExpectedDeliveryDateChange={setExpectedDeliveryDate}
@@ -308,6 +557,56 @@ export function ProductTransactionScreen({
308
557
  />
309
558
  </div>
310
559
  </div>
560
+
561
+ {/* ── Price Evaluation Modal ── */}
562
+ {priceEvaluationModal && (
563
+ <Modal
564
+ isOpen={priceEvaluationModal.isOpen}
565
+ onClose={() => {
566
+ if (priceEvaluationModal.type === 'confirm') {
567
+ setPartyId(priceEvaluationModal.pendingPartyId);
568
+ setPartyName(priceEvaluationModal.pendingParty?.name ?? null);
569
+ setPriceEvaluationModal(null);
570
+ }
571
+ }}
572
+ title="Price List Update"
573
+ backdrop="opaque"
574
+ size="sm"
575
+ >
576
+ <div className="text-[14px] text-foreground-1 leading-relaxed">
577
+ {priceEvaluationModal.type === 'confirm'
578
+ ? "A few items have price changes based on the price list. Do you want to apply them?"
579
+ : "Prices need to be changed based on the party selection."}
580
+ </div>
581
+ <ModalFooter>
582
+ {priceEvaluationModal.type === 'confirm' && (
583
+ <Button
584
+ variant="flat"
585
+ color="secondary"
586
+ onClick={() => {
587
+ setPartyId(priceEvaluationModal.pendingPartyId);
588
+ setPartyName(priceEvaluationModal.pendingParty?.name ?? null);
589
+ setPriceEvaluationModal(null);
590
+ }}
591
+ >
592
+ No
593
+ </Button>
594
+ )}
595
+ <Button
596
+ color="primary"
597
+ onClick={() => {
598
+ applyEvaluatedPrices(priceEvaluationModal.evaluatedRules);
599
+ setPartyId(priceEvaluationModal.pendingPartyId);
600
+ setPartyName(priceEvaluationModal.pendingParty?.name ?? null);
601
+ setPriceEvaluationModal(null);
602
+ toast.success('Prices updated based on party selection');
603
+ }}
604
+ >
605
+ {priceEvaluationModal.type === 'confirm' ? 'Yes' : 'OK'}
606
+ </Button>
607
+ </ModalFooter>
608
+ </Modal>
609
+ )}
311
610
  </div>
312
611
  );
313
612
  }
@@ -19,9 +19,9 @@ export interface TransactionItem {
19
19
  is_discountable: boolean;
20
20
  category?: { id: number; name: string };
21
21
  brand?: { id: number; name: string };
22
- uom?: {
23
- id: number;
24
- name: string;
22
+ uom?: {
23
+ id: number;
24
+ name: string;
25
25
  abbreviation: string;
26
26
  uom_group?: {
27
27
  id: number;
@@ -41,6 +41,7 @@ export interface TransactionItemVariant {
41
41
  cost_price?: number;
42
42
  sale_price?: number;
43
43
  additional_price?: number;
44
+ pricing_rules?: any[];
44
45
  }
45
46
 
46
47
  export interface TransactionBatch {
@@ -78,12 +79,16 @@ export interface TransactionLineItem {
78
79
  parent_line_uids?: string[];
79
80
  uom_id?: number;
80
81
  purchase_order_line_id?: number | null;
82
+ sales_order_line_id?: number | null;
83
+ sales_quotation_line_id?: number | null;
81
84
  unit_price: number;
82
85
  discount_percent: number;
83
86
  discount_amount: number;
84
87
  line_total: number;
85
88
  notes?: string;
86
89
  expanded?: boolean; // Whether the detail row is expanded
90
+ pricing_rules?: any[];
91
+ applied_price_rule?: any;
87
92
  }
88
93
 
89
94
  import type { PaymentModeField, PaymentMode, PaymentEntry, BankAccount } from '../PaymentSection';
@@ -150,12 +155,20 @@ export interface TransactionScreenConfig {
150
155
  showStock?: boolean;
151
156
  /** Enable purchase unit selection (dynamic setting) */
152
157
  enablePurchaseUnit?: boolean;
158
+ /** Enable sales unit selection (dynamic setting) */
159
+ enableSalesUnit?: boolean;
153
160
  /** Show expected delivery date */
154
161
  showExpectedDeliveryDate?: boolean;
162
+ /** Label for the expected delivery date field */
163
+ expectedDeliveryDateLabel?: string;
155
164
  /** Disable batch/serial selection completely (e.g. for Purchase Orders and Quotations) */
156
165
  disableTrackingSelection?: boolean;
157
166
  /** Enforce that the sum of payments equals the grand total before allowing submission */
158
167
  requireFullPayment?: boolean;
168
+ /** Disable party selection (e.g. for revisions) */
169
+ disablePartySelection?: boolean;
170
+ /** Disable warehouse selection (e.g. for revisions) */
171
+ disableWarehouseSelection?: boolean;
159
172
  }
160
173
 
161
174
  // ── Module Presets ──
@@ -174,7 +187,7 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
174
187
  submitLabel: 'Create Sales Order',
175
188
  title: 'New Sale',
176
189
  allowCreateBatchAndSerial: false,
177
- allowEditPrice: false,
190
+ allowEditPrice: true,
178
191
  showStock: true,
179
192
  },
180
193
  quotation: {
@@ -190,8 +203,10 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
190
203
  submitLabel: 'Create Quotation',
191
204
  title: 'New Quotation',
192
205
  allowCreateBatchAndSerial: false,
193
- allowEditPrice: false,
206
+ allowEditPrice: true,
194
207
  showStock: true,
208
+ showExpectedDeliveryDate: true,
209
+ expectedDeliveryDateLabel: 'Valid Until',
195
210
  disableTrackingSelection: true,
196
211
  },
197
212
  purchase: {
@@ -243,7 +258,7 @@ export const TRANSACTION_PRESETS: Record<TransactionType, TransactionScreenConfi
243
258
  submitLabel: 'Complete Sale',
244
259
  title: 'POS Checkout',
245
260
  allowCreateBatchAndSerial: false,
246
- allowEditPrice: false,
261
+ allowEditPrice: true,
247
262
  showStock: true,
248
263
  },
249
264
  opening_stock: {
@@ -308,6 +323,7 @@ export interface TransactionPayload {
308
323
  transaction_date?: string;
309
324
  expected_delivery_date?: string;
310
325
  notes?: string;
326
+ revised_from_id?: number | null;
311
327
  lines: Array<{
312
328
  item_id: number;
313
329
  variant_id?: number | null;
@@ -326,7 +342,7 @@ export interface TransactionPayload {
326
342
  purchase_order_line_id?: number | null;
327
343
  unit_price: number;
328
344
  discount_percent: number;
329
- discount_amount: number;
345
+ unit_discount: number;
330
346
  line_total: number;
331
347
  }>;
332
348
  payments?: PaymentEntry[];