563 lines
18 KiB
Markdown
563 lines
18 KiB
Markdown
|
|
# 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)
|
|||
|
|
```
|