بناء وحدات Odoo مخصصة: البرنامج التعليمي للمطور
يعد نظام وحدات Odoo واحدًا من أقوى أطر الامتداد في عالم تخطيط موارد المؤسسات (ERP). كل ميزة في Odoo — بدءًا من المحاسبة إلى المخزون وحتى إدارة علاقات العملاء — عبارة عن وحدة نمطية. وهذا يعني أن بناء الوظائف المخصصة يتبع نفس الأنماط التي يستخدمها مطورو Odoo، مما يتيح لك الوصول إلى إطار العمل الكامل دون الحاجة إلى تشعب النواة.
يغطي هذا البرنامج التعليمي دورة الحياة الكاملة لوحدة Odoo 19 المخصصة: بدءًا من دعم بنية الدليل وتحديد النماذج ووصولاً إلى إنشاء طرق العرض وتأمين الوصول والنشر إلى الإنتاج. في النهاية، سيكون لديك وحدة عمل تتبع اتفاقيات Odoo 19 Enterprise وتكون جاهزة للسوق.
الوجبات الرئيسية
- كل وحدة من وحدات Odoo عبارة عن حزمة Python ذات واصف
__manifest__.py- ترث النماذج من
models.Modelويتم تعيينها مباشرة إلى جداول PostgreSQL- يتم تعريف طرق العرض في XML وحقول النموذج المرجعي بالاسم
- يتم فرض الأمان من خلال
ir.model.accessCSV وقواعد التسجيل- تتعامل المعالجات (
TransientModel) مع تفاعلات المستخدم متعددة الخطوات- تقوم الحقول المحسوبة وطرق التغيير بتحديث الحقول ذات الصلة ديناميكيًا
- تعمل الإجراءات الآلية والمهام المجدولة على تشغيل المنطق من جانب الخادم على المشغلات
- تضمن تبعيات الوحدة ترتيب التحميل الصحيح وتوافر الميزات
هيكل الوحدة والسقالات
كل وحدة من وحدات Odoo عبارة عن دليل ذو بنية محددة. استخدم أمر السقالة المدمج في Odoo لإنشاء النموذج النموذجي:
# From your Odoo addons directory
python odoo-bin scaffold my_module /path/to/addons
هذا يولد:
my_module/
├── __init__.py
├── __manifest__.py
├── models/
│ ├── __init__.py
│ └── my_model.py
├── views/
│ └── my_model_views.xml
├── security/
│ ├── ir.model.access.csv
│ └── my_module_security.xml
├── data/
│ └── my_module_data.xml
├── wizard/
│ └── my_wizard.py
├── report/
│ └── my_report.xml
└── static/
└── src/
└── js/
ملف البيان (__manifest__.py)
{
'name': 'My Custom Module',
'version': '19.0.1.0.0',
'summary': 'Short description for module list',
'description': """
Extended description of what this module does.
Can be multi-line RST text.
""",
'author': 'ECOSIRE Private Limited',
'website': 'https://ecosire.com',
'category': 'Sales/CRM',
'depends': ['sale', 'account', 'stock'],
'data': [
'security/ir.model.access.csv',
'security/my_module_security.xml',
'data/my_module_data.xml',
'views/my_model_views.xml',
'views/menu_views.xml',
'report/my_report.xml',
'wizard/my_wizard_views.xml',
],
'assets': {
'web.assets_backend': [
'my_module/static/src/js/my_widget.js',
'my_module/static/src/css/my_styles.css',
],
},
'license': 'OPL-1',
'installable': True,
'application': False,
'auto_install': False,
'price': 249.0,
'currency': 'USD',
}
اتفاقية ترقيم الإصدارات: {odoo_version}.{major}.{minor}.{patch}. ابدأ دائمًا من 19.0.1.0.0 للوحدات الجديدة.
تعريف النماذج
النماذج هي قلب أي وحدة في Odoo. وهي تحدد بنية البيانات ومنطق الأعمال.
# models/service_request.py
from odoo import api, fields, models
from odoo.exceptions import ValidationError, UserError
class ServiceRequest(models.Model):
_name = 'my.service.request'
_description = 'Service Request'
_inherit = ['mail.thread', 'mail.activity.mixin']
_order = 'date_request desc, name'
_rec_name = 'name'
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default=lambda self: self.env['ir.sequence'].next_by_code('my.service.request')
)
state = fields.Selection([
('draft', 'Draft'),
('submitted', 'Submitted'),
('in_progress', 'In Progress'),
('done', 'Completed'),
('cancelled', 'Cancelled'),
], string='Status', default='draft', tracking=True)
partner_id = fields.Many2one(
'res.partner', string='Customer',
required=True, tracking=True,
domain=[('customer_rank', '>', 0)]
)
user_id = fields.Many2one(
'res.users', string='Assigned To',
default=lambda self: self.env.user
)
date_request = fields.Datetime(
string='Request Date',
default=fields.Datetime.now,
required=True
)
date_deadline = fields.Date(string='Deadline')
description = fields.Html(string='Description')
priority = fields.Selection([
('0', 'Normal'),
('1', 'Low'),
('2', 'High'),
('3', 'Urgent'),
], string='Priority', default='0')
tag_ids = fields.Many2many(
'my.service.tag', string='Tags'
)
line_ids = fields.One2many(
'my.service.request.line', 'request_id',
string='Service Lines'
)
amount_total = fields.Float(
string='Total Amount',
compute='_compute_amount_total',
store=True
)
company_id = fields.Many2one(
'res.company', string='Company',
required=True,
default=lambda self: self.env.company
)
@api.depends('line_ids.subtotal')
def _compute_amount_total(self):
for request in self:
request.amount_total = sum(request.line_ids.mapped('subtotal'))
@api.constrains('date_deadline', 'date_request')
def _check_deadline(self):
for record in self:
if record.date_deadline and record.date_request:
if record.date_deadline < record.date_request.date():
raise ValidationError("Deadline cannot be before the request date.")
@api.onchange('partner_id')
def _onchange_partner_id(self):
if self.partner_id:
self.user_id = self.partner_id.user_id or self.env.user
def action_submit(self):
for record in self:
if not record.line_ids:
raise UserError("Cannot submit a request without service lines.")
record.state = 'submitted'
record.message_post(body="Service request submitted for processing.")
def action_start_progress(self):
self.write({'state': 'in_progress'})
def action_mark_done(self):
self.write({'state': 'done'})
def action_cancel(self):
for record in self:
if record.state == 'done':
raise UserError("Cannot cancel a completed request.")
record.state = 'cancelled'
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if vals.get('name', 'New') == 'New':
vals['name'] = self.env['ir.sequence'].next_by_code(
'my.service.request'
) or 'New'
return super().create(vals_list)
نموذج خط طلب الخدمة:
class ServiceRequestLine(models.Model):
_name = 'my.service.request.line'
_description = 'Service Request Line'
request_id = fields.Many2one(
'my.service.request', string='Request',
required=True, ondelete='cascade'
)
product_id = fields.Many2one(
'product.product', string='Service',
required=True,
domain=[('type', '=', 'service')]
)
description = fields.Text(string='Description')
quantity = fields.Float(string='Quantity', default=1.0)
price_unit = fields.Float(string='Unit Price')
subtotal = fields.Float(
string='Subtotal',
compute='_compute_subtotal',
store=True
)
@api.depends('quantity', 'price_unit')
def _compute_subtotal(self):
for line in self:
line.subtotal = line.quantity * line.price_unit
@api.onchange('product_id')
def _onchange_product_id(self):
if self.product_id:
self.price_unit = self.product_id.lst_price
self.description = self.product_id.description_sale
إنشاء طرق العرض
تحدد طرق العرض كيفية عرض السجلات في واجهة المستخدم. يستخدم Odoo لغة XML لوصف النماذج والقوائم ولوحات كانبان والمزيد.
<!-- views/service_request_views.xml -->
<odoo>
<!-- Form View -->
<record id="view_service_request_form" model="ir.ui.view">
<field name="name">my.service.request.form</field>
<field name="model">my.service.request</field>
<field name="arch" type="xml">
<form string="Service Request">
<header>
<button name="action_submit" string="Submit"
type="object" class="oe_highlight"
invisible="state != 'draft'"/>
<button name="action_start_progress" string="Start"
type="object" class="oe_highlight"
invisible="state != 'submitted'"/>
<button name="action_mark_done" string="Mark Done"
type="object" class="oe_highlight"
invisible="state != 'in_progress'"/>
<button name="action_cancel" string="Cancel"
type="object"
invisible="state in ['done', 'cancelled']"/>
<field name="state" widget="statusbar"
statusbar_visible="draft,submitted,in_progress,done"/>
</header>
<sheet>
<div class="oe_title">
<h1>
<field name="name" readonly="1"/>
</h1>
</div>
<group>
<group>
<field name="partner_id"
options="{'no_create': True}"/>
<field name="user_id"/>
<field name="priority" widget="priority"/>
</group>
<group>
<field name="date_request"/>
<field name="date_deadline"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
</group>
<field name="tag_ids" widget="many2many_tags"/>
<notebook>
<page string="Service Lines">
<field name="line_ids">
<tree editable="bottom">
<field name="product_id"/>
<field name="description"/>
<field name="quantity"/>
<field name="price_unit"/>
<field name="subtotal" readonly="1"/>
</tree>
</field>
<group class="oe_subtotal_footer">
<field name="amount_total"
widget="monetary"
class="oe_subtotal_footer_separator"/>
</group>
</page>
<page string="Description">
<field name="description" widget="html"
placeholder="Detailed description..."/>
</page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids"/>
<field name="activity_ids"/>
<field name="message_ids"/>
</div>
</form>
</field>
</record>
<!-- List View -->
<record id="view_service_request_tree" model="ir.ui.view">
<field name="name">my.service.request.list</field>
<field name="model">my.service.request</field>
<field name="arch" type="xml">
<tree string="Service Requests" decoration-danger="state=='cancelled'"
decoration-success="state=='done'">
<field name="name"/>
<field name="partner_id"/>
<field name="user_id" optional="show"/>
<field name="priority" widget="priority"/>
<field name="date_request"/>
<field name="date_deadline" optional="show"/>
<field name="amount_total" sum="Total"/>
<field name="state" widget="badge"
decoration-info="state=='draft'"
decoration-warning="state=='submitted'"
decoration-primary="state=='in_progress'"
decoration-success="state=='done'"
decoration-danger="state=='cancelled'"/>
</tree>
</field>
</record>
<!-- Search View -->
<record id="view_service_request_search" model="ir.ui.view">
<field name="name">my.service.request.search</field>
<field name="model">my.service.request</field>
<field name="arch" type="xml">
<search>
<field name="name" string="Reference"/>
<field name="partner_id"/>
<field name="user_id"/>
<filter string="My Requests" name="my_requests"
domain="[('user_id', '=', uid)]"/>
<filter string="In Progress" name="in_progress"
domain="[('state', '=', 'in_progress')]"/>
<filter string="Urgent" name="urgent"
domain="[('priority', '=', '3')]"/>
<separator/>
<filter string="This Month" name="this_month"
domain="[('date_request', '>=',
(context_today() - relativedelta(day=1)).strftime('%Y-%m-%d'))]"/>
<group expand="0" string="Group By">
<filter string="Customer" name="group_partner"
context="{'group_by': 'partner_id'}"/>
<filter string="Status" name="group_state"
context="{'group_by': 'state'}"/>
<filter string="Assigned To" name="group_user"
context="{'group_by': 'user_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action -->
<record id="action_service_request" model="ir.actions.act_window">
<field name="name">Service Requests</field>
<field name="res_model">my.service.request</field>
<field name="view_mode">list,form,kanban</field>
<field name="search_view_id" ref="view_service_request_search"/>
<field name="context">{'search_default_in_progress': 1}</field>
</record>
</odoo>
تكوين الأمان
الأمان إلزامي لأي وحدة إنتاج.
قائمة التحكم بالوصول (security/ir.model.access.csv):
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_service_request_user,service.request.user,model_my_service_request,base.group_user,1,1,1,0
access_service_request_manager,service.request.manager,model_my_service_request,base.group_system,1,1,1,1
access_service_request_line_user,service.request.line.user,model_my_service_request_line,base.group_user,1,1,1,1
** قواعد التسجيل (security/my_module_security.xml):**
<odoo>
<!-- Users can only see their own requests unless they're managers -->
<record id="rule_service_request_own" model="ir.rule">
<field name="name">Service Request: Own Records</field>
<field name="model_id" ref="model_my_service_request"/>
<field name="domain_force">
[('user_id', '=', user.id)]
</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="True"/>
<field name="perm_create" eval="True"/>
<field name="perm_unlink" eval="False"/>
</record>
</odoo>
المعالجات (TransientModel)
المعالجات هي نماذج مؤقتة للإجراءات الموجهة متعددة الخطوات.
# wizard/service_request_wizard.py
from odoo import api, fields, models
class ServiceRequestBulkAssign(models.TransientModel):
_name = 'my.service.request.bulk.assign'
_description = 'Bulk Assign Service Requests'
user_id = fields.Many2one(
'res.users', string='Assign To', required=True
)
request_ids = fields.Many2many(
'my.service.request', string='Requests',
default=lambda self: self.env.context.get('active_ids', [])
)
note = fields.Text(string='Note')
def action_assign(self):
self.request_ids.write({'user_id': self.user_id.id})
if self.note:
for request in self.request_ids:
request.message_post(body=self.note)
return {'type': 'ir.actions.act_window_close'}
الإجراءات والتسلسلات الآلية
تسلسل الترقيم التلقائي:
<record id="seq_service_request" model="ir.sequence">
<field name="name">Service Request</field>
<field name="code">my.service.request</field>
<field name="prefix">SRQ/%(year)s/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
** الإجراء المجدول (وظيفة كرون):**
# In the model
def _cron_remind_overdue_requests(self):
overdue = self.search([
('state', 'in', ['submitted', 'in_progress']),
('date_deadline', '<', fields.Date.today()),
])
for request in overdue:
request.activity_schedule(
'mail.mail_activity_data_warning',
summary='Overdue Service Request',
user_id=request.user_id.id
)
<record id="ir_cron_remind_overdue" model="ir.cron">
<field name="name">Remind Overdue Service Requests</field>
<field name="model_id" ref="model_my_service_request"/>
<field name="state">code</field>
<field name="code">model._cron_remind_overdue_requests()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
<field name="active">True</field>
</record>
الأسئلة المتداولة
ما الفرق بينmodels.Model وmodels.TransientModel وmodels.AbstractModel؟
models.Model يقوم بإنشاء جدول دائم في قاعدة البيانات. models.TransientModel يقوم بإنشاء جدول مؤقت يتم مسحه بشكل دوري (يستخدم للمعالجات). لا يقوم models.AbstractModel بإنشاء أي جدول - إنه مزيج يمكن للنماذج الأخرى أن ترثه للحصول على الأساليب والحقول دون إنشاء جدول منفصل.
كيف يمكنني توسيع نموذج Odoo الحالي دون تعديل الكود الأساسي؟
استخدم _inherit مع اسم النموذج الحالي واحذف _name. يؤدي ذلك إلى إضافة الحقول والأساليب الخاصة بك إلى النموذج الحالي: class SaleOrder(models.Model): _inherit = 'sale.order'. لإنشاء نموذج جديد ينسخ سلوك شخص آخر كنقطة بداية، استخدم كلاً من _name (الاسم الجديد) و_inherit (النموذج المصدر).
كيف يجب أن أتعامل مع عمليات الترحيل عندما يتغير نموذج بيانات الوحدة الخاصة بي؟
قم بإنشاء برنامج نصي للترحيل في my_module/migrations/{version}/pre-migrate.py أو post-migrate.py. تعمل هذه البرامج النصية تلقائيًا أثناء تحديث الوحدة النمطية. لإعادة تسمية الأعمدة، استخدم مساعدات openupgradelib. اختبر دائمًا عمليات الترحيل على نسخة من قاعدة بيانات الإنتاج قبل التقديم على الإنتاج.
هل يمكنني تجاوز طرق عرض Odoo الحالية دون تعديل ملفات XML الأساسية؟
نعم. استخدم inherit_id للإشارة إلى العرض الذي تريد توسيعه، ثم استخدم تعبيرات xpath لتحديد موقع العنصر والسمة position (قبل، بعد، داخل، استبدال، السمات) لتحديد التعديل. يؤدي هذا إلى إبقاء تغييراتك معزولة وآمنة للترقية.
كيف يمكنني إنشاء مجال خاص بالشركة في بيئة متعددة الشركات؟
استخدم company_dependent=True في تعريف الحقل: my_field = fields.Char(company_dependent=True). يؤدي هذا إلى تخزين قيمة منفصلة لكل شركة، بحيث يمكن أن يكون لدى الشركة "أ" والشركة "ب" قيم مختلفة لنفس السجل. يُستخدم هذا لقوائم الأسعار وحسابات الضرائب والتكوينات الأخرى الخاصة بالشركة.
ما هي الطريقة الصحيحة لتسجيل الرسائل وتصحيح الأخطاء أثناء التطوير؟
استخدم وحدة logging الخاصة ببايثون: import logging; _logger = logging.getLogger(__name__). استخدم _logger.info()، _logger.warning()، _logger.error() لمستويات الخطورة المختلفة. لا تستخدم أبدًا عبارات print() في كود الإنتاج. أثناء التطوير، قم بتشغيل Odoo باستخدام --log-level=debug لرؤية جميع مخرجات تصحيح الأخطاء.
الخطوات التالية
يتطلب إنشاء وحدة Odoo جاهزة للإنتاج معرفة عميقة بإطار العمل، واعتبارات أداء PostgreSQL، وأنماط الترقية الآمنة، والاختبار الشامل. تخضع الوحدات المخصصة لسوق Odoo للتحقق الإضافي من الأمان والأداء وجودة التعليمات البرمجية.
تقوم ECOSIRE بتطوير وحدات Odoo 19 Enterprise مخصصة لمتطلبات أعمال محددة - بدءًا من سير العمل في الصناعة المتخصصة وحتى وحدات موصل السوق. يتبع فريق التطوير لدينا إرشادات الترميز الرسمية الخاصة بـ Odoo، ويتضمن اختبارات شاملة للوحدات، ويقدم وحدات مع التوثيق الكامل.
تكليف وحدة Odoo مخصصة من ECOSIRE →
شارك متطلباتك وسنقوم بتوسيع نطاق جهود التطوير وتوفير جدول زمني وتقديم وحدة تتكامل بشكل واضح مع تثبيت Odoo 19 Enterprise.
بقلم
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
مقالات ذات صلة
محاسبة Odoo مقابل QuickBooks: مقارنة تفصيلية 2026
مقارنة متعمقة لعام 2026 بين محاسبة Odoo وQuickBooks التي تغطي الميزات والتسعير والتكامل وقابلية التوسع والنظام الأساسي الذي يناسب احتياجات عملك.
دراسة حالة: ترحيل التجارة الإلكترونية إلى Shopify باستخدام Odoo Backend
كيف انتقل بائع تجزئة للأزياء من WooCommerce إلى Shopify وربطه بـ Odoo ERP، مما أدى إلى تقليل وقت تنفيذ الطلب بنسبة 71% وزيادة الإيرادات بنسبة 43%.
دراسة الحالة: تنفيذ تخطيط موارد المؤسسات (ERP) للتصنيع باستخدام Odoo 19
كيف خفضت شركة تصنيع قطع غيار السيارات الباكستانية وقت معالجة الطلب بنسبة 68% وخفضت تباين المخزون إلى أقل من 2% من خلال تنفيذ Odoo 19 من ECOSIRE.