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..9b55718ec85 --- /dev/null +++ b/estate_account/__manifest__.py @@ -0,0 +1,8 @@ +{ + 'name': 'Estate Account Module', + 'depends': ['base', 'account'], + 'data': [ + 'security/ir.model.access.csv', + ], + 'license': 'LGPL-3', +} diff --git a/estate_account/models/__init__.py b/estate_account/models/__init__.py new file mode 100644 index 00000000000..f4c8fd6db6d --- /dev/null +++ b/estate_account/models/__init__.py @@ -0,0 +1 @@ +from . import estate_property \ No newline at end of file diff --git a/estate_account/models/estate_account.py b/estate_account/models/estate_account.py new file mode 100644 index 00000000000..b22650491c1 --- /dev/null +++ b/estate_account/models/estate_account.py @@ -0,0 +1,48 @@ +from odoo import fields, models, api +from odoo.exceptions import UserError +from odoo import Command + + +class EstateAccount(models.Model): + _inherit = 'estate.property' + + def action_sold(self): + for property_record in self: + if property_record.state == 'cancelled': + raise UserError("A cancelled property cannot be set as sold") + + partner_id = property_record.buyer_id + selling_price = property_record.selling_price + + invoice_line_1 = Command.create( + { + 'name': 'Property Sale', + 'quantity': 1, + 'price_unit': selling_price * 1.06, + }, + ) + + invoice_line_2 = Command.create( + { + 'name': 'Administrative Fees', + 'quantity': 1, + 'price_unit': 100.00, + }, + ) + + journal_id = self.env['account.journal'].search([('type', '=', 'sale')], limit=1) + + if not journal_id: + raise UserError("No sale journal found") + + invoice_values = { + 'partner_id': partner_id.id, + 'move_type': 'out_invoice', + 'journal_id': journal_id.id, + 'invoice_date': fields.Date.today(), + 'invoice_line_ids': [invoice_line_1, invoice_line_2], + } + + self.env['account.move'].create(invoice_values) + + return super().action_sold() diff --git a/estate_account/security/ir.model.access.csv b/estate_account/security/ir.model.access.csv new file mode 100644 index 00000000000..b3e04f21b9d --- /dev/null +++ b/estate_account/security/ir.model.access.csv @@ -0,0 +1,2 @@ +'id','name','model_id:id','group_id:id','perm_read','perm_write','perm_create','perm_unlink' +'model_estate_property_base_group_allow_all','model.estate.property.base.group.allow_all','model_estate_property','base.group_user',1,1,1,1 diff --git a/estate_property/__init__.py b/estate_property/__init__.py new file mode 100644 index 00000000000..0650744f6bc --- /dev/null +++ b/estate_property/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/estate_property/__manifest__.py b/estate_property/__manifest__.py new file mode 100644 index 00000000000..f80492fd024 --- /dev/null +++ b/estate_property/__manifest__.py @@ -0,0 +1,14 @@ +{ + 'name': 'Estate Module', + 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + 'views/estate_res_users.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_menus.xml', + ], + 'license': 'LGPL-3', +} diff --git a/estate_property/models/__init__.py b/estate_property/models/__init__.py new file mode 100644 index 00000000000..9a2189b6382 --- /dev/null +++ b/estate_property/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_property/models/estate_property.py b/estate_property/models/estate_property.py new file mode 100644 index 00000000000..6c53800c39d --- /dev/null +++ b/estate_property/models/estate_property.py @@ -0,0 +1,156 @@ +from datetime import date +from dateutil.relativedelta import relativedelta + +from odoo import api, fields, models +from odoo.exceptions import UserError, ValidationError +from odoo.tools.float_utils import float_compare, float_is_zero + + +class EstateProperty(models.Model): + _name = 'estate.property' + _description = 'Real Estate Property' + _order = 'id desc' + _sql_constraints = [ + ( + 'expected_price_strictly_positive', + 'CHECK(expected_price > 0)', + 'Expected price must be strictly positive', + ), + ( + 'selling_price_positive', + 'CHECK(selling_price >= 0)', + 'Selling price must be positive', + ), + ] + + property_type_id = fields.Many2one( + 'estate.property.type', + string='Property Type', + ) + buyer_id = fields.Many2one( + 'res.partner', + compute='_compute_infos_from_accepted_offer', + string='Buyer', + copy=False, + ) + salesperson_id = fields.Many2one( + 'res.users', + string='Salesperson', + default=lambda self: self.env.user, + copy=False, + ) + tag_ids = fields.Many2many('estate.property.tag') + offer_ids = fields.One2many('estate.property.offer', 'property_id', string='Offers') + total_area = fields.Float(compute='_compute_total_area', string='Total Area (sqm)') + best_offer = fields.Float(compute='_compute_best_offer', string='Best Offer') + active = fields.Boolean(default=True) + name = fields.Char(string='Title', required=True) + description = fields.Text(string='Description') + postcode = fields.Char(string='Postcode') + date_availability = fields.Date( + string='Available From', + copy=False, + default=date.today() + relativedelta(months=3), # noqa: DTZ011 + ) + expected_price = fields.Float(string='Expected Price', required=True) + selling_price = fields.Float( + compute='_compute_infos_from_accepted_offer', + string='Selling Price', + readonly=True, + copy=False, + ) + bedrooms = fields.Integer(string='Bedrooms', default=2) + living_area = fields.Integer(string='Living Area (sqm)') + facades = fields.Integer(string='Number of Facades') + garage = fields.Boolean(string='Has Garage?') + garden = fields.Boolean(string='Has Garden?') + garden_area = fields.Integer(string='Garden Area (sqm)') + garden_orientation = fields.Selection( + selection=[ + ('north', 'North'), + ('south', 'South'), + ('east', 'East'), + ('west', 'West'), + ], + string='Garden Orientation', + ) + state = fields.Selection( + selection=[ + ('new', 'New'), + ('offer_received', 'Offer Received'), + ('offer_accepted', 'Offer Accepted'), + ('sold', 'Sold'), + ('cancelled', 'Cancelled'), + ], + string='Status', + required=True, + copy=False, + default='new', + ) + note = fields.Text('Special mentions about the house') + + @api.depends('offer_ids.status') + def _compute_infos_from_accepted_offer(self): + for property in self: + for offer in property.offer_ids: + if offer.status == 'accepted': + property.buyer_id = offer.partner_id + property.selling_price = offer.price + return + property.buyer_id = None + property.selling_price = None + + @api.depends('living_area', 'garden_area') + def _compute_total_area(self): + for property in self: + property.total_area = property.living_area + property.garden_area + + @api.depends('offer_ids.price') + def _compute_best_offer(self): + for property in self: + if property.offer_ids: + property.best_offer = max(map(lambda r: r.price, property.offer_ids)) + else: + property.best_offer = None + + @api.constrains('expected_price', 'selling_price') + def _check_selling_price(self): + for property in self: + if not float_is_zero(property.selling_price, precision_digits=2): + if ( + float_compare( + property.selling_price, + property.expected_price * 0.9, + precision_digits=2, + ) + < 0 + ): + raise ValidationError("The selling price cannot be lower than 90% of the expected price!") + + @api.onchange('garden') + def _onchange_partner_id(self): + if self.garden: + self.garden_area = 10 + self.garden_orientation = 'north' + else: + self.garden_area = 0 + self.garden_orientation = None + + @api.ondelete(at_uninstall=False) + def _unlink_only_new_and_cancelled(self): + if any(property.state != 'new' and property.state != 'cancelled' for property in self): + raise UserError("Can't delete if state is not New or Cancelled!") + + def action_sold(self): + for property in self: + if property.state != 'cancelled': + property.state = 'sold' + return True + raise UserError("A cancelled property cannot be set as sold") + + def action_cancel(self): + for property in self: + if property.state != 'sold': + property.state = 'cancelled' + return True + raise UserError("A sold property cannot be set as cancelled") diff --git a/estate_property/models/estate_property_offer.py b/estate_property/models/estate_property_offer.py new file mode 100644 index 00000000000..c7ba6a74547 --- /dev/null +++ b/estate_property/models/estate_property_offer.py @@ -0,0 +1,64 @@ +from datetime import timedelta + +from odoo import api, fields, models +from odoo.exceptions import ValidationError + + +class EstatePropertyOffer(models.Model): + _name = 'estate.property.offer' + _description = 'Real Estate Property Offer' + _order = 'price desc' + _sql_constraints = [ + ( + 'offer_price_strictly_positive', + 'CHECK(price > 0)', + 'Offer price must be strictly positive', + ), + ] + + date_deadline = fields.Date( + compute='_compute_date_deadline', + inverse='_inverse_date_deadline', + string='Date Deadline', + ) + price = fields.Float() + validity = fields.Integer(default=7, string='Validity (days)') + status = fields.Selection( + [('accepted', 'Accepted'), ('refused', 'Refused')], + copy=False, + ) + partner_id = fields.Many2one('res.partner', required=True) + property_id = fields.Many2one('estate.property', required=True) + property_type_id = fields.Many2one(related='property_id.property_type_id', store=True) + + @api.depends('validity') + def _compute_date_deadline(self): + for record in self: + record.date_deadline = (record.create_date.date() or fields.Date.now()) + timedelta(days=record.validity) + + def _inverse_date_deadline(self): + for record in self: + record.validity = (record.date_deadline -(record.create_date.date() or fields.Date.now())).days + + @api.model + def create(self, vals): + property_id = vals.get('property_id') + + property = self.env['estate.property'].browse(property_id) + + property.state = 'offer_received' + + existing_offers = self.env['estate.property.offer'].search([('property_id', '=', property_id), ('price', '>=', vals['price'])]) + + if existing_offers: + raise ValidationError("The offer value must be higher than existing offers.") + + return super().create(vals) + + def action_accept_offer(self): + for record in self: + record.status = 'accepted' + + def action_refuse_offer(self): + for record in self: + record.status = 'refused' diff --git a/estate_property/models/estate_property_tag.py b/estate_property/models/estate_property_tag.py new file mode 100644 index 00000000000..e6f4238e3b3 --- /dev/null +++ b/estate_property/models/estate_property_tag.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class EstatePropertyTag(models.Model): + _name = 'estate.property.tag' + _description = 'Real Estate Property Tag' + + name = fields.Char(string='Name', required=True) + color = fields.Integer(string='Color') + _order = 'name' + _sql_constraints = [('name_unique', 'UNIQUE(name)', 'Property tag name must be unique')] diff --git a/estate_property/models/estate_property_type.py b/estate_property/models/estate_property_type.py new file mode 100644 index 00000000000..83d7f46114f --- /dev/null +++ b/estate_property/models/estate_property_type.py @@ -0,0 +1,21 @@ +from odoo import api, fields, models + + +class EstatePropertyType(models.Model): + _name = 'estate.property.type' + _description = 'Real Estate Property Type' + _order = 'name' + + sequence = fields.Integer('Sequence', default=1) + + name = fields.Char(string='Title', required=True) + property_ids = fields.One2many('estate.property', 'property_type_id', string='Properties') + _sql_constraints = [('name_unique', 'UNIQUE(name)', 'Property type name must be unique')] + + offer_ids = fields.One2many('estate.property.offer', 'property_type_id', string='Offers') + offer_count = fields.Integer(compute='_compute_offer_count', string='Offer Count') + + @api.depends('offer_ids') + def _compute_offer_count(self): + for record in self: + record.offer_count = len(record.offer_ids) diff --git a/estate_property/models/res_users.py b/estate_property/models/res_users.py new file mode 100644 index 00000000000..bd8b0b5b40e --- /dev/null +++ b/estate_property/models/res_users.py @@ -0,0 +1,7 @@ +from odoo import fields, models + + +class ResUsers(models.Model): + _inherit = "res.users" + + property_ids = fields.One2many("estate.property", "salesperson_id", string="Properties") diff --git a/estate_property/security/ir.model.access.csv b/estate_property/security/ir.model.access.csv new file mode 100644 index 00000000000..867406d0f9c --- /dev/null +++ b/estate_property/security/ir.model.access.csv @@ -0,0 +1,5 @@ +'id','name','model_id:id','group_id:id','perm_read','perm_write','perm_create','perm_unlink' +'model_estate_property_base_group_allow_all','model.estate.property.base.group.allow_all','model_estate_property','base.group_user',1,1,1,1 +'model_estate_property_type_base_group_allow_all','model.estate.property.type.base.group.allow_all','model_estate_property_type','base.group_user',1,1,1,1 +'model_estate_property_tag_base_group_allow_all','model.estate.property.tag.base.group.allow_all','model_estate_property_tag','base.group_user',1,1,1,1 +'model_estate_property_offer_base_group_allow_all','model.estate.property.offer.base.group.allow_all','model_estate_property_offer','base.group_user',1,1,1,1 diff --git a/estate_property/views/estate_menus.xml b/estate_property/views/estate_menus.xml new file mode 100644 index 00000000000..5ef037a559b --- /dev/null +++ b/estate_property/views/estate_menus.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/estate_property/views/estate_property_offer_views.xml b/estate_property/views/estate_property_offer_views.xml new file mode 100644 index 00000000000..a71a4d4e20c --- /dev/null +++ b/estate_property/views/estate_property_offer_views.xml @@ -0,0 +1,44 @@ + + + Property Offers + estate.property.offer + list,form + [('property_type_id', '=', active_id)] + + + + estate.property.offer.view.list + estate.property.offer + + + + + + + + + +

+ +

+ + + + + + + + + + + +
+ +
+
+ + + estate.property.type.view.search + estate.property.type + + + + + + + +
\ No newline at end of file diff --git a/estate_property/views/estate_property_views.xml b/estate_property/views/estate_property_views.xml new file mode 100644 index 00000000000..e894e5c4382 --- /dev/null +++ b/estate_property/views/estate_property_views.xml @@ -0,0 +1,172 @@ + + + Properties + estate.property + list,form,kanban + {'search_default_available': True} + + + + estate.property.view.kanban + estate.property + + + + + +
+
+ + + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+ + + estate.property.view.list + estate.property + + + + + + + + + + + + + + + + estate.property.view.form + estate.property + +
+
+
+ + +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + + estate.property.view.search + estate.property + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/estate_property/views/res_users_views.xml b/estate_property/views/res_users_views.xml new file mode 100644 index 00000000000..9fba54336ee --- /dev/null +++ b/estate_property/views/res_users_views.xml @@ -0,0 +1,15 @@ + + + res.users.views.form.inherit.estate.property + res.users + + + + + + + + + + + \ No newline at end of file diff --git a/website_airproof/README.md b/website_airproof/README.md new file mode 100644 index 00000000000..6c6390b9424 --- /dev/null +++ b/website_airproof/README.md @@ -0,0 +1,18 @@ +# Odoo Tutorial : Build a website theme + +This branch contains the code necessary for the creation of the website for our Airproof example. +Example used to illustrate the exercises given in the Odoo tutorial: Build a website theme. + +Here is the final design of the 4 pages of Airproof that will be created throughout this tutorial. + +**Home** +![Airproof home page](airproof-home-page.jpg) + +**Contact page** +![Airproof contact page](airproof-contact-page.jpg) + +**Shop page** +![Airproof shop page](airproof-shop-page.jpg) + +**Product page** +![Airproof product page](airproof-product-page.jpg) diff --git a/website_airproof/__init__.py b/website_airproof/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/website_airproof/__manifest__.py b/website_airproof/__manifest__.py new file mode 100644 index 00000000000..439d09e6b72 --- /dev/null +++ b/website_airproof/__manifest__.py @@ -0,0 +1,49 @@ +{ + 'name': 'Airproof Theme', + 'description': 'Airproof Theme - Drones, modelling, camera', + 'category': 'Website/Theme', + # 'version': '18.0.1.0', + 'author': 'PSBE Designers', + 'license': 'LGPL-3', + 'depends': ['website_sale', 'website_sale_wishlist', 'website_blog', 'website_mass_mailing'], + 'data': [ + # Options + 'data/presets.xml', + # Menu + 'data/menu.xml', + # Shapes + 'data/shapes.xml', + # Pages + 'data/pages/home.xml', + 'data/pages/contact.xml', + # Frontend + 'views/website_templates.xml', + 'views/website_sale_templates.xml', + 'views/website_sale_wishlist_templates.xml', + # Snippets + 'views/snippets/options.xml', + 'views/snippets/s_airproof_carousel.xml', + # Images + 'data/images.xml', + ], + 'assets': { + 'web._assets_primary_variables': [ + 'website_airproof/static/src/scss/primary_variables.scss', + ], + 'web._assets_frontend_helpers': [ + ('prepend', 'website_airproof/static/src/scss/bootstrap_overridden.scss'), + ], + 'web.assets_frontend': [ + # SCSS + 'website_airproof/static/src/scss/components/mouse_follower.scss', + 'website_airproof/static/src/scss/layout/header.scss', + 'website_airproof/static/src/scss/pages/product_page.scss', + 'website_airproof/static/src/scss/pages/shop.scss', + 'website_airproof/static/src/scss/snippets/caroussel.scss', + 'website_airproof/static/src/scss/snippets/newsletter.scss', + 'website_airproof/static/src/snippets/s_airproof_carousel/000.scss', + # JS + 'website_airproof/static/src/js/mouse_follower.js', + ], + }, +} diff --git a/website_airproof/airproof-contact-page.jpg b/website_airproof/airproof-contact-page.jpg new file mode 100644 index 00000000000..cc1008a28cb Binary files /dev/null and b/website_airproof/airproof-contact-page.jpg differ diff --git a/website_airproof/airproof-home-page.jpg b/website_airproof/airproof-home-page.jpg new file mode 100644 index 00000000000..53eb1d617b2 Binary files /dev/null and b/website_airproof/airproof-home-page.jpg differ diff --git a/website_airproof/airproof-product-page.jpg b/website_airproof/airproof-product-page.jpg new file mode 100644 index 00000000000..bf07f58651a Binary files /dev/null and b/website_airproof/airproof-product-page.jpg differ diff --git a/website_airproof/airproof-shop-page.jpg b/website_airproof/airproof-shop-page.jpg new file mode 100644 index 00000000000..0a6a5d50a44 Binary files /dev/null and b/website_airproof/airproof-shop-page.jpg differ diff --git a/website_airproof/data/images.xml b/website_airproof/data/images.xml new file mode 100644 index 00000000000..08018dc2ccd --- /dev/null +++ b/website_airproof/data/images.xml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + White arrow icon + + ir.ui.view + + + + Glasses icon + + ir.ui.view + + + + 4K icon + + ir.ui.view + + + + Hand with drone icon + + ir.ui.view + + + + Control icon + + ir.ui.view + + + + Shopping icon + + ir.ui.view + + + + Phone icon + + ir.ui.view + + + + Envelop icon + + ir.ui.view + + + + Arrow icon + + ir.ui.view + + + + Small arrow icon + + ir.ui.view + + + + Check icon + + ir.ui.view + + + + Black Phone icon + + ir.ui.view + + + + Black Envelop icon + + ir.ui.view + + + + + + Drone Airproof Mini + + ir.ui.view + + + + Drone Airproof Pro + + ir.ui.view + + + + Drone Airproof Robin + + ir.ui.view + + + + Drone Airproof Falcon + + ir.ui.view + + + + Drone Airproof Eagle + + ir.ui.view + + + + + + + Drone accessories picture + + ir.ui.view + + + + Drone Robin picture + + ir.ui.view + + + + Drone school picture + + ir.ui.view + + + + Drone flying picture + + ir.ui.view + + + + + + Fields topview + + ir.ui.view + + + + + + Highway topview + + ir.ui.view + + + + + + Drone with blue background picture + + ir.ui.view + + + + + + Sticker logo + + ir.ui.view + + + + Drone picture + + ir.ui.view + + + diff --git a/website_airproof/data/menu.xml b/website_airproof/data/menu.xml new file mode 100644 index 00000000000..ee0a9319f19 --- /dev/null +++ b/website_airproof/data/menu.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + Waterproof drones + + 1 + 10 + +
+ +
+
+
+ + + + About us + /about-us + 1 + + 20 + + + + + Blog + + 1 + 30 + + + Our latest news + /blog/our-latest-news-2 + 1 + + 31 + + + Tutorials + /blog/tutorials-3 + 1 + + 32 + + + + + Contact us + /contactus + 1 + + 40 + +
diff --git a/website_airproof/data/pages/contact.xml b/website_airproof/data/pages/contact.xml new file mode 100644 index 00000000000..f6a4502dc75 --- /dev/null +++ b/website_airproof/data/pages/contact.xml @@ -0,0 +1,156 @@ + + + + + + + + + Contact us + + website_airproof.page_contact + /contactus + + qweb + + + + + Contact us + + + +