From 16a23a63c955b45998215e58c716b1e918d92565 Mon Sep 17 00:00:00 2001 From: David Date: Fri, 6 Sep 2024 10:27:08 +0200 Subject: [PATCH] [ADD] product_catalog: new module Backport from the v17 functionality TT50477 --- product_catalog/README.rst | 104 ++++ product_catalog/__init__.py | 2 + product_catalog/__manifest__.py | 23 + product_catalog/controllers/__init__.py | 1 + product_catalog/controllers/catalog.py | 54 +++ product_catalog/models/__init__.py | 1 + .../models/product_catalog_mixin.py | 146 ++++++ product_catalog/readme/CONTEXT.md | 2 + product_catalog/readme/CONTRIBUTORS.md | 2 + product_catalog/readme/DESCRIPTION.md | 5 + product_catalog/readme/ROADMAP.md | 1 + product_catalog/readme/USAGE.md | 3 + product_catalog/static/description/index.html | 453 ++++++++++++++++++ .../product_catalog/kanban_controller.esm.js | 52 ++ .../src/product_catalog/kanban_model.esm.js | 36 ++ .../src/product_catalog/kanban_record.esm.js | 141 ++++++ .../src/product_catalog/kanban_record.xml | 25 + .../product_catalog/kanban_renderer.esm.js | 37 ++ .../src/product_catalog/kanban_renderer.xml | 29 ++ .../src/product_catalog/kanban_view.esm.js | 18 + .../product_catalog/kanban_view_buttons.xml | 18 + .../order_line/order_line.esm.js | 41 ++ .../order_line/order_line.scss | 20 + .../product_catalog/order_line/order_line.xml | 92 ++++ .../search/search_panel.esm.js | 55 +++ .../product_catalog/search/search_panel.xml | 50 ++ product_catalog/views/product_views.xml | 140 ++++++ .../odoo/addons/product_catalog | 1 + setup/product_catalog/setup.py | 6 + 29 files changed, 1558 insertions(+) create mode 100644 product_catalog/README.rst create mode 100644 product_catalog/__init__.py create mode 100644 product_catalog/__manifest__.py create mode 100644 product_catalog/controllers/__init__.py create mode 100644 product_catalog/controllers/catalog.py create mode 100644 product_catalog/models/__init__.py create mode 100644 product_catalog/models/product_catalog_mixin.py create mode 100644 product_catalog/readme/CONTEXT.md create mode 100644 product_catalog/readme/CONTRIBUTORS.md create mode 100644 product_catalog/readme/DESCRIPTION.md create mode 100644 product_catalog/readme/ROADMAP.md create mode 100644 product_catalog/readme/USAGE.md create mode 100644 product_catalog/static/description/index.html create mode 100644 product_catalog/static/src/product_catalog/kanban_controller.esm.js create mode 100644 product_catalog/static/src/product_catalog/kanban_model.esm.js create mode 100644 product_catalog/static/src/product_catalog/kanban_record.esm.js create mode 100644 product_catalog/static/src/product_catalog/kanban_record.xml create mode 100644 product_catalog/static/src/product_catalog/kanban_renderer.esm.js create mode 100644 product_catalog/static/src/product_catalog/kanban_renderer.xml create mode 100644 product_catalog/static/src/product_catalog/kanban_view.esm.js create mode 100644 product_catalog/static/src/product_catalog/kanban_view_buttons.xml create mode 100644 product_catalog/static/src/product_catalog/order_line/order_line.esm.js create mode 100644 product_catalog/static/src/product_catalog/order_line/order_line.scss create mode 100644 product_catalog/static/src/product_catalog/order_line/order_line.xml create mode 100644 product_catalog/static/src/product_catalog/search/search_panel.esm.js create mode 100644 product_catalog/static/src/product_catalog/search/search_panel.xml create mode 100644 product_catalog/views/product_views.xml create mode 120000 setup/product_catalog/odoo/addons/product_catalog create mode 100644 setup/product_catalog/setup.py diff --git a/product_catalog/README.rst b/product_catalog/README.rst new file mode 100644 index 000000000000..7713936189b3 --- /dev/null +++ b/product_catalog/README.rst @@ -0,0 +1,104 @@ +=============== +Product Catalog +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3d34fa62aed534517dd4da5bbb4a76ef626aa74cd47b3bd7a0c726c8d7de853e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fproduct--attribute-lightgray.png?logo=github + :target: https://github.com/OCA/product-attribute/tree/16.0/product_catalog + :alt: OCA/product-attribute +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/product-attribute-16-0/product-attribute-16-0-product_catalog + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/product-attribute&target_branch=16.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +A backport of Odoo's v17 Product Catalog. + +Changes over mainstream: + +:: + + - Price is an optional value now so we can use it in other models. + +**Table of contents** + +.. contents:: + :local: + +Use Cases / Context +=================== + +The product catalog is a nice addon the version 17 features that allows +users to use a simple product picker to quickly add products to a +document (sale or purchase order). + +Usage +===== + +To use this module, you need to: + +1. Go to ... + +Known issues / Roadmap +====================== + +- Be able to filter just the added products. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Odoo SA +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__ + + - David Vidal + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/product-attribute `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_catalog/__init__.py b/product_catalog/__init__.py new file mode 100644 index 000000000000..91c5580fed36 --- /dev/null +++ b/product_catalog/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/product_catalog/__manifest__.py b/product_catalog/__manifest__.py new file mode 100644 index 000000000000..bce9ac30c89f --- /dev/null +++ b/product_catalog/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2024 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Product Catalog", + "summary": "Backport of Odoos v17 product catalog", + "version": "16.0.1.0.0", + "author": "Odoo SA, Tecnativa, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/product-attribute", + "license": "AGPL-3", + "category": "Product", + "depends": ["product"], + "data": [ + "views/product_views.xml", + ], + "demo": [], + "assets": { + "web.assets_backend": [ + "product_catalog/static/src/product_catalog/**/*.js", + "product_catalog/static/src/product_catalog/**/*.xml", + "product_catalog/static/src/product_catalog/**/*.scss", + ], + }, +} diff --git a/product_catalog/controllers/__init__.py b/product_catalog/controllers/__init__.py new file mode 100644 index 000000000000..e67fc1889923 --- /dev/null +++ b/product_catalog/controllers/__init__.py @@ -0,0 +1 @@ +from . import catalog diff --git a/product_catalog/controllers/catalog.py b/product_catalog/controllers/catalog.py new file mode 100644 index 000000000000..47c5a224435d --- /dev/null +++ b/product_catalog/controllers/catalog.py @@ -0,0 +1,54 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.http import Controller, request, route + + +class ProductCatalogController(Controller): + @route("/product/catalog/order_lines_info", auth="user", type="json") + def product_catalog_get_order_lines_info( + self, res_model, order_id, product_ids, **kwargs + ): + """Returns products information to be shown in the catalog. + + :param string res_model: The order model. + :param int order_id: The order id. + :param list product_ids: The products currently displayed in the product + catalog, as a list of `product.product` ids. + :rtype: dict + :return: A dict with the following structure: + { + product.id: { + 'productId': int + 'quantity': float (optional) + 'price': float + 'readOnly': bool (optional) + } + } + """ + order = request.env[res_model].browse(order_id) + return order.with_company( + order.company_id + )._get_product_catalog_order_line_info( + product_ids, + **kwargs, + ) + + @route("/product/catalog/update_order_line_info", auth="user", type="json") + def product_catalog_update_order_line_info( + self, res_model, order_id, product_id, quantity=0, **kwargs + ): + """Update order line information on a given order for a given product. + + :param string res_model: The order model. + :param int order_id: The order id. + :param int product_id: The product, as a `product.product` id. + :return: The unit price price of the product, based on the pricelist of the order and + the quantity selected. + :rtype: float + """ + order = request.env[res_model].browse(order_id) + return order.with_company(order.company_id)._update_order_line_info( + product_id, + quantity, + **kwargs, + ) diff --git a/product_catalog/models/__init__.py b/product_catalog/models/__init__.py new file mode 100644 index 000000000000..7a9dc77ecb63 --- /dev/null +++ b/product_catalog/models/__init__.py @@ -0,0 +1 @@ +from . import product_catalog_mixin diff --git a/product_catalog/models/product_catalog_mixin.py b/product_catalog/models/product_catalog_mixin.py new file mode 100644 index 000000000000..e656b397ef33 --- /dev/null +++ b/product_catalog/models/product_catalog_mixin.py @@ -0,0 +1,146 @@ +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import _, fields, models + + +class ProductCatalogMixin(models.AbstractModel): + """This mixin should be inherited when the model should be able to work + with the product catalog. + It assumes the model using this mixin has a O2M field where the products are + added/removed and this field's co-related model should has a method named + `_get_product_catalog_lines_data`. + """ + + _name = "product.catalog.mixin" + _description = "Product Catalog Mixin" + + catalog_button_text = fields.Text(compute="_compute_catalog_button_text") + + def _compute_catalog_button_text(self): + quotations = self.filtered(lambda x: x.state in {"draft", "sent"}) + quotations.catalog_button_text = _("Back to Quotation") + (self - quotations).catalog_button_text = _("Back to Order") + + def action_add_from_catalog(self): + kanban_view_id = self.env.ref("product_catalog.product_view_kanban_catalog").id + search_view_id = self.env.ref("product_catalog.product_view_search_catalog").id + additional_context = self._get_action_add_from_catalog_extra_context() + return { + "type": "ir.actions.act_window", + "name": _("Products"), + "res_model": "product.product", + "views": [(kanban_view_id, "kanban"), (False, "form")], + "search_view_id": [search_view_id, "search"], + "domain": self._get_product_catalog_domain(), + "context": {**self.env.context, **additional_context}, + } + + def _default_order_line_values(self): + return { + "quantity": 0, + "readOnly": self._is_readonly() if self else False, + } + + def _get_product_catalog_domain(self): + """Get the domain to search for products in the catalog. + + For a model that uses products that has to be hidden in the catalog, it + must override this method and extend the appropriate domain. + :returns: A list of tuples that represents a domain. + :rtype: list + """ + return [("company_id", "in", [self.company_id.id, False])] + + def _get_product_catalog_record_lines(self, product_ids): + """Returns the record's lines grouped by product. + Must be overrided by each model using this mixin. + + :param list product_ids: The ids of the products currently displayed in the + product catalog. + :rtype: dict + """ + return {} + + def _get_product_catalog_order_data(self, products, **kwargs): + """Returns a dict containing the products' data. Those data are for products + who aren't in the record yet. For products already in the record, see + `_get_product_catalog_lines_data`. + + For each product, its id is the key and the value is another dict with all + needed data. By default, the price is the only needed data but each model is + free to add more data. Must be overrided by each model using this mixin. + + :param products: Recordset of `product.product`. + :param dict kwargs: additional values given for inherited models. + :rtype: dict + :return: A dict with the following structure: + { + 'productId': int + 'quantity': float (optional) + 'productType': string + 'price': float + 'readOnly': bool (optional) + } + """ + res = {} + for product in products: + res[product.id] = {"productType": product.type} + return res + + def _get_product_catalog_order_line_info(self, product_ids, **kwargs): + """Returns products information to be shown in the catalog. + :param list product_ids: The products currently displayed in the product + catalog, as a list of `product.product` ids. + :param dict kwargs: additional values given for inherited models. + :rtype: dict + :return: A dict with the following structure: + { + 'productId': int + 'quantity': float (optional) + 'productType': string + 'price': float (optional) + 'readOnly': bool (optional) + } + """ + order_line_info = {} + default_data = self._default_order_line_values() + + for product, record_lines in self._get_product_catalog_record_lines( + product_ids + ).items(): + order_line_info[product.id] = { + **record_lines._get_product_catalog_lines_data(**kwargs), + "productType": product.type, + } + product_ids.remove(product.id) + + products = self.env["product.product"].browse(product_ids) + product_data = self._get_product_catalog_order_data(products, **kwargs) + for product_id, data in product_data.items(): + order_line_info[product_id] = {**default_data, **data} + return order_line_info + + def _get_action_add_from_catalog_extra_context(self): + return { + "product_catalog_order_id": self.id, + "product_catalog_order_model": self._name, + } + + def _is_readonly(self): + """Must be overrided by each model using this mixin. + :return: Whether the record is read-only or not. + :rtype: bool + """ + return False + + def _update_order_line_info(self, product_id, quantity, **kwargs): + """Update the line information for a given product or create a new one if none + exists yet. Must be overrided by each model using this mixin. + :param int product_id: The product, as a `product.product` id. + :param int quantity: The product's quantity. + :param dict kwargs: additional values given for inherited models. + :return: The unit price of the product, based on the pricelist of the + purchase order and the quantity selected. + :rtype: float + """ + return 0 diff --git a/product_catalog/readme/CONTEXT.md b/product_catalog/readme/CONTEXT.md new file mode 100644 index 000000000000..9376c2aee5b9 --- /dev/null +++ b/product_catalog/readme/CONTEXT.md @@ -0,0 +1,2 @@ +The product catalog is a nice addon the version 17 features that allows users to use +a simple product picker to quickly add products to a document (sale or purchase order). diff --git a/product_catalog/readme/CONTRIBUTORS.md b/product_catalog/readme/CONTRIBUTORS.md new file mode 100644 index 000000000000..3a16ddf047ae --- /dev/null +++ b/product_catalog/readme/CONTRIBUTORS.md @@ -0,0 +1,2 @@ +- [Tecnativa](https://tecnativa.com) + - David Vidal diff --git a/product_catalog/readme/DESCRIPTION.md b/product_catalog/readme/DESCRIPTION.md new file mode 100644 index 000000000000..24d8670b85cb --- /dev/null +++ b/product_catalog/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +A backport of Odoo's v17 Product Catalog. + +Changes over mainstream: + + - Price is an optional value now so we can use it in other models. diff --git a/product_catalog/readme/ROADMAP.md b/product_catalog/readme/ROADMAP.md new file mode 100644 index 000000000000..0df50824ab14 --- /dev/null +++ b/product_catalog/readme/ROADMAP.md @@ -0,0 +1 @@ +- Be able to filter just the added products. diff --git a/product_catalog/readme/USAGE.md b/product_catalog/readme/USAGE.md new file mode 100644 index 000000000000..66681dab4bc2 --- /dev/null +++ b/product_catalog/readme/USAGE.md @@ -0,0 +1,3 @@ +To use this module, you need to: + +1. Go to ... diff --git a/product_catalog/static/description/index.html b/product_catalog/static/description/index.html new file mode 100644 index 000000000000..46846d9bd5b7 --- /dev/null +++ b/product_catalog/static/description/index.html @@ -0,0 +1,453 @@ + + + + + +Product Catalog + + + +
+

