├── .gitignore ├── CONTRIBUTING.md ├── README.md ├── estate ├── __init__.py ├── __manifest__.py ├── models │ ├── __init__.py │ ├── estate_property.py │ ├── estate_property_offer.py │ ├── estate_property_tag.py │ ├── estate_property_type.py │ └── res_users.py ├── security │ └── ir.model.access.csv └── views │ ├── estate_menus.xml │ ├── estate_property_offer_views.xml │ ├── estate_property_tag_views.xml │ ├── estate_property_type_views.xml │ ├── estate_property_views.xml │ └── res_users_views.xml └── estate_account ├── __init__.py ├── __manifest__.py └── models ├── __init__.py └── estate_property.py /.gitignore: -------------------------------------------------------------------------------- 1 | # sphinx build directories 2 | _build/ 3 | 4 | # dotfiles 5 | .* 6 | !.gitignore 7 | !.mailmap 8 | # compiled python files 9 | *.py[co] 10 | # setup.py egg_info 11 | *.egg-info 12 | # emacs backup files 13 | *~ 14 | # hg stuff 15 | *.orig 16 | status 17 | # odoo filestore 18 | openerp/filestore 19 | # maintenance migration scripts 20 | openerp/addons/base/maintenance 21 | 22 | # generated for windows installer? 23 | install/win32/*.bat 24 | install/win32/meta.py 25 | 26 | # various virtualenv 27 | /bin/ 28 | /build/ 29 | /dist/ 30 | /include/ 31 | /lib/ 32 | /man/ 33 | /share/ 34 | /src/ 35 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Guidelines to write a tutorial 2 | 3 | Here is some useful information you should take into account when writing a tutorial, in particular 4 | the various Advanced topics. 5 | 6 | ## Getting started 7 | 8 | It is always easier for a reader to have a reference starting point at the beginning of a tutorial. 9 | The Core Training tutorial should give a decent starting point for any advanced topic: the module 10 | contains various models, multiple type of fields, views and actions. It will save you and the 11 | reader time by avoiding the boiler plate of re-creating a business scenario from scratch. 12 | 13 | Be clear about the requirements. It might be general knowledge or practical information. Example: 14 | 15 | > This tutorial assumes you followed the Core Training. 16 | > 17 | > To do the exercise, fetch the branch `14.0-core` from the repository XXX. It contains 18 | > a basic module we will use as a starting point. 19 | 20 | There is no magic recipe to write a tutorial, but here is an idea of what could be done: 21 | 22 | 1. Write the Table of Contents: list all the concepts you want to cover. This will help you to know 23 | where to start. Do not try to cover too many concepts: the purpose of a tutorial is to help the 24 | reader getting a first grasp on a topic, not turn him/her into an expert. 25 | 2. Write the solution: before writing any line of text, write the solution. This will give you 26 | the finish line you want to reach. It will also help you to realize if it is possible to 27 | easily cover the various concepts. 28 | 3. Write the tutorial: now you have the starting and finish points, it is time to write the path 29 | to join them. 30 | 31 | In all cases, keep it 'easy': the purpose of a tutorial is to give a limited quantity of 32 | information but be sure that everything is understood. 33 | 34 | ## Business need 35 | 36 | The first step of the tutorial is to explain what is the business need behind the new topic you are 37 | going to introduce. Write a few words to summarize what we did in the Core Training, then 38 | explain what is missing for the business case. It's important for a reader to understand **why** 39 | he/she is doing something. Example: 40 | 41 | > The previous chapter introduced the possibility to add some business logic to our model. 42 | > We can now link buttons to business code. But how can we prevent users from encoding 43 | > incorrect data? For example, in our real estate module, nothing prevents the 44 | > user to set a negative expected price. 45 | 46 | Then, give a few words about the solution Odoo provides for the need. Example: 47 | 48 | > Odoo provides two ways to set up automatically verified invariants Python constraints and 49 | > SQL constraints. 50 | 51 | The reader knows to what business need the topic is useful. 52 | 53 | ## Technical information 54 | 55 | When the context is set, you can introduce the technical solution. Do not hesitate to split it 56 | into relevant sections. In the previous example, the split is obvious: a section is dedicated 57 | to SQL constraints, the other is dedicated to Python constraints. 58 | 59 | Start each section by a clear reference to the documentation linked to the topic. Example: 60 | 61 | > **Reference**: the documentation related to this topic can be found in `XXX` and `YYY`. 62 | 63 | Refrain from 'hiding' links within the text: it makes the reader unaware of what are the important 64 | documents referenced. 65 | 66 | Then, give the goal of the section: what is the reader supposed to achieve at the end of the 67 | section? Example: 68 | 69 | > **Goal**: at the end of this section: 70 | > - Amounts should be (strictly) positive 71 | > - Property types and tags have a unique name 72 | 73 | If possible, add screenshots or short animated gifs for a better visualization of what 74 | is expected. It helps the reader searching by himself for a solution matching the screenshot. 75 | 76 | Now that the reader knows where to find extra documentation and what is the purpose of the section, 77 | give the necessary technical information. Do not hesitate to give code snippets or link 78 | to simple examples in the Odoo codebase. It's not always easy to find a good balance between 79 | the necessary information to solve the exercise and being complete. Once again, it might be 80 | better to give an incomplete but useful information rather than an exhaustive list of all the 81 | options which will be forgotten soon after the training. 82 | 83 | Try to think it this way: what does the reader already know and what else does he/she need to know 84 | to solve the exercise? 85 | 86 | Do not hesitate to split complex sections in various steps. For example, the chapter 87 | 'Interact With Other Modules' splits the invoice creation into 3 exercises: 88 | 89 | - override the necessary method and call `super` 90 | - in the override, create an empty invoice 91 | - add the invoice lines to the invoice creation 92 | 93 | It gives the reader intermediary points where he/she can make sure to be on the right track. 94 | 95 | ## Limitations and pitfalls 96 | 97 | All technical solution has its limitations and pitfalls. It might be a matter of performance 98 | (e.g. computed fields), maintenance (e.g. custom JS widgets), or... anything. 99 | Be more specific about when to use this solution and when not use it. 100 | 101 | ## Test and review your tutorial 102 | 103 | Ask a colleague for a review of your tutorial: something that might be obvious to you 104 | might not be for someone else. 105 | 106 | ## Add your solution to this repository 107 | 108 | Create a specific branch in this repository with the solution. For the Advanced topics, it is 109 | suggested to create a new branch from `14.0-core` and add a single commit with the solution. 110 | Multiple commits might be tempting (e.g. to show the step-by-step solution), but they are way more 111 | complex to maintain. 112 | 113 | In the end, if the exercise was explained clearly, only the final solution should be useful to 114 | the reader. 115 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Odoo Tutorials Solutions 2 | 3 | This repository contains a *suggested* solution of the 4 | [Odoo tutorials](https://www.odoo.com/documentation/). Each tutorial has its own branch with 5 | the following naming convention: `-`. 6 | 7 | Here is the exhaustive list: 8 | 9 | | Training | Branch | 10 | |----------------------------------------------------------|--------------------------------------| 11 | | Core Training | `14.0-core` | 12 | | Advanced A: Internationalization | `14.0-A_i18n` | 13 | | Advanced B: ACL and Record Rules | `14.0-B_acl_irrules` | 14 | | Advanced C: Master and Demo Data | `14.0-C_data` | 15 | | Advanced D: Mixins | `14.0-D_mixins` | 16 | | Advanced E: Python Unit Tests | `14.0-E_unittest` | 17 | | Advanced F: JS Tours | `14.0-F_jstour` | 18 | | Advanced G: Controllers & Website | `14.0-G_website` | 19 | | Advanced H: Advanced Views | `14.0-H_adv_views` | 20 | | Advanced I: Custom JS Widget | `14.0-I_jswidget` | 21 | | Advanced J: PDF Reports | `14.0-J_reports` | 22 | | Advanced K: Dashboards | `14.0-K_dashboard` | 23 | | Advanced L: Scheduled & Server Actions | `14.0-L_cron` | 24 | | Advanced M: Migrations | `14.0-M_migration` | 25 | | Advanced N: Security | `14.0-N_security` | 26 | | Advanced O: Performances | `14.0-O_perf` | 27 | -------------------------------------------------------------------------------- /estate/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import models 4 | -------------------------------------------------------------------------------- /estate/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # More info at https://www.odoo.com/documentation/master/reference/module.html 3 | 4 | { 5 | "name": "Real Estate", 6 | "category": 'Real Estate/Brokerage', 7 | "depends": [ 8 | "base", 9 | "web", 10 | ], 11 | "data": [ 12 | "security/ir.model.access.csv", 13 | "views/estate_property_offer_views.xml", 14 | "views/estate_property_tag_views.xml", 15 | "views/estate_property_type_views.xml", 16 | "views/estate_property_views.xml", 17 | "views/res_users_views.xml", 18 | "views/estate_menus.xml", 19 | ], 20 | "application": True, 21 | } 22 | -------------------------------------------------------------------------------- /estate/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import estate_property 2 | from . import estate_property_offer 3 | from . import estate_property_tag 4 | from . import estate_property_type 5 | from . import res_users 6 | -------------------------------------------------------------------------------- /estate/models/estate_property.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from dateutil.relativedelta import relativedelta 4 | 5 | from odoo import api, fields, models 6 | from odoo.exceptions import UserError, ValidationError 7 | from odoo.tools import float_compare, float_is_zero 8 | 9 | 10 | class EstateProperty(models.Model): 11 | 12 | # ---------------------------------------- Private Attributes --------------------------------- 13 | 14 | _name = "estate.property" 15 | _description = "Real Estate Property" 16 | _order = "id desc" 17 | _sql_constraints = [ 18 | ("check_expected_price", "CHECK(expected_price > 0)", "The expected price must be strictly positive"), 19 | ("check_selling_price", "CHECK(selling_price >= 0)", "The offer price must be positive"), 20 | ] 21 | 22 | # ---------------------------------------- Default Methods ------------------------------------ 23 | 24 | def _default_date_availability(self): 25 | return fields.Date.context_today(self) + relativedelta(months=3) 26 | 27 | # --------------------------------------- Fields Declaration ---------------------------------- 28 | 29 | # Basic 30 | name = fields.Char("Title", required=True) 31 | description = fields.Text("Description") 32 | postcode = fields.Char("Postcode") 33 | date_availability = fields.Date("Available From", default=lambda self: self._default_date_availability(), copy=False) 34 | expected_price = fields.Float("Expected Price", required=True) 35 | selling_price = fields.Float("Selling Price", copy=False, readonly=True) 36 | bedrooms = fields.Integer("Bedrooms", default=2) 37 | living_area = fields.Integer("Living Area (sqm)") 38 | facades = fields.Integer("Facades") 39 | garage = fields.Boolean("Garage") 40 | garden = fields.Boolean("Garden") 41 | garden_area = fields.Integer("Garden Area (sqm)") 42 | garden_orientation = fields.Selection( 43 | selection=[ 44 | ("N", "North"), 45 | ("S", "South"), 46 | ("E", "East"), 47 | ("W", "West"), 48 | ], 49 | string="Garden Orientation", 50 | ) 51 | 52 | # Special 53 | state = fields.Selection( 54 | selection=[ 55 | ("new", "New"), 56 | ("offer_received", "Offer Received"), 57 | ("offer_accepted", "Offer Accepted"), 58 | ("sold", "Sold"), 59 | ("canceled", "Canceled"), 60 | ], 61 | string="Status", 62 | required=True, 63 | copy=False, 64 | default="new", 65 | ) 66 | active = fields.Boolean("Active", default=True) 67 | 68 | # Relational 69 | property_type_id = fields.Many2one("estate.property.type", string="Property Type") 70 | user_id = fields.Many2one("res.users", string="Salesman", default=lambda self: self.env.user) 71 | buyer_id = fields.Many2one("res.partner", string="Buyer", readonly=True, copy=False) 72 | tag_ids = fields.Many2many("estate.property.tag", string="Tags") 73 | offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers") 74 | 75 | # Computed 76 | total_area = fields.Integer( 77 | "Total Area (sqm)", 78 | compute="_compute_total_area", 79 | help="Total area computed by summing the living area and the garden area", 80 | ) 81 | best_price = fields.Float("Best Offer", compute="_compute_best_price", help="Best offer received") 82 | 83 | # ---------------------------------------- Compute methods ------------------------------------ 84 | 85 | @api.depends("living_area", "garden_area") 86 | def _compute_total_area(self): 87 | for prop in self: 88 | prop.total_area = prop.living_area + prop.garden_area 89 | 90 | @api.depends("offer_ids.price") 91 | def _compute_best_price(self): 92 | for prop in self: 93 | prop.best_price = max(prop.offer_ids.mapped("price")) if prop.offer_ids else 0.0 94 | 95 | # ----------------------------------- Constrains and Onchanges -------------------------------- 96 | 97 | @api.constrains("expected_price", "selling_price") 98 | def _check_price_difference(self): 99 | for prop in self: 100 | if ( 101 | not float_is_zero(prop.selling_price, precision_rounding=0.01) 102 | and float_compare(prop.selling_price, prop.expected_price * 90.0 / 100.0, precision_rounding=0.01) < 0 103 | ): 104 | raise ValidationError( 105 | "The selling price must be at least 90% of the expected price! " 106 | + "You must reduce the expected price if you want to accept this offer." 107 | ) 108 | 109 | @api.onchange("garden") 110 | def _onchange_garden(self): 111 | if self.garden: 112 | self.garden_area = 10 113 | self.garden_orientation = "N" 114 | else: 115 | self.garden_area = 0 116 | self.garden_orientation = False 117 | 118 | # ------------------------------------------ CRUD Methods ------------------------------------- 119 | 120 | def unlink(self): 121 | if not set(self.mapped("state")) <= {"new", "canceled"}: 122 | raise UserError("Only new and canceled properties can be deleted.") 123 | return super().unlink() 124 | 125 | # ---------------------------------------- Action Methods ------------------------------------- 126 | 127 | def action_sold(self): 128 | if "canceled" in self.mapped("state"): 129 | raise UserError("Canceled properties cannot be sold.") 130 | return self.write({"state": "sold"}) 131 | 132 | def action_cancel(self): 133 | if "sold" in self.mapped("state"): 134 | raise UserError("Sold properties cannot be canceled.") 135 | return self.write({"state": "canceled"}) 136 | -------------------------------------------------------------------------------- /estate/models/estate_property_offer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from dateutil.relativedelta import relativedelta 3 | 4 | from odoo import api, fields, models 5 | from odoo.exceptions import UserError 6 | from odoo.tools import float_compare 7 | 8 | 9 | class EstatePropertyOffer(models.Model): 10 | 11 | # ---------------------------------------- Private Attributes --------------------------------- 12 | 13 | _name = "estate.property.offer" 14 | _description = "Real Estate Property Offer" 15 | _order = "price desc" 16 | _sql_constraints = [ 17 | ("check_price", "CHECK(price > 0)", "The price must be strictly positive"), 18 | ] 19 | 20 | # --------------------------------------- Fields Declaration ---------------------------------- 21 | 22 | # Basic 23 | price = fields.Float("Price", required=True) 24 | validity = fields.Integer(string="Validity (days)", default=7) 25 | 26 | # Special 27 | state = fields.Selection( 28 | selection=[ 29 | ("accepted", "Accepted"), 30 | ("refused", "Refused"), 31 | ], 32 | string="Status", 33 | copy=False, 34 | default=False, 35 | ) 36 | 37 | # Relational 38 | partner_id = fields.Many2one("res.partner", string="Partner", required=True) 39 | property_id = fields.Many2one("estate.property", string="Property", required=True) 40 | # For stat button: 41 | property_type_id = fields.Many2one( 42 | "estate.property.type", related="property_id.property_type_id", string="Property Type", store=True 43 | ) 44 | 45 | # Computed 46 | date_deadline = fields.Date(string="Deadline", compute="_compute_date_deadline", inverse="_inverse_date_deadline") 47 | 48 | # ---------------------------------------- Compute methods ------------------------------------ 49 | 50 | @api.depends("create_date", "validity") 51 | def _compute_date_deadline(self): 52 | for offer in self: 53 | date = offer.create_date.date() if offer.create_date else fields.Date.today() 54 | offer.date_deadline = date + relativedelta(days=offer.validity) 55 | 56 | def _inverse_date_deadline(self): 57 | for offer in self: 58 | date = offer.create_date.date() if offer.create_date else fields.Date.today() 59 | offer.validity = (offer.date_deadline - date).days 60 | 61 | # ------------------------------------------ CRUD Methods ------------------------------------- 62 | 63 | @api.model 64 | def create(self, vals): 65 | if vals.get("property_id") and vals.get("price"): 66 | prop = self.env["estate.property"].browse(vals["property_id"]) 67 | # We check if the offer is higher than the existing offers 68 | if prop.offer_ids: 69 | max_offer = max(prop.mapped("offer_ids.price")) 70 | if float_compare(vals["price"], max_offer, precision_rounding=0.01) <= 0: 71 | raise UserError("The offer must be higher than %.2f" % max_offer) 72 | prop.state = "offer_received" 73 | return super().create(vals) 74 | 75 | # ---------------------------------------- Action Methods ------------------------------------- 76 | 77 | def action_accept(self): 78 | if "accepted" in self.mapped("property_id.offer_ids.state"): 79 | raise UserError("An offer as already been accepted.") 80 | self.write( 81 | { 82 | "state": "accepted", 83 | } 84 | ) 85 | return self.mapped("property_id").write( 86 | { 87 | "state": "offer_accepted", 88 | "selling_price": self.price, 89 | "buyer_id": self.partner_id.id, 90 | } 91 | ) 92 | 93 | def action_refuse(self): 94 | return self.write( 95 | { 96 | "state": "refused", 97 | } 98 | ) 99 | -------------------------------------------------------------------------------- /estate/models/estate_property_tag.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import fields, models 4 | 5 | 6 | class EstatePropertyType(models.Model): 7 | 8 | # ---------------------------------------- Private Attributes --------------------------------- 9 | 10 | _name = "estate.property.tag" 11 | _description = "Real Estate Property Tag" 12 | _order = "name" 13 | _sql_constraints = [ 14 | ("check_name", "UNIQUE(name)", "The name must be unique"), 15 | ] 16 | 17 | # --------------------------------------- Fields Declaration ---------------------------------- 18 | 19 | # Basic 20 | name = fields.Char("Name", required=True) 21 | color = fields.Integer("Color Index") 22 | -------------------------------------------------------------------------------- /estate/models/estate_property_type.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import fields, models 4 | 5 | 6 | class EstatePropertyType(models.Model): 7 | 8 | # ---------------------------------------- Private Attributes --------------------------------- 9 | 10 | _name = "estate.property.type" 11 | _description = "Real Estate Property Type" 12 | _order = "sequence, name" 13 | _sql_constraints = [ 14 | ("check_name", "UNIQUE(name)", "The name must be unique"), 15 | ] 16 | 17 | # --------------------------------------- Fields Declaration ---------------------------------- 18 | 19 | # Basic 20 | name = fields.Char("Name", required=True) 21 | sequence = fields.Integer("Sequence", default=10) 22 | 23 | # Relational (for inline view) 24 | property_ids = fields.One2many("estate.property", "property_type_id", string="Properties") 25 | 26 | # Computed (for stat button) 27 | offer_count = fields.Integer(string="Offers Count", compute="_compute_offer") 28 | offer_ids = fields.Many2many("estate.property.offer", string="Offers", compute="_compute_offer") 29 | 30 | # ---------------------------------------- Compute methods ------------------------------------ 31 | 32 | def _compute_offer(self): 33 | # This solution is quite complex. It is likely that the trainee would have done a search in 34 | # a loop. 35 | data = self.env["estate.property.offer"].read_group( 36 | [("property_id.state", "!=", "canceled"), ("property_type_id", "!=", False)], 37 | ["ids:array_agg(id)", "property_type_id"], 38 | ["property_type_id"], 39 | ) 40 | mapped_count = {d["property_type_id"][0]: d["property_type_id_count"] for d in data} 41 | mapped_ids = {d["property_type_id"][0]: d["ids"] for d in data} 42 | for prop_type in self: 43 | prop_type.offer_count = mapped_count.get(prop_type.id, 0) 44 | prop_type.offer_ids = mapped_ids.get(prop_type.id, []) 45 | 46 | # ---------------------------------------- Action Methods ------------------------------------- 47 | 48 | def action_view_offers(self): 49 | res = self.env.ref("estate.estate_property_offer_action").read()[0] 50 | res["domain"] = [("id", "in", self.offer_ids.ids)] 51 | return res 52 | -------------------------------------------------------------------------------- /estate/models/res_users.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import fields, models 4 | 5 | 6 | class ResUsers(models.Model): 7 | 8 | # ---------------------------------------- Private Attributes --------------------------------- 9 | 10 | _inherit = "res.users" 11 | 12 | # --------------------------------------- Fields Declaration ---------------------------------- 13 | 14 | # Relational 15 | # This domain gives the opportunity to mention the evaluated and non-evaluated domains 16 | property_ids = fields.One2many( 17 | "estate.property", "user_id", string="Properties", domain=[("state", "in", ["new", "offer_received"])] 18 | ) 19 | -------------------------------------------------------------------------------- /estate/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | access_estate_property,access.estate.property,estate.model_estate_property,base.group_user,1,1,1,1 3 | access_estate_property_offer,access.estate.property.offer,estate.model_estate_property_offer,base.group_user,1,1,1,1 4 | access_estate_property_tag,access.estate.property.tag,estate.model_estate_property_tag,base.group_user,1,1,1,1 5 | access_estate_property_type,access.estate.property.type,estate.model_estate_property_type,base.group_user,1,1,1,1 6 | -------------------------------------------------------------------------------- /estate/views/estate_menus.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /estate/views/estate_property_offer_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | estate.property.offer.form 6 | estate.property.offer 7 | 8 |
9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 | estate.property.offer.tree 19 | estate.property.offer 20 | 21 | 22 | 23 | 24 | 25 | 26 | 14 | 15 | 18 | 19 |
20 |

