diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000000..9ba478bfe1f
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,467 @@
+# Ignore everything
+*
+
+# Whitelist web and board
+!addons
+addons/*
+!addons/web
+!addons/web/**/*
+!addons/board
+!addons/board/**/*
+
+# Whitelist web_enterprise
+!web_enterprise
+!web_enterprise/**/*
+
+# Whitelist web_mobile
+!web_mobile
+!web_mobile/**/*
+
+# Whitelist web_studio
+!web_studio
+!web_studio/**/*
+
+# BlackList libs
+addons/web/static/lib/**/*
+
+# Whitelist Hoot
+!addons/web/static/lib/hoot
+!addons/web/static/lib/hoot/**/*
+
+# Ignore everything in web legacy but the top level (adapters)
+addons/web/static/src/legacy/**/*
+!addons/web/static/src/legacy
+!addons/web/static/src/legacy/*.js
+
+# Ignore everything in web_enterprise legacy but the top level (adapters)
+web_enterprise/static/src/legacy/**/*
+!web_enterprise/static/src/legacy
+!web_enterprise/static/src/legacy/*.js
+
+# Ignore everything in web_studio legacy but the top level (adapters)
+web_studio/static/src/legacy/**/*
+!web_studio/static/src/legacy
+!web_studio/static/src/legacy/*.js
+
+# Ignore all legacy related tests
+addons/web/static/tests/**/legacy/*
+web_enterprise/static/tests/**/legacy/*
+web_studio/static/tests/**/legacy/*
+
+# base_import
+# whitelist new code
+!addons/base_import
+!addons/base_import/**/*
+# blacklist legacy
+addons/base_import/static/src/legacy/**/*
+
+# web_cohort
+# whitelist new code
+!web_cohort
+!web_cohort/**/*
+
+# blacklist legacy
+web_cohort/static/src/legacy/**/*
+web_cohort/static/tests/legacy/**/*
+
+# web_gantt
+# whitelist new code
+!web_gantt
+!web_gantt/**/*
+
+# blacklist legacy
+web_gantt/static/src/legacy/**/*
+web_gantt/static/tests/legacy/**/*
+
+# Whitelist html_editor
+!addons/html_editor
+!addons/html_editor/**/*
+
+# Whitelist html_builder
+!addons/html_builder
+!addons/html_builder/**/*
+
+# blacklist html_editor libs
+addons/html_editor/static/lib/diff2html/*.js
+
+# Whitelist website
+!addons/website
+!addons/website/**/*
+
+# Whitelist website blog
+!addons/website_blog
+!addons/website_blog/**/*
+
+# Whitelist test_website
+!addons/test_website
+!addons/test_website/**/*
+
+# Whitelist test_website_modules
+!addons/test_website_modules
+!addons/test_website_modules/**/*
+
+# Whitelist website_customer
+!addons/website_customer
+!addons/website_customer/**/*
+
+# Whitelist website_google_map
+!addons/website_google_map
+!addons/website_google_map/**/*
+
+# Whitelist website_cf_turnstile
+!addons/website_cf_turnstile
+!addons/website_cf_turnstile/**/*
+
+# Whitelist website_project
+!addons/website_project
+!addons/website_project/**/*
+
+# Whitelist website_enterprise
+!website_enterprise
+!website_enterprise/**/*
+
+# Whitelist website_studio
+!website_studio
+!website_studio/**/*
+
+# Whitelist website_generator
+!website_generator
+!website_generator/**/*
+
+# Whitelist website_documents
+!website_documents
+!website_documents/**/*
+
+# Whitelist website_product_barecodelookup
+!website_product_barcodelookup
+!website_product_barcodelookup/**/*
+
+# Whitelist web_grid
+!web_grid
+!web_grid/**/*
+
+# Whitelist timesheet_grid
+!timesheet_grid
+!timesheet_grid/**/*
+
+# Whitelist timer
+!timer
+!timer/**/*
+
+# Whitelist industry_fsm
+!industry_fsm
+!industry_fsm/**/*
+
+# Whitelist helpdesk
+!helpdesk
+!helpdesk/**
+!helpdesk_timesheet
+!helpdesk_timesheet/**
+!helpdesk_sale_timesheet
+!helpdesk_sale_timesheet/**
+
+# planning
+# whitelist new code
+!planning
+!planning/static
+!planning/static/src
+!planning/static/src/*.js
+!planning/static/tests
+!planning/static/tests/planning_gantt_tests.js
+
+# project_enterprise
+# whitelist new code
+!project_enterprise
+!project_enterprise/static
+!project_enterprise/static/src
+!project_enterprise/static/src/*.js
+!project_enterprise/static/tests
+!project_enterprise/static/tests/*.js
+
+# web_map
+# whitelist new code
+!web_map
+!web_map/**/*
+
+# blacklist legacy
+web_map/static/src/legacy/**/*
+web_map/static/tests/legacy/**/*
+
+# whitelist web_tour
+!addons/web_tour
+!addons/web_tour/**/*
+
+# whitelist base_setup
+!addons/base_setup
+!addons/base_setup/**/*
+
+# whitelist purchase setup
+!addons/purchase
+!addons/purchase/**/*
+
+# Whitelist documents_document
+!documents
+!documents/**/*
+
+# Whitelist documents_spreadsheet
+!documents_spreadsheet
+!documents_spreadsheet/**/*
+
+# Whitelist spreadsheet
+!addons/spreadsheet
+!addons/spreadsheet/**/*
+
+# blacklist o-spreadsheet lib
+addons/spreadsheet/static/src/o_spreadsheet/o_spreadsheet.js
+
+# Whitelist spreadsheet_edition
+!spreadsheet_edition
+!spreadsheet_edition/**/*
+
+# Whitelist spreadsheet_account
+!addons/spreadsheet_account
+!addons/spreadsheet_account/**/*
+
+# Whitelist spreadsheet_dashboard
+!addons/spreadsheet_dashboard
+!addons/spreadsheet_dashboard/**/*
+
+# Whitelist spreadsheet_dashboard_account
+!addons/spreadsheet_dashboard_account
+!addons/spreadsheet_dashboard_account/**/*
+
+# Whitelist spreadsheet_dashboard_hr_expense
+!addons/spreadsheet_dashboard_hr_expense
+!addons/spreadsheet_dashboard_hr_expense/**/*
+
+# Whitelist spreadsheet_dashboard_pos_hr
+!addons/spreadsheet_dashboard_pos_hr
+!addons/spreadsheet_dashboard_pos_hr/**/*
+
+# Whitelist spreadsheet_dashboard_sale
+!addons/spreadsheet_dashboard_sale
+!addons/spreadsheet_dashboard_sale/**/*
+
+# Whitelist spreadsheet_dashboard_event_sale
+!addons/spreadsheet_dashboard_event_sale
+!addons/spreadsheet_dashboard_event_sale/**/*
+
+# Whitelist spreadsheet_dashboard_crm
+!spreadsheet_dashboard_crm
+!spreadsheet_dashboard_crm/**/*
+
+# Whitelist spreadsheet_dashboard_edition
+!spreadsheet_dashboard_edition
+!spreadsheet_dashboard_edition/**/*
+
+# Whitelist spreadsheet_dashboard_documents
+!spreadsheet_dashboard_documents
+!spreadsheet_dashboard_documents/**/*
+
+# Whitelist spreadsheet_sale_management
+!spreadsheet_sale_management
+!spreadsheet_sale_management/**/*
+
+# Whitelist bus
+!addons/bus/
+!addons/bus/**/*
+
+# Whitelist mail & dependents (with a lot of JS overrides)
+!addons/calendar
+!addons/calendar/**/*
+!addons/hr
+!addons/hr/**/*
+!addons/hr_holidays
+!addons/hr_holidays/**/*
+!addons/hr_skills
+!addons/hr_skills/**/*
+!addons/im_livechat
+!addons/im_livechat/**/*
+!addons/mail
+!addons/mail/**/*
+!addons/portal
+!addons/portal/**/*
+!addons/snailmail
+!addons/snailmail/**/*
+!addons/test_discuss_full
+!addons/test_discuss_full/**/*
+!addons/test_mail
+!addons/test_mail/**/*
+!addons/website_livechat
+!addons/website_livechat/**/*
+!addons/website_slides
+!addons/website_slides/**/*
+!approvals
+!approvals/**/*
+!test_discuss_full_enterprise
+!test_discuss_full_enterprise/**/*
+!test_mail_enterprise
+!test_mail_enterprise/**/*
+!whatsapp
+!whatsapp/**/*
+
+# Whitelist voip modules
+!voip*
+!voip*/**/*
+
+# BlackList voip libs
+voip/static/lib/**/*
+
+# Whitelist stock_barcode modules
+!stock_barcode
+!stock_barcode/**/*
+!stock_barcode_barcodelookup
+!stock_barcode_barcodelookup/**/*
+!stock_barcode_mrp
+!stock_barcode_mrp/**/*
+!stock_barcode_mrp_subcontracting
+!stock_barcode_mrp_subcontracting/**/*
+!stock_barcode_picking_batch
+!stock_barcode_picking_batch/**/*
+!stock_barcode_product_expiry
+!stock_barcode_product_expiry/**/*
+!stock_barcode_quality_control
+!stock_barcode_quality_control/**/*
+!stock_barcode_quality_control_picking_batch
+!stock_barcode_quality_control_picking_batch/**/*
+!stock_barcode_quality_mrp
+!stock_barcode_quality_mrp/**/*
+
+# Whitelist Brazilian eCommerce adaptations
+!addons/l10n_br_website_sale
+!addons/l10n_br_website_sale/**/*
+
+# Whitelist point_of_sale
+!addons/point_of_sale
+!addons/point_of_sale/**/*
+
+# Whitelist community pos modules
+!addons/iot_drivers
+!addons/iot_drivers/**/*
+!addons/l10n_ar_pos
+!addons/l10n_ar_pos/**/*
+!addons/l10n_co_pos
+!addons/l10n_co_pos/**/*
+!addons/l10n_es_pos
+!addons/l10n_es_pos/**/*
+!addons/l10n_fr_pos_cert
+!addons/l10n_fr_pos_cert/**/*
+!addons/l10n_gcc_pos
+!addons/l10n_gcc_pos/**/*
+!addons/l10n_in_pos
+!addons/l10n_in_pos/**/*
+!addons/l10n_sa_pos
+!addons/l10n_sa_pos/**/*
+!addons/pos_adyen
+!addons/pos_adyen/**/*
+!addons/pos_discount
+!addons/pos_discount/**/*
+!addons/pos_epson_printer
+!addons/pos_epson_printer/**/*
+!addons/pos_hr
+!addons/pos_hr/**/*
+!addons/pos_hr_restaurant
+!addons/pos_hr_restaurant/**/*
+!addons/pos_loyalty
+!addons/pos_loyalty/**/*
+!addons/pos_mrp
+!addons/pos_mrp/**/*
+!addons/pos_online_payment
+!addons/pos_online_payment/**/*
+!addons/pos_online_payment_self_order
+!addons/pos_online_payment_self_order/**/*
+!addons/pos_restaurant
+!addons/pos_restaurant/**/*
+!addons/pos_restaurant_adyen
+!addons/pos_restaurant_adyen/**/*
+!addons/pos_restaurant_stripe
+!addons/pos_restaurant_stripe/**/*
+!addons/pos_sale
+!addons/pos_sale/**/*
+!addons/pos_sale_loyalty
+!addons/pos_sale_loyalty/**/*
+!addons/pos_sale_margin
+!addons/pos_sale_margin/**/*
+!addons/pos_self_order
+!addons/pos_self_order/**/*
+!addons/pos_self_order_adyen
+!addons/pos_self_order_adyen/**/*
+!addons/pos_self_order_epson_printer
+!addons/pos_self_order_epson_printer/**/*
+!addons/pos_self_order_sale
+!addons/pos_self_order_sale/**/*
+!addons/pos_self_order_stripe
+!addons/pos_self_order_stripe/**/*
+!addons/pos_stripe
+!addons/pos_stripe/**/*
+!addons/spreadsheet_dashboard_pos_hr
+!addons/spreadsheet_dashboard_pos_hr/**/*
+
+# Whitelist enterprise pos modules
+!l10n_cl_edi_pos
+!l10n_cl_edi_pos/**/*
+!l10n_de_pos_cert
+!l10n_de_pos_cert/**/*
+!l10n_de_pos_res_cert
+!l10n_de_pos_res_cert/**/*
+!l10n_in_reports_gstr_pos
+!l10n_in_reports_gstr_pos/**/*
+!l10n_mx_edi_pos
+!l10n_mx_edi_pos/**/*
+!l10n_pl_reports_pos_jpk
+!l10n_pl_reports_pos_jpk/**/*
+!l10n_br_edi_pos
+!l10n_br_edi_pos/**/*
+!pos_account_reports
+!pos_account_reports/**/*
+!pos_blackbox_be
+!pos_blackbox_be/**/*
+!pos_enterprise
+!pos_enterprise/**/*
+!pos_hr_mobile
+!pos_hr_mobile/**/*
+!pos_iot
+!pos_iot/**/*
+!pos_iot_six
+!pos_iot_six/**/*
+!l10n_se_pos
+!l10n_se_pos/**/*
+!pos_online_payment_self_order_preparation_display
+!pos_online_payment_self_order_preparation_display/**/*
+!pos_order_tracking_display
+!pos_order_tracking_display/**/*
+!pos_restaurant_appointment
+!pos_restaurant_appointment/**/*
+!pos_restaurant_preparation_display
+!pos_restaurant_preparation_display/**/*
+!pos_sale_stock_renting
+!pos_sale_stock_renting/**/*
+!pos_self_order_preparation_display
+!pos_self_order_preparation_display/**/*
+!pos_settle_due
+!pos_settle_due/**/*
+!pos_tyro
+!pos_tyro/**/*
+
+# Whitelist misc enterprise modules
+!sign
+!sign/**
+
+!sign_itsme
+!sign_itsme/**
+
+# Whitelist the shop floor module
+!mrp_workorder
+!mrp_workorder/**
+
+!ai
+!ai/**
+
+!ai_livechat
+!ai_livechat/**
+
+!ai_website_livechat
+!ai_website_livechat/**
+
+!website_helpdesk_livechat
+!website_helpdesk_livechat/**
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 00000000000..d2072556e3d
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,69 @@
+{
+ "extends": ["eslint:recommended", "plugin:prettier/recommended"],
+ "parserOptions": {
+ "sourceType": "module",
+ "ecmaVersion": 2022
+ },
+ "env": {
+ "browser": true,
+ "es2022": true,
+ "qunit": true
+ },
+ "rules": {
+ "prettier/prettier": ["error", {
+ "tabWidth": 4,
+ "semi": true,
+ "singleQuote": false,
+ "printWidth": 100,
+ "endOfLine": "auto"
+ }],
+ "no-undef": "error",
+ "no-restricted-globals": ["error", "event", "self"],
+ "no-const-assign": ["error"],
+ "no-debugger": ["error"],
+ "no-dupe-class-members": ["error"],
+ "no-dupe-keys": ["error"],
+ "no-dupe-args": ["error"],
+ "no-dupe-else-if": ["error"],
+ "no-unsafe-negation": ["error"],
+ "no-duplicate-imports": ["error"],
+ "valid-typeof": ["error"],
+ "no-unused-vars": ["error", { "vars": "all", "args": "none", "ignoreRestSiblings": false, "caughtErrors": "all" }],
+ "curly": ["error", "all"],
+ "no-restricted-syntax": ["error", "PrivateIdentifier"],
+ "prefer-const": ["error", {
+ "destructuring": "all",
+ "ignoreReadBeforeAssign": true
+ }],
+ "arrow-body-style": ["error", "as-needed"]
+ },
+ "globals": {
+ "odoo": "readonly",
+ "$": "readonly",
+ "jQuery": "readonly",
+ "Chart": "readonly",
+ "fuzzy": "readonly",
+ "StackTrace": "readonly",
+ "QUnit": "readonly",
+ "luxon": "readonly",
+ "py": "readonly",
+ "FullCalendar": "readonly",
+ "globalThis": "readonly",
+ "ScrollSpy": "readonly",
+ "module": "readonly",
+ "chai": "readonly",
+ "describe": "readonly",
+ "it": "readonly",
+ "mocha": "readonly",
+ "DOMPurify": "readonly",
+ "Prism": "readonly",
+
+ "Alert": "readonly",
+ "Collapse": "readonly",
+ "Dropdown": "readonly",
+ "Modal": "readonly",
+ "Offcanvas": "readonly",
+ "Popover": "readonly",
+ "Tooltip": "readonly"
+ }
+}
diff --git a/.gitignore b/.gitignore
index b6e47617de1..409fab2b7c8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,6 +25,13 @@ share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
+
+
+*.sublime-project
+*.sublime-workspace
+
+node_modules/
+
MANIFEST
# PyInstaller
diff --git a/Estate/__init__.py b/Estate/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/Estate/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/Estate/__manifest__.py b/Estate/__manifest__.py
new file mode 100644
index 00000000000..02d808fb97d
--- /dev/null
+++ b/Estate/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ 'name': 'estate',
+ 'depends': ['base'],
+ 'application': True,
+ 'installable': True,
+ 'author': 'estate',
+ 'category': 'Tutorials',
+ 'license': 'AGPL-3',
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'data/date_cron.xml',
+ 'views/estate_property_type_views.xml',
+ 'views/estate_property_offer_views.xml',
+ 'views/estate_property_tags_views.xml',
+ 'views/estate_property_views.xml',
+ 'views/res_users_view.xml',
+ 'views/estate_menus.xml',
+ ],
+}
diff --git a/Estate/data/date_cron.xml b/Estate/data/date_cron.xml
new file mode 100644
index 00000000000..abcc6e9f648
--- /dev/null
+++ b/Estate/data/date_cron.xml
@@ -0,0 +1,10 @@
+
+
+ Sold if validity exceeds
+
+ code
+ model._cron_move_sold
+ 1
+ days
+
+
diff --git a/Estate/models/__init__.py b/Estate/models/__init__.py
new file mode 100644
index 00000000000..32834cf0ac3
--- /dev/null
+++ b/Estate/models/__init__.py
@@ -0,0 +1,5 @@
+from . import estate_property
+from . import estate_property_offer
+from . import estate_property_type
+from . import estate_property_tag
+from . import res_users
diff --git a/Estate/models/estate_property.py b/Estate/models/estate_property.py
new file mode 100644
index 00000000000..b01007a5626
--- /dev/null
+++ b/Estate/models/estate_property.py
@@ -0,0 +1,136 @@
+from datetime import timedelta
+
+from odoo import models, fields, api
+from odoo.exceptions import ValidationError, UserError
+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 "
+ name = fields.Char(required=True)
+ description = fields.Text()
+ postcode = fields.Char()
+ create_date = fields.Datetime()
+ expected_price = fields.Float(required=True)
+ selling_price = fields.Float(readonly=True)
+ bedrooms = fields.Integer()
+ living_area = fields.Integer()
+ facades = fields.Integer()
+ garage = fields.Boolean()
+ garden = fields.Boolean()
+ garden_area = fields.Integer()
+ garden_orientation = fields.Selection(
+ [
+ ("north", "North"),
+ ("south", "South"),
+ ("east", "East"),
+ ("west", "West"),
+ ]
+ )
+ active = fields.Boolean(default=True)
+ state = fields.Selection(
+ [
+ ("new", "New"),
+ ("offer_received", "Offer Received"),
+ ("offer_accepted", "Offer Accepted"),
+ ("sold", "Sold"),
+ ("canceled", "Canceled"),
+ ],
+ required=True,
+ copy=False,
+ default="new"
+ )
+ property_type_id = fields.Many2one("estate.property.type", string="Property Type")
+ buyer_id = fields.Many2one("res.partner", string="Buyer")
+ salesperson_id = fields.Many2one("res.users", string="Salesperson")
+ tag_ids = fields.Many2many("estate.property.tag", string="Tags")
+ offer_ids = fields.One2many("estate.property.offer", "property_id", string="Offers")
+ total_area = fields.Float(compute="_compute_total_area", string="Total Area", store=True)
+ best_price = fields.Float(compute="_compute_best_price", string="Best Offer", store=True)
+ validity_days = fields.Integer(default=7)
+ date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline", store=True)
+
+ _check_price = models.Constraint(
+ 'CHECK(expected_price > 0 AND selling_price >= 0)',
+ 'The Price of a property must be strictly positive.',
+ )
+
+ @api.depends("living_area", "garden_area")
+ def _compute_total_area(self):
+ for record in self:
+ record.total_area = (record.living_area or 0) + (record.garden_area or 0)
+
+ @api.depends("offer_ids.price", "state")
+ def _compute_best_price(self):
+ for record in self:
+ record.best_price = max(record.offer_ids.mapped("price")) if record.offer_ids else 0.0
+
+ @api.depends("create_date", "validity_days")
+ def _compute_date_deadline(self):
+ for record in self:
+ create_date = record.create_date or fields.Date.today()
+ if hasattr(create_date, "date"):
+ create_date = create_date.date()
+ record.date_deadline = create_date + timedelta(days=record.validity_days)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ create_date = record.create_date or fields.Date.today()
+ if hasattr(create_date, "date"):
+ create_date = create_date.date()
+ delta = (record.date_deadline - create_date).days if record.date_deadline else 0
+ record.validity_days = delta
+
+ @api.constrains('selling_price', 'expected_price')
+ def _check_selling_price(self):
+ for record in self:
+ if not float_is_zero(record.selling_price, precision_digits=2):
+ if float_compare(record.selling_price, 0.9 * record.expected_price, precision_digits=2) < 0:
+ raise ValidationError("The selling price cannot be lower than 90% of the expected price.")
+
+ @api.onchange("garden")
+ def _onchange_garden(self):
+ if self.garden:
+ self.garden_area = 10
+ self.garden_orientation = "north"
+ else:
+ self.garden_area = 0
+ self.garden_orientation = False
+
+ @api.model
+ def create(self, vals):
+ record = super().create(vals)
+ if record.offer_ids:
+ record.state = 'offer_received'
+ return record
+
+ def write(self, vals):
+ res = super().write(vals)
+ if 'offer_ids' in vals:
+ for record in self:
+ if record.offer_ids and record.state != 'offer_received':
+ record.state = 'offer_received'
+ return res
+
+ def action_set_sold(self):
+ for record in self:
+ if record.state != 'canceled':
+ record.state = 'sold'
+ else:
+ raise UserError("once sold cannot be canceled")
+
+ def action_set_canceled(self):
+ for record in self:
+ record.state = "canceled"
+
+ def action_back_to_new(self):
+ for record in self:
+ record.state = "new"
+
+ def _unlink(self):
+ for record in self:
+ if record.state in ["new"]:
+ raise ValidationError("You cannot delete a new or canceled property.")
+ return super().unlink()
diff --git a/Estate/models/estate_property_offer.py b/Estate/models/estate_property_offer.py
new file mode 100644
index 00000000000..ec86599b22e
--- /dev/null
+++ b/Estate/models/estate_property_offer.py
@@ -0,0 +1,78 @@
+from datetime import timedelta
+from odoo import models, fields, api
+from odoo.exceptions import UserError
+
+
+class EstatePropertyOffer(models.Model):
+ _name = "estate.property.offer"
+ _description = "Real Estate Property Offer"
+ _order = "price desc"
+
+ price = fields.Float(string="Price", required=True)
+ status = fields.Selection(
+ [('accepted', 'Accepted'), ('refused', 'Refused')],
+ string="Status",
+ copy=False
+ )
+ partner_id = fields.Many2one("res.partner", string="Buyer", required=True)
+ property_id = fields.Many2one("estate.property", string="Property", required=True)
+ validity = fields.Integer(default=7, string="Validity (Days)")
+ date_deadline = fields.Date(compute="_compute_date_deadline", inverse="_inverse_date_deadline", string="Deadline")
+
+ _check_price = models.Constraint(
+ 'CHECK(price > 0)',
+ 'The offer price must be strictly positive'
+ )
+
+ @api.depends("create_date", "validity")
+ def _compute_date_deadline(self):
+ for record in self:
+ base_date = record.create_date.date() if record.create_date else fields.Date.today()
+ record.date_deadline = base_date + timedelta(days=record.validity)
+
+ def _inverse_date_deadline(self):
+ for record in self:
+ base_date = record.create_date.date() if record.create_date else fields.Date.today()
+ if record.date_deadline:
+ record.validity = (record.date_deadline - base_date).days
+
+ def action_accept_offer(self):
+ for record in self:
+ if record.status == 'accepted':
+ continue
+
+ record.status = 'accepted'
+ record.property_id.write({
+ 'selling_price': record.price,
+ 'buyer_id': record.partner_id.id,
+ 'state': 'offer_accepted'
+ })
+
+ record.property_id.offer_ids.filtered(lambda o: o.id != record.id).write({
+ 'status': 'refused'
+ })
+ return True
+
+ def action_refuse_offer(self):
+ for record in self:
+ record.status = 'refused'
+ return True
+
+ @api.model
+ def create(self, vals):
+ if len(vals) > 0:
+ property = self.env['estate.property'].browse(vals[0]['property_id'])
+ for record in vals:
+ if property.state == 'new':
+ property.state = 'offer_received'
+ if record['price'] < property.best_price:
+ raise UserError("Offer must be higher or equal than %d" % property.best_price)
+ return super().create(vals)
+
+ @api.model
+ def _cron_move_sold(self):
+ expired_offers = self.search([
+ ('status', '=', False),
+ ('date_deadline', '<', fields.Date.today())
+ ])
+ expired_offers.write({'status': 'refused'})
diff --git a/Estate/models/estate_property_tag.py b/Estate/models/estate_property_tag.py
new file mode 100644
index 00000000000..79681138d22
--- /dev/null
+++ b/Estate/models/estate_property_tag.py
@@ -0,0 +1,10 @@
+from odoo import models, fields
+
+
+class EstatePropertyTag(models.Model):
+ _name = "estate.property.tag"
+ _description = "Real Estate Property Tag"
+ _order = "name desc "
+ color = fields.Integer(string='Color Index', default=3)
+
+ name = fields.Char(required=True)
diff --git a/Estate/models/estate_property_type.py b/Estate/models/estate_property_type.py
new file mode 100644
index 00000000000..63320a03408
--- /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 = "Real Estate Property Type"
+ _order = "sequence , name"
+
+ name = fields.Char(required=True)
+ property_ids = fields.One2many("estate.property", "property_type_id", string="Properties")
+ sequence = fields.Integer('Sequence', default=7)
+ offer_count = fields.Integer(string="Number of Offers", compute="_compute_offer_count")
+
+ _check_type_name_unique_ratio = models.Constraint(
+ 'CHECK(name)',
+ 'The property name must be unique.'
+ )
+
+ @api.depends('property_ids.offer_ids')
+ def _compute_offer_count(self):
+ for property_type in self:
+ offer_count = 0
+ for property in property_type.property_ids:
+ offer_count += len(property.offer_ids)
+ property_type.offer_count = offer_count
diff --git a/Estate/models/res_users.py b/Estate/models/res_users.py
new file mode 100644
index 00000000000..69bfe368338
--- /dev/null
+++ b/Estate/models/res_users.py
@@ -0,0 +1,9 @@
+from odoo import models, fields
+
+
+class ResUsers(models.Model):
+ _inherit = "res.users"
+
+ estate_property_ids = fields.One2many(
+ "estate.property", "salesperson_id", string="Properties as Salesperson"
+ )
diff --git a/Estate/security/ir.model.access.csv b/Estate/security/ir.model.access.csv
new file mode 100644
index 00000000000..a5c45357f59
--- /dev/null
+++ b/Estate/security/ir.model.access.csv
@@ -0,0 +1,6 @@
+id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
+estate.access_estate_property,access_estate_property,model_estate_property,base.group_user,1,1,1,1
+estate.access_estate_property_offer,access_estate_property_offer,model_estate_property_offer,base.group_user,1,1,1,1
+estate.access_estate_property_type,access_estate_property_type,model_estate_property_type,base.group_user,1,1,1,1
+estate.access_estate_property_tag,access_estate_property_tag,model_estate_property_tag,base.group_user,1,1,1,1
+estate.access_res_users,access_res_users,model_res_users,base.group_user,1,1,1,1
diff --git a/Estate/views/estate_menus.xml b/Estate/views/estate_menus.xml
new file mode 100644
index 00000000000..c8d2f50b7cd
--- /dev/null
+++ b/Estate/views/estate_menus.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Estate/views/estate_property_offer_views.xml b/Estate/views/estate_property_offer_views.xml
new file mode 100644
index 00000000000..78ebd4890cf
--- /dev/null
+++ b/Estate/views/estate_property_offer_views.xml
@@ -0,0 +1,45 @@
+
+
+ estate.property.offer.list
+ estate.property.offer
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.offer.form
+ estate.property.offer
+
+
+
+
+
+
+ Property Offers
+ estate.property.offer
+ list,form
+ [('property_id', '=', active_id)]
+
+
diff --git a/Estate/views/estate_property_tags_views.xml b/Estate/views/estate_property_tags_views.xml
new file mode 100644
index 00000000000..83fc22c71cd
--- /dev/null
+++ b/Estate/views/estate_property_tags_views.xml
@@ -0,0 +1,18 @@
+
+
+ Property Tags
+ estate.property.tag
+ list,form
+
+
+
+ estate.property.tag.list
+ estate.property.tag
+
+
+
+
+
+
+
+
diff --git a/Estate/views/estate_property_type_views.xml b/Estate/views/estate_property_type_views.xml
new file mode 100644
index 00000000000..8c506ee4ffb
--- /dev/null
+++ b/Estate/views/estate_property_type_views.xml
@@ -0,0 +1,41 @@
+
+
+ Property Types
+ estate.property.type
+ list,form
+
+
+ estate.property.type.list
+ estate.property.type
+
+
+
+
+
+
+
+
+ estate.property.type.form
+ estate.property.type
+
+
+
+
+
diff --git a/Estate/views/estate_property_views.xml b/Estate/views/estate_property_views.xml
new file mode 100644
index 00000000000..81bc52d3732
--- /dev/null
+++ b/Estate/views/estate_property_views.xml
@@ -0,0 +1,146 @@
+
+
+
+
+ Properties
+ estate.property
+ list,form,kanban
+
+
+ Create a new property
+
+
+
+
+
+ estate.property.list
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.form
+ estate.property
+
+
+
+
+
+
+ Properties(Form)
+ estate.property
+ form
+
+
+
+
+ estate.property.search
+ estate.property
+
+
+
+
+
+
+
+
+
+
+
+
+
+ estate.property.kanban
+ estate.property
+
+
+
+
+
+
+
+
+
+ Expected Price:
+
+
+ Best Price:
+
+
+ Selling Price:
+
+
+
+
+
+
+
+
+
+ Properties(Kanban)
+ estate.property
+ kanban,form
+
+
+
+
diff --git a/Estate/views/res_users_view.xml b/Estate/views/res_users_view.xml
new file mode 100644
index 00000000000..e1c9e06cdbf
--- /dev/null
+++ b/Estate/views/res_users_view.xml
@@ -0,0 +1,25 @@
+
+
+ res.users.form.inherit.properties
+ res.users
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Users
+ res.users
+ form
+
+
+
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..2aed8e3b7a0
--- /dev/null
+++ b/Estate_account/__manifest__.py
@@ -0,0 +1,13 @@
+{
+ 'name': 'Estate_account',
+ 'depends': ['base', 'Estate', 'account'],
+ 'application': True,
+ 'installable': True,
+ 'author': 'Estate_account',
+ 'category': 'Tutorials',
+ 'license': 'AGPL-3',
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/estate_move.xml'
+ ],
+}
diff --git a/Estate_account/models/__init__.py b/Estate_account/models/__init__.py
new file mode 100644
index 00000000000..e6773ef3d05
--- /dev/null
+++ b/Estate_account/models/__init__.py
@@ -0,0 +1,2 @@
+from . import estate_property
+from . import account_move
diff --git a/Estate_account/models/account_move.py b/Estate_account/models/account_move.py
new file mode 100644
index 00000000000..9e28b2a0056
--- /dev/null
+++ b/Estate_account/models/account_move.py
@@ -0,0 +1,7 @@
+from odoo import models, fields
+
+
+class EstateMove(models.Model):
+ _inherit = "account.move"
+
+ property_id = fields.Char()
diff --git a/Estate_account/models/estate_property.py b/Estate_account/models/estate_property.py
new file mode 100644
index 00000000000..fd7b4b8b77c
--- /dev/null
+++ b/Estate_account/models/estate_property.py
@@ -0,0 +1,31 @@
+from odoo import models, Command
+from odoo.exceptions import ValidationError
+
+
+class EstateProperty(models.Model):
+ _inherit = "estate.property"
+
+ def action_set_sold(self):
+ for property in self:
+ if not property.buyer_id:
+ raise ValidationError("A property must have a buyer in order to be sold.")
+ invoice_vals = {
+ "partner_id": property.buyer_id.id,
+ "move_type": "out_invoice",
+ "invoice_line_ids": [
+ Command.create({
+ "name": "Commission (6%)",
+ "quantity": 1,
+ "price_unit": property.selling_price * 0.06,
+ }),
+
+ Command.create({
+ "name": "Administrative Fee",
+ "quantity": 1,
+ "price_unit": 100,
+ }),
+ ],
+ }
+ self.env["account.move"].create(invoice_vals)
+
+ return super().action_set_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..b6073e28e51
--- /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
+access_estate_property,Estate Property,model_estate_property,base.group_user,1,1,1,1
diff --git a/Estate_account/views/estate_move.xml b/Estate_account/views/estate_move.xml
new file mode 100644
index 00000000000..295fa2435b3
--- /dev/null
+++ b/Estate_account/views/estate_move.xml
@@ -0,0 +1,12 @@
+
+
+ account.move.view.form
+ account.move
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/awesome_dashboard/__manifest__.py b/awesome_dashboard/__manifest__.py
index a1cd72893d7..f975393b1d3 100644
--- a/awesome_dashboard/__manifest__.py
+++ b/awesome_dashboard/__manifest__.py
@@ -23,7 +23,21 @@
],
'assets': {
'web.assets_backend': [
- 'awesome_dashboard/static/src/**/*',
+ 'awesome_dashboard/static/src/dashboard_action.js',
+ ],
+ 'awesome_dashboard.dashboard': [
+ 'awesome_dashboard/static/src/dashboard_items_service.js',
+ 'awesome_dashboard/static/src/dashboard_items.js',
+ 'awesome_dashboard/static/src/dashboard/**/*.js',
+ 'awesome_dashboard/static/src/dashboard/**/*.xml',
+ 'awesome_dashboard/static/src/piechart/**/*.js',
+ 'awesome_dashboard/static/src/piechart/**/*.xml',
+ 'awesome_dashboard/static/src/dashboard_item/**/*.js',
+ 'awesome_dashboard/static/src/dashboard_item/**/*.xml',
+ 'awesome_dashboard/static/src/dashboard_configuration_dialog.js',
+ 'awesome_dashboard/static/src/dashboard_configuration_dialog.xml',
+ 'awesome_dashboard/static/src/statistics_service.js',
+ 'awesome_dashboard/static/src/dashboard.scss',
],
},
'license': 'AGPL-3'
diff --git a/awesome_dashboard/controllers/controllers.py b/awesome_dashboard/controllers/controllers.py
index 56d4a051287..5ead01ca69c 100644
--- a/awesome_dashboard/controllers/controllers.py
+++ b/awesome_dashboard/controllers/controllers.py
@@ -8,8 +8,9 @@
logger = logging.getLogger(__name__)
+
class AwesomeDashboard(http.Controller):
- @http.route('/awesome_dashboard/statistics', type='json', auth='user')
+ @http.route('/awesome_dashboard/statistics', type='jsonrpc', auth='user')
def get_statistics(self):
"""
Returns a dict of statistics about the orders:
@@ -33,4 +34,3 @@ def get_statistics(self):
},
'total_amount': random.randint(100, 1000)
}
-
diff --git a/awesome_dashboard/static/src/dashboard.js b/awesome_dashboard/static/src/dashboard.js
deleted file mode 100644
index c4fb245621b..00000000000
--- a/awesome_dashboard/static/src/dashboard.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Component } from "@odoo/owl";
-import { registry } from "@web/core/registry";
-
-class AwesomeDashboard extends Component {
- static template = "awesome_dashboard.AwesomeDashboard";
-}
-
-registry.category("actions").add("awesome_dashboard.dashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard.scss b/awesome_dashboard/static/src/dashboard.scss
new file mode 100644
index 00000000000..32862ec0d82
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard.scss
@@ -0,0 +1,3 @@
+.o_dashboard {
+ background-color: gray;
+}
diff --git a/awesome_dashboard/static/src/dashboard.xml b/awesome_dashboard/static/src/dashboard.xml
deleted file mode 100644
index 1a2ac9a2fed..00000000000
--- a/awesome_dashboard/static/src/dashboard.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- hello dashboard
-
-
-
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.js b/awesome_dashboard/static/src/dashboard/dashboard.js
new file mode 100644
index 00000000000..5ad1a8e166c
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.js
@@ -0,0 +1,62 @@
+import { Component, useState } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { Layout } from "@web/search/layout";
+import { useService } from "@web/core/utils/hooks";
+import { DashboardItem } from "../dashboard_item/dashboard_item";
+import { PieChart } from "../piechart/piechart";
+import { getDashboardItems } from "../dashboard_items_service";
+import { DashboardConfigurationDialog } from "../dashboard_configuration_dialog";
+
+export class AwesomeDashboard extends Component {
+ static template = "awesome_dashboard.AwesomeDashboard";
+ static components = { Layout, DashboardItem, PieChart };
+
+ setup() {
+ this.action = useService("action");
+ const statisticsService = useService("awesome_dashboard.statistics");
+ this.statistics = useState(statisticsService.state);
+ const storedConfig = localStorage.getItem("dashboard_configuration");
+ this.hiddenItems = storedConfig ? JSON.parse(storedConfig) : [];
+ const allItems = getDashboardItems();
+ this.items = allItems.filter((item) => !this.hiddenItems.includes(item.id));
+ }
+
+ openCustomers() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "Customers",
+ res_model: "res.partner",
+ views: [
+ [false, "kanban"],
+ [false, "form"],
+ [false, "list"],
+ ],
+ });
+ }
+
+ openLeads() {
+ this.action.doAction({
+ type: "ir.actions.act_window",
+ name: "Leads",
+ res_model: "crm.lead",
+ views: [
+ [false, "list"],
+ [false, "form"],
+ ],
+ });
+ }
+
+ openConfigurationDialog() {
+ this.env.services.dialog.add(DashboardConfigurationDialog, {
+ onConfigChange: () => {
+ const storedConfig = localStorage.getItem("dashboard_configuration");
+ this.hiddenItems = storedConfig ? JSON.parse(storedConfig) : [];
+ const allItems = getDashboardItems();
+ this.items = allItems.filter((item) => !this.hiddenItems.includes(item.id));
+ this.render();
+ },
+ });
+ }
+}
+
+registry.category("lazy_components").add("AwesomeDashboard", AwesomeDashboard);
diff --git a/awesome_dashboard/static/src/dashboard/dashboard.xml b/awesome_dashboard/static/src/dashboard/dashboard.xml
new file mode 100644
index 00000000000..3a2b5a98066
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/dashboard.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Number of new orders this month
+
+
+
+
+
+
+ Total amount of new orders this month
+
+
+
+
+
+
+ Average amount of t-shirt by order this month
+
+
+
+
+
+
+ Number of cancelled orders this month
+
+
+
+
+
+
+ Average time for an order to go from ‘new’ to ‘sent’ or ‘cancelled’
+
+
+
+
+
+
+ Shirt order by size
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/number_card.js b/awesome_dashboard/static/src/dashboard/number_card.js
new file mode 100644
index 00000000000..a90203a33c4
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card.js
@@ -0,0 +1,13 @@
+import { Component } from "@odoo/owl";
+
+export class NumberCard extends Component {
+ static template = "awesome_dashboard.NumberCard";
+ static props = {
+ title: {
+ type: String,
+ },
+ value: {
+ type: [String, Number],
+ },
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/number_card.xml b/awesome_dashboard/static/src/dashboard/number_card.xml
new file mode 100644
index 00000000000..55c22c4513f
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/number_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.js b/awesome_dashboard/static/src/dashboard/pie_chart_card.js
new file mode 100644
index 00000000000..6599224eb20
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.js
@@ -0,0 +1,17 @@
+import { Component } from "@odoo/owl";
+import { PieChart } from "../piechart/piechart";
+
+export class PieChartCard extends Component {
+ static template = "awesome_dashboard.PieChartCard";
+ static components = { PieChart };
+ static props = {
+ title: {
+ type: String,
+ },
+ items: {
+ type: Object,
+ optional: true,
+ default: { m: 0, s: 0, xl: 0 },
+ },
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard/pie_chart_card.xml b/awesome_dashboard/static/src/dashboard/pie_chart_card.xml
new file mode 100644
index 00000000000..dc7691c5674
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard/pie_chart_card.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_action.js b/awesome_dashboard/static/src/dashboard_action.js
new file mode 100644
index 00000000000..28c825183ae
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_action.js
@@ -0,0 +1,15 @@
+/** @odoo-module */
+
+import { Component, xml } from "@odoo/owl";
+import { registry } from "@web/core/registry";
+import { LazyComponent } from "@web/core/assets";
+
+export class AwesomeDashboardLoader extends Component {
+ static components = { LazyComponent };
+ static template = xml`
+ `;
+}
+registry.category("actions").add("awesome_dashboard.dashboard_action", AwesomeDashboardLoader);
diff --git a/awesome_dashboard/static/src/dashboard_configuration_dialog.js b/awesome_dashboard/static/src/dashboard_configuration_dialog.js
new file mode 100644
index 00000000000..06e008f4ec9
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_configuration_dialog.js
@@ -0,0 +1,44 @@
+import { Component, useState } from "@odoo/owl";
+import { Dialog } from "@web/core/dialog/dialog";
+import { registry } from "@web/core/registry";
+import { getDashboardItems } from "./dashboard_items_service";
+
+export class DashboardConfigurationDialog extends Component {
+ static template = "awesome_dashboard.DashboardConfigurationDialog";
+ static components = { Dialog };
+
+ setup() {
+ const allItems = getDashboardItems();
+ const storedConfig = localStorage.getItem("dashboard_configuration");
+ const hiddenItems = storedConfig ? JSON.parse(storedConfig) : [];
+
+ this.state = useState({
+ items: allItems.map((item) => ({
+ ...item,
+ hidden: hiddenItems.includes(item.id),
+ })),
+ });
+ }
+
+ onSave() {
+ const hiddenItemIds = this.state.items.filter((item) => item.hidden).map((item) => item.id);
+ localStorage.setItem("dashboard_configuration", JSON.stringify(hiddenItemIds));
+ if (this.props.onConfigChange) {
+ this.props.onConfigChange();
+ }
+ this.props.close();
+ }
+
+ onCancel() {
+ this.props.close();
+ }
+
+ toggleItemVisibility(itemId) {
+ const item = this.state.items.find((item) => item.id === itemId);
+ if (item) {
+ item.hidden = !item.hidden;
+ }
+ }
+}
+
+registry.category("view_dialogs").add("DashboardConfigurationDialog", DashboardConfigurationDialog);
diff --git a/awesome_dashboard/static/src/dashboard_configuration_dialog.xml b/awesome_dashboard/static/src/dashboard_configuration_dialog.xml
new file mode 100644
index 00000000000..18312ec6cbb
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_configuration_dialog.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.js b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js
new file mode 100644
index 00000000000..f42171b1311
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.js
@@ -0,0 +1,18 @@
+import { Component } from "@odoo/owl";
+
+export class DashboardItem extends Component {
+ static template = "awesome_dashboard.DashboardItem";
+ static props = {
+ slots: {
+ type: Object,
+ shape: {
+ default: Object,
+ },
+ },
+ size: {
+ type: Number,
+ default: 1,
+ optional: true,
+ },
+ };
+}
diff --git a/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml
new file mode 100644
index 00000000000..e558121f630
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_item/dashboard_item.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/dashboard_items.js b/awesome_dashboard/static/src/dashboard_items.js
new file mode 100644
index 00000000000..0dffac0f641
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_items.js
@@ -0,0 +1,71 @@
+import { registry } from "@web/core/registry";
+import { NumberCard } from "./dashboard/number_card";
+import { PieChartCard } from "./dashboard/pie_chart_card";
+
+const dashboardItemsRegistry = registry.category("awesome_dashboard.items");
+
+dashboardItemsRegistry.add("nb_new_orders", {
+ id: "nb_new_orders",
+ description: "Number of new orders this month",
+ Component: NumberCard,
+ size: 3,
+ props: (data) => ({
+ title: "Number of new orders this month",
+ value: data.nb_new_orders,
+ }),
+});
+
+dashboardItemsRegistry.add("total_amount", {
+ id: "total_amount",
+ description: "Total amount of new orders this month",
+ Component: NumberCard,
+ size: 3,
+ props: (data) => ({
+ title: "Total amount of new orders this month",
+ value: data.total_amount,
+ }),
+});
+
+dashboardItemsRegistry.add("average_quantity", {
+ id: "average_quantity",
+ description: "Average amount of t-shirt",
+ Component: NumberCard,
+ size: 3,
+ props: (data) => ({
+ title: "Average amount of t-shirt by order this month",
+ value: data.average_quantity,
+ }),
+});
+
+dashboardItemsRegistry.add("nb_cancelled_orders", {
+ id: "nb_cancelled_orders",
+ description: "Number of cancelled orders this month",
+ Component: NumberCard,
+ size: 3,
+ props: (data) => ({
+ title: "Number of cancelled orders this month",
+ value: data.nb_cancelled_orders,
+ }),
+});
+
+dashboardItemsRegistry.add("average_time", {
+ id: "average_time",
+ description: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ Component: NumberCard,
+ size: 3,
+ props: (data) => ({
+ title: "Average time for an order to go from 'new' to 'sent' or 'cancelled'",
+ value: data.average_time,
+ }),
+});
+
+dashboardItemsRegistry.add("orders_by_size", {
+ id: "orders_by_size",
+ description: "Shirt order by size",
+ Component: PieChartCard,
+ size: 3,
+ props: (data) => ({
+ title: "Shirt order by size",
+ items: data.orders_by_size,
+ }),
+});
diff --git a/awesome_dashboard/static/src/dashboard_items_service.js b/awesome_dashboard/static/src/dashboard_items_service.js
new file mode 100644
index 00000000000..10d7d8e7e26
--- /dev/null
+++ b/awesome_dashboard/static/src/dashboard_items_service.js
@@ -0,0 +1,10 @@
+import { registry } from "@web/core/registry";
+
+export function getDashboardItems() {
+ const itemsRegistry = registry.category("awesome_dashboard.items");
+ const items = [];
+ for (const entry of itemsRegistry.getEntries()) {
+ items.push(entry[1]);
+ }
+ return items;
+}
diff --git a/awesome_dashboard/static/src/piechart/piechart.js b/awesome_dashboard/static/src/piechart/piechart.js
new file mode 100644
index 00000000000..259a66c49ed
--- /dev/null
+++ b/awesome_dashboard/static/src/piechart/piechart.js
@@ -0,0 +1,58 @@
+import { Component, onWillStart, onMounted, useRef } from "@odoo/owl";
+import { loadJS } from "@web/core/assets";
+
+export class PieChart extends Component {
+ static template = "awesome_dashboard.PieChart";
+
+ static props = {
+ items: {
+ type: Object,
+ optional: true,
+ default: { m: 0, s: 0, xl: 0 },
+ },
+ };
+
+ setup() {
+ this.chartRef = useRef("pie-canvas");
+
+ onWillStart(async () => {
+ await loadJS("/web/static/lib/Chart/Chart.js");
+
+ this.pieChartData = {
+ labels: ["M", "S", "XL"],
+ datasets: [
+ {
+ label: "Sales Count",
+ data: [
+ this.props.items.m ?? 0,
+ this.props.items.s ?? 0,
+ this.props.items.xl ?? 0,
+ ],
+ },
+ ],
+ };
+
+ this.pieChartOptions = {
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: { legend: { position: "top" } },
+ };
+ });
+
+ onMounted(() => {
+ const canvasEl = this.chartRef.el;
+ if (!canvasEl) {
+ return;
+ }
+ const ctx = canvasEl.getContext("2d");
+ if (!ctx || !window.Chart) {
+ return;
+ }
+ this.myPie = new window.Chart(ctx, {
+ type: "pie",
+ data: this.pieChartData,
+ options: this.pieChartOptions,
+ });
+ });
+ }
+}
diff --git a/awesome_dashboard/static/src/piechart/piechart.xml b/awesome_dashboard/static/src/piechart/piechart.xml
new file mode 100644
index 00000000000..baf6ebaf0a6
--- /dev/null
+++ b/awesome_dashboard/static/src/piechart/piechart.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/awesome_dashboard/static/src/statistics_service.js b/awesome_dashboard/static/src/statistics_service.js
new file mode 100644
index 00000000000..da3d6a4c2bf
--- /dev/null
+++ b/awesome_dashboard/static/src/statistics_service.js
@@ -0,0 +1,21 @@
+import { registry } from "@web/core/registry";
+import { rpc } from "@web/core/network/rpc";
+import { reactive } from "@odoo/owl";
+
+const statisticsService = {
+ async: ["loadStatistics"],
+ start() {
+ const loadStatistics = () => rpc("/awesome_dashboard/statistics");
+ const state = reactive({ data: null });
+ const fetchData = async () => {
+ state.data = await loadStatistics();
+ };
+ fetchData();
+ setInterval(fetchData, 10000);
+ return {
+ state: state,
+ };
+ },
+};
+
+registry.category("services").add("awesome_dashboard.statistics", statisticsService);
diff --git a/awesome_dashboard/views/views.xml b/awesome_dashboard/views/views.xml
index 47fb2b6f258..4303647398e 100644
--- a/awesome_dashboard/views/views.xml
+++ b/awesome_dashboard/views/views.xml
@@ -2,10 +2,9 @@
Dashboard
- awesome_dashboard.dashboard
+ awesome_dashboard.dashboard_action
-
-
+
diff --git a/awesome_owl/__manifest__.py b/awesome_owl/__manifest__.py
index e8ac1cda552..cd5c52e8ff6 100644
--- a/awesome_owl/__manifest__.py
+++ b/awesome_owl/__manifest__.py
@@ -31,10 +31,13 @@
('include', 'web._assets_helpers'),
'web/static/src/scss/pre_variables.scss',
'web/static/lib/bootstrap/scss/_variables.scss',
+ 'web/static/lib/bootstrap/scss/_maps.scss',
('include', 'web._assets_bootstrap'),
('include', 'web._assets_core'),
'web/static/src/libs/fontawesome/css/font-awesome.css',
- 'awesome_owl/static/src/**/*',
+ 'awesome_owl/static/src/**/*js',
+ 'awesome_owl/static/src/**/*.scss',
+ 'awesome_owl/static/src/**/*.xml',
],
},
'license': 'AGPL-3'
diff --git a/awesome_owl/controllers/controllers.py b/awesome_owl/controllers/controllers.py
index bccfd6fe283..0d0dd8f3160 100644
--- a/awesome_owl/controllers/controllers.py
+++ b/awesome_owl/controllers/controllers.py
@@ -1,5 +1,5 @@
from odoo import http
-from odoo.http import request, route
+from odoo.http import request
class OwlPlayground(http.Controller):
@http.route(['/awesome_owl'], type='http', auth='public')
diff --git a/awesome_owl/static/src/card/card.js b/awesome_owl/static/src/card/card.js
new file mode 100644
index 00000000000..e7794b03502
--- /dev/null
+++ b/awesome_owl/static/src/card/card.js
@@ -0,0 +1,15 @@
+import { Component } from "@odoo/owl";
+
+
+export class Card extends Component {
+ static template = "awesome_owl.Card";
+ static props = {
+ title: String,
+ slots: {
+ type: Object,
+ shape: {
+ default: true
+ },
+ }
+ };
+}
diff --git a/awesome_owl/static/src/card/card.xml b/awesome_owl/static/src/card/card.xml
new file mode 100644
index 00000000000..5c284e81757
--- /dev/null
+++ b/awesome_owl/static/src/card/card.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/counter/counter.js b/awesome_owl/static/src/counter/counter.js
new file mode 100644
index 00000000000..4d1b37b4a9b
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.js
@@ -0,0 +1,20 @@
+import {Component, useState} from "@odoo/owl";
+
+
+export class Counter extends Component {
+ static template = "awesome_owl.counter";
+ static props = {
+ onChange : {type: Function, optional: true},
+ };
+
+ setup() {
+ this.state = useState({value: 1});
+ }
+
+ increment() {
+ this.state.value = this.state.value + 1;
+ if (this.props.onChange) {
+ this.props.onChange(this.state.value);
+ }
+ }
+}
diff --git a/awesome_owl/static/src/counter/counter.xml b/awesome_owl/static/src/counter/counter.xml
new file mode 100644
index 00000000000..fd454150cdf
--- /dev/null
+++ b/awesome_owl/static/src/counter/counter.xml
@@ -0,0 +1,8 @@
+
+
+
+ Counter:
+
+
+
+
diff --git a/awesome_owl/static/src/main.js b/awesome_owl/static/src/main.js
index 1aaea902b55..1af6c827e0b 100644
--- a/awesome_owl/static/src/main.js
+++ b/awesome_owl/static/src/main.js
@@ -4,9 +4,8 @@ import { Playground } from "./playground";
const config = {
dev: true,
- name: "Owl Tutorial"
+ name: "Owl Tutorial",
};
// Mount the Playground component when the document.body is ready
whenReady(() => mountComponent(Playground, document.body, config));
-
diff --git a/awesome_owl/static/src/playground.js b/awesome_owl/static/src/playground.js
index 4ac769b0aa5..823eacfd2bd 100644
--- a/awesome_owl/static/src/playground.js
+++ b/awesome_owl/static/src/playground.js
@@ -1,5 +1,19 @@
-import { Component } from "@odoo/owl";
+import { Component, markup, useState } from "@odoo/owl";
+import { Counter } from "./counter/counter";
+import { Card } from "./card/card";
+import { TodoList } from "./todo_list/todo_list";
export class Playground extends Component {
static template = "awesome_owl.playground";
+ static components = { Counter, Card, TodoList };
+
+ setup() {
+ this.str1 = "
some content
";
+ this.str2 = markup("some content
");
+ this.sum = useState({ value: 2 });
+ }
+
+ incrementSum() {
+ this.sum.value++;
+ }
}
diff --git a/awesome_owl/static/src/playground.xml b/awesome_owl/static/src/playground.xml
index 4fb905d59f9..43878b04346 100644
--- a/awesome_owl/static/src/playground.xml
+++ b/awesome_owl/static/src/playground.xml
@@ -1,10 +1,26 @@
-
-
-
- hello world
+
+
+
+
+
+ content of card 1
+
+
+
+
+
+
+
+
-
diff --git a/awesome_owl/static/src/todo_list/todo_item.js b/awesome_owl/static/src/todo_list/todo_item.js
new file mode 100644
index 00000000000..3ec6782557c
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.js
@@ -0,0 +1,11 @@
+import { Component } from "@odoo/owl";
+
+export class TodoItem extends Component {
+ static template = "awesome_owl.TodoItem";
+ static props = {
+ todo: {
+ type: Object,
+ shape: { id: Number, description: String, isCompleted: Boolean },
+ },
+ };
+}
diff --git a/awesome_owl/static/src/todo_list/todo_item.xml b/awesome_owl/static/src/todo_list/todo_item.xml
new file mode 100644
index 00000000000..f0d8eb33f9d
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_item.xml
@@ -0,0 +1,8 @@
+
+
+
+ .
+
+
+
+
diff --git a/awesome_owl/static/src/todo_list/todo_list.js b/awesome_owl/static/src/todo_list/todo_list.js
new file mode 100644
index 00000000000..33aa12c1645
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.js
@@ -0,0 +1,27 @@
+import { Component, useState } from "@odoo/owl";
+import { TodoItem } from "./todo_item";
+import { useAutofocus } from "../utils";
+
+export class TodoList extends Component {
+ static template = "awesome_owl.TodoList";
+ static components = { TodoItem };
+
+ setup() {
+ this.nextId = 0;
+ this.state = useState({
+ todos: [],
+ });
+ useAutofocus("input");
+ }
+
+ addTodo(ev) {
+ if (ev.keyCode === 13 && ev.target.value != "") {
+ this.state.todos.push({
+ id: this.nextId++,
+ description: ev.target.value,
+ isCompleted: false,
+ });
+ ev.target.value = "";
+ }
+ }
+}
diff --git a/awesome_owl/static/src/todo_list/todo_list.xml b/awesome_owl/static/src/todo_list/todo_list.xml
new file mode 100644
index 00000000000..78fc80d1898
--- /dev/null
+++ b/awesome_owl/static/src/todo_list/todo_list.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/awesome_owl/static/src/utils.js b/awesome_owl/static/src/utils.js
new file mode 100644
index 00000000000..f452f103aa0
--- /dev/null
+++ b/awesome_owl/static/src/utils.js
@@ -0,0 +1,8 @@
+import { useRef, onMounted } from "@odoo/owl";
+
+export function useAutofocus(refName) {
+ const ref = useRef(refName);
+ onMounted(() => {
+ ref.el.focus();
+ });
+}
diff --git a/awesome_owl/views/templates.xml b/awesome_owl/views/templates.xml
index aa54c1a7241..7d2d138094b 100644
--- a/awesome_owl/views/templates.xml
+++ b/awesome_owl/views/templates.xml
@@ -6,7 +6,6 @@
-