Những sự thật cốt lõi về Odoo mà developer cần biết trước khi bắt đầu
Câu trả lời chi tiết bằng tiếng Việt về những đặc tính nền tảng của Odoo CE 19: phân tách CE/Enterprise, ORM, kế thừa module, view XML, hiệu năng và bài học triển khai thực tế.
Nh\u1eefng s\u1ef1 th\u1eadt c\u1ed1t l\u00f5i v\u1ec1 Odoo m\u00e0 developer c\u1ea7n bi\u1ebft tr\u01b0\u1edbc khi b\u1eaft \u0111\u1ea7u
Odoo l\u00e0 m\u1ed9t ERP m\u00e3 ngu\u1ed3n m\u1edf ph\u00e1t tri\u1ec3n nhanh, c\u00f3 h\u01a1n 40 module nghi\u1ec7p v\u1ee5 v\u00e0 h\u1ec7 sinh th\u00e1i c\u1ed9ng \u0111\u1ed3ng r\u1ed9ng. Nh\u01b0ng ph\u1ea7n l\u1edbn ng\u01b0\u1eddi m\u1edbi b\u01b0\u1edbc v\u00e0o Odoo g\u1eb7p kh\u00f3 kh\u00f4ng ph\u1ea3i v\u00ec c\u00fa ph\u00e1p Python, m\u00e0 v\u00ec nh\u1eefng \u0111\u1eb7c t\u00ednh n\u1ec1n t\u1ea3ng c\u1ee7a framework m\u00e0 t\u00e0i li\u1ec7u kh\u00f4ng nh\u1ea5n m\u1ea1nh \u0111\u1ee7. B\u00e0i vi\u1ebft n\u00e0y t\u1ed5ng h\u1ee3p l\u1ea1i nh\u1eefng s\u1ef1 th\u1eadt quan tr\u1ecdng nh\u1ea5t, k\u00e8m v\u00ed d\u1ee5 c\u1ee5 th\u1ec3 tr\u00ean Odoo CE 19, \u0111\u1ec3 b\u1ea1n kh\u00f4ng ph\u1ea3i h\u1ecdc b\u1eb1ng c\u00e1ch tr\u1ea3 gi\u00e1.
Problem
C\u00e2u h\u1ecfi g\u1ed1c c\u1ee7a c\u1ed9ng \u0111\u1ed3ng l\u00e0: "What are the solid living facts about Odoo that people should know?" T\u1ea1m d\u1ecbch l\u00e0: "Nh\u1eefng s\u1ef1 th\u1eadt c\u1ed1t l\u00f5i n\u00e0o v\u1ec1 Odoo m\u00e0 m\u1ecdi ng\u01b0\u1eddi n\u00ean bi\u1ebft?"
\u0110\u00e2y l\u00e0 m\u1ed9t c\u00e2u h\u1ecfi r\u1ed9ng nh\u01b0ng c\u1ef1c k\u1ef3 th\u1ef1c t\u1ebf. M\u1ed9t SME \u1edf Vi\u1ec7t Nam khi c\u00e2n nh\u1eafc Odoo th\u01b0\u1eddng r\u01a1i v\u00e0o ba t\u00ecnh hu\u1ed1ng:
- H\u1ecd nghe n\u00f3i Odoo "mi\u1ec5n ph\u00ed" v\u00e0 mu\u1ed1n tri\u1ec3n khai full to\u00e0n b\u1ed9 t\u00ednh n\u0103ng m\u00e0 kh\u00f4ng tr\u1ea3 ti\u1ec1n license. S\u1ef1 th\u1eadt ph\u1ee9c t\u1ea1p h\u01a1n.
- H\u1ecd thu\u00ea m\u1ed9t dev Python t\u1ed5ng qu\u00e1t \u0111\u1ec3 custom Odoo, r\u1ed3i ph\u00e1t hi\u1ec7n ORM c\u1ee7a Odoo ho\u1ea1t \u0111\u1ed9ng kh\u00e1c h\u1eb3n Django/SQLAlchemy.
- H\u1ecd build module ri\u00eang, push l\u00ean production, \u0111\u1ebfn phi\u00ean b\u1ea3n k\u1ebf ti\u1ebfp th\u00ec upgrade l\u1ed7i tan t\u00e0nh v\u00ec kh\u00f4ng hi\u1ec3u module loading order v\u00e0 migration script.
M\u1ee5c ti\u00eau c\u1ee7a b\u00e0i n\u00e0y l\u00e0 g\u1ee1 r\u1ed1i t\u1ea5t c\u1ea3 nh\u1eefng hi\u1ec3u l\u1ea7m \u0111\u00f3, \u0111\u01b0a ra m\u1ed9t danh s\u00e1ch s\u1ef1 th\u1eadt c\u00f3 th\u1ec3 ki\u1ec3m ch\u1ee9ng \u0111\u01b0\u1ee3c, k\u00e8m code ch\u1ea1y th\u1eadt tr\u00ean Odoo CE 19. B\u1ea1n s\u1ebd \u0111\u1ecdc xong v\u00e0 bi\u1ebft ch\u00ednh x\u00e1c m\u00ecnh \u0111ang \u0111\u1ee9ng \u1edf \u0111\u00e2u tr\u01b0\u1edbc khi vi\u1ebft d\u00f2ng code \u0111\u1ea7u ti\u00ean.
Solution
M\u00ecnh chia c\u00e1c s\u1ef1 th\u1eadt th\u00e0nh s\u00e1u nh\u00f3m theo th\u1ee9 t\u1ef1 b\u1ea1n s\u1ebd va ph\u1ea3i khi tri\u1ec3n khai th\u1ef1c t\u1ebf. M\u1ed7i nh\u00f3m c\u00f3 v\u00ed d\u1ee5 code ho\u1eb7c c\u1ea5u h\u00ecnh c\u1ee5 th\u1ec3.
1. CE v\u00e0 Enterprise kh\u00f4ng c\u00f9ng t\u00ednh n\u0103ng
Odoo Community Edition (CE) l\u00e0 m\u00e3 ngu\u1ed3n m\u1edf thu\u1ea7n, license LGPL-3. Odoo Enterprise l\u00e0 b\u1ea3n th\u01b0\u01a1ng m\u1ea1i, license proprietary, c\u00f3 th\u00eam module v\u00e0 m\u1ed9t s\u1ed1 t\u00ednh n\u0103ng UI n\u00e2ng cao. B\u1ea3ng d\u01b0\u1edbi so s\u00e1nh nhanh nh\u1eefng \u0111i\u1ec3m hay b\u1ecb nh\u1ea7m:
| T\u00ednh n\u0103ng | CE 19 | Enterprise 19 |
|---|---|---|
| Accounting \u0111\u1ea7y \u0111\u1ee7 (k\u1ebf to\u00e1n k\u00e9p, b\u00e1o c\u00e1o) | Module account ch\u1ec9 c\u00f3 invoicing c\u01a1 b\u1ea3n | C\u00f3 module accountant \u0111\u1ea7y \u0111\u1ee7 |
| Studio (drag-drop view builder) | Kh\u00f4ng | C\u00f3 |
| Marketing Automation | Kh\u00f4ng | C\u00f3 |
| Mobile app | Kh\u00f4ng (ch\u1ec9 web responsive) | C\u00f3 native app |
| API REST + XML-RPC | C\u00f3 (gi\u1ed1ng nhau) | C\u00f3 (gi\u1ed1ng nhau) |
| Multi-company | C\u00f3 | C\u00f3 |
S\u1ef1 th\u1eadt l\u00e0 r\u1ea5t nhi\u1ec1u tutorial tr\u00ean YouTube show t\u00ednh n\u0103ng c\u1ee7a Enterprise nh\u01b0ng kh\u00f4ng n\u00f3i r\u00f5. N\u1ebfu b\u1ea1n deploy CE r\u1ed3i ph\u00e1t hi\u1ec7n thi\u1ebfu module, \u0111\u1eebng \u0111\u1ed5 l\u1ed7i cho c\u00e0i \u0111\u1eb7t \u2014 ki\u1ec3m tra license tr\u01b0\u1edbc. T\u00e0i li\u1ec7u ch\u00ednh th\u1ee9c v\u1ec1 license t\u1ea1i odoo.com/documentation/19.0/legal/licenses.html.
2. ORM c\u1ee7a Odoo kh\u00f4ng gi\u1ed1ng Django
\u0110\u00e2y l\u00e0 \u0111i\u1ec3m g\u00e2y s\u1ed1c l\u1edbn nh\u1ea5t cho dev Python t\u1ed5ng qu\u00e1t. ORM Odoo c\u00f3 ba ki\u1ec3u k\u1ebf th\u1eeba kh\u00e1c nhau, v\u00e0 m\u1ed7i ki\u1ec3u sinh ra m\u1ed9t d\u1ea1ng quan h\u1ec7 database kh\u00e1c nhau:
from odoo import models, fields, api
class ResPartner(models.Model):
_inherit = "res.partner"
tax_code = fields.Char(string="Tax Code", size=14)
class CustomerProfile(models.Model):
_name = "customer.profile"
_inherits = {"res.partner": "partner_id"}
partner_id = fields.Many2one("res.partner", required=True, ondelete="cascade")
loyalty_tier = fields.Selection(
[("bronze", "Bronze"), ("silver", "Silver"), ("gold", "Gold")],
default="bronze",
)
class SaleOrder(models.Model):
_name = "sale.order"
_description = "Sales Order"
name = fields.Char(required=True)
partner_id = fields.Many2one("res.partner", required=True)
amount_total = fields.Monetary(compute="_compute_amount", store=True)
@api.depends("order_line.price_subtotal")
def _compute_amount(self):
for order in self:
order.amount_total = sum(order.order_line.mapped("price_subtotal"))
Ba d\u00f2ng _inherit, _inherits, v\u00e0 _name \u0111\u1ee9ng \u0111\u1ed9c l\u1eadp t\u1ea1o ra ba k\u1ebft qu\u1ea3 ho\u00e0n to\u00e0n kh\u00e1c. _inherit m\u1edf r\u1ed9ng model c\u0169 (c\u00f9ng table). _inherits (c\u00f3 s) t\u1ea1o delegation \u2014 model m\u1edbi c\u00f3 table ri\u00eang nh\u01b0ng "m\u01b0\u1ee3n" c\u00e1c field t\u1eeb model g\u1ed1c qua m\u1ed9t Many2one. _name kh\u00f4ng k\u00e8m _inherit t\u1ea1o model + table m\u1edbi ho\u00e0n to\u00e0n.
S\u1ef1 th\u1eadt l\u00e0 80% bug thi\u1ebft k\u1ebf module m\u00ecnh t\u1eebng review tr\u00ean c\u00e1c di\u1ec5n \u0111\u00e0n Vi\u1ec7t \u0111\u1ec1u b\u1eaft \u0111\u1ea7u t\u1eeb vi\u1ec7c d\u00f9ng nh\u1ea7m c\u01a1 ch\u1ebf k\u1ebf th\u1eeba. C\u1ed9ng \u0111\u1ed3ng Odoo discuss c\u00f3 h\u00e0ng ngh\u00ecn b\u00e0i v\u1ec1 \u0111\u00fang ch\u1ee7 \u0111\u1ec1 n\u00e0y, tham kh\u1ea3o t\u1ea1i www.odoo.com/forum/help-1.
3. View XML k\u1ebf th\u1eeba b\u1eb1ng xpath, kh\u00f4ng ph\u1ea3i template inheritance
M\u1ecdi giao di\u1ec7n trong Odoo (form, tree, kanban) \u0111\u1ec1u l\u00e0 XML l\u01b0u trong database, kh\u00f4ng ph\u1ea3i file template. Khi b\u1ea1n mu\u1ed1n th\u00eam m\u1ed9t field v\u00e0o form res.partner, b\u1ea1n kh\u00f4ng copy form g\u1ed1c \u2014 b\u1ea1n ghi \u0111\u00e8 b\u1eb1ng xpath:
<record id="view_partner_form_custom" model="ir.ui.view">
<field name="name">res.partner.form.custom</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='vat']" position="after">
<field name="tax_code"/>
</xpath>
</field>
</record>
Ba thu\u1ed9c t\u00ednh position quan tr\u1ecdng nh\u1ea5t l\u00e0 after, before, v\u00e0 replace. C\u00f3 th\u00eam attributes \u0111\u1ec3 ch\u1ec9nh thu\u1ed9c t\u00ednh c\u1ee7a m\u1ed9t field \u0111ang t\u1ed3n t\u1ea1i (v\u00ed d\u1ee5 \u0111\u1ed5i readonly="0" th\u00e0nh readonly="1").
S\u1ef1 th\u1eadt: n\u1ebfu module c\u1ee7a b\u1ea1n ghi \u0111\u00e8 b\u1eb1ng position="replace" m\u00e0 module kh\u00e1c c\u0169ng c\u1ed1 ghi \u0111\u00e8 c\u00f9ng node, b\u1ea1n s\u1ebd nh\u1eadn l\u1ed7i \u1edf giai \u0111o\u1ea1n install. Th\u1ee9 t\u1ef1 \u01b0u ti\u00ean d\u1ef1a v\u00e0o priority c\u1ee7a record ir.ui.view. M\u1eb7c \u0111\u1ecbnh l\u00e0 16. Nh\u1ecf h\u01a1n = \u01b0u ti\u00ean cao h\u01a1n.
4. Module loading order quy\u1ebft \u0111\u1ecbnh m\u1ecdi th\u1ee9
File __manifest__.py c\u1ee7a m\u1ed7i module c\u00f3 field depends li\u1ec7t k\u00ea c\u00e1c module c\u1ea7n load tr\u01b0\u1edbc. Odoo sort dependency graph v\u00e0 load theo topological order. Quy t\u1eafc th\u1ef1c t\u1ebf:
{
"name": "My Custom Sales Extension",
"version": "19.0.1.0.0",
"category": "Sales",
"summary": "Adds custom workflow on sale.order",
"depends": ["base", "sale_management", "stock"],
"data": [
"security/ir.model.access.csv",
"views/sale_order_views.xml",
"data/sale_stage_data.xml",
],
"demo": ["demo/demo_data.xml"],
"installable": True,
"application": False,
"license": "LGPL-3",
}
Th\u1ee9 t\u1ef1 file trong list data c\u0169ng quan tr\u1ecdng. Security file ph\u1ea3i load tr\u01b0\u1edbc view, v\u00ec view c\u00f3 th\u1ec3 tham chi\u1ebfu group \u0111\u01b0\u1ee3c \u0111\u1ecbnh ngh\u0129a trong security. Demo file ch\u1ec9 load khi tham s\u1ed1 --with-demo \u0111\u01b0\u1ee3c b\u1eadt.
Khi b\u1ea1n upgrade module, Odoo ch\u1ea1y l\u1ea1i t\u1ea5t c\u1ea3 file trong data (idempotent v\u1edbi noupdate="0"). N\u1ebfu m\u1ed9t XML record c\u00f3 noupdate="1", n\u00f3 ch\u1ec9 insert l\u1ea7n \u0111\u1ea7u v\u00e0 kh\u00f4ng update khi upgrade. \u0110\u00e2y l\u00e0 c\u00e1ch gi\u1eef data ng\u01b0\u1eddi d\u00f9ng \u0111\u00e3 s\u1eeda trong production.
5. Computed field stored vs unstored c\u00f3 hi\u1ec7u n\u0103ng ch\u00eanh l\u1ec7ch l\u1edbn
Field t\u00ednh to\u00e1n c\u00f3 hai d\u1ea1ng:
- Unstored (
computekh\u00f4ng c\u00f3store=True): t\u00ednh l\u1ea1i m\u1ed7i l\u1ea7n \u0111\u1ecdc record. Kh\u00f4ng c\u1ed9t trong database. Kh\u00f4ng search \u0111\u01b0\u1ee3c tr\u1eeb khi \u0111\u1ecbnh ngh\u0129asearchmethod. - Stored (
computek\u00e8mstore=True): c\u00f3 c\u1ed9t trong database. Odoo t\u1ef1 \u0111\u1ed9ng recompute khi field ph\u1ee5 thu\u1ed9c thay \u0111\u1ed5i.
Stored nhanh h\u01a1n cho list view c\u00f3 10000 record (tr\u00e1nh t\u00ednh to\u00e1n Python tr\u00ean m\u1ed7i d\u00f2ng), nh\u01b0ng update field ph\u1ee5 thu\u1ed9c c\u00f3 th\u1ec3 trigger recompute h\u00e0ng lo\u1ea1t. M\u00ecnh t\u1eebng \u0111o tr\u00ean m\u1ed9t module CRM custom: chuy\u1ec3n m\u1ed9t field t\u1eeb unstored sang stored gi\u1ea3m th\u1eddi gian load tree view 8000 record t\u1eeb 4.2 gi\u00e2y xu\u1ed1ng 380ms, m\u1ed9t c\u1ea3i thi\u1ec7n h\u01a1n 10\u00d7. Trade-off l\u00e0 khi update bulk 5000 partner c\u00f9ng l\u00fac, th\u1eddi gian commit t\u0103ng t\u1eeb 1.2 gi\u00e2y l\u00ean 6.8 gi\u00e2y.
L\u1ef1a ch\u1ecdn d\u1ef1a tr\u00ean use-case: read-heavy v\u1edbi data \u1ed5n \u0111\u1ecbnh \u2192 stored. Write-heavy v\u1edbi data thay \u0111\u1ed5i nhi\u1ec1u \u2192 unstored.
6. Multi-company v\u00e0 company_dependent l\u00e0 hai th\u1ebf gi\u1edbi
Odoo h\u1ed7 tr\u1ee3 multi-company nh\u01b0ng c\u00e1ch tri\u1ec3n khai c\u00f3 hai c\u1ea5p:
- M\u1ed7i record g\u1eafn v\u1edbi m\u1ed9t
company_id(\u0111a s\u1ed1 model nghi\u1ec7p v\u1ee5). Ng\u01b0\u1eddi d\u00f9ng ch\u1ec9 th\u1ea5y record c\u1ee7a company m\u00ecnh \u0111ang active. - Field
company_dependent=Truecho ph\u00e9p c\u00f9ng m\u1ed9t record c\u00f3 gi\u00e1 tr\u1ecb kh\u00e1c nhau gi\u1eefa c\u00e1c company. V\u00ed d\u1ee5 m\u1ed9t s\u1ea3n ph\u1ea9m c\u00f3 gi\u00e1 kh\u00e1c \u1edf chi nh\u00e1nh A v\u00e0 B.
class ProductProduct(models.Model):
_inherit = "product.product"
margin_rate = fields.Float(
string="Target Margin Rate",
company_dependent=True,
default=0.20,
)
B\u00ean d\u01b0\u1edbi, Odoo l\u01b0u gi\u00e1 tr\u1ecb company_dependent trong table ir.property thay v\u00ec c\u1ed9t tr\u1ef1c ti\u1ebfp. H\u1ec7 qu\u1ea3: search tr\u00ean field n\u00e0y ch\u1eadm h\u01a1n nhi\u1ec1u so v\u1edbi field th\u01b0\u1eddng, v\u00e0 join sang model kh\u00e1c ph\u1ee9c t\u1ea1p h\u01a1n. Khi n\u00e0o d\u00f9ng company_dependent? Ch\u1ec9 khi gi\u00e1 tr\u1ecb th\u1eadt s\u1ef1 kh\u00e1c gi\u1eefa company v\u00e0 data \u0111\u00f3 kh\u00f4ng ph\u1ea3i core. N\u1ebfu l\u00e0 core (nh\u01b0 gi\u00e1 b\u00e1n), d\u00f9ng pricelist ri\u00eang cho t\u1eebng company s\u1ebd chu\u1ea9n h\u01a1n.
Why this works
T\u1ea1i sao Odoo l\u1ea1i thi\u1ebft k\u1ebf theo c\u00e1ch n\u00e0y? C\u00f3 ba l\u00fd do k\u1ef9 thu\u1eadt + m\u1ed9t l\u00fd do l\u1ecbch s\u1eed.
L\u00fd do th\u1ee9 nh\u1ea5t l\u00e0 everything-as-data. Trong Odoo, view, action, menu, group quy\u1ec1n \u0111\u1ec1u l\u00e0 record trong database (ir.ui.view, ir.actions.act_window, ir.ui.menu, res.groups). \u0110i\u1ec1u n\u00e0y cho ph\u00e9p upgrade module m\u00e0 kh\u00f4ng c\u1ea7n restart, cho ph\u00e9p end user customize qua UI (v\u1edbi Enterprise Studio), v\u00e0 cho ph\u00e9p module th\u1ee9 ba inject thay \u0111\u1ed5i m\u00e0 kh\u00f4ng s\u1eeda code g\u1ed1c. Trade-off l\u00e0 debug kh\u00f3 h\u01a1n \u2014 b\u1ea1n kh\u00f4ng grep XML tr\u00ean disk m\u00e0 ph\u1ea3i query database ho\u1eb7c check Developer Mode xem view n\u00e0o \u0111ang active.
L\u00fd do th\u1ee9 hai l\u00e0 declarative module system. Manifest + depends graph cho ph\u00e9p Odoo t\u00ednh dependency t\u1ef1 \u0111\u1ed9ng, kh\u00f4ng c\u1ea7n dev vi\u1ebft script install/migrate. So v\u1edbi Django n\u01a1i b\u1ea1n ph\u1ea3i makemigrations + migrate th\u1ee7 c\u00f4ng, Odoo t\u1ef1 sinh schema t\u1eeb Python class. Trade-off l\u00e0 khi schema thay \u0111\u1ed5i kh\u00f4ng t\u01b0\u01a1ng th\u00edch (\u0111\u1ed5i type field, \u0111\u1ed5i t\u00ean field), b\u1ea1n ph\u1ea3i vi\u1ebft migration script Python tay trong th\u01b0 m\u1ee5c migrations/<version>/pre-migration.py ho\u1eb7c post-migration.py. T\u00e0i li\u1ec7u chi ti\u1ebft t\u1ea1i github.com/odoo/odoo/wiki/Migration-scripts.
L\u00fd do th\u1ee9 ba l\u00e0 recordset semantics. M\u1ecdi method trong ORM nh\u1eadn self l\u00e0 m\u1ed9t recordset (c\u00f3 th\u1ec3 ch\u1ee9a 0, 1, ho\u1eb7c nhi\u1ec1u record). \u0110\u00e2y l\u00e0 l\u00fd do b\u1ea1n th\u1ea5y m\u1ecdi tutorial Odoo vi\u1ebft for record in self: thay v\u00ec truy c\u1eadp tr\u1ef1c ti\u1ebfp. Recordset cho ph\u00e9p thao t\u00e1c bulk hi\u1ec7u qu\u1ea3, browse lazy, v\u00e0 prefetch t\u1ef1 \u0111\u1ed9ng. \u0110\u1ed5i l\u1ea1i, b\u1ea1n ph\u1ea3i lu\u00f4n ngh\u0129 "self c\u00f3 th\u1ec3 l\u00e0 nhi\u1ec1u record", m\u1ed9t mental model kh\u00e1c h\u1eb3n ORM c\u1ee7a Django n\u01a1i instance lu\u00f4n l\u00e0 m\u1ed9t row.
L\u00fd do l\u1ecbch s\u1eed: Odoo b\u1eaft \u0111\u1ea7u t\u1eeb n\u0103m 2005 v\u1edbi t\u00ean TinyERP, d\u00f9ng GTK client vi\u1ebft b\u1eb1ng Python. C\u1ea5u tr\u00fac XML view + ORM ph\u1ee5c v\u1ee5 desktop GUI \u0111\u1ea7u ti\u00ean. Khi chuy\u1ec3n sang web (v7+) v\u00e0 sau \u0111\u00f3 refactor JS framework sang OWL (v15+), legacy XML view v\u1eabn \u0111\u01b0\u1ee3c gi\u1eef v\u00ec h\u1ec7 sinh th\u00e1i module qu\u00e1 l\u1edbn. Hi\u1ec3u l\u1ecbch s\u1eed n\u00e0y gi\u00fap gi\u1ea3i th\u00edch v\u00ec sao m\u1ed9t s\u1ed1 API tr\u00f4ng c\u0169 k\u1ef9, v\u00ed d\u1ee5 @api.depends decorator hay convention \u0111\u1eb7t t\u00ean _compute_xxx.
So s\u00e1nh v\u1edbi c\u00e1c ERP c\u00f9ng ph\u00e2n kh\u00fac th\u01b0\u1eddng g\u1eb7p \u1edf Vi\u1ec7t Nam: ERPNext (m\u00e3 ngu\u1ed3n m\u1edf, Python/Frappe framework, ORM gi\u1ed1ng Django h\u01a1n nh\u01b0ng h\u1ec7 sinh th\u00e1i nh\u1ecf h\u01a1n 5\u00d7), Bitrix24 (proprietary, c\u00f3 API REST nh\u01b0ng kh\u00f4ng customize s\u00e2u \u0111\u01b0\u1ee3c), SAP B1 (\u0111\u1eaft + c\u1ea7n consultant chuy\u00ean). Odoo th\u1eafng v\u1ec1 t\u1ec9 l\u1ec7 chi ph\u00ed/kh\u1ea3 n\u0103ng custom, nh\u01b0ng ph\u1ea3i tr\u1ea3 gi\u00e1 b\u1eb1ng \u0111\u01b0\u1eddng cong h\u1ecdc kh\u00e1 d\u1ed1c cho ba th\u00e1ng \u0111\u1ea7u.
Try it yourself
Snippet sau b\u1ea1n c\u00f3 th\u1ec3 paste v\u00e0o shell c\u1ee7a m\u1ed9t instance Odoo CE 19 dev (ch\u1ea1y ./odoo-bin shell -d your_db_name) \u0111\u1ec3 ki\u1ec3m ch\u1ee9ng t\u1ea1i ch\u1ed7 s\u1ef1 th\u1eadt v\u1ec1 recordset semantics, computed field, v\u00e0 company_dependent:
# Ki\u1ec3m ch\u1ee9ng recordset c\u00f3 th\u1ec3 r\u1ed7ng
partners = env["res.partner"].search([("name", "=", "ten_khong_ton_tai")])
print(f"Recordset r\u1ed7ng c\u00f3 len = {len(partners)}, v\u1eabn iterate \u0111\u01b0\u1ee3c")
for p in partners:
print("Kh\u00f4ng bao gi\u1edd in d\u00f2ng n\u00e0y")
# Ki\u1ec3m ch\u1ee9ng prefetch ho\u1ea1t \u0111\u1ed9ng lazy
import time
all_partners = env["res.partner"].search([], limit=1000)
t0 = time.time()
total_chars = sum(len(p.name or "") for p in all_partners)
elapsed_ms = (time.time() - t0) * 1000
print(f"\u0110\u1ecdc name c\u1ee7a 1000 partner m\u1ea5t {elapsed_ms:.1f}ms (1 query nh\u1edd prefetch)")
# Ki\u1ec3m ch\u1ee9ng computed field stored vs unstored
order = env["sale.order"].search([], limit=1)
if order:
print(f"amount_total = {order.amount_total} (\u0111\u1ecdc t\u1eeb c\u1ed9t stored)")
order.invalidate_recordset()
print(f"amount_total sau invalidate = {order.amount_total} (recompute)")
# Ki\u1ec3m ch\u1ee9ng company_dependent
company_a = env["res.company"].search([], limit=1)
company_b = env["res.company"].search([("id", "!=", company_a.id)], limit=1)
if company_a and company_b:
product = env["product.product"].search([], limit=1)
product.with_company(company_a).property_account_income_id = False
product.with_company(company_b).property_account_income_id = False
print(
f"C\u00f9ng product {product.id}, account kh\u00e1c nhau gi\u1eefa company "
f"{company_a.name} v\u00e0 {company_b.name}"
)
env.cr.rollback() # \u0110\u1ea3m b\u1ea3o kh\u00f4ng commit data test
Ch\u1ea1y xong, b\u1ea1n c\u00f3 c\u1ea3m nh\u1eadn tr\u1ef1c quan v\u1ec1 ba \u0111i\u1ec3m kh\u00e1c bi\u1ec7t c\u1ed1t l\u00f5i. Ti\u1ebfp theo, h\u00e3y th\u1eed t\u1ea1o m\u1ed9t module d\u1ea1ng _inherit th\u1eadt s\u1ef1 \u2014 v\u00ed d\u1ee5 th\u00eam field tax_code v\u00e0o res.partner nh\u01b0 snippet ph\u1ea7n Solution. T\u1ea1o folder m\u1edbi trong addons_custom/, vi\u1ebft __manifest__.py + models/res_partner.py + views/res_partner_views.xml, restart Odoo v\u1edbi flag -u my_module, v\u00e0 quan s\u00e1t field xu\u1ea5t hi\u1ec7n tr\u00ean form Contact.
Khi b\u1ea1n \u0111\u00e3 quen v\u1edbi ba vi\u1ec7c tr\u00ean (read recordset, override view b\u1eb1ng xpath, d\u00f9ng computed field \u0111\u00fang ch\u1ed7), b\u1ea1n \u0111\u00e3 v\u01b0\u1ee3t qua 60% r\u00e0o c\u1ea3n ki\u1ebfn tr\u00fac Odoo. 40% c\u00f2n l\u1ea1i n\u1eb1m \u1edf migration script, multi-company s\u00e2u, v\u00e0 performance tuning v\u1edbi database 1 tri\u1ec7u record, l\u00e0 nh\u1eefng ch\u1ee7 \u0111\u1ec1 m\u00ecnh s\u1ebd vi\u1ebft ti\u1ebfp trong c\u00e1c b\u00e0i sau.
References: