Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
9db531d
Update folder structure and add Technical Training link for Odoo 16.0
Sep 16, 2025
1eecc7c
Update estate module version to 18.0.1.0.0 and standardize manifest f…
Sep 16, 2025
e85f7f6
Rename TestModel to EstateProperty and add description for clarity
Sep 16, 2025
8baa1ba
Add basic fields to EstateProperty model including title, description…
Sep 16, 2025
da04814
Add security access control for EstateProperty model and update manif…
Sep 16, 2025
ed95dd2
Implement estate property views and update manifest to include new vi…
Sep 16, 2025
eda4739
Add estate menus XML file and update manifest to include it
Sep 16, 2025
bfc946e
Update view mode in estate property action from 'tree' to 'list'
Sep 16, 2025
535dcdb
Enhance EstateProperty model by adding default values for date_availa…
Sep 16, 2025
e405ad1
Expand status options in EstateProperty model by adding 'Offer Receiv…
Sep 16, 2025
dabf46c
Add list view for estate.property model to display key property detai…
Sep 16, 2025
abd796b
Add form view for estate.property model to facilitate detailed proper…
Sep 16, 2025
3741db7
Add search view for estate.property model to enable efficient propert…
Sep 16, 2025
6951850
Add estate property type model and views, including list and form vie…
Sep 16, 2025
5e9f9c1
Add estate property tag model, views, and access control. Update esta…
Sep 16, 2025
34fbb4f
Add estate property offer model, views, and access control. Update es…
Sep 16, 2025
354f489
Change view mode from 'tree' to 'list' for offers in estate property …
Sep 16, 2025
a2cfc40
Update context for offer_ids field in estate property views to use 'i…
Sep 16, 2025
772bca6
Add computed fields for total area and best price in EstateProperty m…
Sep 16, 2025
7fe65e5
Add onchange methods for validity and date_deadline in EstateProperty…
Sep 16, 2025
6c5a82c
Add action methods for accepting and refusing offers in EstatePropert…
Sep 16, 2025
a075b47
gitignore: ignore Markdown files (*.md) and stop tracking existing .m…
Sep 16, 2025
5165c66
Refactor button attributes in estate property views to simplify visib…
Sep 16, 2025
5cbde4f
Enhance button attributes in estate property views by adding icons fo…
Sep 16, 2025
a2e8b86
Update offer buttons in estate property views to include icons and ad…
Sep 16, 2025
e1481aa
Add action buttons for accepting and refusing offers in estate proper…
Sep 16, 2025
8b761a4
Add SQL constraints to ensure positive pricing in EstateProperty, Est…
Sep 16, 2025
b03eac0
Add Python-level validation constraints in EstateProperty model to en…
Sep 16, 2025
ca829f4
Enhance estate models by adding new fields for property type and colo…
Sep 16, 2025
33e4823
Refactor offer field in estate property views to streamline readonly …
Sep 16, 2025
1758280
Add action for estate property offers in views to enhance user naviga…
Sep 16, 2025
6367393
Remove unused action definition for estate property offers to streaml…
Sep 16, 2025
766ff8d
Refactor estate property type model and views by removing unused fiel…
Sep 16, 2025
c842006
Add property relationship to EstatePropertyType model and update view…
Sep 16, 2025
7712ccf
Enhance EstatePropertyOffer and EstateProperty models by adding casca…
Sep 16, 2025
6a43569
Add ordering and sequence fields to EstatePropertyTag and EstatePrope…
Sep 16, 2025
b64b90b
Update estate property type views to enhance list display with condit…
Sep 16, 2025
3bb1083
Update estate property list view to apply conditional decorations for…
Sep 16, 2025
2dba87b
Remove estate property model file and update estate property list vie…
Sep 16, 2025
223e4ee
Update estate property list view to change decoration style for 'offe…
Sep 16, 2025
7ea2418
Update estate property list view to include color options for tags an…
Sep 17, 2025
9638bf4
Update estate property list view to correct decoration attribute for …
Sep 17, 2025
c93bf25
Add logic to mark property as having an offer upon creation of Estate…
Sep 17, 2025
7a0e09a
Update estate property list view to modify button visibility logic ba…
Sep 17, 2025
16ae53a
Add offer management features to EstatePropertyType model and views
Sep 17, 2025
3c2b55b
Refactor estate manifest to remove duplicate offer view entry and mai…
Sep 17, 2025
79ec28e
Fix button context in estate_property_type_views.xml to use 'id' inst…
Sep 17, 2025
292f7c0
Add property management features for users
Sep 17, 2025
db9b969
Add estate accounting integration module
Sep 17, 2025
bbd4ea1
Add kanban view for estate properties and update action view mode
Sep 17, 2025
606b60f
Enhance kanban view for estate properties with additional fields and …
Sep 17, 2025
f7689ed
Refactor estate_property model to enhance validation and action methods
Sep 17, 2025
8a7d7ec
Refactor estate models for improved clarity and functionality
Oct 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ install/win32/meta.py
/man/
/share/
/src/
# Ignore Markdown files
*.md
4 changes: 0 additions & 4 deletions README.md

