@open-mercato/core 0.4.5-develop-3ce83a8b24 → 0.4.5-develop-4849712ccb

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 (163) 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/generated/entities/catalog_product/index.ts +8 -0
  94. package/generated/entities/catalog_product_unit_conversion/index.ts +12 -0
  95. package/generated/entities/sales_credit_memo_line/index.ts +3 -0
  96. package/generated/entities/sales_invoice_line/index.ts +3 -0
  97. package/generated/entities/sales_order_line/index.ts +3 -0
  98. package/generated/entities/sales_quote_line/index.ts +3 -0
  99. package/generated/entities.ids.generated.ts +1 -0
  100. package/generated/entity-fields-registry.ts +2 -0
  101. package/package.json +2 -2
  102. package/src/modules/auth/i18n/de.json +1 -1
  103. package/src/modules/auth/i18n/en.json +1 -1
  104. package/src/modules/auth/i18n/es.json +1 -1
  105. package/src/modules/auth/i18n/pl.json +1 -1
  106. package/src/modules/catalog/api/prices/route.ts +213 -81
  107. package/src/modules/catalog/api/product-unit-conversions/route.ts +195 -0
  108. package/src/modules/catalog/api/products/route.ts +638 -402
  109. package/src/modules/catalog/backend/catalog/products/[id]/page.tsx +2085 -1072
  110. package/src/modules/catalog/backend/catalog/products/create/page.tsx +1288 -593
  111. package/src/modules/catalog/commands/index.ts +1 -0
  112. package/src/modules/catalog/commands/productUnitConversions.ts +626 -0
  113. package/src/modules/catalog/commands/products.ts +1151 -693
  114. package/src/modules/catalog/commands/shared.ts +19 -5
  115. package/src/modules/catalog/components/products/ProductUomSection.tsx +745 -0
  116. package/src/modules/catalog/components/products/productForm.ts +369 -256
  117. package/src/modules/catalog/components/products/productFormUtils.ts +82 -0
  118. package/src/modules/catalog/data/entities.ts +82 -1
  119. package/src/modules/catalog/data/validators.ts +118 -34
  120. package/src/modules/catalog/events.ts +3 -0
  121. package/src/modules/catalog/i18n/de.json +56 -0
  122. package/src/modules/catalog/i18n/en.json +56 -0
  123. package/src/modules/catalog/i18n/es.json +56 -0
  124. package/src/modules/catalog/i18n/pl.json +56 -0
  125. package/src/modules/catalog/lib/unitCodes.ts +1 -0
  126. package/src/modules/catalog/lib/unitResolution.ts +62 -0
  127. package/src/modules/catalog/migrations/.snapshot-open-mercato.json +245 -0
  128. package/src/modules/catalog/migrations/Migration20260218225422.ts +21 -0
  129. package/src/modules/catalog/migrations/Migration20260219084500.ts +26 -0
  130. package/src/modules/catalog/search.ts +73 -1
  131. package/src/modules/catalog/seed/examples.ts +552 -479
  132. package/src/modules/dashboards/i18n/de.json +1 -1
  133. package/src/modules/dashboards/i18n/en.json +1 -1
  134. package/src/modules/dashboards/i18n/es.json +1 -1
  135. package/src/modules/dashboards/i18n/pl.json +1 -1
  136. package/src/modules/dashboards/seed/analytics.ts +3 -0
  137. package/src/modules/sales/api/order-lines/route.ts +158 -68
  138. package/src/modules/sales/api/quote-lines/route.ts +161 -67
  139. package/src/modules/sales/api/quotes/public/[token]/route.ts +122 -36
  140. package/src/modules/sales/commands/documents.ts +4250 -2424
  141. package/src/modules/sales/commands/shared.ts +7 -2
  142. package/src/modules/sales/components/documents/ItemsSection.tsx +580 -310
  143. package/src/modules/sales/components/documents/LineItemDialog.tsx +1988 -833
  144. package/src/modules/sales/components/documents/ShipmentsSection.tsx +17 -3
  145. package/src/modules/sales/components/documents/lineItemTypes.ts +6 -0
  146. package/src/modules/sales/data/entities.ts +53 -0
  147. package/src/modules/sales/data/validators.ts +36 -0
  148. package/src/modules/sales/frontend/quote/[token]/page.tsx +25 -1
  149. package/src/modules/sales/i18n/de.json +23 -3
  150. package/src/modules/sales/i18n/en.json +23 -3
  151. package/src/modules/sales/i18n/es.json +23 -3
  152. package/src/modules/sales/i18n/pl.json +23 -3
  153. package/src/modules/sales/lib/types.ts +30 -0
  154. package/src/modules/sales/migrations/.snapshot-open-mercato.json +172 -0
  155. package/src/modules/sales/migrations/Migration20260218225423.ts +37 -0
  156. package/src/modules/sales/migrations/Migration20260219084501.ts +73 -0
  157. package/src/modules/sales/search.ts +28 -0
  158. package/src/modules/sales/seed/examples.ts +20 -1
  159. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +1 -1
  160. package/src/modules/workflows/i18n/de.json +4 -4
  161. package/src/modules/workflows/i18n/en.json +4 -4
  162. package/src/modules/workflows/i18n/es.json +4 -4
  163. package/src/modules/workflows/i18n/pl.json +4 -4