Product Catalog

+ + +

Beta License: AGPL-3 OCA/product-attribute Translate me on Weblate Try me on Runboat

+

A backport of Odoo’s v17 Product Catalog.

+

Changes over mainstream:

+
+- Price is an optional value now so we can use it in other models.
+
+

Table of contents

+ +
+

Use Cases / Context

+

The product catalog is a nice addon the version 17 features that allows +users to use a simple product picker to quickly add products to a +document (sale or purchase order).

+
+
+

Usage

+

To use this module, you need to:

+
    +
  1. Go to …
  2. +
+
+
+

Known issues / Roadmap

+
    +
  • Be able to filter just the added products.
  • +
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Odoo SA
  • +
  • Tecnativa
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/product-attribute project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_catalog/static/src/product_catalog/kanban_controller.esm.js b/product_catalog/static/src/product_catalog/kanban_controller.esm.js new file mode 100644 index 000000000000..1fa6a4a88d6f --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_controller.esm.js @@ -0,0 +1,52 @@ +/** @odoo-module **/ +import {KanbanController} from "@web/views/kanban/kanban_controller"; +import {onWillStart} from "@odoo/owl"; +import {useService} from "@web/core/utils/hooks"; + +export class ProductCatalogKanbanController extends KanbanController { + setup() { + super.setup(); + this.action = useService("action"); + this.orm = useService("orm"); + this.orderId = this.props.context.order_id; + this.orderResModel = this.props.context.product_catalog_order_model; + onWillStart(async () => this._defineButtonContent()); + } + + // Force the slot for the "Back to Quotation" button to always be shown. + get canCreate() { + return true; + } + + async _defineButtonContent() { + // Render the button content depending on the model. + const text_button = await this.orm.read( + this.orderResModel, + [this.orderId], + ["catalog_button_text"] + ); + this.buttonString = text_button[0].catalog_button_text; + } + + async backToQuotation() { + // Restore the last form view from the breadcrumbs if breadcrumbs are available. + // If, for some weird reason, the user reloads the page then the breadcrumbs are + // lost, and we fall back to the form view ourselves. + // Also we need to make little wait to avoid that if the user clicks right after + // manually editing a quantity in the input we don't miss the update in the + // order form + setTimeout(async () => { + if (this.env.config.breadcrumbs.length > 1) { + await this.action.restore(); + } else { + await this.action.doAction({ + type: "ir.actions.act_window", + res_model: this.orderResModel, + views: [[false, "form"]], + view_mode: "form", + res_id: this.orderId, + }); + } + }, 500); + } +} diff --git a/product_catalog/static/src/product_catalog/kanban_model.esm.js b/product_catalog/static/src/product_catalog/kanban_model.esm.js new file mode 100644 index 000000000000..8dc6f485a85b --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_model.esm.js @@ -0,0 +1,36 @@ +/** @odoo-module */ +import {KanbanDynamicRecordList, KanbanModel} from "@web/views/kanban/kanban_model"; + +export class ProductCatalogDynamicRecordList extends KanbanDynamicRecordList { + async _loadRecords() { + const records = await super._loadRecords(); + const orderLinesInfo = await this.model.rpc( + "/product/catalog/order_lines_info", + { + order_id: this.context.order_id, + product_ids: records.map((rec) => rec.resId), + res_model: this.context.product_catalog_order_model, + } + ); + for (const record of records) { + record.productCatalogData = orderLinesInfo[record.resId]; + } + return records; + } +} + +export class ProductCatalogKanbanModel extends KanbanModel { + async _loadData(params) { + if (!params.isMonoRecord && !params.groupBy && !params.groupBy.length) { + const orderLinesInfo = await this.rpc("/product/catalog/order_lines_info", { + order_id: params.context.order_id, + product_ids: this.root.records.map((rec) => rec.resId), + res_model: params.context.product_catalog_order_model, + }); + for (const record of this.root.records) { + record.productCatalogData = orderLinesInfo[record.id]; + } + } + } +} +ProductCatalogKanbanModel.DynamicRecordList = ProductCatalogDynamicRecordList; diff --git a/product_catalog/static/src/product_catalog/kanban_record.esm.js b/product_catalog/static/src/product_catalog/kanban_record.esm.js new file mode 100644 index 000000000000..d9aa9c4de414 --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_record.esm.js @@ -0,0 +1,141 @@ +/** @odoo-module */ +import {KanbanRecord} from "@web/views/kanban/kanban_record"; +import {ProductCatalogOrderLine} from "./order_line/order_line.esm"; +import {useDebounced} from "@web/core/utils/timing"; +import {useService} from "@web/core/utils/hooks"; +import {useSubEnv} from "@odoo/owl"; + +export class ProductCatalogKanbanRecord extends KanbanRecord { + setup() { + super.setup(); + this.rpc = useService("rpc"); + this.debouncedUpdateQuantity = useDebounced(this._updateQuantity, 500, { + execBeforeUnmount: true, + }); + + useSubEnv({ + currencyId: this.props.record.context.product_catalog_currency_id, + orderId: this.props.record.context.product_catalog_order_id, + orderResModel: this.props.record.context.product_catalog_order_model, + digits: this.props.record.context.product_catalog_digits, + displayUoM: this.props.record.context.display_uom, + precision: this.props.record.context.precision, + productId: this.props.record.resId, + addProduct: this.addProduct.bind(this), + removeProduct: this.removeProduct.bind(this), + increaseQuantity: this.increaseQuantity.bind(this), + setQuantity: this.setQuantity.bind(this), + decreaseQuantity: this.decreaseQuantity.bind(this), + }); + } + + get orderLineComponent() { + return ProductCatalogOrderLine; + } + + get productCatalogData() { + return this.props.record.productCatalogData; + } + + onGlobalClick(ev) { + // Avoid a concurrent update when clicking on the buttons (that are inside the record) + if (ev.target.closest(".o_product_catalog_cancel_global_click")) { + return; + } + if (this.productCatalogData.quantity === 0) { + this.addProduct(); + } else { + this.increaseQuantity(); + } + } + + // -------------------------------------------------------------------------- + // Data Exchanges + // -------------------------------------------------------------------------- + + updateProductCatalogData() { + this.props.record.update({productCatalogData: this.productCatalogData}); + } + + async _updateQuantity() { + const price = await this._updateQuantityAndGetPrice(); + // When no price is given, avoid parsing a price resulting in 0, as we don't + // want to show any price tag + if (price !== undefined) { + this.productCatalogData.price = parseFloat(price); + } + this.updateProductCatalogData(); + } + + _updateQuantityAndGetPrice() { + return this.rpc( + "/product/catalog/update_order_line_info", + this._getUpdateQuantityAndGetPrice() + ); + } + + _getUpdateQuantityAndGetPrice() { + return { + order_id: this.env.orderId, + product_id: this.env.productId, + quantity: this.productCatalogData.quantity, + res_model: this.env.orderResModel, + }; + } + + // -------------------------------------------------------------------------- + // Handlers + // -------------------------------------------------------------------------- + + updateQuantity(quantity) { + if (this.productCatalogData.readOnly) { + return; + } + this.productCatalogData.quantity = quantity || 0; + this.debouncedUpdateQuantity(); + } + + /** + * Add the product to the order + * @param {Number} qty + */ + addProduct(qty = 1) { + this.updateQuantity(qty); + } + + /** + * Remove the product to the order + */ + removeProduct() { + this.updateQuantity(0); + } + + /** + * Increase the quantity of the product on the order line. + * @param {Number} qty + */ + increaseQuantity(qty = 1) { + this.updateQuantity(this.productCatalogData.quantity + qty); + } + + /** + * Set the quantity of the product on the order line. + * + * @param {Event} event + */ + setQuantity(event) { + this.updateQuantity(parseFloat(event.target.value)); + } + + /** + * Decrease the quantity of the product on the order line. + */ + decreaseQuantity() { + this.updateQuantity(parseFloat(this.productCatalogData.quantity - 1)); + } +} +ProductCatalogKanbanRecord.template = "ProductCatalogKanbanRecord"; +ProductCatalogKanbanRecord.components = { + ...KanbanRecord.components, + ProductCatalogOrderLine, +}; diff --git a/product_catalog/static/src/product_catalog/kanban_record.xml b/product_catalog/static/src/product_catalog/kanban_record.xml new file mode 100644 index 000000000000..29356bcc40c2 --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_record.xml @@ -0,0 +1,25 @@ + + + +
+
+ + +
+
+
+
diff --git a/product_catalog/static/src/product_catalog/kanban_renderer.esm.js b/product_catalog/static/src/product_catalog/kanban_renderer.esm.js new file mode 100644 index 000000000000..639d1454b64d --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_renderer.esm.js @@ -0,0 +1,37 @@ +/** @odoo-module **/ + +import {KanbanRenderer} from "@web/views/kanban/kanban_renderer"; +import {ProductCatalogKanbanRecord} from "./kanban_record.esm"; +import {useService} from "@web/core/utils/hooks"; + +export class ProductCatalogKanbanRenderer extends KanbanRenderer { + setup() { + super.setup(); + this.action = useService("action"); + } + + get createProductContext() { + return {}; + } + + async createProduct() { + await this.action.doAction( + { + type: "ir.actions.act_window", + res_model: "product.product", + target: "new", + views: [[false, "form"]], + view_mode: "form", + context: this.createProductContext, + }, + { + onClose: () => this.props.list.model.load(), + } + ); + } +} +ProductCatalogKanbanRenderer.template = "ProductCatalogKanbanRenderer"; +ProductCatalogKanbanRenderer.components = { + ...KanbanRenderer.components, + KanbanRecord: ProductCatalogKanbanRecord, +}; diff --git a/product_catalog/static/src/product_catalog/kanban_renderer.xml b/product_catalog/static/src/product_catalog/kanban_renderer.xml new file mode 100644 index 000000000000..42db20960e5a --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_renderer.xml @@ -0,0 +1,29 @@ + + + + + + + + + + diff --git a/product_catalog/static/src/product_catalog/kanban_view.esm.js b/product_catalog/static/src/product_catalog/kanban_view.esm.js new file mode 100644 index 000000000000..7c999a167cb8 --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_view.esm.js @@ -0,0 +1,18 @@ +/** @odoo-module **/ +import {ProductCatalogKanbanController} from "./kanban_controller.esm"; +import {ProductCatalogKanbanModel} from "./kanban_model.esm"; +import {ProductCatalogKanbanRenderer} from "./kanban_renderer.esm"; +import {ProductCatalogSearchPanel} from "./search/search_panel.esm"; +import {kanbanView} from "@web/views/kanban/kanban_view"; +import {registry} from "@web/core/registry"; + +export const productCatalogKanbanView = { + ...kanbanView, + Controller: ProductCatalogKanbanController, + Model: ProductCatalogKanbanModel, + Renderer: ProductCatalogKanbanRenderer, + SearchPanel: ProductCatalogSearchPanel, + buttonTemplate: "ProductCatalogKanbanButtons", +}; + +registry.category("views").add("product_kanban_catalog", productCatalogKanbanView); diff --git a/product_catalog/static/src/product_catalog/kanban_view_buttons.xml b/product_catalog/static/src/product_catalog/kanban_view_buttons.xml new file mode 100644 index 000000000000..45798cfd739e --- /dev/null +++ b/product_catalog/static/src/product_catalog/kanban_view_buttons.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + +
+ +
+ +
+
+ +
+
diff --git a/product_catalog/static/src/product_catalog/search/search_panel.esm.js b/product_catalog/static/src/product_catalog/search/search_panel.esm.js new file mode 100644 index 000000000000..a4738b85f5f5 --- /dev/null +++ b/product_catalog/static/src/product_catalog/search/search_panel.esm.js @@ -0,0 +1,55 @@ +/** @odoo-module **/ + +import {SearchPanel} from "@web/search/search_panel/search_panel"; +import {useState} from "@odoo/owl"; + +export class ProductCatalogSearchPanel extends SearchPanel { + setup() { + super.setup(); + + this.state = useState({ + ...this.state, + sectionOfAttributes: {}, + }); + } + + updateActiveValues() { + super.updateActiveValues(); + this.state.sectionOfAttributes = this.buildSection(); + } + + buildSection() { + const values = this.env.searchModel.filters[0].values; + const sections = new Map(); + + values.forEach((element) => { + const name = element.display_name; + const id = element.id; + const count = element.__count; + + if (sections.has(name)) { + const currentAttr = sections.get(name); + currentAttr.get("ids").push(id); + currentAttr.set("count", currentAttr.get("count") + count); + } else { + const newAttr = new Map(); + newAttr.set("ids", [id]); + newAttr.set("count", count); + sections.set(name, newAttr); + } + }); + + return sections; + } + + toggleSectionFilterValue(filterId, attrIds, {currentTarget}) { + attrIds.forEach((id) => { + this.toggleFilterValue(filterId, id, {currentTarget}); + }); + } +} + +ProductCatalogSearchPanel.subTemplates = { + ...SearchPanel.subTemplates, + filtersGroup: "ProductCatalogSearchPanel.FiltersGroup", +}; diff --git a/product_catalog/static/src/product_catalog/search/search_panel.xml b/product_catalog/static/src/product_catalog/search/search_panel.xml new file mode 100644 index 000000000000..5bba1ca67d00 --- /dev/null +++ b/product_catalog/static/src/product_catalog/search/search_panel.xml @@ -0,0 +1,50 @@ + + + +
  • +
  • + + +
    + + +
    +
  • + +
    +
    diff --git a/product_catalog/views/product_views.xml b/product_catalog/views/product_views.xml new file mode 100644 index 000000000000..1decb87e7f4d --- /dev/null +++ b/product_catalog/views/product_views.xml @@ -0,0 +1,140 @@ + + + + product.view.kanban.catalog + product.product + + + + + + +
    + Edit +
    +
    + +
    +
    + Product +
    +
    +
    +
    + +

    + +

    +
    +
    + [] +
    + +
    + +
    +
    +
    + + + + + + + product.view.search.catalog + product.product + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/setup/product_catalog/odoo/addons/product_catalog b/setup/product_catalog/odoo/addons/product_catalog new file mode 120000 index 000000000000..196ef7a9f03c --- /dev/null +++ b/setup/product_catalog/odoo/addons/product_catalog @@ -0,0 +1 @@ +../../../../product_catalog \ No newline at end of file diff --git a/setup/product_catalog/setup.py b/setup/product_catalog/setup.py new file mode 100644 index 000000000000..28c57bb64031 --- /dev/null +++ b/setup/product_catalog/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)