@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-539cff4960

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 (184) hide show
  1. package/dist/generated/entities/catalog_product/index.js +16 -0
  2. package/dist/generated/entities/catalog_product/index.js.map +2 -2
  3. package/dist/generated/entities/catalog_product_unit_conversion/index.js +27 -0
  4. package/dist/generated/entities/catalog_product_unit_conversion/index.js.map +7 -0
  5. package/dist/generated/entities/sales_credit_memo_line/index.js +7 -1
  6. package/dist/generated/entities/sales_credit_memo_line/index.js.map +2 -2
  7. package/dist/generated/entities/sales_invoice_line/index.js +7 -1
  8. package/dist/generated/entities/sales_invoice_line/index.js.map +2 -2
  9. package/dist/generated/entities/sales_order_line/index.js +6 -0
  10. package/dist/generated/entities/sales_order_line/index.js.map +2 -2
  11. package/dist/generated/entities/sales_quote_line/index.js +6 -0
  12. package/dist/generated/entities/sales_quote_line/index.js.map +2 -2
  13. package/dist/generated/entities.ids.generated.js +1 -0
  14. package/dist/generated/entities.ids.generated.js.map +2 -2
  15. package/dist/generated/entity-fields-registry.js +2 -0
  16. package/dist/generated/entity-fields-registry.js.map +2 -2
  17. package/dist/modules/catalog/api/prices/route.js +123 -8
  18. package/dist/modules/catalog/api/prices/route.js.map +2 -2
  19. package/dist/modules/catalog/api/product-unit-conversions/route.js +194 -0
  20. package/dist/modules/catalog/api/product-unit-conversions/route.js.map +7 -0
  21. package/dist/modules/catalog/api/products/route.js +351 -201
  22. package/dist/modules/catalog/api/products/route.js.map +2 -2
  23. package/dist/modules/catalog/backend/catalog/products/[id]/page.js +1267 -497
  24. package/dist/modules/catalog/backend/catalog/products/[id]/page.js.map +2 -2
  25. package/dist/modules/catalog/backend/catalog/products/create/page.js +733 -210
  26. package/dist/modules/catalog/backend/catalog/products/create/page.js.map +2 -2
  27. package/dist/modules/catalog/commands/index.js +1 -0
  28. package/dist/modules/catalog/commands/index.js.map +2 -2
  29. package/dist/modules/catalog/commands/productUnitConversions.js +503 -0
  30. package/dist/modules/catalog/commands/productUnitConversions.js.map +7 -0
  31. package/dist/modules/catalog/commands/products.js +355 -73
  32. package/dist/modules/catalog/commands/products.js.map +2 -2
  33. package/dist/modules/catalog/commands/shared.js +18 -4
  34. package/dist/modules/catalog/commands/shared.js.map +2 -2
  35. package/dist/modules/catalog/components/products/ProductUomSection.js +591 -0
  36. package/dist/modules/catalog/components/products/ProductUomSection.js.map +7 -0
  37. package/dist/modules/catalog/components/products/productForm.js +66 -5
  38. package/dist/modules/catalog/components/products/productForm.js.map +2 -2
  39. package/dist/modules/catalog/components/products/productFormUtils.js +68 -0
  40. package/dist/modules/catalog/components/products/productFormUtils.js.map +7 -0
  41. package/dist/modules/catalog/data/entities.js +86 -0
  42. package/dist/modules/catalog/data/entities.js.map +2 -2
  43. package/dist/modules/catalog/data/validators.js +65 -3
  44. package/dist/modules/catalog/data/validators.js.map +2 -2
  45. package/dist/modules/catalog/events.js +3 -0
  46. package/dist/modules/catalog/events.js.map +2 -2
  47. package/dist/modules/catalog/lib/unitCodes.js +7 -0
  48. package/dist/modules/catalog/lib/unitCodes.js.map +7 -0
  49. package/dist/modules/catalog/lib/unitResolution.js +53 -0
  50. package/dist/modules/catalog/lib/unitResolution.js.map +7 -0
  51. package/dist/modules/catalog/migrations/Migration20260218225422.js +19 -0
  52. package/dist/modules/catalog/migrations/Migration20260218225422.js.map +7 -0
  53. package/dist/modules/catalog/migrations/Migration20260219084500.js +27 -0
  54. package/dist/modules/catalog/migrations/Migration20260219084500.js.map +7 -0
  55. package/dist/modules/catalog/search.js +69 -1
  56. package/dist/modules/catalog/search.js.map +2 -2
  57. package/dist/modules/catalog/seed/examples.js +91 -42
  58. package/dist/modules/catalog/seed/examples.js.map +2 -2
  59. package/dist/modules/dashboards/seed/analytics.js +3 -0
  60. package/dist/modules/dashboards/seed/analytics.js.map +2 -2
  61. package/dist/modules/sales/api/order-lines/route.js +98 -15
  62. package/dist/modules/sales/api/order-lines/route.js.map +2 -2
  63. package/dist/modules/sales/api/quote-lines/route.js +101 -14
  64. package/dist/modules/sales/api/quote-lines/route.js.map +2 -2
  65. package/dist/modules/sales/api/quotes/public/[token]/route.js +87 -12
  66. package/dist/modules/sales/api/quotes/public/[token]/route.js.map +2 -2
  67. package/dist/modules/sales/commands/documents.js +1424 -260
  68. package/dist/modules/sales/commands/documents.js.map +3 -3
  69. package/dist/modules/sales/commands/shared.js +6 -2
  70. package/dist/modules/sales/commands/shared.js.map +2 -2
  71. package/dist/modules/sales/components/documents/ItemsSection.js +216 -86
  72. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  73. package/dist/modules/sales/components/documents/LineItemDialog.js +913 -241
  74. package/dist/modules/sales/components/documents/LineItemDialog.js.map +3 -3
  75. package/dist/modules/sales/components/documents/ShipmentsSection.js +15 -3
  76. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  77. package/dist/modules/sales/data/entities.js +59 -3
  78. package/dist/modules/sales/data/entities.js.map +2 -2
  79. package/dist/modules/sales/data/validators.js +35 -0
  80. package/dist/modules/sales/data/validators.js.map +2 -2
  81. package/dist/modules/sales/frontend/quote/[token]/page.js +15 -1
  82. package/dist/modules/sales/frontend/quote/[token]/page.js.map +2 -2
  83. package/dist/modules/sales/migrations/Migration20260218225423.js +31 -0
  84. package/dist/modules/sales/migrations/Migration20260218225423.js.map +7 -0
  85. package/dist/modules/sales/migrations/Migration20260219084501.js +71 -0
  86. package/dist/modules/sales/migrations/Migration20260219084501.js.map +7 -0
  87. package/dist/modules/sales/search.js +28 -0
  88. package/dist/modules/sales/search.js.map +2 -2
  89. package/dist/modules/sales/seed/examples.js +14 -1
  90. package/dist/modules/sales/seed/examples.js.map +2 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +1 -1
  92. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  93. package/dist/modules/staff/backend/staff/team-members/[id]/page.js +28 -15
  94. package/dist/modules/staff/backend/staff/team-members/[id]/page.js.map +2 -2
  95. package/dist/modules/staff/translations.js +9 -0
  96. package/dist/modules/staff/translations.js.map +7 -0
  97. package/dist/modules/translations/components/TranslationDrawerAction.js +97 -0
  98. package/dist/modules/translations/components/TranslationDrawerAction.js.map +7 -0
  99. package/dist/modules/translations/lib/extract-record-id.js +31 -2
  100. package/dist/modules/translations/lib/extract-record-id.js.map +2 -2
  101. package/dist/modules/translations/lib/resolve-field-list.js +3 -0
  102. package/dist/modules/translations/lib/resolve-field-list.js.map +2 -2
  103. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js +105 -36
  104. package/dist/modules/translations/widgets/injection/translation-manager/widget.client.js.map +2 -2
  105. package/dist/modules/translations/widgets/injection-table.js +18 -29
  106. package/dist/modules/translations/widgets/injection-table.js.map +2 -2
  107. package/generated/entities/catalog_product/index.ts +8 -0
  108. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  109. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  110. package/generated/entities/sales_invoice_line/index.ts +3 -0
  111. package/generated/entities/sales_order_line/index.ts +3 -0
  112. package/generated/entities/sales_quote_line/index.ts +3 -0
  113. package/generated/entities.ids.generated.ts +1 -0
  114. package/generated/entity-fields-registry.ts +2 -0
  115. package/package.json +2 -2
  116. package/src/modules/auth/i18n/de.json +1 -1
  117. package/src/modules/auth/i18n/en.json +1 -1
  118. package/src/modules/auth/i18n/es.json +1 -1
  119. package/src/modules/auth/i18n/pl.json +1 -1
  120. package/src/modules/catalog/api/prices/route.ts +213 -81
  121. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  122. package/src/modules/catalog/api/products/route.ts +638 -402
  123. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  124. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  125. package/src/modules/catalog/commands/index.ts +1 -0
  126. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  127. package/src/modules/catalog/commands/products.ts +1151 -693
  128. package/src/modules/catalog/commands/shared.ts +19 -5
  129. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  130. package/src/modules/catalog/components/products/productForm.ts +369 -256
  131. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  132. package/src/modules/catalog/data/entities.ts +82 -1
  133. package/src/modules/catalog/data/validators.ts +118 -34
  134. package/src/modules/catalog/events.ts +3 -0
  135. package/src/modules/catalog/i18n/de.json +56 -0
  136. package/src/modules/catalog/i18n/en.json +56 -0
  137. package/src/modules/catalog/i18n/es.json +56 -0
  138. package/src/modules/catalog/i18n/pl.json +56 -0
  139. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  140. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  141. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  142. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  143. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  144. package/src/modules/catalog/search.ts +73 -1
  145. package/src/modules/catalog/seed/examples.ts +552 -479
  146. package/src/modules/dashboards/i18n/de.json +1 -1
  147. package/src/modules/dashboards/i18n/en.json +1 -1
  148. package/src/modules/dashboards/i18n/es.json +1 -1
  149. package/src/modules/dashboards/i18n/pl.json +1 -1
  150. package/src/modules/dashboards/seed/analytics.ts +3 -0
  151. package/src/modules/sales/api/order-lines/route.ts +158 -68
  152. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  153. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  154. package/src/modules/sales/commands/documents.ts +4250 -2424
  155. package/src/modules/sales/commands/shared.ts +7 -2
  156. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  157. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  158. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  159. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  160. package/src/modules/sales/data/entities.ts +53 -0
  161. package/src/modules/sales/data/validators.ts +36 -0
  162. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  163. package/src/modules/sales/i18n/de.json +23 -3
  164. package/src/modules/sales/i18n/en.json +23 -3
  165. package/src/modules/sales/i18n/es.json +23 -3
  166. package/src/modules/sales/i18n/pl.json +23 -3
  167. package/src/modules/sales/lib/types.ts +30 -0
  168. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  169. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  170. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  171. package/src/modules/sales/search.ts +28 -0
  172. package/src/modules/sales/seed/examples.ts +20 -1
  173. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  174. package/src/modules/staff/backend/staff/team-members/[id]/page.tsx +8 -0
  175. package/src/modules/staff/translations.ts +5 -0
  176. package/src/modules/translations/components/TranslationDrawerAction.tsx +107 -0
  177. package/src/modules/translations/lib/extract-record-id.ts +47 -3
  178. package/src/modules/translations/lib/resolve-field-list.ts +4 -0
  179. package/src/modules/translations/widgets/injection/translation-manager/widget.client.tsx +108 -36
  180. package/src/modules/translations/widgets/injection-table.ts +19 -33
  181. package/src/modules/workflows/i18n/de.json +4 -4
  182. package/src/modules/workflows/i18n/en.json +4 -4
  183. package/src/modules/workflows/i18n/es.json +4 -4
  184. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -16,6 +16,9 @@
