تقرير آلية الربط بأودو والمزامنة الحيّة
كفاءة التكامل عبر الـAPI · المزامنة الدوريّة واللحظيّة · سيناريوهات العوائق
هذا التقرير مبنيٌّ على الكود مصدرًا رئيسيًّا — كل آليّة موصوفة هنا مؤكَّدة من ملفّ ومسار في المستودع (طبقة src/services/odoo، الوسيط odoo-proxy، الموديول brandzo_warehouse، إعداد فايربيز، وعقد التكامل). يميّز التقرير بأمانة بين ما هو منفَّذ وما هو مصمَّم بانتظار التنفيذ.
الحُكم الصادق — أين نحن اليوم؟
القرار المعماريّ الحاكم: فايربيز (Firestore) مصدر الحقيقة التشغيليّ، وأودو مرآة محاسبيّة/رقابيّة. البوابة تعمل كاملةً حتى لو سقط أودو — صفر تبعيّة تشغيليّة. التكامل «Integration» لا «Migration»: نغذّي أودو من البوابة ونقرأ منه ما يلزم، ولا ننقل قلب العمليّات إليه. عمليًّا: ~70% من بنية التكامل مبنيّة ومعزولة (عميل + وسيط + موديول جاهزون)، والوضع الافتراضيّ training (محاكٍ)، ومنطق المزامنة نفسه مصمَّمٌ في عقد التكامل بانتظار التوصيل. هذه الشفافيّة هي جوهر الاحترافيّة: لا نَعِد بما ليس موصولًا.
- ~70%جاهزية بنية التكامل
- 5بدائيّات API قياسيّة
- 6نماذج بقائمة سماح الوسيط
- 19إصدار أودو الحاليّ
- 12/12مرحلة الدورة المستندية
- 52اختبارًا خضراء (الموديول)
- · عميل أودو بخمس بدائيّات (authenticate/searchRead/create/write/unlink).
- · مُبدِّل الوضع (تدريب/إنتاج) بمتغيّر بيئة واحد بلا تغيير كود.
- · مُطابِق الحقول (product.product ⇄ الأصناف · stock.move → السجلّ).
- · محاكي أودو أوفلاين (نفس الواجهة، بلا شبكة) للتدريب.
- · وسيطان مرجعيّان (Cloudflare Worker + Firebase Function) بقائمة سماح.
- · الموديول brandzo_warehouse (إصدار 19، 12/12 مرحلة، 52 اختبارًا خضراء).
- · توصيل العميل بشاشة مزامنة + اختبار تكامل يحرسه من التعفّن.
- · السحب الدوريّ (polling) للموردين/الأصناف/الأوامر/الأرصدة.
- · طابور الدفع (outbox) لإرسال المستندات كمسوّدات لأودو.
- · المرايا odoo_mirror_* ووثيقة sync_state وقواعد أمانها.
- · اللحظيّ الحقيقيّ (Webhooks) — مشروطٌ باستضافة أودو عامّة.
- · طبقة Repository لتبديل القاعدة (القسم ٦).
١ · آلية الربط عبر الـAPI وإرسال المستندات كمسوّدات
المتصفّح لا يحمل سرّ أودو أبدًا (سرّه سرقةٌ لو شُحن في الحزمة)، ونقطة أودو /jsonrpc لا تُصدر ترويسات CORS. لذا يمرّ كل استدعاء عبر وسيطٍ خادميّ رقيق يحقن الأسرار، ويصادق، وينادي execute_kw، ويفرض قائمة سماح، ويضيف ترويسات CORS لأصل البوابة.
odooConfig.js
odoo-proxy/
bz_integration
عقد السلك: POST { model, method, args, kwargs } → { result } أو { error } — مرآةٌ شبه مباشرة لـexecute_kw(db, uid, pwd, model, method, args, kwargs). استدعاءٌ خاصّ { method: "authenticate" } يفحص الاتصال ويرجع uid.
البدائيّات الخمس (واجهة موحّدة تُنسخ عليها الصفحات)
| الدالّة | نظير أودو | الغرض |
|---|---|---|
| authenticate() | common.authenticate | التحقّق من الاتصال وإرجاع معرّف المستخدم (uid) — يغذّي زرّ «اختبار الاتصال». |
| searchRead(model, domain, fields) | search_read | قراءة سجلّات مطابقة لنطاق — الأساس لجلب الأصناف/الموردين/الأوامر. |
| create(model, values) | create | إنشاء سجلّ جديد — به تُرسَل مستندات البوابة إلى أودو كمسوّدات. |
| write(model, ids, values) | write | تعديل سجلّات قائمة — لتحديث حقول مطابِقة عند اللزوم. |
| unlink(model, ids) | unlink | حذف سجلّات — محصور بقائمة السماح ونادر الاستعمال. |
ODOO_URL/DB/USER/API_KEY تعيش حصرًا في خزانة أسرار الوسيط — لا في الحزمة ولا في git.
الوسيط يقبل أزواج (نموذج+دالّة) محدّدة فقط ويردّ 403 Blocked لأي استدعاء عشوائيّ.
CORS محصورٌ بأصل البوابة، ومستخدم التكامل bz_integration بدورٍ مقيَّد لا Admin.
إرسال مستندات البوابة إلى أودو كمسوّدات (Drafts)
عند بلوغ مستندٍ حاكمٍ حالة «منجَز» في البوابة، يُقيَّد في طابور الدفع sync_outbox، ثم يدفعه syncService عبر الوسيط بـ create(model, mapper(doc)) فيُنشأ في أودو كمسوّدة بانتظار الاعتماد المحاسبيّ. مفتاح منع التكرار = معرّف مستند Firestore، فلا ازدواج عند إعادة المحاولة.
| مستند البوابة | نموذج أودو | الحالة عند الإنشاء |
|---|---|---|
| PO — أمر شراء | purchase.order | مسوّدة / طلب عرض أسعار |
| PR — طلب شراء | purchase.requisition | مسوّدة |
| GRN — الاستلام | stock.picking (وارد) | مسوّدة (Draft) |
| DN — إذن تسليم | stock.picking (صادر) | مسوّدة (Draft) |
| SO / POS — بيع | sale.order / pos.order | مسوّدة |
| INV — فاتورة | account.move | مسوّدة (Draft) |
الحالة: القناة والوسيط مكتوبان وجاهزان (غير منشورَين)، ودفع المستندات مصمَّمٌ ويحتاج توسعة المُطابِق للنماذج الحاكمة أوّلًا.
٢ · المزامنة الدوريّة — جلب الأصناف والموردين والأوامر أوّلًا بأوّل
آليّة «السحب التزايديّ» (Incremental Polling): يسأل syncService أودو كل فترة عن السجلّات التي write_date > lastSync فقط (لا سحب كامل مكلف)، فيجلب الجديد والمتغيّر فحسب. المؤشّر lastSync محفوظ في وثيقة sync_state بفايربيز، وتُحدَّث مرايا القراءة odoo_mirror_*. المُشغِّل: Cloudflare Cron Trigger (الأنظف، يعمل على Spark المجانيّة)، أو مؤقّت في متصفّح مديرٍ كبديلٍ بلا بنية.
أصناف جديدة/معدّلة + الفئات والوحدات
بيانات الشركاء أوّلًا بأوّل للمرآة
حالة الاعتماد والاستلام تِباعًا
أوامر البيع وحالتها للمرآة المحاسبيّة
التحويلات الداخليّة وحالاتها
الرصيد المحسوب (قراءة فقط) من stock.quant
الحالة: مصمَّمة في عقد التكامل (المرحلة ١) — تعمل على Spark عبر Cloudflare Cron، ولم تُبنَ خدمة syncService بعد. الرصيد يبقى محسوبًا في أودو (مدفوعًا بـstock.quant) فيُسحَب للمرآة فقط، وتبقى أرصدة البوابة التشغيليّة مرجعها فايربيز.
٣ · ربط كيانات البوابة بأودو (أصناف · موردون · عملاء · عمليّات)
المبدأ الحاكم: المعرفة الأودووّية محصورة في طبقتين فقط — المُطابِق odooMapper.js (أسماء الحقول وشكل السجلّ) والموديول brandzo_warehouse (المنطق). لا يعرف أيّ مكوّن آخر شيئًا عن أودو، والمُطابِق يتدهور بأمان عند غياب أي حقلٍ مخصّص. حيث لا مطابقة نظيفة نستعمل حقولًا مخصّصة x_* أو نماذج الموديول الخاصّة bz.* (bz.gate.pass · bz.cycle.count · bz.period.close).
default_code⇄sku · name⇄nameAr · x_name_en⇄nameEn · categ_id→category · uom_id→unit · qty_available→balance (محسوب، قراءة فقط) · x_min_stock⇄minStock
name · ref · vat · phone · email · علم مورّد/عميل — مرآة قراءة في odoo_mirror، ودفع اختياريّ لشريكٍ جديد
product_id→sku (يُستخرج الرمز من «[CODE] Name») · product_uom_qty→qty · date→timestamp
دفع المستندات كمسوّدات + سحب الحالة (draft→done) للمرآة
دفع أمر/طلب الشراء كمسوّدة + سحب حالة الاعتماد والاستلام أوّلًا بأوّل
دفع أمر البيع/الفاتورة كمسوّدة إلى أودو للمرآة المحاسبيّة
٤ · آلية المزامنة بين أودو وفايربيز
القاعدة المعماريّة: فايربيز مصدر الحقيقة التشغيليّ · أودو مرآة محاسبيّة/رقابيّة (امتداد لقرار §19-5 المحسوم). لماذا لا العكس؟ لأن البوابة تعمل أوفلاين بكاش Firestore الدائم، وحرّاسها الخمسة والدورة المستندية مبنيّة على Firestore، وخطّة Spark لا تتحمّل ربطًا حيًّا ثقيلًا بأودو.
المستندات المنجَزة تُدفع عبر الوسيط بـcreate() فتُنشأ في أودو كمسوّدات — طابور sync_outbox بمفتاح idempotency.
السحب الدوريّ يكتب المرايا odoo_mirror_* (قراءة فقط)؛ لاحقًا Webhooks تكتب مباشرةً عبر REST فتُلغي الـpolling.
«الأحدث يكسب + سجلّ تعارض» يُعرض في شاشة «مراقبة المزامنة» (بنمط أودو ListView) — تُبنى لاحقًا.
persistentLocalCache + مدير تبويبات → قراءة أوفلاين وطابور كتابة يُدفع تلقائيًّا عند العودة. القيد الوحيد: أوّل دخول أونلاين.
٥ · آلية النسخ الاحتياطي
- · كاش محلّيّ دائم على القرص (persistentLocalCache) + طابور كتابة — صمودٌ أمام انقطاع الشبكة بلا فقد قيد.
- · متانة مُدارة من Google لبنية Firestore نفسها.
- · قيد Spark: لا نسخ مجدول ولا Cloud Functions — ترقية Blaze تفتح التصدير المجدول (معلّقة على تجديد بطاقة المالك).
- · pg_dump يوميّ مجدول + لقطة إلزاميّة قبل أي ترقية أو تهجير.
- · التغيير بالهجرة لا بالمسح (Odoo/OpenUpgrade في المكان) — ممنوع إعادة إنشاء قاعدة من الصفر على بيانات حيّة.
- · تمرين استعادة شهريّ على قاعدة جانبيّة — «النسخة التي لم تُجرَّب استعادتها ليست نسخة».
- · التحقّق من فصل volume القاعدة عن حاوية أودو في docker-compose.
٦ · آلية الانتقال إلى قاعدة بيانات محليّة خاصّة بالشركة
العائق الحقيقيّ: لا توجد طبقة عزل اليوم — Firestore خام في ~151 موضعًا عبر 25 ملفًّا، و59 مستمع onSnapshot حيّ، والأهمّ أن منطق العمل نفسه يعيش في firestore.rules (اعتماد · لا-تعديل · «الرقم مرّة واحدة») لا في JS.
القالب الصحيح موجودٌ بالفعل داخل المشروع — وهو نفس نمط أودو: واجهةٌ واحدة تُختار بمتغيّر بيئة.
// النمط القائم (odoo/index.js)
export const odoo = isProduction ? realOdooClient : mockOdooClient;
// المستهدف بنفس النمط (src/services/db/index.js)
export const store = PROVIDER === 'firestore' ? firestoreStore : localStore; // PUBLIC_DB_PROVIDER إنشاء src/services/db/ بمستودعات لكل مجموعة تُخفي بدائيّات Firestore — تنتقل الـ151 موضعًا خلف ~15 وحدة، تدريجيًّا، بدءًا بمجموعات الكاشير.
مبدّل PUBLIC_DB_PROVIDER يختار Firestore أو مزوّدًا محلّيًّا (SQLite عبر Tauri · IndexedDB) بلا لمس الصفحات.
العوائق التي يجب أن يحلّها أيّ مزوّد بديل: الاشتراك الحيّ (وإلّا استقصاء)، والذرّية (runTransaction/increment شرط صحّة لا رفاهية)، وطبقة إلزامٍ تكافئ firestore.rules (وإلّا هبطت الضمانات إلى عميلٍ يُتجاوَز). المصادقة يمكن إبقاؤها Firebase Auth، أو هويّة محلّية للمحطّة المنعزلة.
٧ · حوكمة تغيّر النموذج (الإصدار ١٩) وطلب التحديث من المطوّرين
الخطّة الحاليّة مبنيّة على أودو ١٩ (الموديول إصدار 19.0.1.2.0). ولأن المعرفة الأودووّية محصورة في طبقتين فقط، فإن تغيّر النموذج أو الإصدار يُمتَصّ في موضعٍ واحد لا في كل المشروع — وسطح execute_kw مستقرٌّ عبر الإصدارات منذ سنوات، فالقناة نفسها لا تتأثّر بالترقية.
يحمل أسماء الحقول وشكل السجلّ — يُحدَّث وحده إن تغيّرت أسماء الحقول أو بنيتها.
يحمل المنطق الأودو-جانبيّ (4 نماذج خاصّة bz.* + توسعة 7 نماذج قياسيّة) — يُرقَّى ويُختبر بالإصدار الجديد.
مسار الترقية الإلزاميّ (لا إنتاج قبل خُضرة المختبر)
- 1 تثبيت الإصدار الجديد في صورة Docker على فرع ترقية منفصل.
- 2 مختبر بالإصدار الجديد ثم odoo -u brandzo_warehouse.
- 3 تشغيل الاختبارات الـ52 كلّها والتأكّد من خضرتها.
- 4 جدول توافق الحقول — سابقة موثّقة: ترقية 19 كشفت product_uom→product_uom_id وحذف category_id.
- 5 لا نشر للإنتاج قبل خضرة المختبر بالكامل.
آليّة طلب التحديث من إدارة التقنية مستقبلًا
- · مصدر تغيير واحد: تغيّر حقلٍ ← تعديل odooMapper.js وحده · تغيّر منطقٍ ← الموديول + اختباراته. لا انحراف.
- · مسار الطلب: فرع ترقية ← تعديل المُطابِق/الموديول ← تشغيل 52 اختبار الموديول + حواجز البوابة (npm test 657 اختبارًا · npm run audit · npm run lint) ← توثيق جدول توافق الحقول ← لا دمج/نشر قبل الخُضرة.
- · حرّاس صلبة: قائمة سماح الوسيط تمنع أي نموذج غير مُدرَج، وحاجزا النشر يمنعان صفحة يتيمة أو مجموعة بلا قاعدة أمان.
- · توثيق حاكم: عقد التكامل docs/عقد-التكامل-مع-أودو.md + خطة التطوير الشاملة + ODOO_BACKEND_DEVELOPMENT_PLAN.md — أي مطوّر جديد يبدأ منها.
٨ · سيناريوهات العوائق وكفاءة الربط
| السيناريو | السبب | المعالجة |
|---|---|---|
| الوسيط غير منشور بعد | لم يُنشَر Cloudflare Worker ولم يُضبط PUBLIC_ODOO_PROXY_URL | نشر الوسيط على حساب Cloudflare مجانيّ (خطوة المالك) — بلا تغيير كود |
| أودو على localhost:8069 فقط | الوسيط السحابيّ لا يرى عنوانًا محلّيًّا — المزامنة اللحظيّة مشروطة | قرار المالك: استضافة أودو على عنوان عامّ — يفتح المرحلة الفوريّة |
| خطّة Spark بلا Cloud Functions | لا مشغّل خادميّ دائم للسحب الدوريّ | Cloudflare Cron Trigger كمشغّل مجانيّ؛ أو ترقية Blaze لاحقًا |
| تعذّر الوصول للوسيط (شبكة) | انقطاع شبكة/DNS/CORS — بلا استجابة HTTP | العميل يرمي رسالة واضحة، والبوابة تبقى عاملة كاملةً (صفر تبعيّة تشغيليّة) |
| فشل مصادقة أودو | مفتاح API/مستخدم خاطئ — uid فارغ | رسالة «فشلت مصادقة أودو» + زرّ «اختبار الاتصال» يكشفها مبكرًا |
| استدعاء خارج قائمة السماح | نموذج/دالّة غير مُدرَجَين في allowlist الوسيط | الوسيط يردّ 403 Blocked — أي توسعة تُضاف صراحةً بمراجعة |
| تعارض بيانات (عُدِّل الجانبان) | نفس السجلّ تغيّر في البوابة وأودو | سياسة «الأحدث يكسب + سجلّ تعارض» يُعرض في شاشة «مراقبة المزامنة» |
| انقطاع الإنترنت أثناء العمل | الموظّف يعمل في موقعٍ ضعيف/بلا شبكة | كاش Firestore الدائم + طابور كتابة يُدفع تلقائيًّا عند العودة (قيد: أوّل دخول أونلاين) |
| تغيّر إصدار أودو أو أسماء الحقول | ترقية أودو تُغيّر بنية النموذج (سابقة: product_uom→product_uom_id) | مسار ترقية إلزاميّ: فرع منفصل + جدول توافق + 52 اختبارًا قبل الإنتاج |
خلاصة الكفاءة
أقوى خصائص الكفاءة هنا هي صفر تبعيّة تشغيليّة: البوابة تسلّم عملها كاملًا سواء أكان أودو موصولًا أم لا، فالتكامل قيمةٌ مضافة لا نقطة انهيار. البنية جاهزة بنسبة ~70% ومعزولة بنظافة (تُوصَل بلا إعادة تصميم)، والمزامنة الدوريّة تعمل على الخطّة المجانيّة عبر Cloudflare Cron. ما تبقّى خطوتان بيد المالك: نشر الوسيط (يفتح السحب الدوريّ ودفع المستندات)، واستضافة أودو على عنوان عامّ (يفتح المزامنة اللحظيّة الحقيقيّة). حتى ذلك الحين: «شبه لحظيّ» عبر سحبٍ قصير ودفعٍ فور «منجَز».