This file was deleted.

24 changes: 19 additions & 5 deletions estate/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,25 @@
{
"name": "Estate", # The name that will appear in the App list
"version": "16.0.0", # Version
"application": True, # This line says the module is an App, and not a module
"depends": ["base"], # dependencies
"name": "Estate",
"version": "18.0.1.0.0",
"application": True,
"depends": ["base"],
"data": [
# Security files first
"security/ir.model.access.csv",

# Data files in dependency order
# "data/estate_property_type_data.xml", # Base data first
# "data/estate_property_tag_data.xml", # Independent data
# "data/estate_property_offer_data.xml", # Depends on property types

# Views and menus last
"views/estate_property_views.xml",
"views/estate_property_offer_views.xml",
"views/estate_property_type_views.xml",
"views/estate_property_tag_views.xml",
"views/res_users_views.xml",
"views/estate_menus.xml",
],
"installable": True,
'license': 'LGPL-3',
"license": "LGPL-3",
}
Empty file removed estate/models.py
Empty file.
5 changes: 5 additions & 0 deletions estate/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from . import estate_property
from . import estate_property_type
from . import estate_property_tag
from . import estate_property_offer
from . import res_users
180 changes: 180 additions & 0 deletions estate/models/estate_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
from odoo import api, fields, models
from odoo.exceptions import UserError, ValidationError
from odoo.tools.float_utils import float_compare, float_is_zero

class EstateProperty(models.Model):
_name = "estate.property"
_description = "Estate Property"
_order = "id desc"

# Basic fields
name = fields.Char(string="Title", required=True)
description = fields.Text(string="Description")
postcode = fields.Char(string="Postcode")
date_availability = fields.Date(
string="Available From",
copy=False,
default=lambda self: fields.Date.add(fields.Date.today(), months=3),
)
expected_price = fields.Float(string="Expected Price", required=True)
selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
bedrooms = fields.Integer(string="Bedrooms", default=2)
living_area = fields.Integer(string="Living Area (sqm)")
facades = fields.Integer(string="Facades")
garage = fields.Boolean(string="Garage")
garden = fields.Boolean(string="Garden")
garden_area = fields.Integer(string="Garden Area (sqm)")
garden_orientation = fields.Selection(
selection=[
('north', 'North'),
('south', 'South'),
('east', 'East'),
('west', 'West')
],
string="Garden Orientation"
)

# Reserved/common fields
active = fields.Boolean(string="Active", default=True)
state = fields.Selection(
selection=[
("new", "New"),
("offer_received", "Offer Received"),
("offer_accepted", "Offer Accepted"),
("sold", "Sold"),
("cancelled", "Cancelled"),
],
string="Status",
required=True,
copy=False,
default="new",
)

# Relations
property_type_id = fields.Many2one(
comodel_name="estate.property.type",
string="Property Type",
)

# Parties
salesman_id = fields.Many2one(
comodel_name="res.users",
string="Salesman",
default=lambda self: self.env.user,
)
buyer_id = fields.Many2one(
comodel_name="res.partner",
string="Buyer",
copy=False,
)

tag_ids = fields.Many2many(
comodel_name="estate.property.tag",
string="Tags",
)

offer_ids = fields.One2many(
comodel_name="estate.property.offer",
inverse_name="property_id",
string="Offers",
)

# SQL constraints
_sql_constraints = [
(
"expected_price_strictly_positive",
"CHECK(expected_price > 0)",
"The expected price must be strictly positive.",
),
(
"selling_price_positive",
"CHECK(selling_price >= 0)",
"The selling price must be positive.",
),
]

# Computed fields
total_area = fields.Integer(
string="Total Area (sqm)",
compute="_compute_total_area",
store=False,
)
best_price = fields.Float(
string="Best Offer",
compute="_compute_best_price",
store=False,
)

@api.depends("living_area", "garden_area")
def _compute_total_area(self) -> None:
for property in self:
living = property.living_area or 0
garden = property.garden_area or 0
property.total_area = living + garden

@api.depends("offer_ids.price")
def _compute_best_price(self) -> None:
for property in self:
prices = property.offer_ids.mapped("price")
property.best_price = max(prices) if prices else 0.0

@api.onchange("garden")
def _onchange_garden(self) -> None:
if self.garden:
self.garden_area = 10
self.garden_orientation = "north"
else:
self.garden_area = 0
self.garden_orientation = False

# CRUD methods (ORM overrides)
@api.ondelete(at_uninstall=False)
def _unlink_if_allowed(self):
for property in self:
if property.state not in ("new", "cancelled"):
raise UserError("You can only delete properties in New or Cancelled state. Consider archiving instead.")
# clean up children explicitly to avoid FK issues
self.mapped("offer_ids").unlink()

# Python-level validation for clearer errors in the UI
@api.constrains("expected_price")
def _check_expected_price_positive(self):
for property in self:
if property.expected_price is not None and property.expected_price <= 0:
raise ValidationError("The expected price must be strictly positive.")

@api.constrains("selling_price")
def _check_selling_price_non_negative(self):
for property in self:
if property.selling_price is not None and property.selling_price < 0:
raise ValidationError("The selling price must be positive.")

@api.constrains("selling_price", "expected_price")
def _check_selling_price_threshold(self):
precision = 2
for property in self:
# Skip if no selling price yet
if property.selling_price is None or float_is_zero(property.selling_price, precision_digits=precision):
continue
# Require a valid expected price to compare against
if property.expected_price is None or not float_compare(property.expected_price, 0.0, precision_digits=precision) == 1:
# expected price not set/invalid; other constraints will catch it
continue
min_allowed = property.expected_price * 0.9
if float_compare(property.selling_price, min_allowed, precision_digits=precision) == -1:
raise ValidationError("The selling price cannot be lower than 90% of the expected price.")

# Action methods
def action_cancel(self):
self.ensure_one()
if self.state == "sold":
raise UserError("A sold property cannot be cancelled.")
self.state = "cancelled"
return True

def action_set_sold(self):
self.ensure_one()
if self.state == "cancelled":
raise UserError("A cancelled property cannot be sold.")
self.state = "sold"
return True
112 changes: 112 additions & 0 deletions estate/models/estate_property_offer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from odoo import models, fields, api
from odoo.exceptions import UserError
from datetime import date, timedelta


class EstatePropertyOffer(models.Model):
_name = "estate.property.offer"
_description = "Estate Property Offer"
_order = "price desc"

price = fields.Float(string="Price")
status = fields.Selection(
selection=[("accepted", "Accepted"), ("refused", "Refused")],
string="Status",
copy=False,
)
partner_id = fields.Many2one("res.partner", string="Partner", required=True)
property_id = fields.Many2one(
"estate.property",
string="Property",
required=True,
ondelete="cascade",
)
property_type_id = fields.Many2one(
comodel_name="estate.property.type",
string="Property Type",
related="property_id.property_type_id",
store=True,
)

