diff --git a/appointment_filter/__init__.py b/appointment_filter/__init__.py
new file mode 100644
index 00000000000..e046e49fbe2
--- /dev/null
+++ b/appointment_filter/__init__.py
@@ -0,0 +1 @@
+from . import controllers
diff --git a/appointment_filter/__manifest__.py b/appointment_filter/__manifest__.py
new file mode 100644
index 00000000000..a5f9ed2d99d
--- /dev/null
+++ b/appointment_filter/__manifest__.py
@@ -0,0 +1,9 @@
+{
+ "name": "Appointment Filter",
+ "depends": ["website_appointment", "appointment_account_payment"],
+ "data": [
+ "views/website_appointment_filter_template.xml",
+ ],
+ "installable": True,
+ "license": "LGPL-3",
+}
diff --git a/appointment_filter/controllers/__init__.py b/appointment_filter/controllers/__init__.py
new file mode 100644
index 00000000000..c9bc0f941bd
--- /dev/null
+++ b/appointment_filter/controllers/__init__.py
@@ -0,0 +1 @@
+from . import appointment_filter
diff --git a/appointment_filter/controllers/appointment_filter.py b/appointment_filter/controllers/appointment_filter.py
new file mode 100644
index 00000000000..585239f99db
--- /dev/null
+++ b/appointment_filter/controllers/appointment_filter.py
@@ -0,0 +1,48 @@
+from odoo.addons.website_appointment.controllers.appointment import WebsiteAppointment
+from odoo.http import request
+
+
+class AppointmentFilterController(WebsiteAppointment):
+ def _appointments_base_domain(
+ cls,
+ filter_appointment_type_ids,
+ search=False,
+ invite_token=False,
+ additional_domain=None,
+ ):
+ domain = super()._appointments_base_domain(
+ filter_appointment_type_ids, search, invite_token, additional_domain
+ )
+
+ filter_location = request.params.get("filter_location")
+ if filter_location == "Online":
+ domain.append(("location_id", "=", False))
+ elif filter_location == "Offline":
+ domain.append(("location_id", "!=", False))
+
+ filter_based_on = request.params.get("filter_based_on")
+ if filter_based_on == "Users":
+ domain.append(("schedule_based_on", "=", "users"))
+ elif filter_based_on == "Resources":
+ domain.append(("schedule_based_on", "=", "resources"))
+
+ filter_payment = request.params.get("filter_payment")
+ if filter_payment == "Required":
+ domain.append(("has_payment_step", "=", True))
+ elif filter_payment == "No Required":
+ domain.append(("has_payment_step", "=", False))
+
+ return domain
+
+ def _prepare_appointments_cards_data(self, page, appointment_types, **kwargs):
+ context = super()._prepare_appointments_cards_data(
+ page, appointment_types, **kwargs
+ )
+
+ context["filters"] = {
+ "filter_location": kwargs.get("filter_location"),
+ "filter_based_on": kwargs.get("filter_based_on"),
+ "filter_payment": kwargs.get("filter_payment"),
+ }
+
+ return context
diff --git a/appointment_filter/views/website_appointment_filter_template.xml b/appointment_filter/views/website_appointment_filter_template.xml
new file mode 100644
index 00000000000..3bf6aa9fa20
--- /dev/null
+++ b/appointment_filter/views/website_appointment_filter_template.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
diff --git a/billing_address_in_website_sale/__init__.py b/billing_address_in_website_sale/__init__.py
new file mode 100644
index 00000000000..e046e49fbe2
--- /dev/null
+++ b/billing_address_in_website_sale/__init__.py
@@ -0,0 +1 @@
+from . import controllers
diff --git a/billing_address_in_website_sale/__manifest__.py b/billing_address_in_website_sale/__manifest__.py
new file mode 100644
index 00000000000..e1e8b23d551
--- /dev/null
+++ b/billing_address_in_website_sale/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ "name": "Billing Address In Website",
+ "depends": [
+ "website_sale",
+ ],
+ "data": [
+ "views/templates.xml",
+ ],
+ "assets": {
+ "web.assets_frontend": [
+ "billing_address_in_website_sale/static/src/**/*",
+ ],
+ "web.assets_tests": [
+ "billing_address_in_website_sale/static/tests/**/*",
+ ],
+ },
+ "installable": True,
+ "license": "LGPL-3",
+}
diff --git a/billing_address_in_website_sale/controllers/__init__.py b/billing_address_in_website_sale/controllers/__init__.py
new file mode 100644
index 00000000000..12a7e529b67
--- /dev/null
+++ b/billing_address_in_website_sale/controllers/__init__.py
@@ -0,0 +1 @@
+from . import main
diff --git a/billing_address_in_website_sale/controllers/main.py b/billing_address_in_website_sale/controllers/main.py
new file mode 100644
index 00000000000..5c933725108
--- /dev/null
+++ b/billing_address_in_website_sale/controllers/main.py
@@ -0,0 +1,43 @@
+from odoo.addons.website_sale.controllers.main import WebsiteSale
+from odoo.http import request, route
+
+
+class WebsiteSaleInherit(WebsiteSale):
+ @route("/shop/vat/address", type="json", auth="public", website=True)
+ def get_address(self, vat=None):
+ if not vat:
+ return {"error": "Please enter VAT number"}
+ address_values = request.env["res.partner"].sudo().enrich_by_gst(vat)
+ if not address_values:
+ return {"error": "Please enter valid VAT number"}
+ return address_values
+
+ @route(
+ "/shop/billing_address/submit",
+ type="json",
+ auth="public",
+ website=True,
+ )
+ def shop_billing_address_submit(self, address=None, name=None, partner_id=None):
+ order_sudo = request.website.sale_get_order()
+ if partner_id:
+ partner_sudo = request.env["res.partner"].browse(int(partner_id))
+ partner_sudo.write({"name": name})
+ return True
+ partner_sudo = request.env["res.partner"].sudo().create({
+ 'name': name,
+ 'company_type': 'company',
+ 'parent_id': False,
+ 'street': address.get('street'),
+ 'street2': address.get('street2'),
+ 'city': address.get('city'),
+ 'state_id': address.get('state_id', {}).get('id', False),
+ 'country_id': address.get('country_id', {}).get('id', False),
+ 'zip': address.get('zip'),
+ 'vat': address.get('vat'),
+ 'email': address.get("email") or order_sudo.partner_id.email,
+ 'phone': address.get("phone") or order_sudo.partner_id.phone,
+ })
+ order_sudo._update_address(partner_sudo.id, {"partner_invoice_id"})
+ order_sudo.partner_id.write({"type": "delivery", "parent_id": partner_sudo})
+ return True
diff --git a/billing_address_in_website_sale/static/src/checkout.js b/billing_address_in_website_sale/static/src/checkout.js
new file mode 100644
index 00000000000..f763e6a766b
--- /dev/null
+++ b/billing_address_in_website_sale/static/src/checkout.js
@@ -0,0 +1,38 @@
+import WebsiteSaleCheckout from '@website_sale/js/checkout';
+import { rpc } from '@web/core/network/rpc';
+
+WebsiteSaleCheckout.include({
+
+ events: Object.assign({
+ 'change #want_tax_credit_checkbox': '_onTaxCreditToggle',
+ 'change #vat_number': '_onChangeVat',
+ }, WebsiteSaleCheckout.prototype.events),
+
+ async start() {
+ this.vatLable = this.el.querySelector('#vat_label');
+ this.companyName = this.el.querySelector('#company_name');
+ this.address = this.el.querySelector('#address');
+ this.partnerId = this.el.querySelector('#partner_id');
+ return this._super(...arguments);
+ },
+
+ async _onTaxCreditToggle(ev) {
+ const checkbox = ev.currentTarget;
+ const taxContainer = this.el.querySelector('#tax_credit_container');
+ taxContainer.classList.toggle('d-none', !checkbox.checked);
+ if (!checkbox.checked) {
+ const selectedDeliveryAddress = this._getSelectedAddress('delivery');
+ await this._selectMatchingBillingAddress(selectedDeliveryAddress.dataset.partnerId);
+ }
+ },
+
+ async _onChangeVat(ev) {
+ const vat = ev.currentTarget.value.trim();
+ const addressValues = await rpc('/shop/vat/address', { vat });
+ this.vatLable.textContent = addressValues.country_id ? addressValues.country_id.display_name : "Vat Number";
+ this.companyName.value = addressValues.name || "";
+ this.address.value = JSON.stringify(addressValues);
+ this.partnerId.value = "";
+ },
+
+});
diff --git a/billing_address_in_website_sale/static/src/website_sale_tracking.js b/billing_address_in_website_sale/static/src/website_sale_tracking.js
new file mode 100644
index 00000000000..89028bf2234
--- /dev/null
+++ b/billing_address_in_website_sale/static/src/website_sale_tracking.js
@@ -0,0 +1,37 @@
+import websiteSaleTracking from '@website_sale/js/website_sale_tracking'
+import { rpc } from "@web/core/network/rpc";
+
+websiteSaleTracking.include({
+
+ events: Object.assign({
+ 'click #confirm_btn': '_onConfirmClick',
+ }, websiteSaleTracking.prototype.events),
+
+ async _onConfirmClick(ev) {
+ ev.preventDefault();
+ const checkbox = this.el.querySelector("#want_tax_credit_checkbox");
+ if (checkbox && checkbox.checked) {
+ const name = this.el.querySelector("#company_name").value.trim();
+ const partner_id = this.el.querySelector('input[name="partner_id"]').value;
+ const address = JSON.parse(this.el.querySelector("#address").value || '{"error": "Please enter VAT number"}');
+ if (address.error) {
+ this._displayError(address.error);
+ return;
+ }
+ if (!name) {
+ this._displayError("Company name is required.");
+ return;
+ }
+ await rpc('/shop/billing_address/submit', { name, partner_id, address, });
+ }
+ window.location.href = "/shop/confirm_order";
+ },
+
+ _displayError(msg) {
+ const errorsDiv = this.el.querySelector("#errors");
+ const errorHeader = document.createElement('h5');
+ errorHeader.classList.add('text-danger', 'alert', 'alert-danger');
+ errorHeader.textContent = msg;
+ errorsDiv.replaceChildren(errorHeader);
+ },
+})
diff --git a/billing_address_in_website_sale/static/tests/tours/test_tax_credit_checkbox.js b/billing_address_in_website_sale/static/tests/tours/test_tax_credit_checkbox.js
new file mode 100644
index 00000000000..225963a344d
--- /dev/null
+++ b/billing_address_in_website_sale/static/tests/tours/test_tax_credit_checkbox.js
@@ -0,0 +1,64 @@
+import { registry } from "@web/core/registry";
+import * as tourUtils from "@website_sale/js/tours/tour_utils";
+
+registry.category("web_tour.tours").add("tax_credit_checkbox_test", {
+ test: true,
+ url: "/shop",
+ steps: () => [
+ ...tourUtils.addToCart({ productName: "Office Chair Black TEST" }),
+ tourUtils.goToCart({ quantity: 1 }),
+ tourUtils.goToCheckout(),
+ {
+ content: "Click 'Want Tax Credit' checkbox",
+ trigger: "input#want_tax_credit_checkbox",
+ run: "click",
+ },
+ {
+ content: "Check VAT and Company fields are visible",
+ trigger: '#vat_number:visible, #company_name:visible',
+ },
+ {
+ content: "Click Confirm button",
+ trigger: "#confirm_btn",
+ run: "click",
+ },
+ {
+ content: "Error should show: please enter correct Vat Number",
+ trigger: "#errors:contains('Please enter VAT number')",
+ },
+ {
+ content: "Uncheck 'Want Tax Credit' checkbox",
+ trigger: "input#want_tax_credit_checkbox:checked",
+ run: "click",
+ },
+ {
+ content: "Click Confirm button",
+ trigger: "#confirm_btn",
+ run: "click",
+ },
+ {
+ content: "Check delivery and billing are same",
+ trigger: "#delivery_and_billing",
+ run: () => {
+ const address_card = document.querySelector("#delivery_and_billing");
+ const address_text = address_card.innerText;
+ if (!address_text.includes("Delivery & Billing")) {
+ throw new Error("addresses are not the same");
+ }
+ }
+ },
+ {
+ content: "Click Edit button on payment page",
+ trigger: "#delivery_and_billing a[href='/shop/checkout']",
+ run: "click"
+ },
+ {
+ content: "Verify Want Tax Credit checkbox is unchecked",
+ trigger: "#want_tax_credit_checkbox:not(:checked)",
+ },
+ {
+ content: "Verify tax credit container is hidden",
+ trigger: "#tax_credit_container:not(:visible)",
+ },
+ ],
+});
diff --git a/billing_address_in_website_sale/tests/__init__.py b/billing_address_in_website_sale/tests/__init__.py
new file mode 100644
index 00000000000..7837c60e353
--- /dev/null
+++ b/billing_address_in_website_sale/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_tax_credit_checkbox
diff --git a/billing_address_in_website_sale/tests/test_tax_credit_checkbox.py b/billing_address_in_website_sale/tests/test_tax_credit_checkbox.py
new file mode 100644
index 00000000000..ca55b420771
--- /dev/null
+++ b/billing_address_in_website_sale/tests/test_tax_credit_checkbox.py
@@ -0,0 +1,13 @@
+import odoo.tests
+
+
+class TestTaxCreditCheckbox(odoo.tests.HttpCase):
+ def test_tax_credit_checkbox_flow(self):
+ self.env["product.product"].create(
+ {
+ "name": "Office Chair Black TEST",
+ "list_price": 12.50,
+ }
+ )
+
+ self.start_tour("/", "tax_credit_checkbox_test", login="admin")
diff --git a/billing_address_in_website_sale/views/templates.xml b/billing_address_in_website_sale/views/templates.xml
new file mode 100644
index 00000000000..d0a7ae55a85
--- /dev/null
+++ b/billing_address_in_website_sale/views/templates.xml
@@ -0,0 +1,87 @@
+
+
+
+
+ d-none
+
+
+
+ d-none
+
+
+
+
+
+ d-none
+
+
+
+
+
+
BILLING ADDRESS
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ "confirm_btn" if xmlid == 'website_sale.checkout' else None
+
+
+
+
diff --git a/distribute_cost_task/__init__.py b/distribute_cost_task/__init__.py
new file mode 100644
index 00000000000..b60c2d2e62d
--- /dev/null
+++ b/distribute_cost_task/__init__.py
@@ -0,0 +1,2 @@
+from . import model
+from . import wizard
diff --git a/distribute_cost_task/__manifest__.py b/distribute_cost_task/__manifest__.py
new file mode 100644
index 00000000000..fe37d695322
--- /dev/null
+++ b/distribute_cost_task/__manifest__.py
@@ -0,0 +1,11 @@
+{
+ "name": "Distribute Cost Price",
+ "depends": ["sale_management"],
+ "data": [
+ "security/ir.model.access.csv",
+ "wizard/order_line_wizard.xml",
+ "views/sale_oder_inherit_view.xml",
+ ],
+ "installable": True,
+ "license": "LGPL-3",
+}
diff --git a/distribute_cost_task/model/__init__.py b/distribute_cost_task/model/__init__.py
new file mode 100644
index 00000000000..85d9886c49b
--- /dev/null
+++ b/distribute_cost_task/model/__init__.py
@@ -0,0 +1,3 @@
+from . import order_line_cost_divide
+from . import sale_order
+from . import sale_order_line
diff --git a/distribute_cost_task/model/order_line_cost_divide.py b/distribute_cost_task/model/order_line_cost_divide.py
new file mode 100644
index 00000000000..ce3e1cfad59
--- /dev/null
+++ b/distribute_cost_task/model/order_line_cost_divide.py
@@ -0,0 +1,15 @@
+from odoo import fields, models
+
+
+class OrderLineCostDivide(models.Model):
+ _name = "order.line.cost.divide"
+ _description = "order line cost divide"
+
+ cost = fields.Float("Divided Cost")
+ divide_from_order_line = fields.Many2one(
+ "sale.order.line", string="Divide from order line"
+ )
+ divide_to_order_line = fields.Many2one(
+ "sale.order.line", string="Divide to Order Line"
+ )
+ order_id = fields.Many2one("sale.order", string="Order Id", required=True)
diff --git a/distribute_cost_task/model/sale_order.py b/distribute_cost_task/model/sale_order.py
new file mode 100644
index 00000000000..85c108d7c88
--- /dev/null
+++ b/distribute_cost_task/model/sale_order.py
@@ -0,0 +1,32 @@
+from odoo import api, fields, models
+
+
+class SaleOrder(models.Model):
+ _inherit = "sale.order"
+
+ divide_column = fields.Boolean(
+ string="Divide Column",
+ compute="_compute_divide_column",
+ store=True,
+ default=False,
+ )
+
+ @api.depends("order_line.divide_cost")
+ def _compute_divide_column(self):
+ data = dict(
+ self.env["order.line.cost.divide"]._read_group(
+ [("order_id", "in", self.ids)], ["order_id"], ["__count"]
+ )
+ )
+ for order in self:
+ if data.get(order):
+ has_zero_divide_cost = any(
+ line.divide_cost == 0 for line in order.order_line
+ )
+ order.divide_column = has_zero_divide_cost
+
+ def _get_order_lines_to_report(self):
+ order_lines = super()._get_order_lines_to_report()
+ return order_lines.filtered(
+ lambda line: not line.divide_from_order_lines or line.divide_cost > 0
+ )
diff --git a/distribute_cost_task/model/sale_order_line.py b/distribute_cost_task/model/sale_order_line.py
new file mode 100644
index 00000000000..1fade13d36f
--- /dev/null
+++ b/distribute_cost_task/model/sale_order_line.py
@@ -0,0 +1,45 @@
+from odoo import api, fields, models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ divide_cost = fields.Float("Devision")
+ divide_to_order_lines = fields.One2many(
+ "order.line.cost.divide", "divide_to_order_line", string="Divided to order line"
+ )
+ divide_from_order_lines = fields.One2many(
+ "order.line.cost.divide",
+ "divide_from_order_line",
+ string="Divided from order line",
+ )
+
+ def action_open_order_line_wizard(self):
+ return {
+ "name": "Order Line Wizard",
+ "type": "ir.actions.act_window",
+ "res_model": "order.wizard",
+ "view_mode": "form",
+ "target": "new",
+ "context": {
+ "order_id": self.order_id.id,
+ },
+ }
+
+ @api.ondelete(at_uninstall=False)
+ def _unlink_order_line(self):
+ for record in self:
+ for line in record.divide_to_order_lines:
+ line.divide_from_order_line.divide_cost += line.cost
+ line.cost = 0.0
+ for line in record.divide_from_order_lines:
+ line.divide_to_order_line.divide_cost -= line.cost
+ line.cost = 0.0
+
+ @api.depends("product_uom_qty", "discount", "price_unit", "tax_id", "divide_cost")
+ def _compute_amount(self):
+ super()._compute_amount()
+ for record in self:
+ if record.divide_from_order_lines:
+ record.price_subtotal -= record.price_unit
+ record.price_subtotal += record.divide_cost
diff --git a/distribute_cost_task/security/ir.model.access.csv b/distribute_cost_task/security/ir.model.access.csv
new file mode 100644
index 00000000000..88258c8c2e7
--- /dev/null
+++ b/distribute_cost_task/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_order_wizard,access_order_wizard,model_order_wizard,base.group_user,1,1,1,0
+access_order_line_wizard,access_order_line_wizard,model_order_line_wizard,base.group_user,1,1,1,0
+access_order_line_cost_divide,access_order_line_cost_divide,model_order_line_cost_divide,base.group_user,1,1,1,1
diff --git a/distribute_cost_task/views/sale_oder_inherit_view.xml b/distribute_cost_task/views/sale_oder_inherit_view.xml
new file mode 100644
index 00000000000..0bcf468a039
--- /dev/null
+++ b/distribute_cost_task/views/sale_oder_inherit_view.xml
@@ -0,0 +1,15 @@
+
+
+ sale.order.line.inherit.view
+ sale.order
+
+
+
+
+
+
+
+
+
diff --git a/distribute_cost_task/wizard/__init__.py b/distribute_cost_task/wizard/__init__.py
new file mode 100644
index 00000000000..bdf6f5c2ae8
--- /dev/null
+++ b/distribute_cost_task/wizard/__init__.py
@@ -0,0 +1,2 @@
+from . import order_line_wizard
+from . import order_wizard
diff --git a/distribute_cost_task/wizard/order_line_wizard.py b/distribute_cost_task/wizard/order_line_wizard.py
new file mode 100644
index 00000000000..e6a5ec014f9
--- /dev/null
+++ b/distribute_cost_task/wizard/order_line_wizard.py
@@ -0,0 +1,14 @@
+from odoo import fields, models
+
+
+class OrderLineWizardLine(models.TransientModel):
+ _name = "order.line.wizard"
+ _description = "order line wizard line"
+
+ wizard_id = fields.Many2one("order.wizard", string="Wizard")
+ price = fields.Float(string="Price", store=True)
+ product_template_id = fields.Many2one("product.template", string="Product")
+ order_line_id = fields.Many2one("sale.order.line", string="Order Line")
+ include_for_division = fields.Boolean(
+ "Include orderline for division", default="true"
+ )
diff --git a/distribute_cost_task/wizard/order_line_wizard.xml b/distribute_cost_task/wizard/order_line_wizard.xml
new file mode 100644
index 00000000000..23c55b216cb
--- /dev/null
+++ b/distribute_cost_task/wizard/order_line_wizard.xml
@@ -0,0 +1,25 @@
+
+
+ order.line.wizard.form
+ order.wizard
+
+
+
+
+
diff --git a/distribute_cost_task/wizard/order_wizard.py b/distribute_cost_task/wizard/order_wizard.py
new file mode 100644
index 00000000000..a80f77cbc9e
--- /dev/null
+++ b/distribute_cost_task/wizard/order_wizard.py
@@ -0,0 +1,69 @@
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+
+class OrderLineWizard(models.TransientModel):
+ _name = "order.wizard"
+ _description = "Order Line Wizard"
+
+ order_line_ids = fields.One2many(
+ "order.line.wizard", "wizard_id", string="Order Lines"
+ )
+ exclude_line_id = fields.Many2one("sale.order.line", string="Order Line")
+
+ @api.model
+ def default_get(self, fields_list):
+ res = super().default_get(fields_list)
+ exclude_line_id = self.env["sale.order.line"].browse(
+ self.env.context.get("active_id")
+ )
+ order_lines = []
+ order = self.env["sale.order"].browse(self.env.context.get("order_id"))
+ total_lines = order.order_line - exclude_line_id
+ if len(total_lines) == 0:
+ raise UserError("No order line to divide cost")
+ cost = exclude_line_id.price_unit / len(total_lines)
+ for line in order.order_line - exclude_line_id:
+ order_lines.append(
+ (
+ 0,
+ 0,
+ {
+ "product_template_id": line.product_template_id.id,
+ "price": cost,
+ "order_line_id": line.id,
+ "include_for_division": True,
+ },
+ )
+ )
+ res["exclude_line_id"] = exclude_line_id.id
+ res["order_line_ids"] = order_lines
+ return res
+
+ def action_add_cost(self):
+ include_for_division_lines = self.order_line_ids.filtered(
+ lambda l: l.include_for_division
+ )
+ if not include_for_division_lines:
+ raise UserError(
+ "At least one order line must be selected for cost division."
+ )
+ total_cost = sum(include_for_division_lines.mapped("price"))
+ if total_cost <= self.exclude_line_id.price_unit:
+ self.exclude_line_id.divide_cost += (
+ self.exclude_line_id.price_unit - total_cost
+ )
+ for wiz_line in include_for_division_lines:
+ wiz_line.order_line_id.divide_cost += wiz_line.price
+ self.env["order.line.cost.divide"].create(
+ {
+ "cost": wiz_line.price,
+ "divide_from_order_line": self.exclude_line_id.id,
+ "divide_to_order_line": wiz_line.order_line_id.id,
+ "order_id": self.exclude_line_id.order_id.id,
+ }
+ )
+ else:
+ raise UserError(
+ f"Total cost of all product greater than {self.exclude_line_id.price_unit}"
+ )
diff --git a/estate/report/estate_user_properties_report.xml b/estate/report/estate_user_properties_report.xml
new file mode 100644
index 00000000000..7fd9c5b114b
--- /dev/null
+++ b/estate/report/estate_user_properties_report.xml
@@ -0,0 +1,10 @@
+
+
+ User Properties Report
+ res.users
+ estate.report_estate_properties
+ qweb-pdf
+
+ report
+
+
diff --git a/product_warranty/__init__.py b/product_warranty/__init__.py
new file mode 100644
index 00000000000..9b4296142f4
--- /dev/null
+++ b/product_warranty/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/product_warranty/__manifest__.py b/product_warranty/__manifest__.py
new file mode 100644
index 00000000000..89e17dfa8ac
--- /dev/null
+++ b/product_warranty/__manifest__.py
@@ -0,0 +1,20 @@
+{
+ "name": "Product Warranty",
+ "depends": [
+ "sale_management",
+ "website_sale",
+ ],
+ "data": [
+ "security/ir.model.access.csv",
+ "wizard/order_wizard_view.xml",
+ "views/product_template_view.xml",
+ "views/warranty_configration.xml",
+ "views/warranty_configration_menu.xml",
+ "views/sale_order_views.xml",
+ ],
+ "demo": [
+ "demo/product_warranty_demo.xml",
+ ],
+ "installable": True,
+ "license": "LGPL-3",
+}
diff --git a/product_warranty/demo/product_warranty_demo.xml b/product_warranty/demo/product_warranty_demo.xml
new file mode 100644
index 00000000000..ac322c570d7
--- /dev/null
+++ b/product_warranty/demo/product_warranty_demo.xml
@@ -0,0 +1,32 @@
+
+
+
+ Extended Warranty 1 Yr
+
+
+ Extended Warranty 2 Yr
+
+
+ Extended Warranty 3 Yr
+
+
+
+ 1 Yr
+
+ 10
+ 1
+
+
+ 2 Yr
+
+ 15
+ 2
+
+
+ 3 Yr
+
+ 20
+ 3
+
+
+
diff --git a/product_warranty/models/__init__.py b/product_warranty/models/__init__.py
new file mode 100644
index 00000000000..d23f0ca7977
--- /dev/null
+++ b/product_warranty/models/__init__.py
@@ -0,0 +1,3 @@
+from . import product_template
+from . import product_warranty
+from . import sale_order_line
diff --git a/product_warranty/models/product_template.py b/product_warranty/models/product_template.py
new file mode 100644
index 00000000000..e9fdae5db6c
--- /dev/null
+++ b/product_warranty/models/product_template.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class ProductTemplate(models.Model):
+ _inherit = "product.template"
+
+ is_warranty_available = fields.Boolean("Is Warranty Available", default=False)
diff --git a/product_warranty/models/product_warranty.py b/product_warranty/models/product_warranty.py
new file mode 100644
index 00000000000..59ccd05a318
--- /dev/null
+++ b/product_warranty/models/product_warranty.py
@@ -0,0 +1,13 @@
+from odoo import fields, models
+
+
+class ProductWarranty(models.Model):
+ _name = "product.warranty"
+ _description = "Product Warranty"
+
+ name = fields.Char("Name", required=True)
+ percentage = fields.Float("Percentage", required=True)
+ product_template_id = fields.Many2one(
+ "product.template", string="Product", required=True
+ )
+ year = fields.Integer("Year", required=True)
diff --git a/product_warranty/models/sale_order_line.py b/product_warranty/models/sale_order_line.py
new file mode 100644
index 00000000000..13289ac6af3
--- /dev/null
+++ b/product_warranty/models/sale_order_line.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ connected_order_line_id = fields.Many2one("sale.order.line", ondelete="cascade")
diff --git a/product_warranty/security/ir.model.access.csv b/product_warranty/security/ir.model.access.csv
new file mode 100644
index 00000000000..b3b284cc674
--- /dev/null
+++ b/product_warranty/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_product_warranty,access_product_warranty,model_product_warranty,base.group_user,1,1,1,1
+access_order_wizard,access_order_wizard,model_order_wizard,base.group_user,1,1,1,1
+access_product_warranty_wizard,access_product_warranty_wizard,model_product_warranty_wizard,base.group_user,1,1,1,1
diff --git a/product_warranty/views/product_template_view.xml b/product_warranty/views/product_template_view.xml
new file mode 100644
index 00000000000..363aa320c73
--- /dev/null
+++ b/product_warranty/views/product_template_view.xml
@@ -0,0 +1,14 @@
+
+
+ product.template.inherit.form
+ product.template
+
+
+
+
+
+
+
+
+
+
diff --git a/product_warranty/views/sale_order_views.xml b/product_warranty/views/sale_order_views.xml
new file mode 100644
index 00000000000..7e8459746c5
--- /dev/null
+++ b/product_warranty/views/sale_order_views.xml
@@ -0,0 +1,17 @@
+
+
+ sale.order.inherit.form
+ sale.order
+
+
+
+
+
+
+
+
diff --git a/product_warranty/views/warranty_configration.xml b/product_warranty/views/warranty_configration.xml
new file mode 100644
index 00000000000..af0e0e42e8c
--- /dev/null
+++ b/product_warranty/views/warranty_configration.xml
@@ -0,0 +1,19 @@
+
+
+ Product Warranty
+ product.warranty
+ list,form
+
+
+
+ warranty_configuration_list_view
+ product.warranty
+
+
+
+
+
+
+
+
+
diff --git a/product_warranty/views/warranty_configration_menu.xml b/product_warranty/views/warranty_configration_menu.xml
new file mode 100644
index 00000000000..576371f57d5
--- /dev/null
+++ b/product_warranty/views/warranty_configration_menu.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/product_warranty/wizard/__init__.py b/product_warranty/wizard/__init__.py
new file mode 100644
index 00000000000..03ae7818698
--- /dev/null
+++ b/product_warranty/wizard/__init__.py
@@ -0,0 +1,2 @@
+from . import order_wizard
+from . import product_warranty_wizard
diff --git a/product_warranty/wizard/order_wizard.py b/product_warranty/wizard/order_wizard.py
new file mode 100644
index 00000000000..d9a6d06f91e
--- /dev/null
+++ b/product_warranty/wizard/order_wizard.py
@@ -0,0 +1,51 @@
+from odoo import api, fields, models
+from odoo.exceptions import UserError
+
+
+class OrderWizard(models.TransientModel):
+ _name = "order.wizard"
+ _description = "Order Wizard"
+
+ order_line_ids = fields.One2many(
+ "product.warranty.wizard", "wizard_id", string="Order Lines"
+ )
+
+ @api.model
+ def default_get(self, fields_list):
+ res = super().default_get(fields_list)
+ order_lines = self.env["sale.order"].browse(self.env.context.get("active_id"))
+ filter_oder_lines = order_lines.order_line.filtered(
+ lambda l: l.product_template_id.is_warranty_available
+ )
+ order_lines = []
+ if len(filter_oder_lines) == 0:
+ raise UserError("No order line to divide cost")
+ for line in filter_oder_lines:
+ order_lines.append(
+ (
+ 0,
+ 0,
+ {
+ "order_line_id": line.id,
+ "product_template_id": line.product_template_id.id,
+ },
+ )
+ )
+ res["order_line_ids"] = order_lines
+ return res
+
+ def action_add_warranty(self):
+ for line in self.order_line_ids:
+ if line.year:
+ self.env["sale.order.line"].create(
+ {
+ "product_template_id": line.year.product_template_id,
+ "price_unit": (
+ line.order_line_id.price_unit * line.year.percentage
+ )
+ / 100,
+ "name": f"Warranty for {line.order_line_id.name} (Ends: {line.end_date})",
+ "order_id": line.order_line_id.order_id.id,
+ "connected_order_line_id": line.order_line_id.id,
+ }
+ )
diff --git a/product_warranty/wizard/order_wizard_view.xml b/product_warranty/wizard/order_wizard_view.xml
new file mode 100644
index 00000000000..fd21d5e1ffc
--- /dev/null
+++ b/product_warranty/wizard/order_wizard_view.xml
@@ -0,0 +1,33 @@
+
+
+
+ Product Warranty Wizard
+ order.wizard
+ form
+ new
+
+
+
+ order.wizard.form
+ order.wizard
+
+
+
+
+
diff --git a/product_warranty/wizard/product_warranty_wizard.py b/product_warranty/wizard/product_warranty_wizard.py
new file mode 100644
index 00000000000..169e0041ee4
--- /dev/null
+++ b/product_warranty/wizard/product_warranty_wizard.py
@@ -0,0 +1,18 @@
+from odoo import api, fields, models
+from datetime import date, timedelta
+
+
+class ProductWarrantyWizard(models.TransientModel):
+ _name = "product.warranty.wizard"
+ _description = "Product Warranty Wizard"
+
+ year = fields.Many2one("product.warranty", string="year")
+ product_template_id = fields.Many2one("product.template", string="Product")
+ end_date = fields.Date("End Date", compute="_compute_end_date")
+ order_line_id = fields.Many2one("sale.order.line")
+ wizard_id = fields.Many2one("order.wizard", required=True)
+
+ @api.depends("year")
+ def _compute_end_date(self):
+ for line in self:
+ line.end_date = date.today() + timedelta(days=365 * line.year.year)
diff --git a/sale_book_price_practice/__init__.py b/sale_book_price_practice/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/sale_book_price_practice/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/sale_book_price_practice/__manifest__.py b/sale_book_price_practice/__manifest__.py
new file mode 100644
index 00000000000..df5a486b95a
--- /dev/null
+++ b/sale_book_price_practice/__manifest__.py
@@ -0,0 +1,12 @@
+{
+ "name": "Sale Book Price Practice",
+ "depends": [
+ "sale_management",
+ ],
+ "data": [
+ "views/sale_oder_inherit_view.xml",
+ "views/account_move_inherit_view.xml",
+ ],
+ "installable": True,
+ "license": "LGPL-3",
+}
diff --git a/sale_book_price_practice/models/__init__.py b/sale_book_price_practice/models/__init__.py
new file mode 100644
index 00000000000..364d2817c62
--- /dev/null
+++ b/sale_book_price_practice/models/__init__.py
@@ -0,0 +1,2 @@
+from . import account_move_line
+from . import sale_oder_line
diff --git a/sale_book_price_practice/models/account_move_line.py b/sale_book_price_practice/models/account_move_line.py
new file mode 100644
index 00000000000..788ba74fd32
--- /dev/null
+++ b/sale_book_price_practice/models/account_move_line.py
@@ -0,0 +1,25 @@
+from odoo import api, fields, models
+
+
+class AccountMoveLine(models.Model):
+ _inherit = "account.move.line"
+
+ book_price = fields.Float(string="Book Price", compute="_compute_book_price")
+
+ @api.depends("product_id", "quantity", "move_id.invoice_line_ids")
+ def _compute_book_price(self):
+ for line in self:
+ if not (line.product_id and line.move_id and line.move_id.invoice_line_ids):
+ line.book_price = 0.0
+ continue
+
+ pricelist = (
+ line.move_id.invoice_line_ids.sale_line_ids.order_id.pricelist_id
+ )
+
+ if pricelist:
+ line.book_price = pricelist._get_product_price(
+ line.product_id, line.quantity, line.product_uom_id
+ )
+ else:
+ line.book_price = 0.0
diff --git a/sale_book_price_practice/models/sale_oder_line.py b/sale_book_price_practice/models/sale_oder_line.py
new file mode 100644
index 00000000000..0d9330b0b23
--- /dev/null
+++ b/sale_book_price_practice/models/sale_oder_line.py
@@ -0,0 +1,19 @@
+from odoo import api, fields, models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ book_price = fields.Float(
+ "Book Price", compute="_compute_book_price",
+ )
+
+ @api.depends("pricelist_item_id")
+ def _compute_book_price(self):
+ for line in self:
+ if line.pricelist_item_id:
+ line.book_price = (
+ line.pricelist_item_id.fixed_price * line.product_uom_qty
+ )
+ else:
+ line.book_price = 0
diff --git a/sale_book_price_practice/views/account_move_inherit_view.xml b/sale_book_price_practice/views/account_move_inherit_view.xml
new file mode 100644
index 00000000000..9f93af09394
--- /dev/null
+++ b/sale_book_price_practice/views/account_move_inherit_view.xml
@@ -0,0 +1,12 @@
+
+
+ account.invoice.line.inherit.view
+ account.move
+
+
+
+
+
+
+
+
diff --git a/sale_book_price_practice/views/sale_oder_inherit_view.xml b/sale_book_price_practice/views/sale_oder_inherit_view.xml
new file mode 100644
index 00000000000..d98d656b0d7
--- /dev/null
+++ b/sale_book_price_practice/views/sale_oder_inherit_view.xml
@@ -0,0 +1,12 @@
+
+
+ sale.order.line.inherit.view
+ sale.order
+
+
+
+
+
+
+
+
diff --git a/sales_person_in_pos/__init__.py b/sales_person_in_pos/__init__.py
new file mode 100644
index 00000000000..0650744f6bc
--- /dev/null
+++ b/sales_person_in_pos/__init__.py
@@ -0,0 +1 @@
+from . import models
diff --git a/sales_person_in_pos/__manifest__.py b/sales_person_in_pos/__manifest__.py
new file mode 100644
index 00000000000..59f845e4590
--- /dev/null
+++ b/sales_person_in_pos/__manifest__.py
@@ -0,0 +1,14 @@
+{
+ "name": "Sale Person In Pos",
+ "depends": ["point_of_sale", "hr"],
+ "data": [
+ "views/pos_views.xml",
+ ],
+ "assets": {
+ "point_of_sale._assets_pos": [
+ "sales_person_in_pos/static/src/**/*",
+ ],
+ },
+ "installable": True,
+ "license": "LGPL-3",
+}
diff --git a/sales_person_in_pos/models/__init__.py b/sales_person_in_pos/models/__init__.py
new file mode 100644
index 00000000000..b2f4b5e054e
--- /dev/null
+++ b/sales_person_in_pos/models/__init__.py
@@ -0,0 +1,2 @@
+from . import pos_order
+from . import pos_session
diff --git a/sales_person_in_pos/models/pos_order.py b/sales_person_in_pos/models/pos_order.py
new file mode 100644
index 00000000000..95afe96ec5d
--- /dev/null
+++ b/sales_person_in_pos/models/pos_order.py
@@ -0,0 +1,7 @@
+from odoo import fields, models
+
+
+class PosOrder(models.Model):
+ _inherit = "pos.order"
+
+ salesperson_id = fields.Many2one("hr.employee", string="Salesperson")
diff --git a/sales_person_in_pos/models/pos_session.py b/sales_person_in_pos/models/pos_session.py
new file mode 100644
index 00000000000..20fd6f6fc27
--- /dev/null
+++ b/sales_person_in_pos/models/pos_session.py
@@ -0,0 +1,11 @@
+from odoo import api, models
+
+
+class PosSession(models.Model):
+ _inherit = "pos.session"
+
+ @api.model
+ def _load_pos_data_models(self, config_id):
+ data = super()._load_pos_data_models(config_id)
+ data.append("hr.employee")
+ return data
diff --git a/sales_person_in_pos/static/src/control_buttons/control_buttons.js b/sales_person_in_pos/static/src/control_buttons/control_buttons.js
new file mode 100644
index 00000000000..a599cfdf25b
--- /dev/null
+++ b/sales_person_in_pos/static/src/control_buttons/control_buttons.js
@@ -0,0 +1,16 @@
+import { patch } from "@web/core/utils/patch";
+import { ControlButtons } from "@point_of_sale/app/screens/product_screen/control_buttons/control_buttons";
+import { SelectSalesPersonButton } from "../select_sales_person_button/sales_person_button";
+
+patch(ControlButtons, {
+ components: {
+ ...ControlButtons.components,
+ SelectSalesPersonButton,
+ },
+});
+
+patch(ControlButtons.prototype, {
+ get salesperson() {
+ return this.pos.get_order().getSalesperson();
+ },
+});
diff --git a/sales_person_in_pos/static/src/control_buttons/control_buttons.xml b/sales_person_in_pos/static/src/control_buttons/control_buttons.xml
new file mode 100644
index 00000000000..c77e078b3f2
--- /dev/null
+++ b/sales_person_in_pos/static/src/control_buttons/control_buttons.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/sales_person_in_pos/static/src/pos_order.js b/sales_person_in_pos/static/src/pos_order.js
new file mode 100644
index 00000000000..f093db5ab7b
--- /dev/null
+++ b/sales_person_in_pos/static/src/pos_order.js
@@ -0,0 +1,14 @@
+import { patch } from "@web/core/utils/patch";
+import { PosOrder } from "@point_of_sale/app/models/pos_order";
+
+patch(PosOrder.prototype, {
+
+ setSalesperson(salesperson) {
+ this.update({ salesperson_id: salesperson });
+ },
+
+ getSalesperson() {
+ return this.salesperson_id;
+ }
+
+});
diff --git a/sales_person_in_pos/static/src/pos_store.js b/sales_person_in_pos/static/src/pos_store.js
new file mode 100644
index 00000000000..e4f1e80f8ff
--- /dev/null
+++ b/sales_person_in_pos/static/src/pos_store.js
@@ -0,0 +1,37 @@
+import { patch } from "@web/core/utils/patch";
+import { PosStore } from "@point_of_sale/app/store/pos_store";
+import { makeAwaitable } from "@point_of_sale/app/store/make_awaitable_dialog";
+import { SelectionPopup } from "@point_of_sale/app/utils/input_popups/selection_popup";
+
+patch(PosStore.prototype, {
+
+ async selectSalesPerson() {
+ const currentOrder = this.get_order();
+ const salesperson_list = this.models['hr.employee'].map((sp) => {
+ return {
+ id: sp.id,
+ item: sp,
+ label: sp.name,
+ isSelected: currentOrder.getSalesperson()?.id === sp.id
+ }
+ });
+
+ const selected_salesperson = await makeAwaitable(
+ this.dialog,
+ SelectionPopup,
+ {
+ title: "Select Salesperson",
+ list: salesperson_list,
+ }
+ );
+
+ if (selected_salesperson) {
+ const existing_salesperson = currentOrder.getSalesperson();
+ if (!(existing_salesperson?.id === selected_salesperson?.id)) {
+ currentOrder.setSalesperson(selected_salesperson);
+ } else {
+ currentOrder.setSalesperson(false)
+ }
+ }
+ }
+});
diff --git a/sales_person_in_pos/static/src/select_sales_person_button/sales_person_button.js b/sales_person_in_pos/static/src/select_sales_person_button/sales_person_button.js
new file mode 100644
index 00000000000..5f8506ab635
--- /dev/null
+++ b/sales_person_in_pos/static/src/select_sales_person_button/sales_person_button.js
@@ -0,0 +1,11 @@
+import { Component, useState } from "@odoo/owl";
+import { usePos } from "@point_of_sale/app/store/pos_hook";
+
+export class SelectSalesPersonButton extends Component {
+ static template = "sales_person_in_pos.SelectSalesPersonButton";
+ static props = ["salesperson?"];
+
+ setup() {
+ this.pos = usePos();
+ }
+}
diff --git a/sales_person_in_pos/static/src/select_sales_person_button/sales_person_button.xml b/sales_person_in_pos/static/src/select_sales_person_button/sales_person_button.xml
new file mode 100644
index 00000000000..591e3b39f97
--- /dev/null
+++ b/sales_person_in_pos/static/src/select_sales_person_button/sales_person_button.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/sales_person_in_pos/views/pos_views.xml b/sales_person_in_pos/views/pos_views.xml
new file mode 100644
index 00000000000..205fbe5edc9
--- /dev/null
+++ b/sales_person_in_pos/views/pos_views.xml
@@ -0,0 +1,24 @@
+
+
+
+ pos.order.form
+ pos.order
+
+
+
+
+
+
+
+
+
+ pos.order.list
+ pos.order
+
+
+
+
+
+
+
+