21 | 22 |

23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 |
39 | 40 | 41 | estate.property.type.tree 42 | estate.property.type 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Property Types 53 | estate.property.type 54 | tree,form 55 | 56 |

57 | Create a property type 58 |

59 |
60 |
61 |
62 | -------------------------------------------------------------------------------- /estate/views/estate_property_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | estate.property.form 6 | estate.property 7 | 8 |
9 |
10 |
15 | 16 |
17 |

18 | 19 |

20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 |
63 |
64 |
65 |
66 | 67 | 68 | estate.property.tree 69 | estate.property 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | estate.property.search 88 | estate.property 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | estate.property.kanban 110 | estate.property 111 | 112 | 113 | 114 | 115 | 116 |
117 |
118 | 119 | 120 | 121 |
122 |
123 | Expected Price: 124 |
125 |
126 | Best Offer: 127 |
128 |
129 | Selling Price: 130 |
131 | 132 |
133 |
134 |
135 |
136 |
137 |
138 | 139 | 140 | Properties 141 | estate.property 142 | tree,kanban,form 143 | {'search_default_available': 1} 144 | 145 |

146 | Create a property advertisement 147 |

148 |

149 | Create real estate properties and follow the selling process. 150 |

151 |
152 |
153 |
154 | -------------------------------------------------------------------------------- /estate/views/res_users_views.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | res.users.form.inherit.estate 5 | res.users 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /estate_account/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from . import models 4 | -------------------------------------------------------------------------------- /estate_account/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | { 4 | "name": "Real Estate Accounting", 5 | "depends": [ 6 | "estate", 7 | "account", 8 | ], 9 | "auto_install": True, 10 | } 11 | -------------------------------------------------------------------------------- /estate_account/models/__init__.py: -------------------------------------------------------------------------------- 1 | from . import estate_property 2 | -------------------------------------------------------------------------------- /estate_account/models/estate_property.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from odoo import models 4 | 5 | 6 | class EstateProperty(models.Model): 7 | 8 | # ---------------------------------------- Private Attributes --------------------------------- 9 | 10 | _inherit = "estate.property" 11 | 12 | # ---------------------------------------- Action Methods ------------------------------------- 13 | 14 | def action_sold(self): 15 | res = super().action_sold() 16 | journal = self.env["account.journal"].search([("type", "=", "sale")], limit=1) 17 | # Another way to get the journal: 18 | # journal = self.env["account.move"].with_context(default_move_type="out_invoice")._get_default_journal() 19 | for prop in self: 20 | self.env["account.move"].create( 21 | { 22 | "partner_id": prop.buyer_id.id, 23 | "move_type": "out_invoice", 24 | "journal_id": journal.id, 25 | "invoice_line_ids": [ 26 | ( 27 | 0, 28 | 0, 29 | { 30 | "name": prop.name, 31 | "quantity": 1.0, 32 | "price_unit": prop.selling_price * 6.0 / 100.0, 33 | }, 34 | ), 35 | ( 36 | 0, 37 | 0, 38 | { 39 | "name": "Administrative fees", 40 | "quantity": 1.0, 41 | "price_unit": 100.0, 42 | }, 43 | ), 44 | ], 45 | } 46 | ) 47 | return res 48 | --------------------------------------------------------------------------------