diff --git a/awesome_estate/__init__.py b/awesome_estate/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/awesome_estate/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/awesome_estate/__manifest__.py b/awesome_estate/__manifest__.py new file mode 100644 index 00000000000..ccc3378dcc6 --- /dev/null +++ b/awesome_estate/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Awesome Estate', + 'version': '0.1', + 'category': 'Tutorials', + 'summary': 'Real Estate Advertisement tutorial module (empty shell)', + 'author': 'Patja', + 'license': 'LGPL-3', + 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + 'views/awesome_estate_property_views.xml', + 'views/awesome_estate_property_type_views.xml', + ], + 'application': True, + 'installable': True, +} diff --git a/awesome_estate/docs/chapter_2.md b/awesome_estate/docs/chapter_2.md new file mode 100644 index 00000000000..622ee340b0c --- /dev/null +++ b/awesome_estate/docs/chapter_2.md @@ -0,0 +1,25 @@ +# Chapter 2 + +## Module dependency + +`base` + +- My module needs Odoo core to be installed first. + +### Where `base` lives in this repo + +`community/odoo/addons/base/` + +### What `base` provides + +- Fundamental UI framework pieces and security bootstrap. +- Core records like languages, users, partners, currencies, companies, and countries. +- Base security, group, and access basics (Chapter 4). + +### Why my module depends on it + +Without `base`, Odoo is missing the required core models, configuration, and security layer, so my module cannot install safely. + +### Notes + +`application: true` suggests that this is an installable app, and `false` means it is a module. diff --git a/awesome_estate/docs/chapter_3.md b/awesome_estate/docs/chapter_3.md new file mode 100644 index 00000000000..60b4a7d9353 --- /dev/null +++ b/awesome_estate/docs/chapter_3.md @@ -0,0 +1,56 @@ +# How fields are converted to the database + +- `fields.Char` → `varchar` if a size is set, otherwise `text` +- `fields.Text` → `text` +- `fields.Integer` → `int4` (PostgreSQL integer) +- `fields.Float` → `numeric` with precision, or `float8` if no digits are set +- `fields.Boolean` → `bool` +- `fields.Date` → `date` +- `fields.Datetime` → `timestamp` without timezone (UTC) +- `fields.Selection` → `varchar` (stores the internal key string) +- `fields.Many2one` → `int4` (foreign key) +- `fields.Binary` → `bytea` if not attachment-backed, otherwise stored in `ir.attachment` +- `fields.Html` → `text` +- `fields.Monetary` → `numeric` linked to a currency + +--- + +## Blueprint, methods, and required fields + +- `class` = blueprint +- `methods` = functions +- `required=True` translates to `NOT NULL` in SQL + +--- + +## Module namespace vs business concept + +- `awesome_estate` is the module namespace prefix +- `property` is the business concept inside that module + +So the technical model name becomes `awesome_estate.property`. + +--- + +## Selection: key vs label + +- **Key** / internal value stored in the database + - `"north"`, `"south"`, `"east"`, `"west"` + +- **Label** / display value shown in the UI + - `"North"`, `"South"`, `"East"`, `"West"` + +--- + +## Chapter 3 verification + +### 1) Upgrade or install the module +`/home/odoo/odoo19/community/odoo-bin -d patja --addons-path=community/addons,enterprise,tutorials -u awesome_estate --stop-after-init` + +### 2) Check the table and columns +`psql -d patja -c "\pset pager off" -c "\d awesome_estate_property"` + +### 3) Check `required=True` becomes `NOT NULL` +`psql -d patja -c "\pset pager off" -c "SELECT column_name, is_nullable FROM information_schema.columns WHERE table_name='awesome_estate_property' AND column_name IN ('name', 'expected_price');"` + +You should see `is_nullable = NO` for `name` and `expected_price`. diff --git a/awesome_estate/docs/chapter_4.md b/awesome_estate/docs/chapter_4.md new file mode 100644 index 00000000000..13ac474398c --- /dev/null +++ b/awesome_estate/docs/chapter_4.md @@ -0,0 +1,103 @@ +# Security + +- module: `awesome_estate` +- model: `awesome_estate.property` +- ACL file: `tutorials/awesome_estate/security/ir.model.access.csv` +- manifest entry: `data: ['security/ir.model.access.csv']` + +If a model has no access rights, Odoo treats it as inaccessible and prints a warning in the logs. + +--- + +1. **Access rights (ACLs)** + Model-level permissions: + - read + - write + - create + - unlink + +2. **Groups** + ACLs are assigned to a group like `base.group_user`. + + Common groups: + - `base.group_user` — internal backend users who can log into the Odoo backend + - `base.group_portal` — portal users who usually access the frontend and their own documents only + - `base.group_public` — public/anonymous users who are not logged in + + Difference: + - internal users work in the backend and can use normal business screens + - portal users have limited frontend access + - public users have the least access and are typically anonymous visitors + +3. **Record rules** + Used later to limit which records a group can see or edit. + +For Chapter 4, the important part is ACLs. + +--- + +## ACL file format + +File: `tutorials/awesome_estate/security/ir.model.access.csv` + +```csv +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_awesome_estate_property,access_awesome_estate_property,model_awesome_estate_property,base.group_user,1,1,1,1 +``` + +### What each part means + +- `id` + External ID of the access rule record. + +- `name` + Human-readable name. + +- `model_id:id` + Model the rule applies to. + For `awesome_estate.property`, the value is: + - `model_awesome_estate_property` + +- `group_id:id` + Group that gets the permissions. + Here: + - `base.group_user` + +- `perm_read` + Can read records. + +- `perm_write` + Can edit records. + +- `perm_create` + Can create records. + +- `perm_unlink` + Can delete records. + In Odoo, `unlink` means delete. + +### What this row gives + +This row gives internal users in `base.group_user` full access to the model: + +- read = 1 +- write = 1 +- create = 1 +- unlink = 1 + +--- + +## Manifest wiring + +File: `tutorials/awesome_estate/__manifest__.py` + +```python +'data': ['security/ir.model.access.csv'], +``` + +Why this matters: + +- Odoo only loads security data files if they are declared in the manifest. +- The file is loaded when the module is installed or upgraded. + +--- diff --git a/awesome_estate/docs/chapter_5.md b/awesome_estate/docs/chapter_5.md new file mode 100644 index 00000000000..64a9ef48156 --- /dev/null +++ b/awesome_estate/docs/chapter_5.md @@ -0,0 +1,48 @@ +# Chapter 5 - First UI + +## Action and Menus +- **Action (`ir.actions.act_window`)**: Connects a model to the UI, specifying view modes like `list,form`. +- **Menu Hierarchy**: 3 levels deep: Root Menu -> First Level Menu -> Action Menu. +- **Manifest Order**: XML files containing these UI definitions must be added to `__manifest__.py` under `data`. Data is loaded sequentially! + +## Field Attributes +- `required=True`: Field cannot be empty. Translates to `NOT NULL` in the DB. +- `copy=False`: Prevents the field value from duplicating when a user clicks the "Duplicate" action on a record. Used for unique or situational data like `date_availability` or `selling_price`. +- `readonly=True`: Makes the field uneditable from the UI. E.g., `selling_price` updates programmatically when an offer is accepted, not by manual entry. + +## Default Values +- Pre-populates a field logically when "New" is clicked. +- Can be a literal (`default=2`) or evaluated via an anonymous function. +- **Why use `lambda self:` for logic?**: If you say `default=date.today()`, Python computes it *once* when the Odoo server boots. Using `lambda self: date.today() + relativedelta(months=3)` evaluates dynamically at the *exact moment* the record is created. + +## Reserved Fields +- **`active`**: Special boolean field. If `False`, the record is "Archived" and automatically hidden from standard searches (without deleting DB row). +- **`state`**: Selection field commonly used to drive business flow (e.g., New -> Offer Received -> Sold). + +## Python / Odoo Conventions +- **String quotes (`''` vs `""`)**: Mechanically identical in Python. By Odoo / PEP 8 convention, use single quotes `''` for internal strings (keys, backend values) and double quotes `""` for UI text or docstrings. + +## Selection Fields +Are lists of tuples acting as Key/Value pairs: `('north', 'North')` +- **Key (`'north'`)**: Backend identifier. Lower-case, internal logic, stored in DB. +- **Label (`'North'`)**: UI string. Shown to the user, can be translated easily. + +## Date Imports +- `datetime.date`: Native module for server calendar dates (`date.today()`). +- `dateutil.relativedelta`: Robust utility that cleanly handles calendar leaps when calculating logic like `months=3`. Other periods supported: `years`, `months`, `weeks`, `days`, `hours`. + +## Implementation Proof +All rules required by the Chapter 5 tutorial (readonly/copy overrides, dynamic default date, correctly formatted status options, active field implementation) have been applied exactly to specification in `awesome_estate_property.py`. + +## Developer Setup Notes (`--dev`) +When executing and testing UI/view creations regularly, use the backend server command flag `--dev=all`. It auto-reloads your codebase so you bypass server restarts. +```bash +./odoo-bin -d patja -u awesome_estate --dev=all +``` +**Common `--dev=` parameters:** +- `all`: Enables all developer configurations below. +- `reload`: Automatically bounces the python worker when Python code changes are detected. +- `qweb`: Forces QWeb templates/XML to read directly from disks instead of reading from the database caching engine. Highly recommended when editing views! +- `werkzeug`: Routes exceptions natively to the debug interactive debugger. +- `xml`: Validates XML files are structurally whole before trying to push them to PostgreSQL. + diff --git a/awesome_estate/docs/chapter_6.md b/awesome_estate/docs/chapter_6.md new file mode 100644 index 00000000000..aa8a9a5f8a9 --- /dev/null +++ b/awesome_estate/docs/chapter_6.md @@ -0,0 +1,116 @@ +# Chapter 6 - Basic Views + +- Model file: `awesome_estate_property.py` +- View file: `awesome_estate_property_views.xml` +- Manifest file: `__manifest__.py` + +The manifest loads the security file first and the views file after it. + +--- + +## Odoo XML basics + +- ``: root tag of the XML file +- ``: creates a database record +- ``: sets a value on that record +- `model="ir.ui.view"`: this record is a view +- `arch`: XML layout of the view +- `type="xml"`: tells Odoo that `arch` is XML text + +### View tags + +- ``: list view +- `
`: form view +- ``: search view +- ``: main form area +- ``: field grouping +- ``: tab container +- ``: one tab inside a notebook + +--- + +## Actions and menus + +### Action +- XML ID: `awesome_estate_property_action` +- Model opened: `awesome.estate.property` +- View mode: `list,form` + +An action tells Odoo which model to open and which views to use. + +### Menus +Menu path: + +- `Real Estate` + - `Properties` + - `Properties` + +Menu IDs: + +- `awesome_estate_root_menu` +- `awesome_estate_first_level_menu` +- `awesome_estate_property_menu` + +The menu IDs are the technical names Odoo uses in XML. + +### Database record example + +| Record type | Example | What it does | +| --- | --- | --- | +| `ir.actions.act_window` | `awesome_estate_property_action` | Opens the property model | +| `ir.ui.menu` | `awesome_estate_property_menu` | Adds the menu entry | +| `ir.ui.view` | property list/form/search views | Defines the screen layout | + +--- + +## `ir.ui.view` + +`ir.ui.view` is the Odoo model that stores view definitions in the database. + +### `arch` +`arch` is the actual XML layout stored inside the view record. + +### Example +A view record like: + +```xml + +``` + +means: +- create a database record +- store it as a view +- give it an XML ID so other XML can reference it + +--- + +## Inheritance + +If we want to change an existing view, we use inheritance. + +- `inherit_id`: points to the original view +- `xpath`: updates part of the XML without replacing the full view + +That is how Odoo extends views cleanly. + +--- + +## What I learned + +- Python defines the model +- XML defines the UI +- `ir.ui.view` stores the UI in the database +- `arch` is the XML layout +- actions open models +- menus make the model reachable +- XML IDs connect records together +- inheritance lets us modify an existing view instead of rewriting it + +--- + +## Quick verify + +```bash +community/odoo-bin -d patja --addons-path=community/addons,enterprise,tutorials -u awesome_estate --dev xml +``` + diff --git a/awesome_estate/docs/chapter_7.md b/awesome_estate/docs/chapter_7.md new file mode 100644 index 00000000000..d4c7c1864fb --- /dev/null +++ b/awesome_estate/docs/chapter_7.md @@ -0,0 +1,44 @@ +# Chapter 7 - Relations Between Models + +- Created `awesome.estate.property.type` +- Added the property type list/form/action/menu +- Imported the new model in `models/__init__.py` +- Loaded the new XML in `__manifest__.py` +- Added ACL access for the new model in `security/ir.model.access.csv` +- Added `property_type_id` on `awesome.estate.property` + + +- `Many2one` means "one thing points to one other thing" +- A property can have one type +- Many properties can share the same type + +Example: +- Property: `Villa 12` +- Property type: `Villa` + +So `property_type_id` is like a pointer from a property to its type. + +# Notes + +- `Many2one` links one record to one record +- `property_type_id` stores the chosen type for each property +- In the UI, you can pick the type from a dropdown +- Odoo saves that choice in the database, so the property remembers its type + +### Database record example + +The property type model becomes a database table record in Odoo, for example: + +- model: `awesome.estate.property.type` +- database row fields: `name = "House"` or `name = "Apartment"` + +The list/form/action/menu XML also becomes database records in: + +- `ir.ui.view` +- `ir.actions.act_window` +- `ir.ui.menu` + +### Why the new field matters + +When you add `property_type_id` to `awesome.estate.property`, each property can point to one of those property type records. + diff --git a/awesome_estate/docs/initial.md b/awesome_estate/docs/initial.md new file mode 100644 index 00000000000..d20f68537eb --- /dev/null +++ b/awesome_estate/docs/initial.md @@ -0,0 +1,45 @@ +# Notes + +## Start Odoo command + +`odoo-bin -d --addons-path=` + +### Breakdown + +- `odoo-bin` starts the Odoo server. +- `-d ` selects which PostgreSQL database to use. +- `--addons-path=` is a comma-separated list of addon folders that Odoo scans. + +### It does + +- Loads already-installed modules. +- Starts the UI and backend services. + +## Upgrade a module + +### Command + +`odoo-bin -d -u --addons-path=` + +### Meaning + +`-u ` reloads the module and applies its model and data changes. + +### It does + +- After changing Python models (ORM), upgrade the module so database schema changes happen. +- After adding security or ACLs, upgrade the module so access rules apply. + +### For me + +`odoo-bin --addons-path=addons,../enterprise/,../tutorials/ -d patja -u awesome_estate` + +## Install a module for the first time + +### Command + +`odoo-bin -d -i --addons-path=` + +### Meaning + +`-i ` installs the module for the first time in that database. diff --git a/awesome_estate/models/__init__.py b/awesome_estate/models/__init__.py new file mode 100644 index 00000000000..8534c314422 --- /dev/null +++ b/awesome_estate/models/__init__.py @@ -0,0 +1,2 @@ +from . import awesome_estate_property +from . import awesome_estate_property_type diff --git a/awesome_estate/models/awesome_estate_property.py b/awesome_estate/models/awesome_estate_property.py new file mode 100644 index 00000000000..ce2640afd4c --- /dev/null +++ b/awesome_estate/models/awesome_estate_property.py @@ -0,0 +1,48 @@ +from datetime import date + +from dateutil.relativedelta import relativedelta + +from odoo import fields, models + + +class AwesomeEstateProperty(models.Model): + _name = 'awesome.estate.property' + _description = "Real Estate Property" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + date_availability = fields.Date( + copy=False, + default=lambda self: date.today() + relativedelta(months=3), + ) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(default=2) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + property_type_id = fields.Many2one('awesome.estate.property.type') + active = fields.Boolean(default=True) + state = fields.Selection( + [ + ('new', "New"), + ('offer_received', "Offer Received"), + ('offer_accepted', "Offer Accepted"), + ('sold', "Sold"), + ('cancelled', "Cancelled"), + ], + required=True, + copy=False, + default='new', + ) + garden_orientation = fields.Selection( + [ + ('north', "North"), + ('south', "South"), + ('east', "East"), + ('west', "West"), + ] + ) diff --git a/awesome_estate/models/awesome_estate_property_type.py b/awesome_estate/models/awesome_estate_property_type.py new file mode 100644 index 00000000000..b26738525ed --- /dev/null +++ b/awesome_estate/models/awesome_estate_property_type.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class AwesomeEstatePropertyType(models.Model): + _name = 'awesome.estate.property.type' + _description = "Real Estate Property Type" + + name = fields.Char(required=True) diff --git a/awesome_estate/security/ir.model.access.csv b/awesome_estate/security/ir.model.access.csv new file mode 100644 index 00000000000..7fd4614fba2 --- /dev/null +++ b/awesome_estate/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_awesome_estate_property,access_awesome_estate_property,model_awesome_estate_property,base.group_user,1,1,1,1 +access_awesome_estate_property_type,access_awesome_estate_property_type,model_awesome_estate_property_type,base.group_user,1,1,1,1 diff --git a/awesome_estate/views/awesome_estate_property_type_views.xml b/awesome_estate/views/awesome_estate_property_type_views.xml new file mode 100644 index 00000000000..2cbdcafc5d2 --- /dev/null +++ b/awesome_estate/views/awesome_estate_property_type_views.xml @@ -0,0 +1,36 @@ + + + + + awesome.estate.property.type.list + awesome.estate.property.type + + + + + + + + + awesome.estate.property.type.form + awesome.estate.property.type + + + + + + + + + + + + + Property Types + awesome.estate.property.type + list,form + + + + + diff --git a/awesome_estate/views/awesome_estate_property_views.xml b/awesome_estate/views/awesome_estate_property_views.xml new file mode 100644 index 00000000000..578d86f2871 --- /dev/null +++ b/awesome_estate/views/awesome_estate_property_views.xml @@ -0,0 +1,99 @@ + + + + + + awesome.estate.property.list + awesome.estate.property + + + + + + + + + + + + + + + + + + + awesome.estate.property.form + awesome.estate.property + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + + awesome.estate.property.search + awesome.estate.property + + + + + + + + + + + + + + + + + + + + Properties + awesome.estate.property + list,form + + + + + + + +