16
16
  "catalog.audit.prices.create": "Utwórz cenę",
17
17
  "catalog.audit.prices.delete": "Usuń cenę",
18
18
  "catalog.audit.prices.update": "Zaktualizuj cenę",
19
+ "catalog.audit.productUnitConversions.create": "Utwórz konwersję jednostki produktu",
20
+ "catalog.audit.productUnitConversions.delete": "Usuń konwersję jednostki produktu",
21
+ "catalog.audit.productUnitConversions.update": "Zaktualizuj konwersję jednostki produktu",
19
22
  "catalog.audit.products.create": "Utwórz produkt",
20
23
  "catalog.audit.products.delete": "Usuń produkt",
21
24
  "catalog.audit.products.update": "Zaktualizuj produkt",
@@ -394,6 +397,7 @@
394
397
  "catalog.products.edit.weight.value": "Waga",
395
398
  "catalog.products.errors.handleExists": "Handle jest już używany.",
396
399
  "catalog.products.errors.skuExists": "SKU jest już używany.",
400
+ "catalog.products.errors.taxClassNotFound": "Nie znaleziono klasy podatkowej.",
397
401
  "catalog.products.filters.active": "Aktywne",
398
402
  "catalog.products.filters.categories": "Kategorie",
399
403
  "catalog.products.filters.categoriesLoadError": "Nie udało się załadować kategorii",
