कस्टम ओडू मॉड्यूल का निर्माण: डेवलपर ट्यूटोरियल
ओडू का मॉड्यूल सिस्टम ईआरपी दुनिया में सबसे शक्तिशाली विस्तार ढांचे में से एक है। ओडू में प्रत्येक सुविधा - लेखांकन से लेकर इन्वेंट्री से लेकर सीआरएम तक - एक मॉड्यूल है। इसका मतलब यह है कि कस्टम कार्यक्षमता का निर्माण ओडू के स्वयं के डेवलपर्स द्वारा उपयोग किए गए बिल्कुल उसी पैटर्न का पालन करता है, जो आपको कोर को फोर्क किए बिना पूर्ण ढांचे तक पहुंच प्रदान करता है।
यह ट्यूटोरियल एक कस्टम ओडू 19 मॉड्यूल के संपूर्ण जीवनचक्र को कवर करता है: निर्देशिका संरचना को तैयार करने और मॉडल को परिभाषित करने से लेकर दृश्य बनाने, पहुंच सुरक्षित करने और उत्पादन में तैनात करने तक। अंत तक, आपके पास एक कार्यशील मॉड्यूल होगा जो ओडू 19 एंटरप्राइज़ सम्मेलनों का पालन करता है और बाज़ार के लिए तैयार है।
मुख्य बातें
- प्रत्येक ओडू मॉड्यूल
__manifest__.pyडिस्क्रिप्टर वाला एक पायथन पैकेज है- मॉडल
models.Modelसे प्राप्त होते हैं और सीधे PostgreSQL तालिकाओं पर मैप होते हैं- दृश्यों को XML और संदर्भ मॉडल फ़ील्ड में नाम से परिभाषित किया गया है
- सुरक्षा
ir.model.accessCSV और रिकॉर्ड नियमों के माध्यम से लागू की जाती है- विज़ार्ड्स (
TransientModel) मल्टी-स्टेप उपयोगकर्ता इंटरैक्शन को संभालते हैं- कंप्यूटेड फ़ील्ड और ऑनचेंज विधियां संबंधित फ़ील्ड को गतिशील रूप से अपडेट करती हैं
- स्वचालित क्रियाएं और निर्धारित नौकरियां ट्रिगर्स पर सर्वर-साइड लॉजिक चलाती हैं
- मॉड्यूल निर्भरता सही लोड क्रम और सुविधा उपलब्धता सुनिश्चित करती है
मॉड्यूल संरचना और मचान
प्रत्येक 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>
जादूगर (क्षणिकमॉडल)
विज़ार्ड निर्देशित बहु-चरणीय क्रियाओं के लिए अस्थायी रूप हैं।
# 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 कोई तालिका नहीं बनाता है - यह एक मिश्रण है जिसे अन्य मॉडल एक अलग तालिका बनाए बिना विधियों और फ़ील्ड प्राप्त करने के लिए प्राप्त कर सकते हैं।
मैं कोर कोड को संशोधित किए बिना मौजूदा ओडू मॉडल का विस्तार कैसे करूं?
मौजूदा मॉडल नाम के साथ _inherit का प्रयोग करें और _name को हटा दें। यह आपके फ़ील्ड और तरीकों को मौजूदा मॉडल में जोड़ता है: class SaleOrder(models.Model): _inherit = 'sale.order'। एक नया मॉडल बनाने के लिए जो किसी अन्य के व्यवहार को शुरुआती बिंदु के रूप में कॉपी करता है, _name (नया नाम) और _inherit (स्रोत मॉडल) दोनों का उपयोग करें।
जब मेरे मॉड्यूल का डेटा मॉडल बदलता है तो मुझे माइग्रेशन कैसे संभालना चाहिए?
my_module/migrations/{version}/pre-migrate.py या post-migrate.py में एक माइग्रेशन स्क्रिप्ट बनाएं। मॉड्यूल अपडेट के दौरान ये स्क्रिप्ट स्वचालित रूप से चलती हैं। कॉलम के नाम बदलने के लिए, openupgradelib सहायकों का उपयोग करें। उत्पादन पर आवेदन करने से पहले हमेशा उत्पादन डेटाबेस की एक प्रति पर माइग्रेशन का परीक्षण करें।
क्या मैं कोर 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 के साथ चलाएँ।
अगले चरण
उत्पादन के लिए तैयार ओडू मॉड्यूल के निर्माण के लिए ढांचे के गहन ज्ञान, पोस्टग्रेएसक्यूएल प्रदर्शन विचार, अपग्रेड-सुरक्षित पैटर्न और संपूर्ण परीक्षण की आवश्यकता होती है। ओडू बाज़ार के लिए इच्छित मॉड्यूल सुरक्षा, प्रदर्शन और कोड गुणवत्ता के लिए अतिरिक्त सत्यापन से गुजरते हैं।
ECOSIRE विशिष्ट व्यावसायिक आवश्यकताओं के लिए कस्टम Odoo 19 एंटरप्राइज मॉड्यूल विकसित करता है - विशेष उद्योग वर्कफ़्लो से लेकर मार्केटप्लेस कनेक्टर मॉड्यूल तक। हमारी विकास टीम ओडू के आधिकारिक कोडिंग दिशानिर्देशों का पालन करती है, इसमें व्यापक इकाई परीक्षण शामिल हैं, और पूर्ण दस्तावेज़ीकरण के साथ मॉड्यूल वितरित करते हैं।
ECOSIRE से एक कस्टम ओडू मॉड्यूल कमीशन करें →
अपनी आवश्यकताओं को साझा करें और हम विकास प्रयासों का दायरा बढ़ाएंगे, एक समयरेखा प्रदान करेंगे, और एक मॉड्यूल प्रदान करेंगे जो आपके ओडू 19 एंटरप्राइज़ इंस्टॉलेशन के साथ स्पष्ट रूप से एकीकृत होगा।
लेखक
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 Accounting vs QuickBooks: Detailed Comparison 2026
In-depth 2026 comparison of Odoo Accounting vs QuickBooks covering features, pricing, integrations, scalability, and which platform fits your business needs.
Case Study: eCommerce Migration to Shopify with Odoo Backend
How a fashion retailer migrated from WooCommerce to Shopify and connected it to Odoo ERP, cutting order fulfillment time by 71% and growing revenue 43%.
Case Study: Manufacturing ERP Implementation with Odoo 19
How a Pakistani auto-parts manufacturer cut order processing time by 68% and reduced inventory variance to under 2% with ECOSIRE's Odoo 19 implementation.