@@ -0,0 +1,82 @@
1
+ import { REFERENCE_UNIT_CODES, type ReferenceUnitCode } from "@open-mercato/shared/lib/units/unitCodes";
2
+ import { canonicalizeUnitCode } from "../../lib/unitCodes";
3
+ import { createCrudFormError } from "@open-mercato/ui/backend/utils/serverErrors";
4
+ import type { ProductUnitConversionDraft } from "./productForm";
5
+
6
+ export const UNIT_PRICE_REFERENCE_UNITS = new Set<ReferenceUnitCode>(REFERENCE_UNIT_CODES);
7
+
8
+ export function toTrimmedOrNull(value: unknown): string | null {
9
+ if (typeof value !== "string") return null;
10
+ const trimmed = value.trim();
11
+ return trimmed.length ? trimmed : null;
12
+ }
13
+
14
+ export function parseNumericInput(value: unknown): number {
15
+ if (typeof value === "number") return value;
16
+ if (typeof value === "string") {
17
+ const normalized = value.trim().replace(/\s+/g, "").replace(/,/g, ".");
18
+ if (!normalized.length) return Number.NaN;
19
+ return Number(normalized);
20
+ }
21
+ return Number(value);
22
+ }
23
+
24
+ export function toPositiveNumberOrNull(value: unknown): number | null {
25
+ const numeric = parseNumericInput(value);
26
+ if (!Number.isFinite(numeric) || numeric <= 0) return null;
27
+ return numeric;
28
+ }
29
+
30
+ export function toIntegerInRangeOrDefault(
31
+ value: unknown,
32
+ min: number,
33
+ max: number,
34
+ fallback: number,
35
+ ): number {
36
+ const numeric = parseNumericInput(value);
37
+ if (!Number.isInteger(numeric) || numeric < min || numeric > max)
38
+ return fallback;
39
+ return numeric;
40
+ }
41
+
42
+ export type ProductUnitConversionInput = {
43
+ id?: string | null;
44
+ unitCode: string;
45
+ toBaseFactor: number;
46
+ sortOrder: number;
47
+ isActive: boolean;
48
+ };
49
+
50
+ export function normalizeProductConversionInputs(
51
+ rows: ProductUnitConversionDraft[] | undefined,
52
+ duplicateMessage: string,
53
+ ): ProductUnitConversionInput[] {
54
+ const list = Array.isArray(rows) ? rows : [];
55
+ const normalized: ProductUnitConversionInput[] = [];
56
+ const seen = new Set<string>();
57
+ for (const row of list) {
58
+ const unitCode = canonicalizeUnitCode(row?.unitCode);
59
+ const toBaseFactor = toPositiveNumberOrNull(row?.toBaseFactor);
60
+ if (!unitCode || toBaseFactor === null) continue;
61
+ const unitKey = unitCode.toLowerCase();
62
+ if (seen.has(unitKey)) {
63
+ throw createCrudFormError(duplicateMessage, {
64
+ unitConversions: duplicateMessage,
65
+ });
66
+ }
67
+ seen.add(unitKey);
68
+ normalized.push({
69
+ id: toTrimmedOrNull(row?.id),
70
+ unitCode,
71
+ toBaseFactor,
72
+ sortOrder: toIntegerInRangeOrDefault(
73
+ row?.sortOrder,
74
+ 0,
75
+ 100000,
76
+ normalized.length * 10,
77
+ ),
78
+ isActive: row?.isActive !== false,
79
+ });
80
+ }
81
+ return normalized;
82
+ }
@@ -15,6 +15,7 @@ import type {
15
15
  CatalogProductRelationType,
16
16
  CatalogProductType,
17
17
  } from './types'
18
+ import type { ReferenceUnitCode } from '@open-mercato/shared/lib/units/unitCodes'
18
19
 
19
20
  @Entity({ tableName: 'catalog_product_option_schemas' })