@@ -454,12 +458,64 @@
454
458
  "catalog.products.types.grouped": "Grupowy",
455
459
  "catalog.products.types.simple": "Prosty",
456
460
  "catalog.products.types.virtual": "Wirtualny",
461
+ "catalog.products.unitPrice.baseQuantity": "Ilość bazowa dla jednostki referencyjnej",
462
+ "catalog.products.unitPrice.enable": "Włącz prezentację ceny jednostkowej UE",
463
+ "catalog.products.unitPrice.errors.baseQuantity": "Ilość bazowa jest wymagana po włączeniu ceny jednostkowej.",
464
+ "catalog.products.unitPrice.errors.referenceUnit": "Jednostka referencyjna jest wymagana po włączeniu ceny jednostkowej.",
465
+ "catalog.products.unitPrice.hint": "Pokaż wyliczoną cenę za wybraną jednostkę referencyjną. W większości przypadków ustaw ilość na 1.",
466
+ "catalog.products.unitPrice.hintWithPreview": "Pokaż wyliczoną cenę za {{quantity}} {{unit}}. Dla większości produktów ustaw 1 (np. 1 kg, 1 l, 1 m²).",
467
+ "catalog.products.unitPrice.options.kg": "1 kg",
468
+ "catalog.products.unitPrice.options.l": "1 l",
469
+ "catalog.products.unitPrice.options.m2": "1 m²",
470
+ "catalog.products.unitPrice.options.m3": "1 m³",
471
+ "catalog.products.unitPrice.options.pc": "1 szt.",
472
+ "catalog.products.unitPrice.referenceUnit": "Jednostka referencyjna",
473
+ "catalog.products.unitPrice.selectReferenceUnit": "Wybierz jednostkę referencyjną",
474
+ "catalog.products.uom.active": "Aktywna",
475
+ "catalog.products.uom.addConversion": "Dodaj konwersję",
476
+ "catalog.products.uom.baseUnit": "Jednostka bazowa",
477
+ "catalog.products.uom.conversionOrderHint": "Użyj strzałek, aby zmienić priorytet konwersji.",
478
+ "catalog.products.uom.conversionPreview": "1 {{fromUnit}} = {{factor}} {{baseUnit}}",
479
+ "catalog.products.uom.conversionUnit": "Jednostka sprzedaży",
480
+ "catalog.products.uom.conversions": "Konwersje produktu",
481
+ "catalog.products.uom.defaultSalesQuantity": "Domyślna ilość sprzedaży",
482
+ "catalog.products.uom.defaultSalesQuantityHint": "Używane do wstępnego wypełniania ilości w pozycjach ofert/zamówień. Wartość jest interpretowana w domyślnej jednostce sprzedaży.",
483
+ "catalog.products.uom.defaultSalesQuantityLabel": "Domyślna ilość linii (w jednostce sprzedaży)",
484
+ "catalog.products.uom.defaultSalesQuantityPreview": "Domyślna pozycja: {{quantity}} {{salesUnit}}.",
485
+ "catalog.products.uom.defaultSalesQuantityPreviewWithNormalization": "Domyślna pozycja: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).",
486
+ "catalog.products.uom.defaultSalesUnit": "Domyślna jednostka sprzedaży",
487
+ "catalog.products.uom.description": "Ustaw jednostkę bazową i sprzedażową oraz konwersje opakowań.",
488
+ "catalog.products.uom.emptyConversions": "Brak skonfigurowanych konwersji.",
489
+ "catalog.products.uom.errors.baseRequired": "Jednostka bazowa jest wymagana, gdy ustawiono domyślną jednostkę sprzedaży.",
490
+ "catalog.products.uom.errors.baseRequiredForConversions": "Jednostka bazowa jest wymagana, gdy skonfigurowano konwersje.",
491
+ "catalog.products.uom.errors.defaultSalesConversionRequired": "Aktywna konwersja dla domyślnej jednostki sprzedaży jest wymagana, gdy różni się od jednostki bazowej.",
492
+ "catalog.products.uom.errors.duplicateConversion": "Duplikat jednostki konwersji nie jest dozwolony.",
493
+ "catalog.products.uom.errors.loadUnits": "Nie udało się załadować jednostek. Spróbuj odświeżyć stronę.",
494
+ "catalog.products.uom.errors.sync": "Nie udało się zsynchronizować konwersji produktu.",
495
+ "catalog.products.uom.loadingUnits": "Ładowanie jednostek...",
496
+ "catalog.products.uom.moveDown": "Przenieś konwersję niżej",
497
+ "catalog.products.uom.moveUp": "Przenieś konwersję wyżej",
498
+ "catalog.products.uom.removeConversion": "Usuń konwersję",
499
+ "catalog.products.uom.roundingMode": "Tryb zaokrąglania",
500
+ "catalog.products.uom.roundingModeDown": "W dół",
501
+ "catalog.products.uom.roundingModeHalfUp": "Połowa w górę",
502
+ "catalog.products.uom.roundingModeUp": "W górę",
503
+ "catalog.products.uom.roundingScale": "Skala zaokrąglania",
504
+ "catalog.products.uom.selectUnit": "Wybierz jednostkę",
505
+ "catalog.products.uom.sortOrder": "Kolejność",
506
+ "catalog.products.uom.title": "Jednostki miary",
507
+ "catalog.products.uom.toBaseFactor": "Współczynnik do bazowej",
508
+ "catalog.products.validation.baseUnitRequired": "Jednostka bazowa jest wymagana, gdy ustawiona jest domyślna jednostka sprzedaży.",
509
+ "catalog.products.validation.handleFormat": "Identyfikator może zawierać tylko małe litery, cyfry, myślniki lub podkreślenia.",
510
+ "catalog.products.validation.referenceUnitRequired": "Jednostka referencyjna jest wymagana, gdy wyświetlanie ceny jednostkowej jest włączone.",
511
+ "catalog.products.validation.titleRequired": "Tytuł jest wymagany.",
457
512
  "catalog.search.badge.category": "Kategoria",
