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 + + + + + + + + +
+

+ +

+
+ + + + + + + + + + + + + +
+
+
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 : +
+
+
+ Best Price : +
+
+
+
+ Selling 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 + +
+ + + + + + + +
+
+
+