diff --git a/estate/__init__.py b/estate/__init__.py
new file mode 100644
index 00000000000..8f5ced4b094
--- /dev/null
+++ b/estate/__init__.py
@@ -0,0 +1,2 @@
+# import the model directory
+from . import models # noqa: F401
diff --git a/estate/__manifest__.py b/estate/__manifest__.py
new file mode 100644
index 00000000000..f1581e314e1
--- /dev/null
+++ b/estate/__manifest__.py
@@ -0,0 +1,22 @@
+{
+ "name": "Real Estate Advertisement ",
+ "version": "1.0",
+ "depends": ["base"],
+ "website": "https://www.odoo.com/app/estate",
+ "summary": "This module is for Real estate advertisement.",
+ "category": "estate",
+ "data": [
+ "security/ir.model.access.csv",
+ "views/estate_property_views.xml",
+ "views/estate_property_type_views.xml",
+ "views/estate_property_tag_views.xml",
+ "views/estate_property_offers_views.xml",
+ "views/estate_menus.xml",
+ "views/res_users_view.xml",
+ "data/estate_demo_data.xml",
+ ],
+ "installable": True,
+ "application": True,
+ "author": "odoo-pupat",
+ "license": "LGPL-3",
+}
diff --git a/estate/data/estate_demo_data.xml b/estate/data/estate_demo_data.xml
new file mode 100644
index 00000000000..5102d4bbe82
--- /dev/null
+++ b/estate/data/estate_demo_data.xml
@@ -0,0 +1,70 @@
+
+
+
+ Home
+
+
+
+ Luxurious
+
+
+
+ Chitrakut Residency
+ 900000
+ 191980
+ Best residency in this city
+
+
+
+
+
+ Villa
+
+
+
+ Cozy
+
+
+
+ Rooftop House
+ 950000
+ 191989
+ It feels like heaven
+
+
+
+
+
+ Penthouse
+
+
+
+ Furnished
+
+
+
+ Swadesh PG
+ 70000
+ 191980
+ Best PG in this city
+
+
+
+
+
+ Palace
+
+
+
+ Precious
+
+
+
+ De glance Palace
+ 1000000
+ 191970
+ Great Indian Palace
+
+
+
+
diff --git a/estate/models/__init__.py b/estate/models/__init__.py
new file mode 100644
index 00000000000..8733276da2f
--- /dev/null
+++ b/estate/models/__init__.py
@@ -0,0 +1,7 @@
+from . import (
+ estate_property, # noqa: F401
+ estate_property_offer, # noqa: F401
+ estate_property_tag, # noqa: F401
+ estate_property_type, # noqa: F401
+ res_users, # noqa: F401
+)
diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py
new file mode 100644
index 00000000000..2723099b206
--- /dev/null
+++ b/estate/models/estate_property.py
@@ -0,0 +1,137 @@
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError, ValidationError
+from odoo.tools import float_compare, float_is_zero
+
+
+class EstateProperty(models.Model):
+ _name = "estate.property"
+ _description = "estate property definition"
+ _order = "id desc"
+
+ name = fields.Char(string="Property Name", required=True)
+ description = fields.Text(string="Description", required=True)
+ postcode = fields.Char(string="Postcode")
+ validity = fields.Integer(default=7)
+ date_deadline = fields.Date()
+ date_availability = fields.Date(
+ string="Available From",
+ default=lambda self: fields.Date.add(fields.Date.today(), months=3),
+ copy=False,
+ )
+ expected_price = fields.Float(string="Expected Price", required=True)
+ selling_price = fields.Float(string="Selling Price", readonly=True, copy=False)
+ available = fields.Char()
+ 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(
+ string="Garden Orientation",
+ selection=[
+ ("north", "North"),
+ ("south", "South"),
+ ("east", "East"),
+ ("west", "West"),
+ ],
+ )
+
+ active = fields.Boolean(default=True)
+ state = fields.Selection(
+ selection=[
+ ("new", "New"),
+ ("offer_received", "Offer Received"),
+ ("offer_accepted", "Offer Accepted"),
+ ("sold", "Sold"),
+ ("cancelled", "Cancelled"),
+ ],
+ required=True,
+ default="new",
+ copy=False,
+ )
+
+ property_type_id = fields.Many2one("estate.property.type", string="Property Type")
+
+ salesman_id = fields.Many2one(
+ "res.users",
+ string="Salesman",
+ default=lambda self: self.env.user,
+ )
+ buyer_id = fields.Many2one("res.partner", string=" Buyer")
+
+ property_offer_ids = fields.One2many("estate.property.offer", "property_id")
+ property_tag_ids = fields.Many2many("estate.property.tag", string="tag")
+ total_area = fields.Float(compute="_compute_total_area", string="Total Area")
+ best_price = fields.Float(
+ string="Best Offer",
+ compute="_compute_best_price",
+ store=True,
+ )
+
+ _check_expected_price = models.Constraint(
+ "CHECK(expected_price > 0)",
+ "Expected price must be positive",
+ )
+
+ @api.depends("garden_area", "living_area")
+ def _compute_total_area(self):
+ for estate in self:
+ estate.total_area = estate.garden_area + estate.living_area
+
+ @api.depends("property_offer_ids.price")
+ def _compute_best_price(self):
+ for record in self:
+ if record.property_offer_ids:
+ record.best_price = max(record.property_offer_ids.mapped("price"))
+ else:
+ record.best_price = 0.0
+
+ @api.constrains("selling_price", "expected_price")
+ def _check_selling_price_validation(self):
+ for record in self:
+ if float_is_zero(record.selling_price, precision_digits=2):
+ continue
+
+ if float_is_zero(record.expected_price, precision_digits=2):
+ continue
+
+ price_limit = record.expected_price * 0.9
+ if (
+ float_compare(record.selling_price, price_limit, precision_digits=2)
+ == -1
+ ):
+ raise ValidationError(
+ _(
+ "Selling price must not be less than 90%% of the expected price.",
+ ),
+ )
+
+ @api.onchange("garden")
+ def _onchange_garden(self):
+ if self.garden:
+ self.garden_area = 10
+ self.garden_orientation = "north"
+ else:
+ self.garden_area = 0
+ self.garden_orientation = ""
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_if_not_allowed(self):
+ for record in self:
+ if record.state not in ("new", "cancelled"):
+ raise UserError(_("User can delete only new or cancelled property"))
+
+ def action_sold(self):
+ for record in self:
+ if record.state == "cancelled":
+ raise UserError(_("property can't be cancelled"))
+ record.state = "sold"
+ return True
+
+ def action_cancel(self):
+ for record in self:
+ if record.state == "sold":
+ raise UserError(_("Cancelled property can't be sold"))
+ record.state = "cancelled"
+ return True
diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..afe92c86d7d
--- /dev/null
+++ b/estate/models/estate_property_offer.py
@@ -0,0 +1,103 @@
+from datetime import timedelta
+
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Estate Property Offer description"
+ _order = "price desc"
+
+ price = fields.Float(string="Price")
+ property_offer_ids = fields.Integer(string="Offer")
+ status = fields.Selection(
+ string="Status",
+ copy=False,
+ selection=[("accepted", "Accepted"), ("refused", "Refused")],
+ )
+ validity = fields.Integer(string="Validity(days)", default=7)
+ date_deadline = fields.Date(
+ compute="_compute_sum_date",
+ inverse="_compute_validity",
+ string="Deadline",
+ )
+
+ partner_id = fields.Many2one("res.partner", required=True, string="Partner")
+ property_id = fields.Many2one("estate.property", required=True)
+ property_type_id = fields.Many2one(
+ "estate.property.type",
+ related="property_id.property_type_id",
+ store=True,
+ readonly=True,
+ )
+
+ @api.depends("validity")
+ def _compute_sum_date(self):
+ for record in self:
+ record.date_deadline = fields.Date.today() + timedelta(days=record.validity)
+
+ _check_price = models.Constraint(
+ "CHECK(price > 0)",
+ "Offer Price field should always be positive",
+ )
+
+ def _compute_validity(self):
+ for record in self:
+ fields.Date.today() == record.date_deadline - timedelta(
+ days=record.validity,
+ )
+
+ @api.onchange("date_deadline")
+ def _onchange_validity(self):
+ if self.date_deadline:
+ create_date = fields.Date.to_date(self.create_date) or fields.Date.today()
+ self.validity = (self.date_deadline - create_date).days
+
+ @api.model
+ def create(self, vals_list):
+
+ for vals in vals_list:
+ property_id = vals.get("property_id")
+ price = vals.get("price")
+
+ if property_id and price:
+ property_rec = self.env["estate.property"].browse(property_id)
+
+ if property_rec.best_price and price <= property_rec.best_price:
+ raise UserError(
+ _("Offer price must be higher than the current best price."),
+ )
+ if property_rec.state == "new":
+ property_rec.state = "offer_received"
+
+ return super().create(vals_list)
+
+ def action_accepted(self):
+ accepted_records = self.search_count(
+ [
+ ("property_id", "=", self.property_id),
+ ("status", "=", "accepted"),
+ ],
+ limit=1,
+ )
+ if accepted_records:
+ raise UserError(_(" multiple offer can't be accepted"))
+
+ self.status = "accepted"
+ self.property_id.selling_price = self.price
+ self.property_id.buyer_id = self.partner_id
+ self.property_id.state = "offer_accepted"
+ other_offers = self.search(
+ [
+ ("property_id", "=", self.property_id),
+ ("status", "!=", "accepted"),
+ ],
+ )
+ other_offers.write({"status": "refused"})
+ return True
+
+ def action_refused(self):
+ for record in self:
+ record.status = "refused"
+ return True
diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..6b09bf79d5b
--- /dev/null
+++ b/estate/models/estate_property_tag.py
@@ -0,0 +1,15 @@
+from odoo import fields, models
+
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _description = "estate property tag"
+ _order = "name"
+
+ name = fields.Char(string="property tag", required=True)
+ color = fields.Integer()
+
+ _check_name_unique = models.Constraint(
+ "unique(name)",
+ "The Property Tag must be unique.",
+ )
diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py
new file mode 100644
index 00000000000..71872bf072c
--- /dev/null
+++ b/estate/models/estate_property_type.py
@@ -0,0 +1,37 @@
+from odoo import api, fields, models
+
+
+class EstatePropertyType(models.Model):
+ _name = "estate.property.type"
+ _description = "estate property type"
+ _order = "sequence,name"
+
+ name = fields.Char(string="Property Category", required=True)
+ offer_count = fields.Integer(compute="_compute_offer_count", string="Offers")
+ property_type_id = fields.Integer()
+ sequence = fields.Integer(
+ "sequence",
+ default=1,
+ help="Used in ordering property,often sold property types are displayed",
+ )
+ property_ids = fields.One2many("estate.property", "property_type_id")
+ property_offer_ids = fields.One2many("estate.property.offer", "property_type_id")
+
+ _check_name_unique = models.Constraint(
+ "unique(name)",
+ "The Property type must be unique.",
+ )
+
+ @api.depends("property_offer_ids")
+ def _compute_offer_count(self):
+ for record in self:
+ record.offer_count = len(record.property_offer_ids)
+
+ def action_view_offers(self):
+ return {
+ "type": "ir.actions.act_window",
+ "name": "Offers",
+ "res_model": "estate.property.offer",
+ "view_mode": "list,form",
+ "domain": [("property_type_id", "=", self.id)],
+ }
diff --git a/estate/models/res_users.py b/estate/models/res_users.py
new file mode 100644
index 00000000000..7a1e696367b
--- /dev/null
+++ b/estate/models/res_users.py
@@ -0,0 +1,11 @@
+from odoo import fields, models
+
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ property_ids = fields.One2many(
+ "estate.property",
+ "salesman_id",
+ string="Available Properties",
+ )
diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..0c0b62b7fee
--- /dev/null
+++ b/estate/security/ir.model.access.csv
@@ -0,0 +1,5 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+estate.access_estate_property,access_estate_property,estate.model_estate_property,base.group_user,1,1,1,1
+estate.access_estate_property_type,access_estate_property_type,estate.model_estate_property_type,base.group_user,1,1,1,1
+estate.access_estate_property_tag,access_estate_property_tag,estate.model_estate_property_tag,base.group_user,1,1,1,1
+estate.access_estate_property_offer,access_estate_property_offer,estate.model_estate_property_offer,base.group_user,1,1,1,1
diff --git a/estate/views/estate_menus.xml b/estate/views/estate_menus.xml
new file mode 100644
index 00000000000..1c32d774ffe
--- /dev/null
+++ b/estate/views/estate_menus.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/estate/views/estate_property_offers_views.xml b/estate/views/estate_property_offers_views.xml
new file mode 100644
index 00000000000..16c4ce521c0
--- /dev/null
+++ b/estate/views/estate_property_offers_views.xml
@@ -0,0 +1,46 @@
+
+
+
+ Estate Property offer
+ estate.property.offer
+ list,form
+ [('property_type_id', '=', active_id)]
+
+
+
+ estate.property.offer.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml
new file mode 100644
index 00000000000..d55a8430e6c
--- /dev/null
+++ b/estate/views/estate_property_tag_views.xml
@@ -0,0 +1,34 @@
+
+
+
+ Estate Property Tag
+ estate.property.tag
+ list,form
+
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
+
+ estate.property.tag.form
+ estate.property.tag
+
+
+
+
+
diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..3c9f7e92215
--- /dev/null
+++ b/estate/views/estate_property_type_views.xml
@@ -0,0 +1,58 @@
+
+
+
+ Estate Property Type
+ estate.property.type
+ list,form
+
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..7b25da3827f
--- /dev/null
+++ b/estate/views/estate_property_views.xml
@@ -0,0 +1,153 @@
+
+
+
+ Estate Property
+ estate.property
+ list,form,kanban
+ {'search_default_availability' : True}
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+ Expected Price :
+
+
+
+
+
+
+
+
+
+
+
+
+ estate_property_search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.view.form
+ estate.property
+
+
+
+
+
diff --git a/estate/views/res_users_view.xml b/estate/views/res_users_view.xml
new file mode 100644
index 00000000000..074c21c0306
--- /dev/null
+++ b/estate/views/res_users_view.xml
@@ -0,0 +1,23 @@
+
+
+
+
+ res.users.form.inherit.estate
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/estate_account/__init__.py b/estate_account/__init__.py
new file mode 100644
index 00000000000..ce9807d14fd
--- /dev/null
+++ b/estate_account/__init__.py
@@ -0,0 +1 @@
+from . import models # noqa: F401
diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py
new file mode 100644
index 00000000000..e1e0a0e7a24
--- /dev/null
+++ b/estate_account/__manifest__.py
@@ -0,0 +1,12 @@
+{
+ "name": "Real estate Account",
+ "version": "1.0",
+ "depends": ["estate", "account"],
+ "website": "https://www.odoo.com/app/estate_account",
+ "summary": "This module is for Real estate invoice creation.",
+ "category": "estate",
+ "data": [],
+ "installable": True,
+ "author": "odoo-pupat",
+ "license": "LGPL-3",
+}
diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py
new file mode 100644
index 00000000000..9d5e62fe812
--- /dev/null
+++ b/estate_account/models/__init__.py
@@ -0,0 +1 @@
+from . import estate_property # noqa: F401
diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py
new file mode 100644
index 00000000000..1d637f4e8f9
--- /dev/null
+++ b/estate_account/models/estate_property.py
@@ -0,0 +1,32 @@
+from odoo import Command, models
+
+
+class EstateProperty(models.Model):
+ _inherit = "estate.property"
+
+ def action_sold(self):
+
+ selling_price = self.selling_price * 0.06
+ self.env["account.move"].create(
+ {
+ "partner_id": self.buyer_id.id,
+ "move_type": "out_invoice",
+ "invoice_line_ids": [
+ Command.create(
+ {
+ "name": "6 % profit",
+ "quantity": 1,
+ "price_unit": selling_price,
+ }
+ ),
+ Command.create(
+ {
+ "name": "Administrative Fees",
+ "quantity": 1,
+ "price_unit": 100.00,
+ }
+ ),
+ ],
+ }
+ )
+ return super().action_sold()
diff --git a/purchase_global_discount/__init__.py b/purchase_global_discount/__init__.py
new file mode 100644
index 00000000000..3f1aab2085f
--- /dev/null
+++ b/purchase_global_discount/__init__.py
@@ -0,0 +1,4 @@
+from . import (
+ models, # noqa : F401
+ wizard, # noqa : F401
+)
diff --git a/purchase_global_discount/__manifest__.py b/purchase_global_discount/__manifest__.py
new file mode 100644
index 00000000000..1762d608944
--- /dev/null
+++ b/purchase_global_discount/__manifest__.py
@@ -0,0 +1,18 @@
+{
+ "name": "purchase_dicount",
+ "description": "Add discount button with its wizard",
+ "author": "odoo-pupat",
+ "website": "https://www.odoo.com/",
+ "category": "Purchase-custom",
+ "version": "0.1",
+ "application": True,
+ "installable": True,
+ "depends": ["purchase"],
+ "data": [
+ "security/ir.model.access.csv",
+ "views/purchase_order_views.xml",
+ "wizard/purchase_order_discount_views.xml",
+ ],
+ "assets": {},
+ "license": "LGPL-3",
+}
diff --git a/purchase_global_discount/models/__init__.py b/purchase_global_discount/models/__init__.py
new file mode 100644
index 00000000000..307a154a11f
--- /dev/null
+++ b/purchase_global_discount/models/__init__.py
@@ -0,0 +1,3 @@
+from . import (
+ purchase_order, # noqa: F401
+)
diff --git a/purchase_global_discount/models/purchase_order.py b/purchase_global_discount/models/purchase_order.py
new file mode 100644
index 00000000000..c7013a035e8
--- /dev/null
+++ b/purchase_global_discount/models/purchase_order.py
@@ -0,0 +1,15 @@
+from odoo import models
+
+
+class InheritedPurchaseOrder(models.Model):
+ _inherit = "purchase.order"
+
+ def action_open_discount_wizard(self):
+ self.ensure_one()
+ return {
+ "name": "Discount",
+ "type": "ir.actions.act_window",
+ "res_model": "purchase.order.discount",
+ "view_mode": "form",
+ "target": "new",
+ }
diff --git a/purchase_global_discount/security/ir.model.access.csv b/purchase_global_discount/security/ir.model.access.csv
new file mode 100644
index 00000000000..f68ace605d0
--- /dev/null
+++ b/purchase_global_discount/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_purchase_discount_wizard,purchase.discount.wizard,model_purchase_order_discount,base.group_user,1,1,1,1
\ No newline at end of file
diff --git a/purchase_global_discount/views/purchase_order_views.xml b/purchase_global_discount/views/purchase_order_views.xml
new file mode 100644
index 00000000000..5352365e4d8
--- /dev/null
+++ b/purchase_global_discount/views/purchase_order_views.xml
@@ -0,0 +1,15 @@
+
+
+
+ purchase.view.form.purchase.discount
+ purchase.order
+
+
+
+
+
+
+
+
+
+
diff --git a/purchase_global_discount/wizard/__init__.py b/purchase_global_discount/wizard/__init__.py
new file mode 100644
index 00000000000..07afb7cc183
--- /dev/null
+++ b/purchase_global_discount/wizard/__init__.py
@@ -0,0 +1 @@
+from . import purchase_order_discount # noqa: F401
diff --git a/purchase_global_discount/wizard/purchase_order_discount.py b/purchase_global_discount/wizard/purchase_order_discount.py
new file mode 100644
index 00000000000..a258d8c8083
--- /dev/null
+++ b/purchase_global_discount/wizard/purchase_order_discount.py
@@ -0,0 +1,50 @@
+from odoo import api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class PurchaseOrderDiscount(models.TransientModel):
+ _name = "purchase.order.discount"
+ _description = "Discount Wizard"
+
+ purchase_order_id = fields.Many2one(
+ "purchase.order",
+ default=lambda self: self.env.context.get("active_id"),
+ required=True,
+ )
+ discount_percentage = fields.Float(string="Percentage")
+ discount_type = fields.Selection(
+ selection=[
+ ("percentage", "%"),
+ ("amount", "$"),
+ ],
+ default="percentage",
+ )
+ percentage = fields.Float(compute="_compute_initial_discount")
+
+ @api.depends("discount_type", "discount_percentage")
+ def _compute_initial_discount(self):
+ if self.discount_type == "amount":
+ if self.purchase_order_id.amount_untaxed == 0:
+ raise ValidationError("No more discount possible")
+ self.percentage = (
+ self.discount_percentage * 100 / self.purchase_order_id.amount_untaxed
+ )
+ else:
+ self.percentage = self.discount_percentage
+
+ def action_apply_discount(self):
+ self.ensure_one()
+ if self.discount_type == "amount":
+ self.purchase_order_id.order_line.write(
+ {
+ "discount": (
+ self.discount_percentage
+ * 100
+ / self.purchase_order_id.amount_untaxed
+ )
+ }
+ )
+ else:
+ self.purchase_order_id.order_line.write(
+ {"discount": self.discount_percentage}
+ )
diff --git a/purchase_global_discount/wizard/purchase_order_discount_views.xml b/purchase_global_discount/wizard/purchase_order_discount_views.xml
new file mode 100644
index 00000000000..f0acb906e5c
--- /dev/null
+++ b/purchase_global_discount/wizard/purchase_order_discount_views.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ purchase.order.discount.form
+ purchase.order.discount
+
+
+
+
+