458
513
  "catalog.search.badge.offer": "Oferta kanału",
459
514
  "catalog.search.badge.optionSchema": "Schemat opcji",
460
515
  "catalog.search.badge.priceKind": "Rodzaj ceny",
461
516
  "catalog.search.badge.product": "Produkt",
462
517
  "catalog.search.badge.tag": "Tag",
518
+ "catalog.search.badge.unitConversion": "Przelicznik jednostek",
463
519
  "catalog.search.badge.variant": "Wariant",
464
520
  "catalog.search.priceKind.excludingTax": "Bez VAT",
465
521
  "catalog.search.priceKind.includingTax": "Z VAT",
@@ -0,0 +1 @@
1
+ export { canonicalizeUnitCode, toUnitLookupKey, REFERENCE_UNIT_CODES, type ReferenceUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
@@ -0,0 +1,62 @@
1
+ import type { EntityManager } from "@mikro-orm/postgresql";
2
+ import { CrudHttpError } from "@open-mercato/shared/lib/crud/errors";
3
+ import { findOneWithDecryption } from "@open-mercato/shared/lib/encryption/find";
4
+ import {
5
+ Dictionary,
6
+ DictionaryEntry,
7
+ } from "@open-mercato/core/modules/dictionaries/data/entities";
8
+ import { canonicalizeUnitCode } from "./unitCodes";
9
+
10
+ export const UOM_DICTIONARY_KEYS = ["unit", "units", "measurement_units"] as const;
11
+
12
+ export async function resolveUnitDictionary(
13
+ em: EntityManager,
14
+ organizationId: string,
15
+ tenantId: string,
16
+ ) {
17
+ return findOneWithDecryption(
18
+ em,
19
+ Dictionary,
20
+ {
21
+ organizationId,
22
+ tenantId,
23
+ key: { $in: UOM_DICTIONARY_KEYS },
24
+ deletedAt: null,
25
+ isActive: true,
26
+ },
27
+ { orderBy: { createdAt: "asc" } },
28
+ );
29
+ }
30
+
31
+ export async function resolveCanonicalUnitCode(
32
+ em: EntityManager,
33
+ params: {
34
+ organizationId: string;
35
+ tenantId: string;
36
+ unitCode: string;
37
+ },
38
+ ): Promise<string> {
39
+ const dictionary = await resolveUnitDictionary(
40
+ em,
41
+ params.organizationId,
42
+ params.tenantId,
43
+ );
44
+ if (!dictionary) {
45
+ return canonicalizeUnitCode(params.unitCode) ?? params.unitCode;
46
+ }
47
+ const unitCode = canonicalizeUnitCode(params.unitCode);
48
+ if (!unitCode) {
49
+ throw new CrudHttpError(400, { error: "uom.unit_not_found" });
50
+ }
51
+ const entry = await findOneWithDecryption(em, DictionaryEntry, {
52
+ dictionary,
53
+ organizationId: dictionary.organizationId,
54
+ tenantId: dictionary.tenantId,
55
+ $or: [{ normalizedValue: unitCode }, { value: unitCode }],
56
+ });
57
+ if (!entry) {
58
+ throw new CrudHttpError(400, { error: "uom.unit_not_found" });
59
+ }
60
+ const canonical = typeof entry.value === "string" ? entry.value.trim() : "";
61
+ return canonical.length ? canonical : unitCode;
62
+ }
@@ -450,6 +450,77 @@
450
450
  "nullable": true,
451
451
  "mappedType": "text"
452
452
  },
453
+ "default_sales_unit": {
454
+ "name": "default_sales_unit",
455
+ "type": "text",
456
+ "unsigned": false,
457
+ "autoincrement": false,
458
+ "primary": false,
459
+ "nullable": true,
460
+ "mappedType": "text"
461
+ },
462
+ "default_sales_unit_quantity": {
463
+ "name": "default_sales_unit_quantity",
464
+ "type": "numeric(18,6)",
465
+ "unsigned": false,
466
+ "autoincrement": false,
467
+ "primary": false,
468
+ "nullable": false,
469
+ "precision": 18,
470
+ "scale": 6,
471
+ "default": "'1'",
472
+ "mappedType": "decimal"
473
+ },
474
+ "uom_rounding_scale": {
475
+ "name": "uom_rounding_scale",
476
+ "type": "smallint",
477
+ "unsigned": false,
478
+ "autoincrement": false,
479
+ "primary": false,
480
+ "nullable": false,
481
+ "default": "4",
482
+ "mappedType": "smallint"
483
+ },
484
+ "uom_rounding_mode": {
485
+ "name": "uom_rounding_mode",
486
+ "type": "text",
487
+ "unsigned": false,
488
+ "autoincrement": false,
489
+ "primary": false,
490
+ "nullable": false,
491
+ "default": "'half_up'",
492
+ "mappedType": "text"
493
+ },
494
+ "unit_price_enabled": {
495
+ "name": "unit_price_enabled",
496
+ "type": "boolean",
497
+ "unsigned": false,
498
+ "autoincrement": false,
499
+ "primary": false,
500
+ "nullable": false,
501
+ "default": "false",
502
+ "mappedType": "boolean"
503
+ },
504
+ "unit_price_reference_unit": {
505
+ "name": "unit_price_reference_unit",
506
+ "type": "text",
507
+ "unsigned": false,
508
+ "autoincrement": false,
509
+ "primary": false,
510
+ "nullable": true,
511
+ "mappedType": "text"
512
+ },
513
+ "unit_price_base_quantity": {
514
+ "name": "unit_price_base_quantity",
515
+ "type": "numeric(18,6)",
516
+ "unsigned": false,
517
+ "autoincrement": false,
518
+ "primary": false,
519
+ "nullable": true,
520
+ "precision": 18,
521
+ "scale": 6,
522
+ "mappedType": "decimal"
523
+ },
453
524
  "default_media_id": {
454
525
  "name": "default_media_id",
455
526
  "type": "uuid",
@@ -1439,6 +1510,180 @@
1439
1510
  },
