diff --git a/estate/__init__.py b/estate/__init__.py new file mode 100644 index 00000000000..d4c3532319d --- /dev/null +++ b/estate/__init__.py @@ -0,0 +1,3 @@ +from . import wizards +from . import controllers +from . import models diff --git a/estate/__manifest__.py b/estate/__manifest__.py new file mode 100644 index 00000000000..007b1432dd6 --- /dev/null +++ b/estate/__manifest__.py @@ -0,0 +1,31 @@ +{ + 'name': "Real State", + 'depends': ['base', 'website'], + 'author': "Sahil Mangukiya", + 'category': 'Real Estate/Brokerage', + 'description': "This is my First tutorial module.", + 'data': [ + 'security/estate_security.xml', + 'security/ir.model.access.csv', + 'data/estate.property.type.csv', + 'reports/estate_property_website_template.xml', + 'reports/estate_property_detail_template.xml', + 'reports/print_offer_table_subtemplate.xml', + 'reports/estate_property_offer_report_template.xml', + 'reports/estate_salesman_property_offer_report_template.xml', + 'reports/estate_property_reports.xml', + 'views/res_users_views.xml', + 'wizards/add_offer.xml', + 'views/estate_property_offer_views.xml', + 'views/estate_property_type_views.xml', + 'views/estate_property_tag_views.xml', + 'views/estate_property_views.xml', + 'views/estate_menu.xml' + ], + 'demo': [ + 'demo/estate_property_demo.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False +} diff --git a/estate/controllers/__init__.py b/estate/controllers/__init__.py new file mode 100644 index 00000000000..c5ba8c48107 --- /dev/null +++ b/estate/controllers/__init__.py @@ -0,0 +1 @@ +from . import property_website diff --git a/estate/controllers/property_website.py b/estate/controllers/property_website.py new file mode 100644 index 00000000000..12d35aa865d --- /dev/null +++ b/estate/controllers/property_website.py @@ -0,0 +1,39 @@ +from odoo import http +from odoo.http import request + +from odoo.addons.portal.controllers.portal import pager as portal_pager + + +class propertyWebsiteController(http.Controller): + + @http.route(['/properties', '/properties/page/<int:page>'], website=True, auth='public', type='http') + def property_website(self, page=1, **kwargs): + property_limit_per_page = 6 + property_count = request.env["estate.property"].sudo().search_count( + domain=[('state','in',('new', 'offer received', 'offer accepted')), ('active', '=', True)] + ) + pager = portal_pager( + url="/properties", + total=property_count, + page=page, + step=property_limit_per_page + ) + property = request.env["estate.property"].sudo().search( + domain=[('state','in',('new', 'offer received', 'offer accepted')), ('active', '=', True)], + offset=pager['offset'], + limit=property_limit_per_page + ) + return request.render('estate.estate_property_website_template', + { + 'properties': property, + 'pager': pager + }) + + + @http.route('/property/<int:property_id>', website=True, auth='public', type='http') + def property_detail_view(self, property_id): + property = request.env['estate.property'].sudo().browse(property_id) + return request.render('estate.estate_property_detail_template', + { + 'property' : property, + }) diff --git a/estate/data/estate.property.type.csv b/estate/data/estate.property.type.csv new file mode 100644 index 00000000000..2dba79266ca --- /dev/null +++ b/estate/data/estate.property.type.csv @@ -0,0 +1,5 @@ +id,name +1,"Residential" +2,"Commercial" +3,"Industrial" +4,"Land" diff --git a/estate/demo/estate_property_demo.xml b/estate/demo/estate_property_demo.xml new file mode 100644 index 00000000000..bad0fc8989c --- /dev/null +++ b/estate/demo/estate_property_demo.xml @@ -0,0 +1,121 @@ +<odoo> + <data> + <record id="id1" model="estate.property"> + <field name="name">Big villa</field> + <field name="state">new</field> + <field name="description">A nice and big villa</field> + <field name="postcode">12345</field> + <field name="data_availability">2020-02-02</field> + <field name="expected_price">1600000</field> + <field name="bedrooms">6</field> + <field name="living_area">100</field> + <field name="facades">4</field> + <field name="garage">True</field> + <field name="garden">True</field> + <field name="garden_area">100000</field> + <field name="garden_orientation">south</field> + <field name="property_type_id" ref="1"/> + </record> + + <record id="id2" model="estate.property"> + <field name="name">Trailer home</field> + <field name="state">cancelled</field> + <field name="description">Home in a trailer park</field> + <field name="postcode">54321</field> + <field name="data_availability">1970-01-01</field> + <field name="expected_price">100000</field> + <field name="selling_price">120000</field> + <field name="bedrooms">1</field> + <field name="living_area">10</field> + <field name="facades">4</field> + <field name="garage">False</field> + <field name="property_type_id" ref="1"/> + </record> + + <record id="id3" model="estate.property"> + <field name="name">Sahil's Land</field> + <field name="state">new</field> + <field name="description">This is my land, Don't play cricket!!</field> + <field name="postcode">12345</field> + <field name="data_availability">2020-02-02</field> + <field name="expected_price">100</field> + <field name="bedrooms">30</field> + <field name="living_area">100</field> + <field name="facades">4</field> + <field name="garage">True</field> + <field name="garden">True</field> + <field name="garden_area">100000</field> + <field name="garden_orientation">south</field> + <field name="property_type_id" ref="4"/> + <field name="offer_ids" eval="[ + Command.create({ + 'partner_id': ref('base.res_partner_12'), + 'price': 92, + 'validity': 100 + }), + Command.create({ + 'partner_id': ref('base.res_partner_2'), + 'price': 94, + 'validity': 100 + })]"/> + </record> + + <record id="offer_1" model="estate.property.offer"> + <field name="partner_id" ref="base.res_partner_12"/> + <field name="price">1600000</field> + <field name="property_id" ref="id1"/> + <field name="validity">14</field> + </record> + + <record id="offer_2" model="estate.property.offer"> + <field name="partner_id" ref="base.res_partner_12"/> + <field name="price">1600001</field> + <field name="property_id" ref="id1"/> + <field name="validity">14</field> + </record> + + <record id="offer_3" model="estate.property.offer"> + <field name="partner_id" ref="base.res_partner_2"/> + <field name="price">1600002</field> + <field name="property_id" ref="id1"/> + <field name="validity">14</field> + </record> + + <record id="offer_4" model="estate.property.offer"> + <field name="partner_id" ref="base.res_partner_2"/> + <field name="price">100001</field> + <field name="property_id" ref="id2"/> + <field name="validity">7</field> + </record> + + <record id="offer_5" model="estate.property.offer"> + <field name="partner_id" ref="base.res_partner_12"/> + <field name="price">100002</field> + <field name="property_id" ref="id2"/> + <field name="validity">7</field> + + </record> + + <function model="estate.property.offer" name="action_accept"> + <value eval="[ref('offer_1')]"/> + </function> + + <function model="estate.property.offer" name="action_refuse"> + <value eval="[ref('offer_2')]"/> + </function> + + <function model="estate.property.offer" name="action_refuse"> + <value eval="[ref('offer_3')]"/> + </function> + + <function model="estate.property.offer" name="action_accept"> + <value eval="[ref('offer_4')]"/> + </function> + + <function model="estate.property.offer" name="action_refuse"> + <value eval="[ref('offer_5')]"/> + </function> + + </data> + +</odoo> diff --git a/estate/models/__init__.py b/estate/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /dev/null +++ b/estate/models/__init__.py @@ -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 diff --git a/estate/models/estate_property.py b/estate/models/estate_property.py new file mode 100644 index 00000000000..541c3b5e08e --- /dev/null +++ b/estate/models/estate_property.py @@ -0,0 +1,98 @@ +from datetime import timedelta + +from odoo import models, fields, api +from odoo.exceptions import UserError, ValidationError + + +class estateProperty(models.Model): + _name = "estate.property" + _description = "This is property Table." + _order = "selling_price desc, name" + + name = fields.Char(required=True) + description = fields.Text() + postcode = fields.Char() + data_availability = fields.Date(default=lambda self: fields.Date.today() + timedelta(days=90), copy=False) + expected_price = fields.Float(required=True) + selling_price = fields.Float(readonly=True, copy=False) + bedrooms = fields.Integer(change_default=True) + living_area = fields.Integer() + facades = fields.Integer() + garage = fields.Boolean() + garden = fields.Boolean() + garden_area = fields.Integer() + garden_orientation = fields.Selection( + string='Orientation', + selection=[('north', 'North'), ('south', 'South'), ('east', 'East'), ('west', 'West')], + help="Garden Orientation is avialable in four faces of direction." + ) + active = fields.Boolean(default=True) + state = fields.Selection( + string='State', + selection=[('new', 'New'), ('offer received', 'Offer Received'), ('offer accepted', 'Offer Accepted'), ('sold', 'Sold'), ('cancelled', 'Cancelled')], + help="Offer available in five type.", + required=True, + copy=False, + default='new' + ) + number = fields.Integer(related='property_type_id.number') + totalArea = fields.Integer(compute='_compute_total_area', store="true") + best_offer = fields.Float(compute='_compute_best_offer') + property_image = fields.Binary() + + property_type_id = fields.Many2one(comodel_name='estate.property.type', string='Property Type', domain="[('number', '>=', 0)]", ondelete='cascade') + seller_id = fields.Many2one('res.users', 'Salesman', default=lambda self: self.env.user) + buyer_id = fields.Many2one('res.partner', 'Buyer') + tag_ids = fields.Many2many(comodel_name='estate.property.tag', string='Tag', relation='property_join_tag', column1='property_id', column2='tag_id') + offer_ids = fields.One2many('estate.property.offer', 'property_id') + company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) + + + _sql_constraints = [ + ('check_selling_price', 'CHECK(selling_price >= 0)', 'A property selling price must be positive.'), + ('check_expected_price', 'CHECK(expected_price > 0)', 'A property expected price must be strictly positive.') + ] + + @api.depends('garden_area', 'living_area') + def _compute_total_area(self): + for record in self: + record.totalArea = record.garden_area + record.living_area + + @api.depends('offer_ids.price') + def _compute_best_offer(self): + for record in self: + if record.offer_ids: + record.best_offer = max(record.offer_ids.mapped('price')) + else: + record.best_offer = 0.0 + + @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 = False + + def action_sold(self): + if(self.state == 'cancelled'): + raise UserError("You can not mark a cancelled property as sold.") + self.state = 'sold' + + def action_cancelled(self): + if(self.state == 'sold'): + raise UserError("You can not mark sold property as cancelled") + self.state = 'cancelled' + + @api.constrains('selling_price', 'expected_price') + def _check_selling_price(self): + for record in self: + if record.selling_price < (0.9 * record.expected_price) and (record.selling_price > 0): + raise ValidationError("Selling price cannot be lower than 90 percentage of the expected price.") + + @api.ondelete(at_uninstall=False) + def _prevent_delete(self): + for record in self: + if record.state not in ("new", "cancelled"): + raise UserError("You can not delete a property which are in offer received, offer accepted or sold state.") diff --git a/estate/models/estate_property_offer.py b/estate/models/estate_property_offer.py new file mode 100644 index 00000000000..bc4c718767b --- /dev/null +++ b/estate/models/estate_property_offer.py @@ -0,0 +1,78 @@ +from datetime import timedelta, datetime + +from odoo import models, fields, api +from odoo.exceptions import UserError + + +class estatePropertyOffer(models.Model): + + _name = "estate.property.offer" + _description = "This is offer table" + _order = "id desc" + + price = fields.Float(required=True) + status = fields.Selection(string='Status', selection=[('accepted', 'Accepted'), ('refused', 'Refused')]) + validity = fields.Integer(default=7, string="Validity") + date_deadline = fields.Date(compute='_compute_deadline', inverse='_inverse_deadline', string="Date Deadline") + sum = fields.Integer(compute='_compute_sum', string="Sum") + sum2 = fields.Integer(compute='_compute_sum2', string="Sum2") + + partner_id = fields.Many2one('res.partner', string='Buyer', required=True) + property_id = fields.Many2one('estate.property', string='Property') + property_type_id = fields.Many2one(comodel_name="estate.property.type", related="property_id.property_type_id", store=True) + + _sql_constraints = [ + ('check_offer_price','CHECK(price > 0)','A property offer price must be strictly positive.') + ] + + @api.depends('validity') + def _compute_deadline(self): + for record in self: + if record.create_date: + record.date_deadline = record.create_date + timedelta(record.validity) + else: + record.date_deadline = datetime.today() + timedelta(record.validity) + + def _inverse_deadline(self): + for record in self: + if record.date_deadline and record.create_date: + record.validity = (record.date_deadline - record.create_date.date()).days + else: + record.validity = (record.date_deadline - datetime.today()).days + + @api.depends('validity') + def _compute_sum(self): + for record in self: + record.sum = 7 + record.validity + + @api.depends('sum') + def _compute_sum2(self): + for record in self: + record.sum2 = 7 + record.sum + + def action_accept(self): + for record in self: + if record.property_id.state in ['sold', 'offer accepted']: + raise UserError("Already one offer is accepted.") + if record.status == 'accepted': + continue + record.status = 'accepted' + record.property_id.state = 'offer accepted' + record.property_id.buyer_id = self.partner_id + record.property_id.selling_price = self.price + + def action_refuse(self): + if(self.status == 'accepted'): + self.status = 'refused' + self.property_id.buyer_id = False + self.property_id.selling_price = False + else: + self.status = 'refused' + + @api.model + def create(self, offer): + property_id = self.env["estate.property"].browse(offer["property_id"]) + if offer["price"] < max(property_id.offer_ids.mapped("price") + [0]): + raise UserError("Offer price must be higher than existing offer.") + property_id.state = "offer received" + return super().create(offer) diff --git a/estate/models/estate_property_tag.py b/estate/models/estate_property_tag.py new file mode 100644 index 00000000000..5988f5c2fab --- /dev/null +++ b/estate/models/estate_property_tag.py @@ -0,0 +1,16 @@ +from odoo import models, fields + + +class estatePropertyTag(models.Model): + + _name = "estate.property.tag" + _description = "This is property Tag model" + _order = "name" + + name = fields.Char(required=True, string="Tag") + active = fields.Boolean(required=True) + color = fields.Integer(default=12) + + _sql_constraints = [ + ('check_unique_tag_name','UNIQUE(name)','This tag is already exists.') + ] diff --git a/estate/models/estate_property_type.py b/estate/models/estate_property_type.py new file mode 100644 index 00000000000..31b225bf68e --- /dev/null +++ b/estate/models/estate_property_type.py @@ -0,0 +1,25 @@ +from odoo import models, fields, api + + +class estatePropertyType(models.Model): + _name = "estate.property.type" + _description = "This is property Type table." + _order = "sequence, name desc" + + name = fields.Char(required=True) + active = fields.Boolean(default=True) + number = fields.Integer(default=10) + sequence = fields.Integer('Sequence', default=5) + offer_count = fields.Integer(compute="_compute_offer_count") + + property_ids = fields.One2many('estate.property', 'property_type_id', string='Property') + offer_ids = fields.One2many('estate.property.offer', 'property_type_id') + + _sql_constraints = [ + ('check_unique_type_name', 'UNIQUE(name)', 'This type is already exists.') + ] + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate/models/res_users.py b/estate/models/res_users.py new file mode 100644 index 00000000000..933b9dcfb69 --- /dev/null +++ b/estate/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class resUser(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many(comodel_name="estate.property", inverse_name="seller_id", domain=[('state', 'in', ['new', 'offer received'])]) diff --git a/estate/reports/estate_property_detail_template.xml b/estate/reports/estate_property_detail_template.xml new file mode 100644 index 00000000000..fb15c543367 --- /dev/null +++ b/estate/reports/estate_property_detail_template.xml @@ -0,0 +1,64 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <template id="estate_property_detail_template" name="Property Detail"> + <title><t t-esc="property.name"/></title> + <t t-call="website.layout"> + <style> + .property-container { + max-width: 800px; + margin: auto; + } + .property-card { + border: 1px solid #ddd; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + .property-image { + width: 100%; + height: 300px; + object-fit: cover; + border-bottom: 1px solid #ddd; + } + .property-details { + padding: 20px; + } + .property-details h5 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 15px; + color: #333; + } + .property-details p { + margin: 5px 0; + font-size: 1rem; + color: #555; + } + .property-details strong { + color: #000; + } + </style> + <section class="container my-5 property-container"> + <h2 class="text-center mb-4"> + <t t-esc="property.name"/> - Details + </h2> + <div class="property-card"> + <t t-if="property.property_image"> + <img t-attf-src="data:image/*;base64,#{property.property_image}" alt="Property Image" class="property-image"/> + </t> + <div class="property-details"> + <h5><t t-esc="property.name"/></h5> + <p><strong>Salesman:</strong> <t t-esc="property.seller_id.name"/></p> + <p><strong>State:</strong> <t t-esc="property.state"/></p> + <p><strong>Expected Price:</strong> <t t-esc="property.expected_price"/> </p> + <p><strong>Postcode:</strong> <t t-esc="property.postcode"/> </p> + <p><strong>Date Availability:</strong> <t t-esc="property.data_availability"/> </p> + <p><strong>Number of Bedrooms:</strong> <t t-esc="property.bedrooms"/> </p> + <p><strong>Living Area:</strong> <t t-esc="property.garden_area"/> </p> + <p><strong>Garden Area:</strong> <t t-esc="property.garden_area"/> </p> + </div> + </div> + </section> + </t> + </template> +</odoo> diff --git a/estate/reports/estate_property_offer_report_template.xml b/estate/reports/estate_property_offer_report_template.xml new file mode 100644 index 00000000000..ac0387c659b --- /dev/null +++ b/estate/reports/estate_property_offer_report_template.xml @@ -0,0 +1,38 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <template id="estate_property_offer_report_template"> + <t t-call="web.html_container"> + <t t-foreach="docs" t-as="property"> + <t t-call="web.external_layout"> + <div class="page"> + <h1> + <b> + Property Name: + </b> + <span t-field="property.name"/> + </h1> + <h6> + <b> + Salesman: + </b> + <span t-field="property.seller_id"/> + </h6> + <h6> + <b> + Expected Price: + </b> + <span t-field="property.expected_price"/> + </h6> + <h6> + <b> + State: + </b> + <span t-field="property.state"/> + </h6> + <t t-call="estate.print_offer_table"/> + </div> + </t> + </t> + </t> + </template> +</odoo> diff --git a/estate/reports/estate_property_reports.xml b/estate/reports/estate_property_reports.xml new file mode 100644 index 00000000000..cc759dfcf17 --- /dev/null +++ b/estate/reports/estate_property_reports.xml @@ -0,0 +1,23 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="estate_property_offer_report" model="ir.actions.report"> + <field name="name">Property Offer Report</field> + <field name="model">estate.property</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">estate.estate_property_offer_report_template</field> + <field name="report_file">estate.estate_property_offer_report_template</field> + <field name="print_report_name">'%s Offers Report' % object.name</field> + <field name="binding_model_id" ref="model_estate_property"/> + <field name="binding_type">report</field> + </record> + <record id="estate_salesman_property_offer_report" model="ir.actions.report"> + <field name="name">Property Offer Report</field> + <field name="model">res.users</field> + <field name="report_type">qweb-pdf</field> + <field name="report_name">estate.estate_salesman_property_offer_report_template</field> + <field name="report_file">estate.estate_salesman_property_offer_report_template</field> + <field name="print_report_name">'%s Properties Offer Report' % object.name</field> + <field name="binding_model_id" ref="model_res_users"/> + <field name="binding_type">report</field> + </record> +</odoo> diff --git a/estate/reports/estate_property_website_template.xml b/estate/reports/estate_property_website_template.xml new file mode 100644 index 00000000000..7a8c9c978c8 --- /dev/null +++ b/estate/reports/estate_property_website_template.xml @@ -0,0 +1,60 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="menu_property" model="website.menu"> + <field name="name">Properties</field> + <field name="url">/properties</field> + <field name="parent_id" ref="website.main_menu"/> + </record> + + <template id="estate_property_website_template" name="Property"> + <title>Properties</title> + <t t-call="website.layout"> + <style> + .property-image { + width: 100%; + height: 200px; + object-fit: cover; + } + </style> + <section class="container my-5"> + <h2 class="text-center mb-4"> + Available Properties + </h2> + <div class="row"> + <t t-foreach="properties" t-as="property"> + <div class="col-lg-4 col-md-6 col-sm-12 mb-4"> + <div class="card shadow border-light"> + <t t-if="property.property_image"> + <img t-attf-src="data:image/*;base64,#{property.property_image}" alt="Property Image"/> + </t> + <div class="card-body"> + <h5 class="card-title"> + <t t-esc="property.name"/> + </h5> + <p class="card-text"> + <strong>Salesman:</strong> + <t t-esc="property.seller_id.name"/> + </p> + <p class="card-text"> + <strong>State:</strong> + <t t-esc="property.state"/> + </p> + <p class="card-text"> + <strong>Expected Price:</strong> + <t t-esc="property.expected_price"/> $ + </p> + <a t-att-href="'/property/' + str(property.id)" class="btn btn-primary">View Details</a> + </div> + </div> + </div> + </t> + </div> + <div class="pagination d-flex justify-content-between align-items-center"> + <a t-att-href="pager['page_previous']['url']" class="btn btn-secondary">Previous</a> + <span>Page <t t-esc="pager['page']['num']"/> of <t t-esc="pager['page_count']"/></span> + <a t-att-href="pager['page_next']['url']" class="btn btn-secondary">Next</a> + </div> + </section> + </t> + </template> +</odoo> diff --git a/estate/reports/estate_salesman_property_offer_report_template.xml b/estate/reports/estate_salesman_property_offer_report_template.xml new file mode 100644 index 00000000000..3d05c2ed7a1 --- /dev/null +++ b/estate/reports/estate_salesman_property_offer_report_template.xml @@ -0,0 +1,45 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <template id="estate_salesman_property_offer_report_template"> + <t t-call="web.html_container"> + <t t-foreach="docs" t-as="res_user"> + <t t-call="web.external_layout"> + <div class="page"> + <h1> + <b> + Salesman: + </b> + <span t-field="res_user.name"/> + </h1> + <t t-if="len(res_user.property_ids) > 0"> + <t t-foreach="res_user.property_ids" t-as="property"> + <h3> + <b> + Property Name: + </b> + <span t-field="property.name"/> + </h3> + <h6> + <b> + Expected Price: + </b> + <span t-field="property.expected_price"/> + </h6> + <h6> + <b> + State: + </b> + <span t-field="property.state"/> + </h6> + <t t-call="estate.print_offer_table"/> + </t> + </t> + <t t-else=""> + <p>No property is under this user.</p> + </t> + </div> + </t> + </t> + </t> + </template> +</odoo> diff --git a/estate/reports/print_offer_table_subtemplate.xml b/estate/reports/print_offer_table_subtemplate.xml new file mode 100644 index 00000000000..78cd5ad87e2 --- /dev/null +++ b/estate/reports/print_offer_table_subtemplate.xml @@ -0,0 +1,42 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <template id="print_offer_table"> + <t t-if="len(property.offer_ids) > 0"> + <table class="table mt16"> + <thead> + <tr> + <th>Price</th> + <th>Partner</th> + <th>Validity (days)</th> + <th>Deadline</th> + <th>Status</th> + </tr> + </thead> + <tbody> + <tr t-foreach="property.offer_ids" t-as="offers"> + <td> + <span t-field="offers.price"/> + </td> + <td> + <span t-field="offers.partner_id"/> + </td> + <td> + <span t-field="offers.validity"/> + </td> + <td> + <span t-field="offers.date_deadline"/> + </td> + <td> + <span t-field="offers.status"/> + </td> + </tr> + </tbody> + </table> + </t> + <t t-else=""> + <p> + No offer exists for property!!! + </p> + </t> + </template> +</odoo> diff --git a/estate/security/estate_security.xml b/estate/security/estate_security.xml new file mode 100644 index 00000000000..836ae6958db --- /dev/null +++ b/estate/security/estate_security.xml @@ -0,0 +1,34 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + + <record id="estate_group_user" model="res.groups"> + <field name="name">Agent</field> + <field name="category_id" ref="base.module_category_real_estate_brokerage"></field> + </record> + + <record id="estate_group_manager" model="res.groups"> + <field name="name">Manager</field> + <field name="category_id" ref="base.module_category_real_estate_brokerage"></field> + <field name="implied_ids" eval="[(4, ref('estate.estate_group_user'))]"/> + <field name="users" eval="[(4, ref('base.user_admin'))]"/> + </record> + + <record id="estate_property_record_rule_agent" model="ir.rule"> + <field name="name">A description of the rule's for agent</field> + <field name="model_id" ref="estate.model_estate_property"/> + <field name="groups" eval="[Command.link(ref('estate.estate_group_user'))]"/> + <field name="domain_force"> + ['|', ('seller_id', '=', user.id), ('seller_id', '=', False), ('company_id', 'in', company_ids)] + </field> + </record> + + <record id="estate_property_record_rule_manager" model="ir.rule"> + <field name="name">A description of the rule's for manager</field> + <field name="model_id" ref="estate.model_estate_property"/> + <field name="groups" eval="[Command.link(ref('estate.estate_group_manager'))]"/> + <field name="domain_force"> + [(1, '=', 1)] + </field> + </record> + +</odoo> diff --git a/estate/security/ir.model.access.csv b/estate/security/ir.model.access.csv new file mode 100644 index 00000000000..ef5f36a60b7 --- /dev/null +++ b/estate/security/ir.model.access.csv @@ -0,0 +1,13 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + +access_estate_property,access_estate_property,model_estate_property,estate.estate_group_manager,1,1,1,0 +access_estate_property_type,access_estate_property_type,model_estate_property_type,estate.estate_group_manager,1,1,1,1 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,estate.estate_group_manager,1,1,1,1 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,estate.estate_group_manager,1,1,1,1 +access_add_offer,access_add_offer,model_add_offer,estate.estate_group_manager,1,1,1,1 + +access_estate_property,access_estate_property,model_estate_property,estate.estate_group_user,1,1,1,0 +access_estate_property_type,access_estate_property_type,model_estate_property_type,estate.estate_group_user,1,0,0,0 +access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,estate.estate_group_user,1,0,0,0 +access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,estate.estate_group_user,1,1,1,1 +access_add_offer,access_add_offer,model_add_offer,estate.estate_group_user,1,1,1,1 diff --git a/estate/views/estate_menu.xml b/estate/views/estate_menu.xml new file mode 100644 index 00000000000..fef0e406a7a --- /dev/null +++ b/estate/views/estate_menu.xml @@ -0,0 +1,12 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <menuitem id="estate_menu_root" name="Real State"> + <menuitem id="estate_menu_root_first_advertisements" name="Advertisements" sequence="1"> + <menuitem id="estate_menu_root_first_second_property" action="estate_property_action"/> + </menuitem> + <menuitem id="estate_menu_root_first_setting" name="Setting" sequence="2" groups="estate.estate_group_manager"> + <menuitem id="estate_menu_root_first_second_type" action="estate_property_type_action" name="Property Type" sequence="1"/> + <menuitem id="estate_menu_root_first_second_tag" action="estate_property_tag_action" name="Property Tag" sequence="2"/> + </menuitem> + </menuitem> +</odoo> diff --git a/estate/views/estate_property_offer_views.xml b/estate/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..73089209f87 --- /dev/null +++ b/estate/views/estate_property_offer_views.xml @@ -0,0 +1,24 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="estate_property_offer_action" model="ir.actions.act_window"> + <field name="name">Offers</field> + <field name="res_model">estate.property.offer</field> + <field name="view_mode">tree,form</field> + <field name="domain">[('property_type_id', '=', active_id)]</field> + </record> + + <record id="estate_property_offer_view_tree" model="ir.ui.view"> + <field name="name">estate_property_offer_view_tree</field> + <field name="model">estate.property.offer</field> + <field name="arch" type="xml"> + <tree decoration-success="status == 'accepted'" decoration-danger="status == 'refused'"> + <field name="property_id"/> + <field name="partner_id"/> + <field name="price"/> + <field name="validity"/> + <field name="date_deadline"/> + <field name="status"/> + </tree> + </field> + </record> +</odoo> diff --git a/estate/views/estate_property_tag_views.xml b/estate/views/estate_property_tag_views.xml new file mode 100644 index 00000000000..5a2cd68dcf6 --- /dev/null +++ b/estate/views/estate_property_tag_views.xml @@ -0,0 +1,63 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="estate_property_tag_action" model="ir.actions.act_window"> + <field name="name">Tag</field> + <field name="res_model">estate.property.tag</field> + <field name="view_mode">tree,form,kanban</field> + </record> + <record id="estate_property_tag_view_tree" model="ir.ui.view"> + <field name="name">estate_property_tag_view_tree</field> + <field name="model">estate.property.tag</field> + <field name="arch" type="xml"> + <tree> + <field name="name"/> + <field name="active"/> + <field name="color"/> + </tree> + </field> + </record> + <record id="estate_property_tag_view_form" model="ir.ui.view"> + <field name="name">estate_property_tag_view_form</field> + <field name="model">estate.property.tag</field> + <field name="arch" type="xml"> + <form> + <sheet> + <h3> + <field name="name"/> + </h3> + <group> + <field name="active"/> + <field name="color" help="Enter a number between 1 to 11 (inclusive)"/> + </group> + </sheet> + </form> + </field> + </record> + <record id="estate_property_tag_view_search" model="ir.ui.view"> + <field name="name">estate_property_tag_view_search</field> + <field name="model">estate.property.tag</field> + <field name="arch" type="xml"> + <search string="Search Tag"> + <field name="color" string="Color"/> + </search> + </field> + </record> + <record id="estate_property_tag_view_kanban" model="ir.ui.view"> + <field name="name">estate_property_tag_view_kanban</field> + <field name="model">estate.property.tag</field> + <field name="arch" type="xml"> + <kanban default_group_by="color"> + <templates> + <t t-name="kanban-box"> + <div class="oe_kanban_global_click"> + <h3> + <field name="name"/> + </h3> + <field name="active"/> + </div> + </t> + </templates> + </kanban> + </field> + </record> +</odoo> diff --git a/estate/views/estate_property_type_views.xml b/estate/views/estate_property_type_views.xml new file mode 100644 index 00000000000..5133186d849 --- /dev/null +++ b/estate/views/estate_property_type_views.xml @@ -0,0 +1,57 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="estate_property_type_action" model="ir.actions.act_window"> + <field name="name">Types</field> + <field name="res_model">estate.property.type</field> + <field name="view_mode">tree,form</field> + </record> + + <record id="estate_property_type_view_tree" model="ir.ui.view"> + <field name="name">estate_property_type_view_tree</field> + <field name="model">estate.property.type</field> + <field name="arch" type="xml"> + <tree string="Property Type"> + <field name="sequence" widget="handle"/> + <field name="name"/> + <field name="active" /> + <field name="number"/> + </tree> + </field> + </record> + + <record id="estate_property_type_view_form" model="ir.ui.view"> + <field name="name">estate_property_type_view_form</field> + <field name="model">estate.property.type</field> + <field name="arch" type="xml"> + <form> + <sheet> + <div class="oe_button_box" name="button_box"> + <button name="%(estate_property_offer_action)d" type="action" class="oe_stat_button" icon="fa-money"> + <field name="offer_count" string="Offers" widget="statinfo"/> + </button> + </div> + <h3> + <field name="name"/> + </h3> + <group> + <field name="active"/> + </group> + <notebook> + <page string="Properties"> + <field name="property_ids"> + <tree create="False" decoration-success="state == 'offer received' or state == 'offer accepted'" decoration-bf="state == 'offer accepted'" decoration-muted="state == 'sold'"> + <field name="name"/> + <field name="bedrooms"/> + <field name="expected_price"/> + <field name="best_offer"/> + <field name="selling_price"/> + <field name="state"/> + </tree> + </field> + </page> + </notebook> + </sheet> + </form> + </field> + </record> +</odoo> diff --git a/estate/views/estate_property_views.xml b/estate/views/estate_property_views.xml new file mode 100644 index 00000000000..2c4b0fc5095 --- /dev/null +++ b/estate/views/estate_property_views.xml @@ -0,0 +1,148 @@ +<?xml version="1.0" encoding="utf-8"?> +<odoo> + <record id="estate_property_action" model="ir.actions.act_window"> + <field name="name">Properties</field> + <field name="res_model">estate.property</field> + <field name="view_mode">list,form,kanban</field> + <field name="context">{'search_default_state' : True}</field> + </record> + + <record id="estate_property_view_tree" model="ir.ui.view"> + <field name="name">estate_property_view_tree</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <tree expand="1" default_order="expected_price desc, name desc" decoration-success="state == 'offer received' or state == 'offer accepted'" decoration-bf="state == 'offer accepted'" decoration-muted="state == 'sold'"> + <header> + <button string="Add Offer" name="%(estate.action_add_offer)d" type="action" class="oe_highlight"/> + </header> + <field name="name"/> + <field name="bedrooms"/> + <field name="expected_price" /> + <field name="selling_price" /> + <field name="best_offer" /> + <field name="state"/> + </tree> + </field> + </record> + + <record id="estate_property_view_form" model="ir.ui.view"> + <field name="name">estate_property_view_form</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <form string="My New House"> + <header> + <button string="Sold" name="action_sold" type="object" class="oe_highlight" invisible="state == 'sold' or state == 'cancelled'" /> + <button string="Cancel" name="action_cancelled" type="object" invisible="state == 'sold' or state == 'cancelled'" /> + <field name="state" widget="statusbar" /> + </header> + <sheet> + <field name="property_image" widget="image" class="oe_avatar"/> + <h2> + Name: + <field name="name"/> + </h2> + <group> + <group> + <field name="property_type_id" options="{'no_create': true}" /> + <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}" /> + <field name="postcode"/> + </group> + <group> + <field name="expected_price"/> + <field name="best_offer" /> + <field name="selling_price" /> + <field name="company_id" /> + </group> + </group> + <notebook> + <page string="Description"> + <group> + <group> + <field name="description"/> + <field name="data_availability"/> + <field name="bedrooms" string="Number of bedrooms"/> + <field name="living_area"/> + <field name="garage"/> + <field name="garden"/> + <field name="garden_area" invisible="not garden"/> + <field name="garden_orientation" invisible="not garden"/> + <field name="totalArea"/> + </group> + </group> + </page> + <page string="Sales Information"> + <group> + <field name="seller_id"/> + <field name="buyer_id"/> + </group> + </page> + <page string="Offer"> + <field name="offer_ids" readonly="state == 'offer accepted' or state == 'sold' or state == 'cancelled'" > + <tree editable="bottom" decoration-success="status == 'accepted'" decoration-danger="status == 'refused'" > + <field name="partner_id"/> + <field name="price"/> + <field name="validity" /> + <field name="date_deadline" /> + <button name="action_accept" type="object" icon="fa-check" invisible="status == 'accepted' or status == 'refused'" /> + <button name="action_refuse" type="object" icon="fa-times" invisible="status == 'accepted' or status == 'refused'" /> + <field name="status" /> + </tree> + </field> + </page> + </notebook> + </sheet> + </form> + </field> + </record> + + <record id="estate_property_view_kanban" model="ir.ui.view"> + <field name="name">estate.property.view.kanban</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <kanban default_group_by="property_type_id" records_draggable="False"> + <field name="state"/> + <templates> + <t t-name="kanban-box"> + <div class="oe_kanban_global_click"> + <h3> + <field name="name"/> + </h3> + <div> + Expected Price: + <field name="expected_price"/> + </div> + <div t-if="record.state.raw_value == 'offer received'" > + Best Offer Price: + <field name="best_offer"/> + </div> + <div t-if="record.state.raw_value == 'offer accepted'" > + Selling price: + <field name="selling_price"/> + </div> + <div style="margin-top: 10px;"></div> + <field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/> + </div> + </t> + </templates> + </kanban> + </field> + </record> + + <record id="estate_property_view_search" model="ir.ui.view"> + <field name="name">estate_property_view_search</field> + <field name="model">estate.property</field> + <field name="arch" type="xml"> + <search> + <field name="name" string="Name" /> + <field name="selling_price" /> + <field name="expected_price" string="Expected Price" /> + <field string="Living Area" name="living_area" filter_domain="[('living_area', '>=', self)]" /> + <filter string="Available" name="state" domain="['|', ('state', '=', 'new'),('state', '=', 'sold')]" /> + <group string="Group By"> + <filter string="Postcode" name="postcode" context="{'group_by':'postcode'}"/> + <filter string="Bedroom" name="bedrooms" context="{'group_by':'bedrooms'}"/> + </group> + </search> + </field> + </record> +</odoo> diff --git a/estate/views/res_users_views.xml b/estate/views/res_users_views.xml new file mode 100644 index 00000000000..f67d9f1dac1 --- /dev/null +++ b/estate/views/res_users_views.xml @@ -0,0 +1,15 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="res_user_views_form" model="ir.ui.view"> + <field name="name">res_user_views_form</field> + <field name="model">res.users</field> + <field name="inherit_id" ref="base.view_users_form"/> + <field name="arch" type="xml"> + <page name="preferences" position="after"> + <page name="estate" string="Real Estate Properties" readonly="true" > + <field name="property_ids"/> + </page> + </page> + </field> + </record> +</odoo> diff --git a/estate/wizards/__init__.py b/estate/wizards/__init__.py new file mode 100644 index 00000000000..7e004753b11 --- /dev/null +++ b/estate/wizards/__init__.py @@ -0,0 +1 @@ +from . import add_offer diff --git a/estate/wizards/add_offer.py b/estate/wizards/add_offer.py new file mode 100644 index 00000000000..565aa65b18e --- /dev/null +++ b/estate/wizards/add_offer.py @@ -0,0 +1,21 @@ +from odoo import models, fields + + +class addOffer(models.TransientModel): + + _name = "add.offer" + _description = "You can add a offer directly in multiple properties." + + price = fields.Float(required=True) + partner_id = fields.Many2one('res.partner', string='Buyer', required=True) + + def action_make_an_offer(self): + select_property_id = self.env.context['active_ids'] + properties = self.env["estate.property"].browse(select_property_id) + + for property in properties: + self.env["estate.property.offer"].create({ + 'price': self.price, + 'partner_id': self.partner_id.id, + 'property_id': property.id + }) diff --git a/estate/wizards/add_offer.xml b/estate/wizards/add_offer.xml new file mode 100644 index 00000000000..e515189f75c --- /dev/null +++ b/estate/wizards/add_offer.xml @@ -0,0 +1,26 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="action_add_offer" model="ir.actions.act_window"> + <field name="name">Add Offer</field> + <field name="res_model">add.offer</field> + <field name="view_mode">form</field> + <field name="target">new</field> + </record> + + <record id="add_offer_view_form" model="ir.ui.view"> + <field name="name">add.offer.view.form</field> + <field name="model">add.offer</field> + <field name="arch" type="xml"> + <form string="Add Offer"> + <group> + <field name="price"/> + <field name="partner_id"/> + </group> + <footer> + <button string="Make an Offer" name="action_make_an_offer" type="object" class="oe_highlight"/> + <button string="Cancel" class="btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> +</odoo> diff --git a/estate_account/__init__.py b/estate_account/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_account/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_account/__manifest__.py b/estate_account/__manifest__.py new file mode 100644 index 00000000000..51c64289705 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': "Estate Account", + 'depends': ['estate', 'account'], + 'author': "Sahil Mangukiya", + 'description': "This is a estate account module.", + 'data': [ + 'reports/estate_property_offer_report_inherit_template.xml', + 'reports/estate_salesman_property_offer_report_inherit_template.xml', + ], + 'demo': [ + ], + 'installable': True, + 'application': False, +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..5e1963c9d2f --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property diff --git a/estate_account/models/estate_property.py b/estate_account/models/estate_property.py new file mode 100644 index 00000000000..86146043e32 --- /dev/null +++ b/estate_account/models/estate_property.py @@ -0,0 +1,37 @@ +from odoo import models, Command, fields + + +class estateProperty(models.Model): + + _inherit = 'estate.property' + + def action_sold(self): + supper_call = super().action_sold() + + invoice_price = { + "name": self.name, + "quantity": 1, + "price_unit": self.selling_price * 1.06 + } + + invoice_administrative_fees = { + "name": "Administrative Fees", + "quantity": 1, + "price_unit": 100.00 + } + + # print(" reached ".center(100, '=')) + # print(self.env["account.move"].check_access_rights('write')) + # print(self.env["account.move"].check_access_rule('write')) + + self.env["account.move"].sudo().create({ + "name": self.name + " Invoice", + "partner_id": self.buyer_id.id, + "move_type": "out_invoice", + "invoice_line_ids": [ + Command.create(invoice_price), + Command.create(invoice_administrative_fees) + ] + }) + + return supper_call diff --git a/estate_account/reports/estate_property_offer_report_inherit_template.xml b/estate_account/reports/estate_property_offer_report_inherit_template.xml new file mode 100644 index 00000000000..fc9c671d14a --- /dev/null +++ b/estate_account/reports/estate_property_offer_report_inherit_template.xml @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <template id="estate_property_offer_report_inherit_template" inherit_id="estate.estate_property_offer_report_template"> + <xpath expr="//div//h6[3]" position="after"> + <t t-if="property.state == 'sold'"> + <p> + <b> + !!! Invoice has already been created !!! + </b> + </p> + </t> + </xpath> + </template> +</odoo> diff --git a/estate_account/reports/estate_salesman_property_offer_report_inherit_template.xml b/estate_account/reports/estate_salesman_property_offer_report_inherit_template.xml new file mode 100644 index 00000000000..214d0750204 --- /dev/null +++ b/estate_account/reports/estate_salesman_property_offer_report_inherit_template.xml @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <template id="estate_salesman_property_offer_report_inherit_template" inherit_id="estate.estate_salesman_property_offer_report_template"> + <xpath expr="//div//h6[2]" position="after"> + <t t-if="property.state == 'offer received'"> + <p> + <b> + !!! Offers are received for this Property. !!! + </b> + </p> + </t> + </xpath> + </template> +</odoo> diff --git a/stock_install/__init__.py b/stock_install/__init__.py new file mode 100644 index 00000000000..9a7e03eded3 --- /dev/null +++ b/stock_install/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/stock_install/__manifest__.py b/stock_install/__manifest__.py new file mode 100644 index 00000000000..0a5f03cf466 --- /dev/null +++ b/stock_install/__manifest__.py @@ -0,0 +1,15 @@ +{ + 'name': "stock_install", + 'depends': ['stock'], + 'author': "sahm@odoo.com", + 'description': "This is my stock_install module.", + 'data': [ + 'views/res_config_settings_views.xml' + ], + 'demo': [ + + ], + 'installable': True, + 'application': False, + 'auto_install': True +} diff --git a/stock_install/models/__init__.py b/stock_install/models/__init__.py new file mode 100644 index 00000000000..e1abf59ce87 --- /dev/null +++ b/stock_install/models/__init__.py @@ -0,0 +1 @@ +from . import res_config_setting \ No newline at end of file diff --git a/stock_install/models/res_config_setting.py b/stock_install/models/res_config_setting.py new file mode 100644 index 00000000000..014bef2a711 --- /dev/null +++ b/stock_install/models/res_config_setting.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + module_stock_transport = fields.Boolean("Dispatch Management System") \ No newline at end of file diff --git a/stock_install/views/res_config_settings_views.xml b/stock_install/views/res_config_settings_views.xml new file mode 100644 index 00000000000..5ef00cea9f1 --- /dev/null +++ b/stock_install/views/res_config_settings_views.xml @@ -0,0 +1,15 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="res_config_setting_inherit_res_config_setting" model="ir.ui.view"> + <field name="name">res_config_setting_inherit_res_config_setting</field> + <field name="model">res.config.settings</field> + <field name="inherit_id" ref="stock.res_config_settings_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//setting[@id='delivery']" position="after"> + <setting id="dispatch_management_system" help="Transport Management: organize pack in your fleet, or carriers."> + <field name="module_stock_transport"></field> + </setting> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/stock_transport/__init__.py b/stock_transport/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/stock_transport/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_transport/__manifest__.py b/stock_transport/__manifest__.py new file mode 100644 index 00000000000..aa65ce2821f --- /dev/null +++ b/stock_transport/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': "stock_transport", + 'depends': ['stock_picking_batch', 'fleet'], + 'author': "sahm@odoo.com", + 'description': "This is my stock_transport module.", + 'data': [ + 'security/stock_transport_security.xml', + 'security/ir.model.access.csv', + 'views/stock_picking_batch_views.xml', + 'views/fleet_vehicle_model_views.xml', + 'views/stock_picking_views.xml', + ], + 'demo': [ + + ], + 'installable': True, + 'application': False, + 'auto_install': False +} diff --git a/stock_transport/models/__init__.py b/stock_transport/models/__init__.py new file mode 100644 index 00000000000..0d3615884af --- /dev/null +++ b/stock_transport/models/__init__.py @@ -0,0 +1,4 @@ +from . import fleet_vehicle_model_category +from . import dock +from . import stock_picking_batch +from . import stock_picking diff --git a/stock_transport/models/dock.py b/stock_transport/models/dock.py new file mode 100644 index 00000000000..06c4cad47b3 --- /dev/null +++ b/stock_transport/models/dock.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class dock(models.Model): + _name = 'dock' + + name = fields.Char() diff --git a/stock_transport/models/fleet_vehicle_model_category.py b/stock_transport/models/fleet_vehicle_model_category.py new file mode 100644 index 00000000000..09df774a749 --- /dev/null +++ b/stock_transport/models/fleet_vehicle_model_category.py @@ -0,0 +1,12 @@ +from odoo import fields, models + + +class FleetVehicleModelCategory(models.Model): + _inherit = 'fleet.vehicle.model.category' + + max_weight = fields.Float() + max_volume = fields.Float() + + def _compute_display_name(self): + for record in self: + record.display_name = record.name + ' (' + str(record.max_weight) + ' kg, ' + str(record.max_volume) + ' m3)' diff --git a/stock_transport/models/stock_picking.py b/stock_transport/models/stock_picking.py new file mode 100644 index 00000000000..3244053dc08 --- /dev/null +++ b/stock_transport/models/stock_picking.py @@ -0,0 +1,15 @@ +from odoo import fields, models, api + + +class Picking(models.Model): + _inherit = 'stock.picking' + + volume = fields.Float(compute="_compute_volume") + + @api.depends('move_ids.product_id.volume') + def _compute_volume(self): + for record in self: + total_volume = 0 + for move_line in record.move_ids: + total_volume = total_volume + move_line.quantity * move_line.product_id.volume + record.volume = total_volume diff --git a/stock_transport/models/stock_picking_batch.py b/stock_transport/models/stock_picking_batch.py new file mode 100644 index 00000000000..aae62bd2e8f --- /dev/null +++ b/stock_transport/models/stock_picking_batch.py @@ -0,0 +1,80 @@ +from odoo import fields, models, api + + +class stockPickingBatch(models.Model): + _inherit = 'stock.picking.batch' + _rec_name = 'name' + + weight_percentage = fields.Float(compute="_compute_weight_percentage", string='Weight') + volume_percentage = fields.Float(compute="_compute_volume_percentage", string='Volume') + + weight = fields.Float(string="Weight", compute="_compute_weight", store=True) + volume = fields.Float(string="Volume", compute="_compute_volume", store=True) + number_of_transfer = fields.Integer(string="Transfer", compute="_compute_number_of_transfer", store=True) + number_of_line = fields.Integer(string="Line", compute="_compute_number_of_line", store=True) + + dock_id = fields.Many2one(comodel_name='dock', string="Dock") + vehicle_id = fields.Many2one(comodel_name='fleet.vehicle', string="Vehicle") + category_id = fields.Many2one(comodel_name='fleet.vehicle.model.category', string='Category', compute='_compute_category', store=True) + + @api.depends('vehicle_id', 'vehicle_id.category_id') + def _compute_category(self): + for record in self: + record.category_id = record.vehicle_id.category_id.id + + @api.depends('picking_ids', 'picking_ids.move_ids', 'category_id.max_weight', 'picking_ids.move_ids.product_id.weight') + def _compute_weight_percentage(self): + for record in self: + max_weight = record.category_id.max_weight + total_weight = 0 + for picking_line in record.picking_ids: + for move_line in picking_line.move_ids: + total_weight = total_weight + move_line.product_id.weight * move_line.quantity + if max_weight == 0: + record.weight_percentage = 0.0 + else: + record.weight_percentage = total_weight / max_weight * 100 + + @api.depends('picking_ids', 'picking_ids.move_ids', 'category_id.max_volume', 'picking_ids.move_ids.product_id.volume') + def _compute_volume_percentage(self): + for record in self: + max_volume = record.category_id.max_volume + total_volume = 0 + for picking_line in record.picking_ids: + for move_line in picking_line.move_ids: + total_volume = total_volume + move_line.product_id.volume * move_line.quantity + if max_volume == 0: + record.volume_percentage = 0.0 + else: + record.volume_percentage = total_volume / max_volume * 100 + + @api.depends('picking_ids', 'picking_ids.move_ids', 'picking_ids.move_ids.product_id.weight') + def _compute_weight(self): + for record in self: + total_weight = 0 + for picking_line in record.picking_ids: + for move_line in picking_line.move_ids: + total_weight = total_weight + move_line.product_id.weight * move_line.quantity + record.weight = total_weight + + @api.depends('picking_ids', 'picking_ids.move_ids', 'picking_ids.move_ids.product_id.volume') + def _compute_volume(self): + for record in self: + total_volume = 0 + for picking_line in record.picking_ids: + for move_line in picking_line.move_ids: + total_volume = total_volume + move_line.product_id.volume * move_line.quantity + record.volume = total_volume + + @api.depends('picking_ids') + def _compute_number_of_transfer(self): + for record in self: + record.number_of_transfer = len(record.picking_ids) + + @api.depends('picking_ids', 'picking_ids.move_ids') + def _compute_number_of_line(self): + for record in self: + total_lines = 0 + for picking_line in record.picking_ids: + total_lines = total_lines + len(picking_line.move_ids) + record.number_of_line = total_lines diff --git a/stock_transport/security/ir.model.access.csv b/stock_transport/security/ir.model.access.csv new file mode 100644 index 00000000000..db73dffc716 --- /dev/null +++ b/stock_transport/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_dock,access_dock,model_dock,stock_transport.group_stock_transport_admin,1,1,1,1 diff --git a/stock_transport/security/stock_transport_security.xml b/stock_transport/security/stock_transport_security.xml new file mode 100644 index 00000000000..5e1bd10f696 --- /dev/null +++ b/stock_transport/security/stock_transport_security.xml @@ -0,0 +1,7 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="group_stock_transport_admin" model="res.groups"> + <field name="name">Admin</field> + <field name="users" eval="[(4, ref('base.user_admin'))]"/> + </record> +</odoo> \ No newline at end of file diff --git a/stock_transport/views/fleet_vehicle_model_views.xml b/stock_transport/views/fleet_vehicle_model_views.xml new file mode 100644 index 00000000000..ba2c186891a --- /dev/null +++ b/stock_transport/views/fleet_vehicle_model_views.xml @@ -0,0 +1,28 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="fleet_vehicle_model_category_view_tree_inherit_stock_transport" model="ir.ui.view"> + <field name="name">fleet.vehicle.model.category.view.tree.inherit</field> + <field name="model">fleet.vehicle.model.category</field> + <field name="inherit_id" ref="fleet.fleet_vehicle_model_category_view_tree"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='name']" position="after"> + <field name="max_weight" groups="stock_transport.group_stock_transport_admin"></field> + <field name="max_volume" groups="stock_transport.group_stock_transport_admin"></field> + </xpath> + </field> + </record> + + <record id="fleet_vehicle_model_category_view_form_inherit_stock_transport" model="ir.ui.view"> + <field name="name">fleet.vehicle.model.category.view.form.inherit</field> + <field name="model">fleet.vehicle.model.category</field> + <field name="inherit_id" ref="fleet.fleet_vehicle_model_category_view_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='name']" position="after"> + <field name="max_weight" groups="stock_transport.group_stock_transport_admin"></field> + </xpath> + <xpath expr="//field[@name='sequence']" position="after"> + <field name="max_volume" groups="stock_transport.group_stock_transport_admin"></field> + </xpath> + </field> + </record> +</odoo> diff --git a/stock_transport/views/stock_picking_batch_views.xml b/stock_transport/views/stock_picking_batch_views.xml new file mode 100644 index 00000000000..7a4cec9cd30 --- /dev/null +++ b/stock_transport/views/stock_picking_batch_views.xml @@ -0,0 +1,51 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="stock_picking_batch_form_inherit_stock_transport" model="ir.ui.view"> + <field name="name">stock.picking.batch.form.inherit</field> + <field name="model">stock.picking.batch</field> + <field name="inherit_id" ref="stock_picking_batch.stock_picking_batch_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='scheduled_date']" position="after"> + <field name="dock_id" groups="stock_transport.group_stock_transport_admin"/> + <field name="vehicle_id" placeholder="Third party provider" groups="stock_transport.group_stock_transport_admin"/> + <field name="category_id" invisible="not vehicle_id" groups="stock_transport.group_stock_transport_admin"/> + <field name="weight_percentage" widget="progressbar" invisible="not category_id" groups="stock_transport.group_stock_transport_admin"/> + <field name="volume_percentage" widget="progressbar" invisible="not category_id" groups="stock_transport.group_stock_transport_admin"/> + </xpath> + <xpath expr="//button[@name='action_open_label_layout']" position="attributes"> + <attribute name="groups">stock_transport.group_stock_transport_admin</attribute> + </xpath> + </field> + </record> + + <record id="stock_picking_batch.stock_picking_batch_action" model="ir.actions.act_window"> + <field name="view_mode">tree,kanban,graph,gantt,form</field> + </record> + + <record id="stock_picking_batch_view_graph" model="ir.ui.view"> + <field name="name">stock.picking.batch.view.graph</field> + <field name="model">stock.picking.batch</field> + <field name="arch" type="xml"> + <graph string="Batch Transfer Graph View" type="bar"> + <field name="scheduled_date" interval="day"></field> + <field name="category_id"></field> + <field name="number_of_transfer" type="measure"></field> + </graph> + </field> + </record> + + <record id="stock_picking_batch_view_gantt" model="ir.ui.view"> + <field name="name">stock.picking.batch.view.gantt</field> + <field name="model">stock.picking.batch</field> + <field name="arch" type="xml"> + <gantt color="id" string="Gantt View" date_start="scheduled_date" date_stop="scheduled_date" + default_group_by="dock_id,vehicle_id" + default_scale="week" + decoration-info="state == 'draft'" + > + <field name="state"></field> + </gantt> + </field> + </record> + +</odoo> diff --git a/stock_transport/views/stock_picking_views.xml b/stock_transport/views/stock_picking_views.xml new file mode 100644 index 00000000000..dabb9596806 --- /dev/null +++ b/stock_transport/views/stock_picking_views.xml @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="vpicktree_inherit_stock_transport" model="ir.ui.view"> + <field name="name">vpicktree.inherit</field> + <field name="model">stock.picking</field> + <field name="inherit_id" ref="stock.vpicktree"/> + <field name="mode">extension</field> + <field name="arch" type="xml"> + <xpath expr="//field[@name='scheduled_date']" position="after"> + <field name="volume" groups="stock_transport.group_stock_transport_admin"/> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/sub_product/__init__.py b/sub_product/__init__.py new file mode 100644 index 00000000000..50235cbb122 --- /dev/null +++ b/sub_product/__init__.py @@ -0,0 +1,3 @@ +from . import models +from . import wizard +from . import report diff --git a/sub_product/__manifest__.py b/sub_product/__manifest__.py new file mode 100644 index 00000000000..25bcf62f638 --- /dev/null +++ b/sub_product/__manifest__.py @@ -0,0 +1,19 @@ +{ + 'name': "sub_product", + 'depends': ['sale_management'], + 'author': "sahm@odoo.com", + 'description': "This module will help to add sub-product in product.", + 'data': [ + 'security/ir.model.access.csv', + 'report/ir_actions_report_templates.xml', + 'wizard/sub_products_views.xml', + 'views/product_views.xml', + 'views/sale_order_views.xml', + ], + 'demo': [ + + ], + 'installable': True, + 'application': False, + 'auto_install': False +} diff --git a/sub_product/models/__init__.py b/sub_product/models/__init__.py new file mode 100644 index 00000000000..8f2f8c0cbc1 --- /dev/null +++ b/sub_product/models/__init__.py @@ -0,0 +1,3 @@ +from . import product_template +from . import sale_order_line +from . import sale_order diff --git a/sub_product/models/product_template.py b/sub_product/models/product_template.py new file mode 100644 index 00000000000..61689af56fb --- /dev/null +++ b/sub_product/models/product_template.py @@ -0,0 +1,9 @@ +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = 'product.template' + + is_kit = fields.Boolean() + sub_product_ids = fields.Many2many('product.product') + diff --git a/sub_product/models/sale_order.py b/sub_product/models/sale_order.py new file mode 100644 index 00000000000..9131218859c --- /dev/null +++ b/sub_product/models/sale_order.py @@ -0,0 +1,8 @@ +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + print_in_report = fields.Boolean() + \ No newline at end of file diff --git a/sub_product/models/sale_order_line.py b/sub_product/models/sale_order_line.py new file mode 100644 index 00000000000..94caaf465d6 --- /dev/null +++ b/sub_product/models/sale_order_line.py @@ -0,0 +1,9 @@ +from odoo import fields, models, api + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + is_kit = fields.Boolean(related='product_template_id.is_kit') + sale_order_line_id = fields.Many2one('sale.order.line', ondelete='cascade') + previously_set_price = fields.Float() diff --git a/sub_product/report/ir_actions_report_templates.xml b/sub_product/report/ir_actions_report_templates.xml new file mode 100644 index 00000000000..14de2733de6 --- /dev/null +++ b/sub_product/report/ir_actions_report_templates.xml @@ -0,0 +1,18 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <template id="report_saleorder_document_inherit_sub_product" name="Report" inherit_id="sale.report_saleorder_document"> + <xpath expr="//tbody//tr" position="attributes"> + <attribute name="t-if">(line.order_id.print_in_report) or (not line.sale_order_line_id)</attribute> + </xpath> + </template> + <template id="sale_order_portal_content_inherit_sub_product" name="Portal Preview" inherit_id="sale.sale_order_portal_content"> + <xpath expr="//tbody[@class='sale_tbody']//tr" position="attributes"> + <attribute name="t-if">(line.order_id.print_in_report) or (not line.sale_order_line_id)</attribute> + </xpath> + </template> + <template id="report_invoice_document_inherit_sub_product" name="Invoice" inherit_id="account.report_invoice_document"> + <xpath expr="//tbody[@class='invoice_tbody']//tr" position="attributes"> + <attribute name="t-if">(line.sale_line_ids.order_id.print_in_report) or (not line.sale_line_ids.sale_order_line_id)</attribute> + </xpath> + </template> +</odoo> \ No newline at end of file diff --git a/sub_product/security/ir.model.access.csv b/sub_product/security/ir.model.access.csv new file mode 100644 index 00000000000..f037aeecff4 --- /dev/null +++ b/sub_product/security/ir.model.access.csv @@ -0,0 +1,4 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink + +access_sub_products,access_sub_products,model_sub_products,base.group_user,1,1,1,1 +access_sub_products_line,access_sub_products_line,model_sub_products_line,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/sub_product/views/product_views.xml b/sub_product/views/product_views.xml new file mode 100644 index 00000000000..65824847f9d --- /dev/null +++ b/sub_product/views/product_views.xml @@ -0,0 +1,14 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="product_template_form_view_inherit_sub_product" model="ir.ui.view"> + <field name="name">product.template.view.form.inherit</field> + <field name="model">product.template</field> + <field name="inherit_id" ref="product.product_template_form_view"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='product_tooltip']" position="after"> + <field name="is_kit"/> + <field name="sub_product_ids" widget="many2many_tags" invisible="not is_kit"/> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/sub_product/views/sale_order_views.xml b/sub_product/views/sale_order_views.xml new file mode 100644 index 00000000000..f971259c7d6 --- /dev/null +++ b/sub_product/views/sale_order_views.xml @@ -0,0 +1,45 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="view_order_form_inherit_sub_product" model="ir.ui.view"> + <field name="name">sale.order.view.form.inherit</field> + <field name="model">sale.order</field> + <field name="inherit_id" ref="sale.view_order_form"/> + <field name="arch" type="xml"> + <xpath expr="//field[@name='payment_term_id']" position="after"> + <field name="print_in_report"/> + </xpath> + <xpath expr="//field[@name='product_template_id']" position="after"> + <field name="is_kit" optional='hide'/> + <field name="sale_order_line_id" optional='hide' readonly="sale_order_line_id"/> + <button string="Sub Product" name="sub_product.action_sub_products" type="action" class="oe_highlight" invisible="state == 'sale' or not is_kit" context="{'product_name': name}" /> + </xpath> + <xpath expr="//tree//field[@name='product_id']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='product_template_id']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='name']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='product_uom_qty']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='customer_lead']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='price_unit']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='tax_id']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='price_subtotal']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + <xpath expr="//tree//field[@name='price_total']" position="attributes"> + <attribute name="readonly">sale_order_line_id</attribute> + </xpath> + </field> + </record> +</odoo> \ No newline at end of file diff --git a/sub_product/wizard/__init__.py b/sub_product/wizard/__init__.py new file mode 100644 index 00000000000..6d701129125 --- /dev/null +++ b/sub_product/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import sub_products +from . import sub_products_line \ No newline at end of file diff --git a/sub_product/wizard/sub_products.py b/sub_product/wizard/sub_products.py new file mode 100644 index 00000000000..896f390ffe0 --- /dev/null +++ b/sub_product/wizard/sub_products.py @@ -0,0 +1,56 @@ +from odoo import fields, models, api + + +class SubProducts(models.TransientModel): + _name = 'sub.products' + _description = "Sub Product" + + name = fields.Char() + sub_product_line_ids = fields.One2many('sub.products.line', 'sub_product_id') + + @api.model + def default_get(self, fields_list): + res = super().default_get(fields_list) + active_id = self.env.context.get('active_id') + sub_product = self.env['sale.order.line'].search([('sale_order_line_id', '=', active_id)]) + sale_order_line_current_record = self.env['sale.order.line'].browse(active_id) + sub_product_lines = [] + if len(sub_product) > 0: + for product in sub_product: + sub_product_lines.append((0, 0, { + 'product_id': product.product_id.id, + 'quantity': product.product_uom_qty, + 'price': product.previously_set_price + })) + else: + for product in sale_order_line_current_record.product_template_id.sub_product_ids: + sub_product_lines.append((0, 0, { + 'product_id' : product.id, + 'quantity': 1.0, + 'price': product.lst_price + })) + res['sub_product_line_ids'] = sub_product_lines + return res + + def action_add_sub_product(self): + active_id = self.env.context.get('active_id') + sub_product = self.env['sale.order.line'].search([('sale_order_line_id', '=', active_id)]) + if len(sub_product) > 0: + self.env['sale.order.line'].search([('sale_order_line_id', '=', active_id)]).unlink() + product_order_line = self.env['sale.order.line'].browse(active_id) + new_price_unit = product_order_line.product_id.lst_price + for sub_product_line in self.sub_product_line_ids: + self.env['sale.order.line'].create( + { + 'order_id': product_order_line.order_id.id, + 'product_id': sub_product_line.product_id.id, + 'name': sub_product_line.product_id.name, + 'product_uom_qty': sub_product_line.quantity, + 'price_unit': 0.0, + 'customer_lead': 0.0, + 'sale_order_line_id': active_id, + 'previously_set_price': sub_product_line.price + } + ) + new_price_unit = new_price_unit + sub_product_line.quantity * sub_product_line.price + product_order_line.price_unit = new_price_unit diff --git a/sub_product/wizard/sub_products_line.py b/sub_product/wizard/sub_products_line.py new file mode 100644 index 00000000000..482b960e945 --- /dev/null +++ b/sub_product/wizard/sub_products_line.py @@ -0,0 +1,12 @@ +from odoo import fields, models, api + + +class SubProuctsLine(models.TransientModel): + _name = 'sub.products.line' + _description = "Sub Product Line" + + sub_product_id = fields.Many2one('sub.products') + product_id = fields.Many2one('product.product') + product_name = fields.Char(related='product_id.name') + quantity = fields.Integer() + price = fields.Float(readonly=False, store=True) diff --git a/sub_product/wizard/sub_products_views.xml b/sub_product/wizard/sub_products_views.xml new file mode 100644 index 00000000000..ad377c3e277 --- /dev/null +++ b/sub_product/wizard/sub_products_views.xml @@ -0,0 +1,35 @@ +<?xml version='1.0' encoding='utf-8'?> +<odoo> + <record id="action_sub_products" model="ir.actions.act_window"> + <field name="name">Add Sub Product</field> + <field name="res_model">sub.products</field> + <field name="view_mode">form</field> + <field name="target">new</field> + <field name="context">{'default_name': context.get('product_name')}</field> + </record> + + <record id="sub_products_view_form" model="ir.ui.view"> + <field name="name">sub.products.view.form</field> + <field name="model">sub.products</field> + <field name="arch" type="xml"> + <form> + <h4> + <field name="name" readonly='1'/> + Sub Products: + </h4> + <field name="sub_product_line_ids"> + <tree create='0' editable='1'> + <field name="product_id"/> + <field name="quantity"/> + <field name="price"/> + </tree> + </field> + <footer> + <button string="Confirm" name="action_add_sub_product" type="object" class="oe_highlight"/> + <button string="Cancel" class="btn btn-secondary" special="cancel"/> + </footer> + </form> + </field> + </record> + +</odoo> \ No newline at end of file