Merge prod_v5 (catalog v2.7, warehouse locking, per-project accept) into integration
Conflict in init_db.py resolved keeping the audit_log table ensure function (new-one) — prod_v5 predates it and its side was empty. Both sides' SQL lands: audit_log table plus the category-free materials model.
This commit is contained in:
@@ -1108,12 +1108,6 @@ export const api = {
|
||||
},
|
||||
create: async (m: Material): Promise<Material> => {
|
||||
const lookup = await fetchLookup();
|
||||
// v2.5: category_id اختیاری است — اگر نامِ تایپشده یکی از ۸ دستهی ثابت
|
||||
// نباشد، متریال بهصورت سراسری (بدون دسته) ساخته میشود و دستهاش بعداً
|
||||
// از اولین تأمینکنندهی مرتبط استنتاج میشود (first-link-wins).
|
||||
// دستهی rogue دیگر هرگز ساخته نمیشود — فقط تطبیق با ۸ دستهی موجود؛
|
||||
// در صورت نبود تطبیق، خطا نمیدهیم و category_id را null میفرستیم.
|
||||
const categoryId = m.category ? lookup.categoryNameToId.get(norm(m.category)) : undefined;
|
||||
// واحد مرجع — بدون مقدار پیشفرض؛ متریال بدون واحدِ صریح ساخته نمیشود
|
||||
// (بکاند canonical_unit_id را الزامی میداند)
|
||||
const unitName = (m.base_unit || "").trim();
|
||||
@@ -1138,7 +1132,6 @@ export const api = {
|
||||
if (defId) attrs.push({ attribute_definition_id: defId, value: spec });
|
||||
}
|
||||
const raw = await postData<RawMaterial>("/materials", {
|
||||
category_id: categoryId ?? null,
|
||||
name,
|
||||
topic: m.topic || null,
|
||||
canonical_unit_id: unitId,
|
||||
@@ -1705,7 +1698,8 @@ export const apiV26 = {
|
||||
const d = await postData<Parameters<typeof rawWarehouse>[0]>(`/inventory/warehouses${projectQuery()}`, body);
|
||||
return rawWarehouse(d);
|
||||
},
|
||||
update: async (id: string, body: { name?: string; description?: string | null; project_id?: string }): Promise<Warehouse> => {
|
||||
// project_id عمداً در تایپ نیست — انبار برای همیشه به پروژهی والدش تعلق دارد
|
||||
update: async (id: string, body: { name?: string; description?: string | null }): Promise<Warehouse> => {
|
||||
const d = await patchData<Parameters<typeof rawWarehouse>[0]>(`/inventory/warehouses/${id}${projectQuery()}`, body);
|
||||
return rawWarehouse(d);
|
||||
},
|
||||
@@ -1783,7 +1777,6 @@ export const apiV26 = {
|
||||
}): Promise<{ id: string; name: string }> =>
|
||||
postData("/materials", {
|
||||
name: opts.name,
|
||||
category_id: null, // دسته از اولین تأمینکنندهی مرتبط استنتاج میشود (v2.5)
|
||||
topic: null,
|
||||
canonical_unit_id: opts.canonical_unit_id,
|
||||
status: "active",
|
||||
|
||||
@@ -35,43 +35,51 @@ export function specsKey(specs: string[]): string {
|
||||
return (specs || []).map((s) => normalize(s)).filter(Boolean).sort().join("|");
|
||||
}
|
||||
|
||||
// specها با جداکنندهی « · »
|
||||
// specها با جداکنندهی « . »
|
||||
export function specsLabel(specs: string[]): string {
|
||||
return specs.join(" · ");
|
||||
return specs.join(" . ");
|
||||
}
|
||||
|
||||
/**
|
||||
* عنوان تمیز و بدون تکرار کالا:
|
||||
* ترکیب هوشمندانهی نام اصلی (materials.name)، برند، و مشخصهها بدون تکرار کلمات مشترک.
|
||||
* ترتیب اکید با جداکنندهی نقطه:
|
||||
* [نام برند] . [نام متریال] . [مشخصه ۱] . [مشخصه ۲]
|
||||
*/
|
||||
export function formatMaterialTitle(
|
||||
m: Pick<Material, "name" | "brand" | "specs" | "category"> | null | undefined,
|
||||
extraSpecs?: string[],
|
||||
): string {
|
||||
if (!m) return "کالا";
|
||||
const rawName = (m.name || "").trim();
|
||||
let rawName = (m.name || "").trim();
|
||||
const rawBrand = (m.brand || "").trim();
|
||||
const rawSpecs = [...(m.specs || []), ...(extraSpecs || [])]
|
||||
.map((s) => (s || "").trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const normName = normalize(rawName);
|
||||
const normBrand = normalize(rawBrand);
|
||||
|
||||
if (!rawName) {
|
||||
if (rawBrand && rawSpecs.length) return `${rawBrand} · ${rawSpecs.join(" · ")}`;
|
||||
if (rawBrand) return rawBrand;
|
||||
if (rawSpecs.length) return rawSpecs.join(" · ");
|
||||
return m.category && m.category !== "؟" ? m.category : "کالا";
|
||||
// اگر نام کالا با نام برند شروع شده باشد، برند را از ابتدای نام برمیداریم تا تکرار نشود
|
||||
if (normBrand && rawName) {
|
||||
const normName = normalize(rawName);
|
||||
if (normName.startsWith(normBrand)) {
|
||||
const rest = rawName.slice(rawBrand.length).trim();
|
||||
rawName = rest.replace(/^[.\s·\-]+/, "").trim();
|
||||
}
|
||||
}
|
||||
|
||||
let title = rawName;
|
||||
if (normBrand && !normName.includes(normBrand)) {
|
||||
title = `${rawBrand} ${title}`;
|
||||
// قطعات نهایی با ترتیب مشخص: [Brand Name] . [Material Name] . [Specification 1] . [Specification 2]
|
||||
const parts: string[] = [];
|
||||
if (rawBrand) {
|
||||
parts.push(rawBrand);
|
||||
}
|
||||
if (rawName) {
|
||||
parts.push(rawName);
|
||||
} else if (!rawBrand) {
|
||||
parts.push(m.category && m.category !== "؟" ? m.category : "کالا");
|
||||
}
|
||||
|
||||
const normTitle = normalize(title);
|
||||
const distinctSpecs: string[] = [];
|
||||
// اضافه کردن مشخصهها بدون تکرار
|
||||
const normCurrent = normalize(parts.join(" "));
|
||||
const seenNormSpecs = new Set<string>();
|
||||
|
||||
for (const s of rawSpecs) {
|
||||
@@ -79,15 +87,12 @@ export function formatMaterialTitle(
|
||||
if (!ns || seenNormSpecs.has(ns)) continue;
|
||||
seenNormSpecs.add(ns);
|
||||
|
||||
// اگر مشخصه قبلاً داخل عنوان یا نام کالا آمده باشد، تکرار نمیکنیم
|
||||
if (normTitle.includes(ns)) continue;
|
||||
distinctSpecs.push(s);
|
||||
// اگر مشخصه قبلاً داخل نام یا برند آمده باشد، تکرار نمیکنیم
|
||||
if (normCurrent.includes(ns)) continue;
|
||||
parts.push(s);
|
||||
}
|
||||
|
||||
if (distinctSpecs.length > 0) {
|
||||
return `${title} · ${distinctSpecs.join(" · ")}`;
|
||||
}
|
||||
return title;
|
||||
return parts.join(" . ");
|
||||
}
|
||||
|
||||
export function materialLabel(m: Pick<Material, "name" | "brand" | "specs" | "category"> | null | undefined): string {
|
||||
|
||||
@@ -20,9 +20,9 @@ export interface Supplier {
|
||||
// ماده/کالا
|
||||
export interface Material {
|
||||
id: string; // "m1", "m2", ...
|
||||
name: string; // نام کامل ذخیرهشده در بکاند (برند + دسته + مشخصه)
|
||||
name: string; // نام کامل ذخیرهشده در بکاند (برند + مشخصه)
|
||||
topic: string;
|
||||
category: string; // مثلاً "میلگرد"
|
||||
category: string; // اختیاری در بکاند؛ همیشه string در فرانت (default: "")
|
||||
brand: string; // مثلاً "ذوب آهن"
|
||||
specs: string[]; // آرایهای از مشخصهها (مثلاً ["A2"])
|
||||
base_unit: string; // واحد پایه
|
||||
|
||||
Reference in New Issue
Block a user