1440
1511
  "nativeEnums": {}
1441
1512
  },
1513
+ {
1514
+ "columns": {
1515
+ "id": {
1516
+ "name": "id",
1517
+ "type": "uuid",
1518
+ "unsigned": false,
1519
+ "autoincrement": false,
1520
+ "primary": false,
1521
+ "nullable": false,
1522
+ "default": "gen_random_uuid()",
1523
+ "mappedType": "uuid"
1524
+ },
1525
+ "product_id": {
1526
+ "name": "product_id",
1527
+ "type": "uuid",
1528
+ "unsigned": false,
1529
+ "autoincrement": false,
1530
+ "primary": false,
1531
+ "nullable": false,
1532
+ "mappedType": "uuid"
1533
+ },
1534
+ "organization_id": {
1535
+ "name": "organization_id",
1536
+ "type": "uuid",
1537
+ "unsigned": false,
1538
+ "autoincrement": false,
1539
+ "primary": false,
1540
+ "nullable": false,
1541
+ "mappedType": "uuid"
1542
+ },
1543
+ "tenant_id": {
1544
+ "name": "tenant_id",
1545
+ "type": "uuid",
1546
+ "unsigned": false,
1547
+ "autoincrement": false,
1548
+ "primary": false,
1549
+ "nullable": false,
1550
+ "mappedType": "uuid"
1551
+ },
1552
+ "unit_code": {
1553
+ "name": "unit_code",
1554
+ "type": "text",
1555
+ "unsigned": false,
1556
+ "autoincrement": false,
1557
+ "primary": false,
1558
+ "nullable": false,
1559
+ "mappedType": "text"
1560
+ },
1561
+ "to_base_factor": {
1562
+ "name": "to_base_factor",
1563
+ "type": "numeric(24,12)",
1564
+ "unsigned": false,
1565
+ "autoincrement": false,
1566
+ "primary": false,
1567
+ "nullable": false,
1568
+ "precision": 24,
1569
+ "scale": 12,
1570
+ "mappedType": "decimal"
1571
+ },
1572
+ "sort_order": {
1573
+ "name": "sort_order",
1574
+ "type": "int",
1575
+ "unsigned": false,
1576
+ "autoincrement": false,
1577
+ "primary": false,
1578
+ "nullable": false,
1579
+ "default": "0",
1580
+ "mappedType": "integer"
1581
+ },
1582
+ "is_active": {
1583
+ "name": "is_active",
1584
+ "type": "boolean",
1585
+ "unsigned": false,
1586
+ "autoincrement": false,
1587
+ "primary": false,
1588
+ "nullable": false,
1589
+ "default": "true",
1590
+ "mappedType": "boolean"
1591
+ },
1592
+ "metadata": {
1593
+ "name": "metadata",
1594
+ "type": "jsonb",
1595
+ "unsigned": false,
1596
+ "autoincrement": false,
1597
+ "primary": false,
1598
+ "nullable": true,
1599
+ "mappedType": "json"
1600
+ },
1601
+ "created_at": {
1602
+ "name": "created_at",
1603
+ "type": "timestamptz",
1604
+ "unsigned": false,
1605
+ "autoincrement": false,
1606
+ "primary": false,
1607
+ "nullable": false,
1608
+ "length": 6,
1609
+ "mappedType": "datetime"
1610
+ },
1611
+ "updated_at": {
1612
+ "name": "updated_at",
1613
+ "type": "timestamptz",
1614
+ "unsigned": false,
1615
+ "autoincrement": false,
1616
+ "primary": false,
1617
+ "nullable": false,
1618
+ "length": 6,
1619
+ "mappedType": "datetime"
1620
+ },
1621
+ "deleted_at": {
1622
+ "name": "deleted_at",
1623
+ "type": "timestamptz",
1624
+ "unsigned": false,
1625
+ "autoincrement": false,
1626
+ "primary": false,
1627
+ "nullable": true,
1628
+ "length": 6,
1629
+ "mappedType": "datetime"
1630
+ }
1631
+ },
1632
+ "name": "catalog_product_unit_conversions",
1633
+ "schema": "public",
1634
+ "indexes": [
1635
+ {
1636
+ "keyName": "catalog_product_unit_conversions_scope_idx",
1637
+ "columnNames": [
1638
+ "organization_id",
1639
+ "tenant_id",
1640
+ "product_id"
1641
+ ],
1642
+ "composite": true,
1643
+ "constraint": false,
1644
+ "primary": false,
1645
+ "unique": false
1646
+ },
1647
+ {
1648
+ "keyName": "catalog_product_unit_conversions_unique",
1649
+ "columnNames": [
1650
+ "product_id",
1651
+ "unit_code"
1652
+ ],
1653
+ "composite": true,
1654
+ "constraint": true,
1655
+ "primary": false,
1656
+ "unique": true
1657
+ },
1658
+ {
1659
+ "keyName": "catalog_product_unit_conversions_pkey",
1660
+ "columnNames": [
1661
+ "id"
1662
+ ],
1663
+ "composite": false,
1664
+ "constraint": true,
1665
+ "primary": true,
1666
+ "unique": true
1667
+ }
1668
+ ],
1669
+ "checks": [],
1670
+ "foreignKeys": {
1671
+ "catalog_product_unit_conversions_product_id_foreign": {
1672
+ "constraintName": "catalog_product_unit_conversions_product_id_foreign",
1673
+ "columnNames": [
1674
+ "product_id"
1675
+ ],
1676
+ "localTableName": "public.catalog_product_unit_conversions",
1677
+ "referencedColumnNames": [
1678
+ "id"
1679
+ ],
1680
+ "referencedTableName": "public.catalog_products",
1681
+ "deleteRule": "cascade",
1682
+ "updateRule": "cascade"
1683
+ }
1684
+ },
1685
+ "nativeEnums": {}
1686
+ },
1442
1687
  {
1443
1688
  "columns": {
1444
1689
  "id": {
@@ -0,0 +1,21 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260218225422 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`create table "catalog_product_unit_conversions" ("id" uuid not null default gen_random_uuid(), "product_id" uuid not null, "organization_id" uuid not null, "tenant_id" uuid not null, "unit_code" text not null, "to_base_factor" numeric(24,12) not null, "sort_order" int not null default 0, "is_active" boolean not null default true, "metadata" jsonb null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, constraint "catalog_product_unit_conversions_pkey" primary key ("id"));`);
7
+ this.addSql(`create index "catalog_product_unit_conversions_scope_idx" on "catalog_product_unit_conversions" ("organization_id", "tenant_id", "product_id");`);
8
+ this.addSql(`alter table "catalog_product_unit_conversions" add constraint "catalog_product_unit_conversions_unique" unique ("product_id", "unit_code");`);
9
+
10
+ this.addSql(`alter table "catalog_product_unit_conversions" add constraint "catalog_product_unit_conversions_product_id_foreign" foreign key ("product_id") references "catalog_products" ("id") on update cascade on delete cascade;`);
11
+
12
+ this.addSql(`alter table "catalog_products" add column "default_sales_unit" text null, add column "default_sales_unit_quantity" numeric(18,6) not null default '1', add column "uom_rounding_scale" smallint not null default 4, add column "uom_rounding_mode" text not null default 'half_up', add column "unit_price_enabled" boolean not null default false, add column "unit_price_reference_unit" text null, add column "unit_price_base_quantity" numeric(18,6) null;`);
13
+ this.addSql(`update "catalog_products" set "default_sales_unit" = "default_unit" where "default_sales_unit" is null and "default_unit" is not null;`);
14
+ }
15
+
16
+ override async down(): Promise<void> {
17
+ this.addSql(`drop table if exists "catalog_product_unit_conversions" cascade;`);
18
+ this.addSql(`alter table "catalog_products" drop column "default_sales_unit", drop column "default_sales_unit_quantity", drop column "uom_rounding_scale", drop column "uom_rounding_mode", drop column "unit_price_enabled", drop column "unit_price_reference_unit", drop column "unit_price_base_quantity";`);
19
+ }
20
+
21
+ }
@@ -0,0 +1,26 @@
1
+ import { Migration } from "@mikro-orm/migrations";
2
+
3
+ // Data migration: normalize legacy 'qty' unit codes to 'pc'
4
+ export class Migration20260219084500 extends Migration {
5
+ override async up(): Promise<void> {
6
+ this.addSql(
7
+ `update "catalog_products" set "default_unit" = 'pc' where lower(btrim(coalesce("default_unit", ''))) = 'qty';`,
8
+ );
9
+ this.addSql(
10
+ `update "catalog_products" set "default_sales_unit" = 'pc' where lower(btrim(coalesce("default_sales_unit", ''))) = 'qty';`,
11
+ );
12
+ this.addSql(
13
+ `update "catalog_products" set "unit_price_reference_unit" = 'pc' where lower(btrim(coalesce("unit_price_reference_unit", ''))) = 'qty';`,
14
+ );
15
+
16
+ this.addSql(
17
+ `delete from "catalog_product_unit_conversions" as legacy where lower(btrim(coalesce(legacy."unit_code", ''))) = 'qty' and exists (select 1 from "catalog_product_unit_conversions" as canonical where canonical."product_id" = legacy."product_id" and canonical."id" <> legacy."id" and lower(btrim(coalesce(canonical."unit_code", ''))) = 'pc');`,
18
+ );
19
+ this.addSql(
20
+ `update "catalog_product_unit_conversions" set "unit_code" = 'pc' where lower(btrim(coalesce("unit_code", ''))) = 'qty';`,
21
+ );
22
+ }
23
+
24
+ // Irreversible: cannot reconstruct original 'qty' values
25
+ override async down(): Promise<void> {}
26
+ }
@@ -242,6 +242,20 @@ function buildPriceKindPresenter(
242
242
  return { title, subtitle, icon: 'dollar-sign', badge: label }
243
243
  }
