first push message
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
# xf_doc_approval_custom — Developer Guide
|
||||
|
||||
## Table of Contents
|
||||
1. [Project Overview](#1-project-overview)
|
||||
2. [Folder Structure](#2-folder-structure)
|
||||
3. [How Odoo Inheritance Works](#3-how-odoo-inheritance-works)
|
||||
4. [Inheriting a Model (Python)](#4-inheriting-a-model-python)
|
||||
5. [Inheriting a View (XML)](#5-inheriting-a-view-xml)
|
||||
6. [Inheriting CSS / Styles](#6-inheriting-css--styles)
|
||||
7. [Inheriting Fonts (Google Fonts)](#7-inheriting-fonts-google-fonts)
|
||||
8. [Key Techniques Used in This Module](#8-key-techniques-used-in-this-module)
|
||||
9. [Docker Commands](#9-docker-commands)
|
||||
10. [Rules — What NOT to Touch](#10-rules--what-not-to-touch)
|
||||
|
||||
---
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| Base module | `xf_doc_approval` (located at `d:\project_v19s\xf_doc_approval`) |
|
||||
| Custom module | `xf_doc_approval_custom` (located at `d:\project_v19s\odoo19-dev\addons\xf_doc_approval_custom`) |
|
||||
| Odoo version | 19.0 |
|
||||
| Purpose | Extend the base module with Khmer language labels, new fields, redesigned form, QR code, and role-based comment visibility — **without modifying the original module** |
|
||||
|
||||
### What we changed vs the base module
|
||||
|
||||
| Feature | What we did |
|
||||
|---------|-------------|
|
||||
| Navbar color | Changed to `#0a5e98` via CSS |
|
||||
| Font | Added Khmer font (Battambang) for Khmer text only |
|
||||
| State labels | Overrode all state labels to Khmer |
|
||||
| Section ក (Approvers) | Renamed to Khmer, added sent_date, round, Khmer column headers, document source row |
|
||||
| Section ខ (Content) | Completely new section with 8 custom fields in a styled table |
|
||||
| New fields | `document_type`, `decision_requester_id`, `reference_note`, `reference_file`, `doc_number`, `qr_code`, `document_source`, `description` (Html override) |
|
||||
| QR Code | Auto-generated when document is approved |
|
||||
| Auto numbering | `ir.sequence` → format `DOC/YYYY/MM/0001` |
|
||||
| Comment visibility | Step-based: each approver sees only notes from their step and earlier |
|
||||
| Button label | "Send for Approval" → "បញ្ជូន" |
|
||||
|
||||
---
|
||||
|
||||
## 2. Folder Structure
|
||||
|
||||
```
|
||||
xf_doc_approval_custom/
|
||||
│
|
||||
├── __manifest__.py ← Module declaration (name, depends, data files)
|
||||
├── __init__.py ← Python package init (imports models/)
|
||||
│
|
||||
├── models/
|
||||
│ ├── __init__.py ← Imports all model files
|
||||
│ ├── document_package.py ← Inherits xf.doc.approval.document.package
|
||||
│ └── document_approver.py ← Inherits xf.doc.approval.document.approver
|
||||
│
|
||||
├── views/
|
||||
│ ├── assets.xml ← Registers CSS + Google Fonts injection
|
||||
│ └── document_package_form.xml ← Inherits and modifies the form view
|
||||
│
|
||||
├── data/
|
||||
│ └── sequences.xml ← Creates ir.sequence for document numbering
|
||||
│
|
||||
└── static/
|
||||
└── src/
|
||||
└── css/
|
||||
└── navbar.css ← All custom CSS (navbar, table, section ខ)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. How Odoo Inheritance Works
|
||||
|
||||
Odoo has **3 layers** you can extend without touching the original code:
|
||||
|
||||
```
|
||||
Original Module Your Custom Module
|
||||
───────────────── ────────────────────────────────────
|
||||
models/ models/ ← _inherit = 'original.model.name'
|
||||
views/ views/ ← inherit_id = ref('original.view.id')
|
||||
static/css/ static/ ← registered via ir.asset record
|
||||
```
|
||||
|
||||
The **golden rule**: never edit the original module. All changes live in your custom module.
|
||||
|
||||
---
|
||||
|
||||
## 4. Inheriting a Model (Python)
|
||||
|
||||
### Basic pattern
|
||||
|
||||
```python
|
||||
# models/my_model.py
|
||||
from odoo import api, fields, models
|
||||
|
||||
class MyCustomExtension(models.Model):
|
||||
_inherit = 'original.model.name' # ← this is the key line
|
||||
|
||||
# Add new fields
|
||||
my_new_field = fields.Char(string='My Field')
|
||||
|
||||
# Override an existing field (change labels, default, etc.)
|
||||
state = fields.Selection(
|
||||
selection=[('draft', 'New Label'), ('done', 'Finished')],
|
||||
# Only list what you want to CHANGE. Odoo merges the rest.
|
||||
)
|
||||
|
||||
# Override an existing method
|
||||
def action_confirm(self):
|
||||
# Do something before
|
||||
result = super().action_confirm() # ← always call super()
|
||||
# Do something after
|
||||
return result
|
||||
```
|
||||
|
||||
### How we used it — `document_package.py`
|
||||
|
||||
```python
|
||||
class DocApprovalDocumentPackageCustom(models.Model):
|
||||
_inherit = 'xf.doc.approval.document.package'
|
||||
|
||||
# 1. New fields added to existing model
|
||||
document_type = fields.Selection(...)
|
||||
doc_number = fields.Char(default='New')
|
||||
qr_code = fields.Image(max_width=0, max_height=0)
|
||||
|
||||
# 2. Override existing field labels only
|
||||
state = fields.Selection(
|
||||
selection=[('draft', 'បង្កើតលិខិតឯកសារ'), ...]
|
||||
)
|
||||
|
||||
# 3. Override existing field type (Text → Html)
|
||||
description = fields.Html(translate=True, sanitize=False)
|
||||
|
||||
# 4. Override a method — inject QR generation after approval
|
||||
def action_finish_approval(self):
|
||||
res = super().action_finish_approval() # run original logic first
|
||||
approved = self.filtered(lambda r: r.state == 'approved')
|
||||
approved._generate_qr_code()
|
||||
return res
|
||||
```
|
||||
|
||||
### Field types reference
|
||||
|
||||
| Field | Use case |
|
||||
|-------|----------|
|
||||
| `fields.Char` | Short text (one line) |
|
||||
| `fields.Text` | Long plain text |
|
||||
| `fields.Html` | Rich text with editor toolbar |
|
||||
| `fields.Integer` | Whole number |
|
||||
| `fields.Float` | Decimal number |
|
||||
| `fields.Boolean` | True/False checkbox |
|
||||
| `fields.Date` | Date only |
|
||||
| `fields.Datetime` | Date + time |
|
||||
| `fields.Selection` | Dropdown list |
|
||||
| `fields.Many2one` | Link to one other record |
|
||||
| `fields.One2many` | List of related records |
|
||||
| `fields.Many2many` | Multiple related records |
|
||||
| `fields.Binary` | File / raw bytes |
|
||||
| `fields.Image` | Image file (served via /web/image/) |
|
||||
|
||||
### Important: `__init__.py` must import every model file
|
||||
|
||||
```python
|
||||
# models/__init__.py
|
||||
from . import document_package
|
||||
from . import document_approver
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Inheriting a View (XML)
|
||||
|
||||
### Basic pattern
|
||||
|
||||
```xml
|
||||
<record id="my_custom_view" model="ir.ui.view">
|
||||
<field name="name">my.custom.view.name</field>
|
||||
<field name="model">original.model.name</field>
|
||||
<!-- Point to the original view's XML id -->
|
||||
<field name="inherit_id" ref="original_module.original_view_id"/>
|
||||
<field name="arch" type="xml">
|
||||
|
||||
<!-- Each change is one <xpath> block -->
|
||||
<xpath expr="//SELECTOR" position="POSITION">
|
||||
<!-- your content here -->
|
||||
</xpath>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
### Finding the original view's `ref`
|
||||
|
||||
1. Go to **Settings → Technical → Views**
|
||||
2. Search for the model name
|
||||
3. The **External ID** column shows the ref, e.g. `xf_doc_approval.xf_doc_approval_document_package_form`
|
||||
|
||||
### xpath `expr` — how to select elements
|
||||
|
||||
| Selector | Example | Selects |
|
||||
|----------|---------|---------|
|
||||
| By element name | `//form` | The `<form>` element |
|
||||
| By name attribute | `//group[@name='approvers']` | `<group name="approvers">` |
|
||||
| By field name | `//field[@name='user_id']` | `<field name="user_id">` |
|
||||
| By button name | `//button[@name='action_confirm']` | `<button name="action_confirm">` |
|
||||
| Nested | `//field[@name='line_ids']//list//field[@name='qty']` | `qty` field inside list inside `line_ids` |
|
||||
|
||||
### xpath `position` — where to insert
|
||||
|
||||
| Position | What it does |
|
||||
|----------|-------------|
|
||||
| `before` | Insert before the selected element |
|
||||
| `after` | Insert after the selected element |
|
||||
| `inside` | Insert as child at the end |
|
||||
| `replace` | Replace the selected element entirely |
|
||||
| `attributes` | Only change attributes of the element |
|
||||
|
||||
### Examples used in this module
|
||||
|
||||
```xml
|
||||
<!-- 1. Change a button label -->
|
||||
<xpath expr="//button[@name='action_send_for_approval']" position="attributes">
|
||||
<attribute name="string">បញ្ជូន</attribute>
|
||||
</xpath>
|
||||
|
||||
<!-- 2. Hide an element -->
|
||||
<xpath expr="//div[@class='oe_title']" position="attributes">
|
||||
<attribute name="invisible">1</attribute>
|
||||
</xpath>
|
||||
|
||||
<!-- 3. Rename a group header -->
|
||||
<xpath expr="//group[@name='approvers']" position="attributes">
|
||||
<attribute name="string">ក. ឋានានុក្រមលំហូរឯកសារ</attribute>
|
||||
</xpath>
|
||||
|
||||
<!-- 4. Add a new element BEFORE another -->
|
||||
<xpath expr="//group[@name='approvers']/field[@name='approver_ids']" position="before">
|
||||
<div class="my-class">
|
||||
<field name="my_new_field" nolabel="1"/>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<!-- 5. Add a column to a list -->
|
||||
<xpath expr="//field[@name='approver_ids']//list//field[@name='notes']" position="after">
|
||||
<field name="sent_date" string="កាលបរិច្ឆេទ"/>
|
||||
</xpath>
|
||||
|
||||
<!-- 6. Replace a field with another -->
|
||||
<xpath expr="//field[@name='approver_ids']//list//field[@name='notes']" position="replace">
|
||||
<field name="notes_display" string="មតិយោបល់" readonly="1"/>
|
||||
</xpath>
|
||||
|
||||
<!-- 7. Add a whole new group after an existing one -->
|
||||
<xpath expr="//group[@name='approvers']" position="after">
|
||||
<group string="ខ. ខ្លឹមសារ" name="section_b">
|
||||
<field name="document_type" nolabel="1"/>
|
||||
</group>
|
||||
</xpath>
|
||||
```
|
||||
|
||||
### Conditional visibility
|
||||
|
||||
```xml
|
||||
<!-- hide when state is not draft -->
|
||||
<field name="my_field" readonly="state != 'draft'"/>
|
||||
|
||||
<!-- hide column in list -->
|
||||
<field name="method" column_invisible="True"/>
|
||||
|
||||
<!-- hide entire group conditionally -->
|
||||
<group invisible="state != 'approved' or not qr_code">
|
||||
...
|
||||
</group>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Inheriting CSS / Styles
|
||||
|
||||
### Step 1 — Create the CSS file
|
||||
|
||||
```
|
||||
static/src/css/navbar.css
|
||||
```
|
||||
|
||||
### Step 2 — Register it in `assets.xml`
|
||||
|
||||
```xml
|
||||
<record id="my_module_css" model="ir.asset">
|
||||
<field name="bundle">web.assets_backend</field>
|
||||
<field name="path">my_module/static/src/css/navbar.css</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
The bundle `web.assets_backend` means it loads in the Odoo backend (not the website/portal).
|
||||
|
||||
### CSS selectors used in this module
|
||||
|
||||
```css
|
||||
/* Target by field name (most reliable) */
|
||||
[name="approver_ids"] .o_list_table thead th { ... }
|
||||
|
||||
/* Target Odoo's built-in classes */
|
||||
.o_main_navbar { background-color: #0a5e98 !important; }
|
||||
|
||||
/* Target a custom class added via view */
|
||||
.o_xf_section_b { ... }
|
||||
.o_xf_row { ... }
|
||||
.o_xf_cell_label { ... }
|
||||
|
||||
/* Target buttons */
|
||||
button.oe_highlight { background-color: #0a5e98 !important; }
|
||||
```
|
||||
|
||||
### Important CSS rules for Odoo
|
||||
|
||||
```css
|
||||
/* Always use !important to override Odoo's built-in Bootstrap styles */
|
||||
.my-class { color: red !important; }
|
||||
|
||||
/* Use specific font list — Khmer font as FALLBACK, not first */
|
||||
body, p, span, td {
|
||||
font-family: 'Segoe UI', Roboto, Arial, 'Battambang', sans-serif;
|
||||
}
|
||||
/* If you put Battambang first, ALL text (including icons) uses Khmer font */
|
||||
|
||||
/* Hide checkbox column in list without breaking layout */
|
||||
[name="approver_ids"] .o_list_record_selector {
|
||||
width: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Inheriting Fonts (Google Fonts)
|
||||
|
||||
CSS `@import url(...)` does **not** work inside Odoo's asset bundle (Docker blocks external HTTP at compile time). Instead, inject a `<link>` tag directly into the HTML `<head>` by inheriting `web.layout`:
|
||||
|
||||
```xml
|
||||
<!-- In assets.xml -->
|
||||
<template id="my_font_template" inherit_id="web.layout" name="My Font">
|
||||
<xpath expr="//head" position="inside">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com"/>
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin=""/>
|
||||
<link rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Battambang:wght@400;700&display=swap"/>
|
||||
</xpath>
|
||||
</template>
|
||||
```
|
||||
|
||||
Note: `&` must be used instead of `&` inside XML attribute values.
|
||||
|
||||
---
|
||||
|
||||
## 8. Key Techniques Used in This Module
|
||||
|
||||
### 8.1 Auto document numbering (ir.sequence)
|
||||
|
||||
```xml
|
||||
<!-- data/sequences.xml -->
|
||||
<record id="seq_xf_doc_approval_document_package" model="ir.sequence">
|
||||
<field name="code">xf.doc.approval.document.package</field>
|
||||
<field name="prefix">DOC/%(year)s/%(month)s/</field>
|
||||
<field name="padding">4</field>
|
||||
</record>
|
||||
```
|
||||
|
||||
```python
|
||||
# In model method:
|
||||
record.doc_number = self.env['ir.sequence'].next_by_code(
|
||||
'xf.doc.approval.document.package'
|
||||
) or 'New'
|
||||
# Result: DOC/2026/04/0001
|
||||
```
|
||||
|
||||
### 8.2 Override a method and call super()
|
||||
|
||||
Always call `super()` unless you deliberately want to block the original behavior.
|
||||
|
||||
```python
|
||||
def action_send_for_approval(self):
|
||||
# Run YOUR code first
|
||||
for record in self:
|
||||
if not record.doc_number or record.doc_number == 'New':
|
||||
record.doc_number = self.env['ir.sequence'].next_by_code(...)
|
||||
# Then run the ORIGINAL method
|
||||
return super().action_send_for_approval()
|
||||
```
|
||||
|
||||
### 8.3 Computed field (non-stored, user-dependent)
|
||||
|
||||
Used for `notes_display` — each user sees different notes based on their step:
|
||||
|
||||
```python
|
||||
notes_display = fields.Text(
|
||||
compute='_compute_notes_display',
|
||||
# No store=True → recomputes every time, never saved to DB
|
||||
)
|
||||
|
||||
@api.depends('notes', 'step', 'document_package_id.approver_ids.step')
|
||||
def _compute_notes_display(self):
|
||||
current_uid = self.env.uid
|
||||
for record in self:
|
||||
my_step = max(
|
||||
record.document_package_id.approver_ids
|
||||
.filtered(lambda a: a.user_id.id == current_uid)
|
||||
.mapped('step') or ['00']
|
||||
)
|
||||
record.notes_display = record.notes if record.step <= my_step else False
|
||||
```
|
||||
|
||||
### 8.4 QR Code generation
|
||||
|
||||
```python
|
||||
# fields.Image serves via /web/image/ — do NOT use fields.Binary with attachment=True
|
||||
qr_code = fields.Image(max_width=0, max_height=0)
|
||||
|
||||
def _generate_qr_code(self):
|
||||
import qrcode, base64, io
|
||||
for record in self:
|
||||
qr = qrcode.QRCode(version=None,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=15, border=4)
|
||||
qr.add_data(record.doc_number) # Keep content SHORT and ASCII-only
|
||||
qr.make(fit=True)
|
||||
img = qr.make_image(fill_color='black', back_color='white')
|
||||
buf = io.BytesIO()
|
||||
img.save(buf, format='PNG')
|
||||
record.write({'qr_code': base64.b64encode(buf.getvalue())})
|
||||
|
||||
# Trigger after approval
|
||||
def action_finish_approval(self):
|
||||
res = super().action_finish_approval()
|
||||
self.filtered(lambda r: r.state == 'approved')._generate_qr_code()
|
||||
return res
|
||||
```
|
||||
|
||||
**Key lesson**: Keep QR content to ASCII-only short text.
|
||||
Khmer Unicode text forces QR version 17+ (810×810 px) → unreadable when scaled down.
|
||||
|
||||
### 8.5 Override field type (Text → Html)
|
||||
|
||||
The base model had `description = fields.Text(...)`.
|
||||
We override it to get a rich-text editor:
|
||||
|
||||
```python
|
||||
# Custom module — overrides the type for this model
|
||||
description = fields.Html(translate=True, sanitize=False)
|
||||
```
|
||||
|
||||
`sanitize=False` is needed to allow file attachments embedded by the HTML editor.
|
||||
|
||||
### 8.6 One2many field in view — show only file column
|
||||
|
||||
```xml
|
||||
<field name="document_ids" nolabel="1" readonly="state != 'draft'">
|
||||
<list editable="bottom">
|
||||
<field name="name" column_invisible="True"/> <!-- hidden but still present -->
|
||||
<field name="file" widget="binary" filename="file_name" string="ឯកសារ"/>
|
||||
<field name="file_name" column_invisible="True"/>
|
||||
</list>
|
||||
</field>
|
||||
```
|
||||
|
||||
`column_invisible` hides the column in list view but keeps the field in the record.
|
||||
|
||||
### 8.7 Radio widget inline (horizontal)
|
||||
|
||||
```xml
|
||||
<field name="document_source" widget="radio" nolabel="1" readonly="state != 'draft'"/>
|
||||
```
|
||||
|
||||
```css
|
||||
/* Force horizontal layout */
|
||||
[name="document_source"] .o_field_radio {
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
gap: 16px !important;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Docker Commands
|
||||
|
||||
### Start the containers
|
||||
```bash
|
||||
cd d:/project_v19s/odoo19-dev
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Stop the containers
|
||||
```bash
|
||||
cd d:/project_v19s/odoo19-dev
|
||||
docker compose down
|
||||
```
|
||||
|
||||
### Update the custom module (after code/view changes)
|
||||
```bash
|
||||
docker exec odoo19_app odoo -u xf_doc_approval_custom -d demo_db --stop-after-init
|
||||
docker restart odoo19_app
|
||||
```
|
||||
|
||||
### First-time install of a new module
|
||||
```bash
|
||||
docker exec odoo19_app odoo -i xf_doc_approval_custom -d demo_db --stop-after-init
|
||||
docker restart odoo19_app
|
||||
```
|
||||
|
||||
### Open Python shell (for debugging / manual fixes)
|
||||
```bash
|
||||
docker exec -i odoo19_app odoo shell -d demo_db --no-http
|
||||
```
|
||||
Inside the shell:
|
||||
```python
|
||||
# Browse a record
|
||||
r = env['xf.doc.approval.document.package'].browse(4)
|
||||
print(r.state, r.doc_number)
|
||||
|
||||
# Run a method manually
|
||||
r._generate_qr_code()
|
||||
env.cr.commit() # save changes
|
||||
```
|
||||
|
||||
### View live logs
|
||||
```bash
|
||||
docker logs odoo19_app -f
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Rules — What NOT to Touch
|
||||
|
||||
| Rule | Reason |
|
||||
|------|--------|
|
||||
| Never edit `d:\project_v19s\xf_doc_approval\` | It is the original base module. Any change there will be overwritten by updates and breaks the separation of concerns. |
|
||||
| Never use `@import url()` in CSS files | Odoo's Docker environment cannot fetch external URLs at CSS compile time. Use `web.layout` template injection instead. |
|
||||
| Never put `font-family: Battambang` as the FIRST font | It will replace Font Awesome icons with broken characters. Always put it as a fallback. |
|
||||
| Never use `fields.Binary(attachment=True)` for images shown with `widget="image"` | The `/web/image/` controller does not serve `attachment=True` Binary fields for custom models. Use `fields.Image` instead. |
|
||||
| Never put `<field>` inside a plain `<div>` when replacing the entire `<sheet>` | OWL (Odoo 19's frontend framework) requires fields to be inside proper Odoo form elements. Use targeted `<xpath>` instead of full sheet replacement. |
|
||||
| Always call `super()` when overriding methods | Without `super()`, the original module's logic is skipped entirely (approvals won't work, emails won't send, etc.). |
|
||||
|
||||
---
|
||||
|
||||
## Quick Inheritance Checklist
|
||||
|
||||
When you want to add something new:
|
||||
|
||||
```
|
||||
[ ] 1. Find the model name in the original module (grep for _name = '...')
|
||||
[ ] 2. Find the view XML ID in Settings → Technical → Views
|
||||
[ ] 3. Add _inherit = 'model.name' in a new Python file under models/
|
||||
[ ] 4. Import the new file in models/__init__.py
|
||||
[ ] 5. Add inherit_id = ref('module.view_id') in your XML view file
|
||||
[ ] 6. Write xpath to target the exact element you want to change
|
||||
[ ] 7. Add CSS in static/src/css/ and register it in assets.xml
|
||||
[ ] 8. Run: docker exec odoo19_app odoo -u your_module -d demo_db --stop-after-init
|
||||
[ ] 9. Run: docker restart odoo19_app
|
||||
[ ] 10. Hard refresh browser (Ctrl+Shift+R)
|
||||
```
|
||||
Reference in New Issue
Block a user