first push message

This commit is contained in:
2026-07-01 14:41:49 +07:00
parent 6667dec2bf
commit 58b5f46cc4
2951 changed files with 316619 additions and 0 deletions
+562
View File
@@ -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&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)
```