244
244
 
245
+ function buildUnitConversionPresenter(
246
+ translate: TranslateFn,
247
+ record: Record<string, unknown>,
248
+ ): SearchResultPresenter {
249
+ const label = translate('catalog.search.badge.unitConversion', 'Unit Conversion')
250
+ const unitCode = readRecordText(record, 'unit_code', 'unitCode')
251
+ const factor = readRecordText(record, 'to_base_factor', 'toBaseFactor')
252
+ const title = unitCode ? `${unitCode} (×${factor ?? '?'})` : label
253
+ const isActive = record.is_active ?? record.isActive
254
+ const statusText = isActive === false ? translate('catalog.search.status.inactive', 'Inactive') : null
255
+ const subtitle = formatSubtitle(unitCode, statusText)
256
+ return { title, subtitle, icon: 'ruler', badge: label }
257
+ }
258
+
245
259
  function buildOptionSchemaPresenter(
246
260
  translate: TranslateFn,
247
261
  record: Record<string, unknown>,
@@ -277,6 +291,26 @@ export const searchConfig: SearchModuleConfig = {
277
291
  appendLine(lines, 'SKU', record.sku)
278
292
  appendLine(lines, 'Handle', record.handle)
279
293
  appendLine(lines, 'Product type', record.product_type ?? record.productType)
294
+ appendLine(lines, 'Base unit', record.default_unit ?? record.defaultUnit)
295
+ appendLine(lines, 'Default sales unit', record.default_sales_unit ?? record.defaultSalesUnit)
296
+ appendLine(
297
+ lines,
298
+ 'Default sales quantity',
299
+ record.default_sales_unit_quantity ?? record.defaultSalesUnitQuantity,
300
+ )
301
+ appendLine(lines, 'UoM rounding mode', record.uom_rounding_mode ?? record.uomRoundingMode)
302
+ appendLine(lines, 'UoM rounding scale', record.uom_rounding_scale ?? record.uomRoundingScale)
303
+ appendLine(lines, 'Unit price enabled', record.unit_price_enabled ?? record.unitPriceEnabled)
304
+ appendLine(
305
+ lines,
306
+ 'Unit price reference unit',
307
+ record.unit_price_reference_unit ?? record.unitPriceReferenceUnit,
308
+ )
309
+ appendLine(
310
+ lines,
311
+ 'Unit price base quantity',
312
+ record.unit_price_base_quantity ?? record.unitPriceBaseQuantity,
313
+ )
280
314
  return buildIndexSource(ctx, buildProductPresenter(translate, record), lines)
281
315
  },
282
316
  formatResult: async (ctx) => {
@@ -285,7 +319,17 @@ export const searchConfig: SearchModuleConfig = {
285
319
  },
286
320
  resolveUrl: async (ctx) => buildProductUrl(readRecordText(ctx.record, 'id')),
287
321
  fieldPolicy: {
288
- searchable: ['title', 'subtitle', 'description', 'sku', 'handle', 'product_type'],
322
+ searchable: [
323
+ 'title',
324
+ 'subtitle',
325
+ 'description',
326
+ 'sku',
327
+ 'handle',
328
+ 'product_type',
329
+ 'default_unit',
330
+ 'default_sales_unit',
331
+ 'unit_price_reference_unit',
332
+ ],
289
333
  excluded: ['metadata', 'dimensions', 'tax_rate_id'],
290
334
  },
291
335
  },
@@ -411,6 +455,34 @@ export const searchConfig: SearchModuleConfig = {
411
455
  searchable: ['title', 'code'],
412
456
  },
413
457
  },