20
21
  @Index({
@@ -70,7 +71,14 @@ export class CatalogOptionSchemaTemplate {
70
71
  @Unique({ name: 'catalog_products_sku_scope_unique', properties: ['organizationId', 'tenantId', 'sku'] })
71
72
  @Unique({ name: 'catalog_products_handle_scope_unique', properties: ['organizationId', 'tenantId', 'handle'] })
72
73
  export class CatalogProduct {
73
- [OptionalProps]?: 'createdAt' | 'updatedAt' | 'deletedAt'
74
+ [OptionalProps]?:
75
+ | 'createdAt'
76
+ | 'updatedAt'
77
+ | 'deletedAt'
78
+ | 'defaultSalesUnitQuantity'
79
+ | 'uomRoundingScale'
80
+ | 'uomRoundingMode'
81
+ | 'unitPriceEnabled'
74
82
 
75
83
  @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
76
84
  id!: string
@@ -114,6 +122,27 @@ export class CatalogProduct {
114
122
  @Property({ name: 'default_unit', type: 'text', nullable: true })
115
123
  defaultUnit?: string | null
116
124
 
125
+ @Property({ name: 'default_sales_unit', type: 'text', nullable: true })
126
+ defaultSalesUnit?: string | null
127
+
128
+ @Property({ name: 'default_sales_unit_quantity', type: 'numeric', precision: 18, scale: 6, default: '1' })
129
+ defaultSalesUnitQuantity: string = '1'
130
+
131
+ @Property({ name: 'uom_rounding_scale', type: 'smallint', default: 4 })
132
+ uomRoundingScale: number = 4
133
+
134
+ @Property({ name: 'uom_rounding_mode', type: 'text', default: 'half_up' })
135
+ uomRoundingMode: 'half_up' | 'down' | 'up' = 'half_up'
136
+
137
+ @Property({ name: 'unit_price_enabled', type: 'boolean', default: false })
138
+ unitPriceEnabled: boolean = false
139
+
140
+ @Property({ name: 'unit_price_reference_unit', type: 'text', nullable: true })
141
+ unitPriceReferenceUnit?: ReferenceUnitCode | null
142
+
143
+ @Property({ name: 'unit_price_base_quantity', type: 'numeric', precision: 18, scale: 6, nullable: true })
144
+ unitPriceBaseQuantity?: string | null
145
+
117
146
  @Property({ name: 'default_media_id', type: 'uuid', nullable: true })
118
147
  defaultMediaId?: string | null
119
148
 
@@ -174,7 +203,59 @@ export class CatalogProduct {
174
203
  @OneToMany(() => CatalogProductTagAssignment, (assignment) => assignment.product)
175
204
  tagAssignments = new Collection<CatalogProductTagAssignment>(this)
176
205
 
206
+ @OneToMany(() => CatalogProductUnitConversion, (conversion) => conversion.product)
207
+ unitConversions = new Collection<CatalogProductUnitConversion>(this)
177
208
  }
209
+
210
+ @Entity({ tableName: 'catalog_product_unit_conversions' })
211
+ @Index({
212
+ name: 'catalog_product_unit_conversions_scope_idx',
213
+ properties: ['organizationId', 'tenantId', 'product'],
214
+ })
215
+ @Unique({
216
+ name: 'catalog_product_unit_conversions_unique',
217
+ properties: ['product', 'unitCode'],
218
+ })
219
+ export class CatalogProductUnitConversion {
220
+ [OptionalProps]?: 'createdAt' | 'updatedAt' | 'deletedAt' | 'sortOrder' | 'isActive'
221
+
222
+ @PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
223
+ id!: string
224
+
225
+ @ManyToOne(() => CatalogProduct, { fieldName: 'product_id', deleteRule: 'cascade' })
226
+ product!: CatalogProduct
227
+
228
+ @Property({ name: 'organization_id', type: 'uuid' })
229
+ organizationId!: string
230
+
231
+ @Property({ name: 'tenant_id', type: 'uuid' })
232
+ tenantId!: string
233
+
234
+ @Property({ name: 'unit_code', type: 'text' })
235
+ unitCode!: string
236
+
237
+ @Property({ name: 'to_base_factor', type: 'numeric', precision: 24, scale: 12 })
238
+ toBaseFactor!: string
239
+
240
+ @Property({ name: 'sort_order', type: 'integer', default: 0 })
241
+ sortOrder: number = 0
242
+
243
+ @Property({ name: 'is_active', type: 'boolean', default: true })
244
+ isActive: boolean = true
245
+
246
+ @Property({ name: 'metadata', type: 'jsonb', nullable: true })
247
+ metadata?: Record<string, unknown> | null
248
+
249
+ @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
250
+ createdAt: Date = new Date()
251
+
252
+ @Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
253
+ updatedAt: Date = new Date()
254
+
255
+ @Property({ name: 'deleted_at', type: Date, nullable: true })
256
+ deletedAt?: Date | null
257
+ }
258
+
178
259
  @Entity({ tableName: 'catalog_product_categories' })
179
260
  @Index({ name: 'catalog_product_categories_scope_idx', properties: ['organizationId', 'tenantId'] })
180
261
  @Unique({ name: 'catalog_product_categories_slug_unique', properties: ['organizationId', 'tenantId', 'slug'] })
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod'
2
2
  import { CATALOG_PRICE_DISPLAY_MODES, CATALOG_PRODUCT_TYPES } from './types'
3
+ import { REFERENCE_UNIT_CODES } from '../lib/unitCodes'
3
4
 
4
5
  const uuid = () => z.string().uuid()
5
6
 
@@ -107,42 +108,102 @@ export const offerUpdateSchema = z
107
108
  )
108
109
 
109
110
  const productTypeSchema = z.enum(CATALOG_PRODUCT_TYPES)
111
+ const uomRoundingModeSchema = z.enum(['half_up', 'down', 'up'])
112
+ const unitPriceReferenceUnitSchema = z.enum(REFERENCE_UNIT_CODES)
113
+ const unitPriceConfigSchema = z.object({
114
+ enabled: z.boolean().optional(),
115
+ referenceUnit: unitPriceReferenceUnitSchema.nullable().optional(),
116
+ baseQuantity: z.coerce.number().positive().optional(),
117
+ })
110
118
 
111
- export const productCreateSchema = scoped.extend({
112
- title: z.string().trim().min(1).max(255),
113
- subtitle: z.string().trim().max(255).optional(),
114
- description: z.string().trim().max(4000).optional(),
115
- sku: skuSchema.optional(),
116
- handle: handleSchema.optional(),
117
- taxRateId: uuid().nullable().optional(),
118
- taxRate: z.coerce.number().min(0).max(100).optional().nullable(),
119
- productType: productTypeSchema.default('simple'),
120
- statusEntryId: uuid().optional(),
121
- primaryCurrencyCode: currencyCodeSchema.optional(),
122
- defaultUnit: z.string().trim().max(50).optional(),
123
- defaultMediaId: uuid().optional().nullable(),
124
- defaultMediaUrl: z.string().trim().max(500).optional().nullable(),
125
- weightValue: z.coerce.number().min(0).optional().nullable(),
126
- weightUnit: z.string().trim().max(25).optional().nullable(),
127
- dimensions: z
128
- .object({
129
- width: z.coerce.number().min(0).optional(),
130
- height: z.coerce.number().min(0).optional(),
131
- depth: z.coerce.number().min(0).optional(),
132
- unit: z.string().trim().max(25).optional(),
119
+ function productUomCrossFieldRefinement(
120
+ input: {
121
+ defaultUnit?: string | null
122
+ defaultSalesUnit?: string | null
123
+ unitPriceEnabled?: boolean
124
+ unitPriceReferenceUnit?: string | null
125
+ unitPriceBaseQuantity?: number
126
+ unitPrice?: { enabled?: boolean; referenceUnit?: string | null; baseQuantity?: number }
127
+ },
128
+ ctx: z.RefinementCtx,
129
+ ) {
130
+ const defaultUnit = typeof input.defaultUnit === 'string' ? input.defaultUnit.trim() : ''
131
+ const defaultSalesUnit =
132
+ typeof input.defaultSalesUnit === 'string' ? input.defaultSalesUnit.trim() : ''
133
+ if (defaultSalesUnit && !defaultUnit) {
134
+ ctx.addIssue({
135
+ code: z.ZodIssueCode.custom,
136
+ path: ['defaultSalesUnit'],
137
+ message: 'catalog.products.validation.baseUnitRequired',
133
138
  })
134
- .optional()
135
- .nullable(),
136
- optionSchemaId: uuid().nullable().optional(),
137
- optionSchema: optionSchema.optional(),
138
- customFieldsetCode: slugSchema.nullable().optional(),
139
- isConfigurable: z.boolean().optional(),
140
- isActive: z.boolean().optional(),
141
- metadata: metadataSchema,
142
- offers: z.array(offerInputSchema.omit({ id: true })).optional(),
143
- categoryIds: z.array(uuid()).max(100).optional(),
144
- tags: z.array(tagLabelSchema).max(100).optional(),
145
- })
139
+ }
140
+ const unitPriceEnabled = input.unitPrice?.enabled ?? input.unitPriceEnabled ?? false
141
+ if (!unitPriceEnabled) return
142
+ const referenceUnit =
143
+ input.unitPrice?.referenceUnit ?? input.unitPriceReferenceUnit ?? null
144
+ const baseQuantity =
145
+ input.unitPrice?.baseQuantity ?? input.unitPriceBaseQuantity ?? null
146
+ if (!referenceUnit) {
147
+ ctx.addIssue({
148
+ code: z.ZodIssueCode.custom,
149
+ path: ['unitPrice'],
150
+ message: 'catalog.products.validation.referenceUnitRequired',
151
+ })
152
+ }
153
+ if (baseQuantity === null || baseQuantity === undefined || Number(baseQuantity) <= 0) {
154
+ ctx.addIssue({
155
+ code: z.ZodIssueCode.custom,
156
+ path: ['unitPrice'],
157
+ message: 'catalog.products.unitPrice.errors.baseQuantity',
158
+ })
159
+ }
160
+ }
161
+
162
+ export const productCreateSchema = scoped
163
+ .extend({
164
+ title: z.string().trim().min(1).max(255),
165
+ subtitle: z.string().trim().max(255).optional(),
166
+ description: z.string().trim().max(4000).optional(),
167
+ sku: skuSchema.optional(),
168
+ handle: handleSchema.optional(),
169
+ taxRateId: uuid().nullable().optional(),
170
+ taxRate: z.coerce.number().min(0).max(100).optional().nullable(),
171
+ productType: productTypeSchema.default('simple'),
172
+ statusEntryId: uuid().optional(),
173
+ primaryCurrencyCode: currencyCodeSchema.optional(),
174
+ defaultUnit: z.string().trim().max(50).optional().nullable(),
175
+ defaultSalesUnit: z.string().trim().max(50).optional().nullable(),
176
+ defaultSalesUnitQuantity: z.coerce.number().positive().optional(),
177
+ uomRoundingScale: z.coerce.number().int().min(0).max(6).optional(),
178
+ uomRoundingMode: uomRoundingModeSchema.optional(),
179
+ unitPriceEnabled: z.boolean().optional(),
180
+ unitPriceReferenceUnit: unitPriceReferenceUnitSchema.nullable().optional(),
181
+ unitPriceBaseQuantity: z.coerce.number().positive().optional(),
182
+ unitPrice: unitPriceConfigSchema.optional(),
183
+ defaultMediaId: uuid().optional().nullable(),
184
+ defaultMediaUrl: z.string().trim().max(500).optional().nullable(),
185
+ weightValue: z.coerce.number().min(0).optional().nullable(),
186
+ weightUnit: z.string().trim().max(25).optional().nullable(),
187
+ dimensions: z
188
+ .object({
189
+ width: z.coerce.number().min(0).optional(),
190
+ height: z.coerce.number().min(0).optional(),
191
+ depth: z.coerce.number().min(0).optional(),
192
+ unit: z.string().trim().max(25).optional(),
193
+ })
194
+ .optional()
195
+ .nullable(),
196
+ optionSchemaId: uuid().nullable().optional(),
197
+ optionSchema: optionSchema.optional(),
198
+ customFieldsetCode: slugSchema.nullable().optional(),
199
+ isConfigurable: z.boolean().optional(),
200
+ isActive: z.boolean().optional(),
201
+ metadata: metadataSchema,
202
+ offers: z.array(offerInputSchema.omit({ id: true })).optional(),
203
+ categoryIds: z.array(uuid()).max(100).optional(),
204
+ tags: z.array(tagLabelSchema).max(100).optional(),
205
+ })
206
+ .superRefine(productUomCrossFieldRefinement)
146
207
 
147
208
  export const productUpdateSchema = z
148
209
  .object({
@@ -152,6 +213,7 @@ export const productUpdateSchema = z
152
213
  .extend({
153
214
  productType: productTypeSchema.optional(),
154
215
  })
216
+ .superRefine(productUomCrossFieldRefinement)
155
217
 
156
218
  export const variantCreateSchema = scoped.extend({
157
219
  productId: uuid(),
@@ -265,6 +327,25 @@ export const categoryUpdateSchema = z
265
327
  })
266
328
  .merge(categoryCreateSchema.partial())
267
329
 
330
+ export const productUnitConversionCreateSchema = scoped.extend({
331
+ productId: uuid(),
332
+ unitCode: z.string().trim().min(1).max(50),
333
+ toBaseFactor: z.coerce.number().positive().max(1_000_000),
334
+ sortOrder: z.coerce.number().int().optional(),
335
+ isActive: z.boolean().optional(),
336
+ metadata: metadataSchema,
337
+ })
338
+
339
+ export const productUnitConversionUpdateSchema = z
340
+ .object({
341
+ id: uuid(),
342
+ })
343
+ .merge(productUnitConversionCreateSchema.omit({ productId: true }).partial())
344
+
345
+ export const productUnitConversionDeleteSchema = scoped.extend({
346
+ id: uuid(),
347
+ })
348
+
268
349
  export type ProductCreateInput = z.infer<typeof productCreateSchema>
269
350
  export type ProductUpdateInput = z.infer<typeof productUpdateSchema>
270
351
  export type VariantCreateInput = z.infer<typeof variantCreateSchema>
@@ -280,3 +361,6 @@ export type CategoryUpdateInput = z.infer<typeof categoryUpdateSchema>
280
361
  export type OfferInput = z.infer<typeof offerInputSchema>
281
362
  export type OfferCreateInput = z.infer<typeof offerCreateSchema>
282
363
  export type OfferUpdateInput = z.infer<typeof offerUpdateSchema>
364
+ export type ProductUnitConversionCreateInput = z.infer<typeof productUnitConversionCreateSchema>
365
+ export type ProductUnitConversionUpdateInput = z.infer<typeof productUnitConversionUpdateSchema>
366
+ export type ProductUnitConversionDeleteInput = z.infer<typeof productUnitConversionDeleteSchema>
@@ -10,6 +10,9 @@ const events = [
10
10
  { id: 'catalog.product.created', label: 'Product Created', entity: 'product', category: 'crud' },
11
11
  { id: 'catalog.product.updated', label: 'Product Updated', entity: 'product', category: 'crud' },
12
12
  { id: 'catalog.product.deleted', label: 'Product Deleted', entity: 'product', category: 'crud' },
13
+ { id: 'catalog.product_unit_conversion.created', label: 'Product Unit Conversion Created', entity: 'product_unit_conversion', category: 'crud' },
14
+ { id: 'catalog.product_unit_conversion.updated', label: 'Product Unit Conversion Updated', entity: 'product_unit_conversion', category: 'crud' },
15
+ { id: 'catalog.product_unit_conversion.deleted', label: 'Product Unit Conversion Deleted', entity: 'product_unit_conversion', category: 'crud' },
13
16
 
14
17
  // Categories
15
18
  { id: 'catalog.category.created', label: 'Category Created', entity: 'category', category: 'crud' },
@@ -16,6 +16,9 @@
16
16
  "catalog.audit.prices.create": "Preis erstellen",
17
17
  "catalog.audit.prices.delete": "Preis löschen",
18
18
  "catalog.audit.prices.update": "Preis aktualisieren",
19
+ "catalog.audit.productUnitConversions.create": "Produkteinheiten-Umrechnung erstellen",
20
+ "catalog.audit.productUnitConversions.delete": "Produkteinheiten-Umrechnung löschen",
21
+ "catalog.audit.productUnitConversions.update": "Produkteinheiten-Umrechnung aktualisieren",
19
22
  "catalog.audit.products.create": "Produkt erstellen",
20
23
  "catalog.audit.products.delete": "Produkt löschen",
21
24
  "catalog.audit.products.update": "Produkt aktualisieren",
@@ -394,6 +397,7 @@
394
397
  "catalog.products.edit.weight.value": "Gewicht",
395
398
  "catalog.products.errors.handleExists": "Handle wird bereits verwendet.",
396
399
  "catalog.products.errors.skuExists": "SKU wird bereits verwendet.",
400
+ "catalog.products.errors.taxClassNotFound": "Steuerklasse nicht gefunden.",
397
401
  "catalog.products.filters.active": "Aktiv",
398
402
  "catalog.products.filters.categories": "Kategorien",
399
403
  "catalog.products.filters.categoriesLoadError": "Kategorien konnten nicht geladen werden",
@@ -454,12 +458,64 @@
454
458
  "catalog.products.types.grouped": "Gruppiert",
455
459
  "catalog.products.types.simple": "Einfach",
456
460
  "catalog.products.types.virtual": "Virtuell",
461
+ "catalog.products.unitPrice.baseQuantity": "Basismenge für Referenz",
462
+ "catalog.products.unitPrice.enable": "EU-Grundpreisanzeige aktivieren",
463
+ "catalog.products.unitPrice.errors.baseQuantity": "Basismenge ist erforderlich, wenn die Grundpreisanzeige aktiviert ist.",
464
+ "catalog.products.unitPrice.errors.referenceUnit": "Referenzeinheit ist erforderlich, wenn die Grundpreisanzeige aktiviert ist.",
465
+ "catalog.products.unitPrice.hint": "Zeigt den berechneten Preis pro ausgewählter Referenzeinheit. In den meisten Fällen Menge 1 verwenden.",
466
+ "catalog.products.unitPrice.hintWithPreview": "Zeigt den berechneten Preis pro {{quantity}} {{unit}}. Für die meisten Produkte 1 verwenden (z. B. 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 Stk.",
472
+ "catalog.products.unitPrice.referenceUnit": "Referenzeinheit",
473
+ "catalog.products.unitPrice.selectReferenceUnit": "Referenzeinheit auswählen",
474
+ "catalog.products.uom.active": "Aktiv",
475
+ "catalog.products.uom.addConversion": "Umrechnung hinzufügen",
476
+ "catalog.products.uom.baseUnit": "Basiseinheit",
477
+ "catalog.products.uom.conversionOrderHint": "Nutze die Pfeile, um die Umrechnungspriorität neu zu ordnen.",
478
+ "catalog.products.uom.conversionPreview": "1 {{fromUnit}} = {{factor}} {{baseUnit}}",
479
+ "catalog.products.uom.conversionUnit": "Verkaufseinheit",
480
+ "catalog.products.uom.conversions": "Produktumrechnungen",
481
+ "catalog.products.uom.defaultSalesQuantity": "Standard-Verkaufsmenge",
482
+ "catalog.products.uom.defaultSalesQuantityHint": "Wird verwendet, um die Menge in Angebots-/Auftragspositionen vorzufüllen. Der Wert wird in der Standard-Verkaufseinheit interpretiert.",
483
+ "catalog.products.uom.defaultSalesQuantityLabel": "Standard-Positionsmenge (in Verkaufseinheit)",
484
+ "catalog.products.uom.defaultSalesQuantityPreview": "Standardposition: {{quantity}} {{salesUnit}}.",
485
+ "catalog.products.uom.defaultSalesQuantityPreviewWithNormalization": "Standardposition: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).",
486
+ "catalog.products.uom.defaultSalesUnit": "Standard-Verkaufseinheit",
487
+ "catalog.products.uom.description": "Basis-/Verkaufseinheiten und Verpackungsumrechnungen festlegen.",
488
+ "catalog.products.uom.emptyConversions": "Noch keine Umrechnungen konfiguriert.",
489
+ "catalog.products.uom.errors.baseRequired": "Basiseinheit ist erforderlich, wenn eine Standard-Verkaufseinheit festgelegt ist.",
490
+ "catalog.products.uom.errors.baseRequiredForConversions": "Basiseinheit ist erforderlich, wenn Umrechnungen konfiguriert sind.",
491
+ "catalog.products.uom.errors.defaultSalesConversionRequired": "Aktive Umrechnung für die Standard-Verkaufseinheit ist erforderlich, wenn sie von der Basiseinheit abweicht.",
492
+ "catalog.products.uom.errors.duplicateConversion": "Doppelte Umrechnungseinheit ist nicht erlaubt.",
493
+ "catalog.products.uom.errors.loadUnits": "Einheiten konnten nicht geladen werden. Bitte versuchen Sie, die Seite zu aktualisieren.",
494
+ "catalog.products.uom.errors.sync": "Produktumrechnungen konnten nicht synchronisiert werden.",
495
+ "catalog.products.uom.loadingUnits": "Einheiten werden geladen...",
496
+ "catalog.products.uom.moveDown": "Umrechnung nach unten verschieben",
497
+ "catalog.products.uom.moveUp": "Umrechnung nach oben verschieben",
498
+ "catalog.products.uom.removeConversion": "Umrechnung entfernen",
499
+ "catalog.products.uom.roundingMode": "Rundungsmodus",
500
+ "catalog.products.uom.roundingModeDown": "Abrunden",
501
+ "catalog.products.uom.roundingModeHalfUp": "Kaufmännisch runden (Standard)",
502
+ "catalog.products.uom.roundingModeUp": "Aufrunden",
503
+ "catalog.products.uom.roundingScale": "Rundungsgenauigkeit (Dezimalstellen)",
504
+ "catalog.products.uom.selectUnit": "Einheit auswählen",
505
+ "catalog.products.uom.sortOrder": "Sortierung",
506
+ "catalog.products.uom.title": "Maßeinheiten",
507
+ "catalog.products.uom.toBaseFactor": "Basisfaktor",
508
+ "catalog.products.validation.baseUnitRequired": "Basiseinheit ist erforderlich, wenn eine Standard-Verkaufseinheit gesetzt ist.",
509
+ "catalog.products.validation.handleFormat": "Handle darf nur Kleinbuchstaben, Ziffern, Bindestriche oder Unterstriche enthalten.",
510
+ "catalog.products.validation.referenceUnitRequired": "Referenzeinheit ist erforderlich, wenn die Grundpreisanzeige aktiviert ist.",
511
+ "catalog.products.validation.titleRequired": "Titel ist erforderlich.",
457
512
  "catalog.search.badge.category": "Kategorie",
458
513
  "catalog.search.badge.offer": "Kanalangebot",
459
514
  "catalog.search.badge.optionSchema": "Optionsschema",
460
515
  "catalog.search.badge.priceKind": "Preisart",
461
516
  "catalog.search.badge.product": "Produkt",
462
517
  "catalog.search.badge.tag": "Schlagwort",
518
+ "catalog.search.badge.unitConversion": "Einheitenumrechnung",
463
519
  "catalog.search.badge.variant": "Variante",
464
520
  "catalog.search.priceKind.excludingTax": "Exkl. MwSt.",
465
521
  "catalog.search.priceKind.includingTax": "Inkl. MwSt.",
@@ -16,6 +16,9 @@
16
16
  "catalog.audit.prices.create": "Create price",
17
17
  "catalog.audit.prices.delete": "Delete price",
18
18
  "catalog.audit.prices.update": "Update price",
19
+ "catalog.audit.productUnitConversions.create": "Create product unit conversion",
20
+ "catalog.audit.productUnitConversions.delete": "Delete product unit conversion",
21
+ "catalog.audit.productUnitConversions.update": "Update product unit conversion",
19
22
  "catalog.audit.products.create": "Create product",
20
23
  "catalog.audit.products.delete": "Delete product",
21
24
  "catalog.audit.products.update": "Update product",
@@ -394,6 +397,7 @@
394
397
  "catalog.products.edit.weight.value": "Weight",
395
398
  "catalog.products.errors.handleExists": "Handle already in use.",
396
399
  "catalog.products.errors.skuExists": "SKU already in use.",
400
+ "catalog.products.errors.taxClassNotFound": "Tax class not found.",
397
401
  "catalog.products.filters.active": "Active",
398
402
  "catalog.products.filters.categories": "Categories",
399
403
  "catalog.products.filters.categoriesLoadError": "Failed to load categories",
@@ -454,12 +458,64 @@
454
458
  "catalog.products.types.grouped": "Grouped",
455
459
  "catalog.products.types.simple": "Simple",
456
460
  "catalog.products.types.virtual": "Virtual",
461
+ "catalog.products.unitPrice.baseQuantity": "Base quantity for reference",
462
+ "catalog.products.unitPrice.enable": "Enable EU unit price display",
463
+ "catalog.products.unitPrice.errors.baseQuantity": "Base quantity is required when unit price display is enabled.",
464
+ "catalog.products.unitPrice.errors.referenceUnit": "Reference unit is required when unit price display is enabled.",
465
+ "catalog.products.unitPrice.hint": "Show calculated price per selected reference unit. In most cases set quantity to 1.",
466
+ "catalog.products.unitPrice.hintWithPreview": "Show calculated price per {{quantity}} {{unit}}. For most products use 1 (for example: 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 pc",
472
+ "catalog.products.unitPrice.referenceUnit": "Reference unit",
473
+ "catalog.products.unitPrice.selectReferenceUnit": "Select reference unit",
474
+ "catalog.products.uom.active": "Active",
475
+ "catalog.products.uom.addConversion": "Add conversion",
476
+ "catalog.products.uom.baseUnit": "Base unit",
477
+ "catalog.products.uom.conversionOrderHint": "Use arrows to reorder conversion priority.",
478
+ "catalog.products.uom.conversionPreview": "1 {{fromUnit}} = {{factor}} {{baseUnit}}",
479
+ "catalog.products.uom.conversionUnit": "Sales unit",
480
+ "catalog.products.uom.conversions": "Product conversions",
481
+ "catalog.products.uom.defaultSalesQuantity": "Default sales quantity",
482
+ "catalog.products.uom.defaultSalesQuantityHint": "Used to prefill quantity in quote/order lines. Value is interpreted in Default sales unit.",
483
+ "catalog.products.uom.defaultSalesQuantityLabel": "Default line quantity (in sales unit)",
484
+ "catalog.products.uom.defaultSalesQuantityPreview": "Default line: {{quantity}} {{salesUnit}}.",
485
+ "catalog.products.uom.defaultSalesQuantityPreviewWithNormalization": "Default line: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).",
486
+ "catalog.products.uom.defaultSalesUnit": "Default sales unit",
487
+ "catalog.products.uom.description": "Set base/sales units and packaging conversions.",
488
+ "catalog.products.uom.emptyConversions": "No conversions configured yet.",
489
+ "catalog.products.uom.errors.baseRequired": "Base unit is required when default sales unit is set.",
490
+ "catalog.products.uom.errors.baseRequiredForConversions": "Base unit is required when conversions are configured.",
491
+ "catalog.products.uom.errors.defaultSalesConversionRequired": "Active conversion for default sales unit is required when it differs from base unit.",
492
+ "catalog.products.uom.errors.duplicateConversion": "Duplicate conversion unit is not allowed.",
493
+ "catalog.products.uom.errors.loadUnits": "Failed to load units. Please try refreshing the page.",
494
+ "catalog.products.uom.errors.sync": "Failed to synchronize product conversions.",
495
+ "catalog.products.uom.loadingUnits": "Loading units...",
496
+ "catalog.products.uom.moveDown": "Move conversion down",
497
+ "catalog.products.uom.moveUp": "Move conversion up",
498
+ "catalog.products.uom.removeConversion": "Remove conversion",
499
+ "catalog.products.uom.roundingMode": "Rounding mode",
500
+ "catalog.products.uom.roundingModeDown": "Down",
501
+ "catalog.products.uom.roundingModeHalfUp": "Half up",
502
+ "catalog.products.uom.roundingModeUp": "Up",
503
+ "catalog.products.uom.roundingScale": "Rounding scale",
504
+ "catalog.products.uom.selectUnit": "Select unit",
505
+ "catalog.products.uom.sortOrder": "Sort",
506
+ "catalog.products.uom.title": "Units of measure",
507
+ "catalog.products.uom.toBaseFactor": "To base factor",
508
+ "catalog.products.validation.baseUnitRequired": "Base unit is required when a default sales unit is set.",
509
+ "catalog.products.validation.handleFormat": "Handle must include lowercase letters, digits, hyphen, or underscore.",
510
+ "catalog.products.validation.referenceUnitRequired": "Reference unit is required when unit price display is enabled.",
511
+ "catalog.products.validation.titleRequired": "Title is required.",
457
512
  "catalog.search.badge.category": "Category",
458
513
  "catalog.search.badge.offer": "Channel Offer",
459
514
  "catalog.search.badge.optionSchema": "Option Schema",
460
515
  "catalog.search.badge.priceKind": "Price Kind",
461
516
  "catalog.search.badge.product": "Product",
462
517
  "catalog.search.badge.tag": "Tag",
518
+ "catalog.search.badge.unitConversion": "Unit Conversion",
463
519
  "catalog.search.badge.variant": "Variant",
464
520
  "catalog.search.priceKind.excludingTax": "Excl. tax",
465
521
  "catalog.search.priceKind.includingTax": "Incl. tax",
@@ -16,6 +16,9 @@
16
16
  "catalog.audit.prices.create": "Crear precio",
17
17
  "catalog.audit.prices.delete": "Eliminar precio",
18
18
  "catalog.audit.prices.update": "Actualizar precio",
19
+ "catalog.audit.productUnitConversions.create": "Crear conversión de unidad de producto",
20
+ "catalog.audit.productUnitConversions.delete": "Eliminar conversión de unidad de producto",
21
+ "catalog.audit.productUnitConversions.update": "Actualizar conversión de unidad de producto",
19
22
  "catalog.audit.products.create": "Crear producto",
20
23
  "catalog.audit.products.delete": "Eliminar producto",
21
24
  "catalog.audit.products.update": "Actualizar producto",
@@ -394,6 +397,7 @@
394
397
  "catalog.products.edit.weight.value": "Peso",
395
398
  "catalog.products.errors.handleExists": "El identificador ya está en uso.",
396
399
  "catalog.products.errors.skuExists": "El SKU ya está en uso.",
400
+ "catalog.products.errors.taxClassNotFound": "Clase de impuesto no encontrada.",
397
401
  "catalog.products.filters.active": "Activo",
398
402
  "catalog.products.filters.categories": "Categorías",
399
403
  "catalog.products.filters.categoriesLoadError": "No se pudieron cargar las categorías",
@@ -454,12 +458,64 @@
454
458
  "catalog.products.types.grouped": "Agrupado",
455
459
  "catalog.products.types.simple": "Simple",
456
460
  "catalog.products.types.virtual": "Virtual",
461
+ "catalog.products.unitPrice.baseQuantity": "Cantidad base de referencia",
462
+ "catalog.products.unitPrice.enable": "Activar precio unitario UE",
463
+ "catalog.products.unitPrice.errors.baseQuantity": "La cantidad base es obligatoria cuando la visualización del precio unitario está activada.",
464
+ "catalog.products.unitPrice.errors.referenceUnit": "La unidad de referencia es obligatoria cuando la visualización del precio unitario está activada.",
465
+ "catalog.products.unitPrice.hint": "Muestra el precio calculado por la unidad de referencia seleccionada. En la mayoría de casos usa cantidad 1.",
466
+ "catalog.products.unitPrice.hintWithPreview": "Muestra el precio calculado por {{quantity}} {{unit}}. Para la mayoría de productos usa 1 (por ejemplo: 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 ud.",
472
+ "catalog.products.unitPrice.referenceUnit": "Unidad de referencia",
473
+ "catalog.products.unitPrice.selectReferenceUnit": "Seleccionar unidad de referencia",
474
+ "catalog.products.uom.active": "Activo",
475
+ "catalog.products.uom.addConversion": "Añadir conversión",
476
+ "catalog.products.uom.baseUnit": "Unidad base",
477
+ "catalog.products.uom.conversionOrderHint": "Usa las flechas para reordenar la prioridad de conversión.",
478
+ "catalog.products.uom.conversionPreview": "1 {{fromUnit}} = {{factor}} {{baseUnit}}",
479
+ "catalog.products.uom.conversionUnit": "Unidad de venta",
480
+ "catalog.products.uom.conversions": "Conversiones de producto",
481
+ "catalog.products.uom.defaultSalesQuantity": "Cantidad de venta predeterminada",
482
+ "catalog.products.uom.defaultSalesQuantityHint": "Se usa para rellenar la cantidad en líneas de oferta/pedido. El valor se interpreta en la unidad de venta predeterminada.",
483
+ "catalog.products.uom.defaultSalesQuantityLabel": "Cantidad predeterminada de línea (en unidad de venta)",
484
+ "catalog.products.uom.defaultSalesQuantityPreview": "Línea predeterminada: {{quantity}} {{salesUnit}}.",
485
+ "catalog.products.uom.defaultSalesQuantityPreviewWithNormalization": "Línea predeterminada: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).",
486
+ "catalog.products.uom.defaultSalesUnit": "Unidad de venta predeterminada",
487
+ "catalog.products.uom.description": "Configurar unidades base/venta y conversiones de empaque.",
488
+ "catalog.products.uom.emptyConversions": "Aún no hay conversiones configuradas.",
489
+ "catalog.products.uom.errors.baseRequired": "La unidad base es obligatoria cuando se establece una unidad de venta predeterminada.",
490
+ "catalog.products.uom.errors.baseRequiredForConversions": "La unidad base es obligatoria cuando hay conversiones configuradas.",
491
+ "catalog.products.uom.errors.defaultSalesConversionRequired": "Se requiere una conversión activa para la unidad de venta predeterminada cuando difiere de la unidad base.",
492
+ "catalog.products.uom.errors.duplicateConversion": "No se permite una unidad de conversión duplicada.",
493
+ "catalog.products.uom.errors.loadUnits": "No se pudieron cargar las unidades. Intente actualizar la página.",
494
+ "catalog.products.uom.errors.sync": "No se pudieron sincronizar las conversiones de producto.",
495
+ "catalog.products.uom.loadingUnits": "Cargando unidades...",
496
+ "catalog.products.uom.moveDown": "Mover conversión hacia abajo",
497
+ "catalog.products.uom.moveUp": "Mover conversión hacia arriba",
498
+ "catalog.products.uom.removeConversion": "Eliminar conversión",
499
+ "catalog.products.uom.roundingMode": "Modo de redondeo",
500
+ "catalog.products.uom.roundingModeDown": "Redondeo hacia abajo",
501
+ "catalog.products.uom.roundingModeHalfUp": "Redondeo comercial (predeterminado)",
502
+ "catalog.products.uom.roundingModeUp": "Redondeo hacia arriba",
503
+ "catalog.products.uom.roundingScale": "Precisión de redondeo (decimales)",
504
+ "catalog.products.uom.selectUnit": "Seleccionar unidad",
505
+ "catalog.products.uom.sortOrder": "Orden",
506
+ "catalog.products.uom.title": "Unidades de medida",
507
+ "catalog.products.uom.toBaseFactor": "Factor base",
508
+ "catalog.products.validation.baseUnitRequired": "La unidad base es obligatoria cuando se establece una unidad de venta predeterminada.",
509
+ "catalog.products.validation.handleFormat": "El identificador solo puede contener letras minúsculas, dígitos, guiones o guiones bajos.",
510
+ "catalog.products.validation.referenceUnitRequired": "La unidad de referencia es obligatoria cuando la visualización del precio unitario está activada.",
511
+ "catalog.products.validation.titleRequired": "El título es obligatorio.",
457
512
  "catalog.search.badge.category": "Categoría",
458
513
  "catalog.search.badge.offer": "Oferta de canal",
459
514
  "catalog.search.badge.optionSchema": "Esquema de opciones",
460
515
  "catalog.search.badge.priceKind": "Tipo de precio",
461
516
  "catalog.search.badge.product": "Producto",
462
517
  "catalog.search.badge.tag": "Etiqueta",
518
+ "catalog.search.badge.unitConversion": "Conversión de unidad",
463
519
  "catalog.search.badge.variant": "Variante",
464
520
  "catalog.search.priceKind.excludingTax": "Excl. impuestos",
465
521
  "catalog.search.priceKind.includingTax": "Incl. impuestos",