Files
2026-07-01 14:41:49 +07:00

563 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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&amp;display=swap"/>
</xpath>
</template>
```
Note: `&amp;` 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)
```