458
+ {
459
+ entityId: 'catalog:catalog_product_unit_conversion',
460
+ enabled: true,
461
+ priority: 3,
462
+ buildSource: async (ctx) => {
463
+ const { t: translate } = await resolveTranslations()
464
+ const record = ctx.record
465
+ const lines: string[] = []
466
+ appendLine(lines, 'Unit code', record.unit_code ?? record.unitCode)
467
+ appendLine(lines, 'To base factor', record.to_base_factor ?? record.toBaseFactor)
468
+ appendLine(lines, 'Sort order', record.sort_order ?? record.sortOrder)
469
+ const isActive = record.is_active ?? record.isActive
470
+ appendLine(lines, 'Active', isActive)
471
+ return buildIndexSource(ctx, buildUnitConversionPresenter(translate, record), lines)
472
+ },
473
+ formatResult: async (ctx) => {
474
+ const { t: translate } = await resolveTranslations()
475
+ return buildUnitConversionPresenter(translate, ctx.record)
476
+ },
477
+ resolveUrl: async (ctx) => {
478
+ const productId = resolveProductId(ctx.record)
479
+ return buildProductUrl(productId)
480
+ },
481
+ fieldPolicy: {
482
+ searchable: ['unit_code'],
483
+ excluded: ['metadata'],
484
+ },
485
+ },
414
486
  {
415
487
  entityId: 'catalog:catalog_option_schema_template',
416
488
  enabled: true,