validity = fields.Integer(string="Validity (days)", default=7)
date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline", store=False)

@api.depends("validity", "create_date")
def _compute_date_deadline(self) -> None:
for offer in self:
created = (offer.create_date or fields.Datetime.now())
base_date = created.date()
offer.date_deadline = base_date + timedelta(days=offer.validity or 0)

def _inverse_date_deadline(self) -> None:
for offer in self:
created = (offer.create_date or fields.Datetime.now())
base_date = created.date()
if offer.date_deadline:
delta = offer.date_deadline - base_date
offer.validity = max(delta.days, 0)

# Make the form reactive while editing (no need to wait for save)
@api.onchange("validity")
def _onchange_validity(self):
for offer in self:
created = (offer.create_date or fields.Datetime.now())
base_date = created.date()
offer.date_deadline = base_date + timedelta(days=offer.validity or 0)

@api.onchange("date_deadline")
def _onchange_date_deadline(self):
for offer in self:
if offer.date_deadline:
created = (offer.create_date or fields.Datetime.now())
base_date = created.date()
delta = offer.date_deadline - base_date
offer.validity = max(delta.days, 0)

# Actions on offers
def action_accept(self):
for offer in self:
if offer.property_id.state == "sold":
raise UserError("Cannot accept an offer on a sold property.")
# set other offers to refused
siblings = offer.property_id.offer_ids - offer
siblings.write({"status": "refused"})
offer.status = "accepted"
offer.property_id.write({
"buyer_id": offer.partner_id.id,
"selling_price": offer.price,
"state": "offer_accepted",
})
return True

def action_refuse(self):
self.status = "refused"
return True

# SQL constraints
_sql_constraints = [
(
"offer_price_strictly_positive",
"CHECK(price > 0)",
"The offer price must be strictly positive.",
),
]

# When an offer is created, mark the property as having an offer
@api.model_create_multi
def create(self, vals_list):
# Business rule: price must be strictly higher than any existing offer on the property
props = self.env["estate.property"]
for vals in vals_list:
property_id = vals.get("property_id")
if property_id:
prop = self.env["estate.property"].browse(property_id)
props |= prop
existing_max = max(prop.offer_ids.mapped("price") or [0.0])
if vals.get("price") is not None and vals["price"] <= existing_max:
raise UserError("Offer must be higher than all existing offers for this property.")

records = super().create(vals_list)
# Set property state to Offer Received for affected properties
props.filtered(lambda p: p.state == "new").state = "offer_received"
return records
14 changes: 14 additions & 0 deletions estate/models/estate_property_tag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from odoo import models, fields


class EstatePropertyTag(models.Model):
_name = "estate.property.tag"
_description = "Estate Property Tag"
_order = "name"

name = fields.Char(string="Name", required=True)
color = fields.Integer(string="Color")

_sql_constraints = [
("estate_property_tag_name_uniq", "unique(name)", "Property tag name must be unique."),
]
33 changes: 33 additions & 0 deletions estate/models/estate_property_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from odoo import models, fields


class EstatePropertyType(models.Model):
_name = "estate.property.type"
_description = "Estate Property Type"
_order = "sequence, name"

name = fields.Char(string="Name", required=True)
sequence = fields.Integer(string="Sequence", default=10)

_sql_constraints = [
("estate_property_type_name_uniq", "unique(name)", "Property type name must be unique."),
]

# Inline list: related properties
property_ids = fields.One2many(
comodel_name="estate.property",
inverse_name="property_type_id",
string="Properties",
)

offer_ids = fields.One2many(
comodel_name="estate.property.offer",
inverse_name="property_type_id",
string="Offers",
)
offer_count = fields.Integer(string="Offers", compute="_compute_offer_count")

def _compute_offer_count(self):
for rec in self:
rec.offer_count = len(rec.offer_ids)

Loading