diff --git a/product_sold_by_delivery_week/README.rst b/product_sold_by_delivery_week/README.rst new file mode 100644 index 000000000..103b7c3cf --- /dev/null +++ b/product_sold_by_delivery_week/README.rst @@ -0,0 +1,139 @@ +========================= +Product weekly sales hint +========================= + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:5c23b69bd0cac593b37140cf67786cb580904f8876975980260f9823ffa7712e + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Fsale--reporting-lightgray.png?logo=github + :target: https://github.com/OCA/sale-reporting/tree/17.0/product_sold_by_delivery_week + :alt: OCA/sale-reporting +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-reporting-17-0/sale-reporting-17-0-product_sold_by_delivery_week + :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/sale-reporting&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module allows to compute a graphical hint, stored in the product +itself, to display whether that product was sold and delivered in recent +weeks. + +|image| + +.. |image| image:: https://raw.githubusercontent.com/OCA/sale-reporting/17.0/product_sold_by_delivery_week/static/description/sold_by_week_hint.png + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Some variables can be configured using config parameter keys: + +- For hint characters: + +.. + + - For the sold char: product_sold_by_delivery_week.sold_char + - For the not sold: product_sold_by_delivery_week.not_sold_char + - For weeks length: product_sold_by_delivery_week.weeks_to_consider + +Assign the security group 'Weekly selling info in order lines' and/or +'Weekly selling info in product list' to users that should see weekly +sales info in product lists or in order lines. (If you want to see the +info in other views like product recommendations, this is not needed.) + +Usage +===== + +Once configured, hints will appear as an optional column in sale order +lines or in products tree. + +In the general products view, general sales info is shown. Meanwhile, in +the sale lines field we can see this field in context by simply +filtering partner sales. + +Only salespeople can access this information. + +Known issues / Roadmap +====================== + +- Services are not taken into consideration. +- A widget could be created on top of the main field to have nicer icons + and probably colors. +- For the near future, a configurable granularity would be desirable, so + behavior could be changed to years, months or days as the period of + choice. To simplify this module, only weekly periods will be + considered for now. + +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 +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - David Vidal + - Carlos Dauden + - César A. Sánchez + - Luis D. Lafaurie + +- Jairo Llopis ([Moduon](https://www.moduon.team/)) +- `Heliconia Solutions Pvt. Ltd. `__ + +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. + +.. |maintainer-chienandalu| image:: https://github.com/chienandalu.png?size=40px + :target: https://github.com/chienandalu + :alt: chienandalu + +Current `maintainer `__: + +|maintainer-chienandalu| + +This module is part of the `OCA/sale-reporting `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/product_sold_by_delivery_week/__init__.py b/product_sold_by_delivery_week/__init__.py new file mode 100644 index 000000000..cc6b6354a --- /dev/null +++ b/product_sold_by_delivery_week/__init__.py @@ -0,0 +1,2 @@ +from . import models +from .hooks import post_init_hook diff --git a/product_sold_by_delivery_week/__manifest__.py b/product_sold_by_delivery_week/__manifest__.py new file mode 100644 index 000000000..ee62351b7 --- /dev/null +++ b/product_sold_by_delivery_week/__manifest__.py @@ -0,0 +1,22 @@ +# Copyright 2021 Tecnativa - David Vidal +# Copyright 2021 Tecnativa - Carlos Dauden +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Product weekly sales hint", + "summary": "Adds a field that graphically hints the weekly product sales", + "version": "17.0.1.0.0", + "development_status": "Beta", + "category": "Sale", + "website": "https://github.com/OCA/sale-reporting", + "author": "Tecnativa, Odoo Community Association (OCA)", + "maintainers": ["chienandalu"], + "license": "AGPL-3", + "depends": ["sale_stock"], + "data": [ + "data/ir_cron.xml", + "security/product_sold_by_delivery_week_security.xml", + "views/product_views.xml", + "views/sale_views.xml", + ], + "post_init_hook": "post_init_hook", +} diff --git a/product_sold_by_delivery_week/data/ir_cron.xml b/product_sold_by_delivery_week/data/ir_cron.xml new file mode 100644 index 000000000..6dcf264fc --- /dev/null +++ b/product_sold_by_delivery_week/data/ir_cron.xml @@ -0,0 +1,17 @@ + + + + Recalculate Weekly Delivered Products + 1 + weeks + -1 + + + model._action_recalculate_all_weekly_sold_delivered() + code + + diff --git a/product_sold_by_delivery_week/hooks.py b/product_sold_by_delivery_week/hooks.py new file mode 100644 index 000000000..b5475a9e7 --- /dev/null +++ b/product_sold_by_delivery_week/hooks.py @@ -0,0 +1,6 @@ +# Copyright 2017-2018 Tecnativa - Pedro M. Baeza +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +def post_init_hook(env): + env["product.product"]._action_recalculate_all_weekly_sold_delivered() diff --git a/product_sold_by_delivery_week/i18n/es.po b/product_sold_by_delivery_week/i18n/es.po new file mode 100644 index 000000000..2cac9a24c --- /dev/null +++ b/product_sold_by_delivery_week/i18n/es.po @@ -0,0 +1,70 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_sold_by_delivery_week +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-11-17 18:34+0000\n" +"PO-Revision-Date: 2023-09-03 13:40+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_template +msgid "Product" +msgstr "Producto" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_product +msgid "Product Variant" +msgstr "Variante del producto" + +#. module: product_sold_by_delivery_week +#: model:ir.actions.server,name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered_ir_actions_server +#: model:ir.cron,cron_name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered +msgid "Recalculate Weekly Delivered Products" +msgstr "Recalcular productos entregados semanalmente" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de venta" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de existencias" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_sale_order_line__weekly_sold_delivered_shown +msgid "Weekly Sold" +msgstr "Venta semanal" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered +msgid "Weekly Sold Delivered" +msgstr "Vendidos y entregados semanalmente" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_order_line +msgid "Weekly selling info in order lines" +msgstr "Información de venta semanal en líneas de pedido" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_product_list +msgid "Weekly selling info in product list" +msgstr "Información de venta semanal en lista de productos" + +#~ msgid "Product Template" +#~ msgstr "Plantilla de producto" diff --git a/product_sold_by_delivery_week/i18n/it.po b/product_sold_by_delivery_week/i18n/it.po new file mode 100644 index 000000000..9f7e08e8e --- /dev/null +++ b/product_sold_by_delivery_week/i18n/it.po @@ -0,0 +1,66 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_sold_by_delivery_week +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-12-12 11:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_template +msgid "Product" +msgstr "Prodotto" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_product +msgid "Product Variant" +msgstr "Variante prodotto" + +#. module: product_sold_by_delivery_week +#: model:ir.actions.server,name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered_ir_actions_server +#: model:ir.cron,cron_name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered +msgid "Recalculate Weekly Delivered Products" +msgstr "Ricalcolo prdotti consegnati settimanalmente" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_sale_order_line +msgid "Sales Order Line" +msgstr "Riga ordine di vendita" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_stock_move +msgid "Stock Move" +msgstr "Movimento di magazzino" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_sale_order_line__weekly_sold_delivered_shown +msgid "Weekly Sold" +msgstr "Venduto settimanale" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered +msgid "Weekly Sold Delivered" +msgstr "Cnsegnato vendite settimanali" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_order_line +msgid "Weekly selling info in order lines" +msgstr "Informazioni vendite settimanali nelle righe ordine" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_product_list +msgid "Weekly selling info in product list" +msgstr "Informazioni vendite settimanali nell'elenco prodotti" diff --git a/product_sold_by_delivery_week/i18n/product_sold_by_delivery_week.pot b/product_sold_by_delivery_week/i18n/product_sold_by_delivery_week.pot new file mode 100644 index 000000000..f40ce8826 --- /dev/null +++ b/product_sold_by_delivery_week/i18n/product_sold_by_delivery_week.pot @@ -0,0 +1,63 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_sold_by_delivery_week +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_template +msgid "Product" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_product +msgid "Product Variant" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:ir.actions.server,name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered_ir_actions_server +#: model:ir.cron,cron_name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered +msgid "Recalculate Weekly Delivered Products" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_sale_order_line__weekly_sold_delivered_shown +msgid "Weekly Sold" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered +msgid "Weekly Sold Delivered" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_order_line +msgid "Weekly selling info in order lines" +msgstr "" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_product_list +msgid "Weekly selling info in product list" +msgstr "" diff --git a/product_sold_by_delivery_week/i18n/pt_BR.po b/product_sold_by_delivery_week/i18n/pt_BR.po new file mode 100644 index 000000000..c2e2b58bb --- /dev/null +++ b/product_sold_by_delivery_week/i18n/pt_BR.po @@ -0,0 +1,67 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_sold_by_delivery_week +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-05-29 16:37+0000\n" +"Last-Translator: Rodrigo Macedo \n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_template +msgid "Product" +msgstr "Produto" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_product_product +msgid "Product Variant" +msgstr "Variante do Produto" + +#. module: product_sold_by_delivery_week +#: model:ir.actions.server,name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered_ir_actions_server +#: model:ir.cron,cron_name:product_sold_by_delivery_week.ir_cron_recalculate_all_weekly_sold_delivered +msgid "Recalculate Weekly Delivered Products" +msgstr "Recalcular produtos entregues semanalmente" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_sale_order_line +msgid "Sales Order Line" +msgstr "Linha de ordem de venda" + +#. module: product_sold_by_delivery_week +#: model:ir.model,name:product_sold_by_delivery_week.model_stock_move +msgid "Stock Move" +msgstr "Movimento de estoque" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered_shown +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_sale_order_line__weekly_sold_delivered_shown +msgid "Weekly Sold" +msgstr "Vendido semanalmente" + +#. module: product_sold_by_delivery_week +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_product__weekly_sold_delivered +#: model:ir.model.fields,field_description:product_sold_by_delivery_week.field_product_template__weekly_sold_delivered +msgid "Weekly Sold Delivered" +msgstr "Vendido semanalmente entregue" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_order_line +msgid "Weekly selling info in order lines" +msgstr "Informações de vendas semanais em linhas de pedidos" + +#. module: product_sold_by_delivery_week +#: model:res.groups,name:product_sold_by_delivery_week.group_weekly_selling_product_list +msgid "Weekly selling info in product list" +msgstr "Informações de vendas semanais na lista de produtos" diff --git a/product_sold_by_delivery_week/models/__init__.py b/product_sold_by_delivery_week/models/__init__.py new file mode 100644 index 000000000..67a036c49 --- /dev/null +++ b/product_sold_by_delivery_week/models/__init__.py @@ -0,0 +1,4 @@ +from . import product_product +from . import product_template +from . import sale_order +from . import stock_move diff --git a/product_sold_by_delivery_week/models/product_product.py b/product_sold_by_delivery_week/models/product_product.py new file mode 100644 index 000000000..05b905b80 --- /dev/null +++ b/product_sold_by_delivery_week/models/product_product.py @@ -0,0 +1,169 @@ +# Copyright 2021 Tecnativa - David Vidal +# Copyright 2021 Tecnativa - Carlos Dauden +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from babel.dates import DateTimeFormat + +from odoo import api, fields, models +from odoo.tools import date_utils + + +class ProductProduct(models.Model): + _inherit = "product.product" + + weekly_sold_delivered = fields.Char( + company_dependent=True, + groups="sales_team.group_sale_salesman", + ) + weekly_sold_delivered_shown = fields.Char( + string="Weekly Sold", + compute="_compute_weekly_sold_delivered_shown", + groups="sales_team.group_sale_salesman", + ) + + @api.model + def _format_weekly_string(self, weekly_string): + params = self.env["ir.config_parameter"].sudo() + sold_char = params.get_param("product_sold_by_delivery_week.sold_char", "●") + not_sold_char = params.get_param( + "product_sold_by_delivery_week.not_sold_char", "◌" + ) + return weekly_string and "".join( + [int(c) and sold_char or not_sold_char for c in weekly_string] + ) + + @api.depends("weekly_sold_delivered") + def _compute_weekly_sold_delivered_shown(self): + """This field is meant to be used only for display purposes so we can use custom + characters show the sales stream. We want to keep the stored one as base 2 + string so we can perform bitwise operations easily""" + services = self.filtered(lambda x: x.type == "service") + services.weekly_sold_delivered_shown = False + for product in self - services: + product.weekly_sold_delivered_shown = self._format_weekly_string( + product.weekly_sold_delivered + ) + + def _weekly_sold_delivered_domain(self, date_start, date_end): + warehouse_id = self.env.context.get("weekly_warehouse_id") + partner_id = self.env.context.get("weekly_partner_id") + # Previous search to improve performance instead of using the ORM huge ids sql + picking_type_domain = [ + ("company_id", "=", self.env.company.id), + ("code", "=", "outgoing"), + ] + if warehouse_id: + picking_type_domain += [("warehouse_id", "=", warehouse_id)] + picking_types = self.env["stock.picking.type"].search(picking_type_domain) + picking_domain = [ + ("date_done", ">=", date_start), + ("date_done", "<", date_end), + ("picking_type_id", "in", picking_types.ids), + ] + if partner_id: + picking_domain += [("partner_id", "child_of", partner_id)] + pickings = self.env["stock.picking"].search(picking_domain) + domain = [ + ("product_id", "in", self.ids), + ("picking_id", "in", pickings.ids), + ("state", "=", "done"), + ("sale_line_id", "!=", False), + ] + return domain + + def _weekly_sold_delivered(self): + params = self.env["ir.config_parameter"].sudo() + weeks_to_consider = int( + params.get_param("product_sold_by_delivery_week.weeks_to_consider", 6) + ) + delta_one_week = date_utils.get_timedelta(1, "week") + start_of_this_week = date_utils.start_of(fields.Datetime.today(), "week") + start_of_next_week = start_of_this_week + delta_one_week + start_of_period = start_of_this_week - date_utils.get_timedelta( + weeks_to_consider - 1, "week" + ) + date_range = date_utils.date_range( + start_of_period, start_of_next_week, delta_one_week + ) + # We'll reuse this dictionary to obtain the final string + week_dates_dict = {} + for week_position in range(weeks_to_consider): + locale_date = DateTimeFormat( + next(date_range), locale=self.env.user.lang or "en_US" + ) + week_dates_dict[week_position] = ( + int(locale_date["w"]), + int(locale_date["y"]), + ) + # Get all the results in a single query + sold_grouped = self.env["stock.move"].read_group( + self._weekly_sold_delivered_domain(start_of_period, start_of_next_week), + ["date", "product_id"], + ["date:week", "product_id"], + lazy=False, + ) + if not sold_grouped: + return {p: "0" * weeks_to_consider for p in self} + weekly_product_ids = {} + for date_product in sold_grouped: + (int(date_product["date:week"][1:3]), int(date_product["date:week"][-4:])) + week_year_tuple = ( + int(date_product["date:week"][1:3]), + int(date_product["date:week"][-4:]), + ) + weekly_product_ids.setdefault(week_year_tuple, []) + weekly_product_ids[week_year_tuple].append(date_product["product_id"][0]) + # We'll get a dict like this + # { + # product.product(1,): '000000', + # product.product(2,): '010110', + # product.product(3,): '000010', + # } + products_weekly = {} + for product in self: + products_weekly[product] = "".join( + [ + product.id in weekly_product_ids.get(w, []) and "1" or "0" + for p, w in week_dates_dict.items() + ] + ) + return products_weekly + + def _recalculate_weekly_sold_delivered(self): + """Over this recordset recalculate the whole strings. We end up with something + like '010111'. Later we can transform it into something nicer for the user.""" + products_weekly = self._weekly_sold_delivered() + # But we want to group by string result so we can minimize the final records + # writes. The number of writes will depends on the variety of the weekly sales + # up to a maximum given by the parameter `weeks_to_consider` allows us in bits. + # So for 8 weeks, a maximum of 256 writes would be made. + weekly_result_dict = {} + for product, weekly_string in products_weekly.items(): + weekly_result_dict.setdefault(weekly_string, self.env["product.product"]) + weekly_result_dict[weekly_string] |= product + for weekly_string, product_recordset in weekly_result_dict.items(): + if not product_recordset: + continue + product_recordset.with_company(self.env.company).write( + {"weekly_sold_delivered": weekly_string} + ) + + @api.model + def _action_recalculate_all_weekly_sold_delivered(self): + """To be launched by the cron or the init hook""" + companies = self.env["res.company"].search([]) + for company in companies: + products = ( + self.env["product.product"] + .search( + [ + ("type", "!=", "service"), + "|", + ("company_id", "=", company.id), + ("company_id", "=", False), + ] + ) + .with_company(company) + ) + if not products: + continue + products._recalculate_weekly_sold_delivered() diff --git a/product_sold_by_delivery_week/models/product_template.py b/product_sold_by_delivery_week/models/product_template.py new file mode 100644 index 000000000..7ff1d3f95 --- /dev/null +++ b/product_sold_by_delivery_week/models/product_template.py @@ -0,0 +1,74 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import api, fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + weekly_sold_delivered = fields.Char( + compute="_compute_weekly_sold_delivered", + groups="sales_team.group_sale_salesman", + readonly=True, + ) + weekly_sold_delivered_shown = fields.Char( + string="Weekly Sold", + compute="_compute_weekly_sold_delivered_shown", + groups="sales_team.group_sale_salesman", + ) + + @api.depends("weekly_sold_delivered") + def _compute_weekly_sold_delivered_shown(self): + """This fields is meant to be used only for display purposes so we can + use custom characters show the sales stream. We want to keep the stored + one as base 2 string so we can perform bitwise operations easily""" + params = self.env["ir.config_parameter"].sudo() + sold_char = params.get_param("product_sold_by_delivery_week.sold_char", "●") + not_sold_char = params.get_param( + "product_sold_by_delivery_week.not_sold_char", "◌" + ) + not_service_products = self.filtered(lambda x: x.type != "service") + (self - not_service_products).weekly_sold_delivered_shown = False + for product in self.filtered(lambda x: x.type != "service"): + product.weekly_sold_delivered_shown = ( + product.weekly_sold_delivered + and "".join( + [ + int(c) and sold_char or not_sold_char + for c in product.weekly_sold_delivered + ] + ) + ) + + @api.depends_context("company") + @api.depends("product_variant_ids.weekly_sold_delivered") + def _compute_weekly_sold_delivered(self): + """Perform a bitwise operation over the variant values to easily check + whether or not any of them has been sold""" + params = self.env["ir.config_parameter"].sudo() + weeks_to_consider = params.get_param( + "product_sold_by_delivery_week.weeks_to_consider", 6 + ) + self.weekly_sold_delivered = False + for product in self.filtered(lambda x: x.type != "service"): + if len(product.product_variant_ids) == 1: + product.weekly_sold_delivered = ( + product.product_variant_ids.weekly_sold_delivered + ) + variants_weekly_sold_delivered = product.product_variant_ids.mapped( + "weekly_sold_delivered" + ) + if not variants_weekly_sold_delivered: + continue + # The computed value is stored in the form of '0' and '1' strings so + # bitwise operations are easilly done. + weekly_sold_delivered = int( + variants_weekly_sold_delivered.pop() or "0", base=2 + ) + while variants_weekly_sold_delivered: + weekly_sold_delivered = weekly_sold_delivered | int( + variants_weekly_sold_delivered.pop() or "0", base=2 + ) + product.weekly_sold_delivered = "{:0{}b}".format( + weekly_sold_delivered, weeks_to_consider + ) diff --git a/product_sold_by_delivery_week/models/sale_order.py b/product_sold_by_delivery_week/models/sale_order.py new file mode 100644 index 000000000..2cadd4b0b --- /dev/null +++ b/product_sold_by_delivery_week/models/sale_order.py @@ -0,0 +1,45 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from collections import defaultdict + +from odoo import api, fields, models + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + weekly_sold_delivered_shown = fields.Char( + string="Weekly Sold", + compute="_compute_weekly_sold_delivered_shown", + ) + + def get_partner_for_reporting(self): + """get partner from line taking into account context parameters""" + if self.env.context.get("use_delivery_address", False): + return self.order_id.partner_shipping_id + return self.order_id.partner_id.commercial_partner_id + + @api.depends("order_id.partner_id", "order_id.warehouse_id", "product_id") + def _compute_weekly_sold_delivered_shown(self): + """Compute dinamically in the view""" + _format_weekly_string = self.env["product.product"]._format_weekly_string + self.weekly_sold_delivered_shown = False + partner_products_dic = defaultdict(lambda: self.env["product.product"].browse()) + to_process_lines = self.filtered( + lambda x: not x.display_type and x.product_id.type != "service" + ) + # Create dict with products by partner + for line in to_process_lines: + partner = line.get_partner_for_reporting() + partner_products_dic[partner] |= line.product_id + partner_products_weekly = {} + # Create dict with partner and product sold delivered info + for partner, products in partner_products_dic.items(): + partner_products_weekly[partner] = products.with_context( + weekly_partner_id=partner.id, + )._weekly_sold_delivered() + for line in to_process_lines: + partner = line.get_partner_for_reporting() + line.weekly_sold_delivered_shown = _format_weekly_string( + partner_products_weekly[partner].get(line.product_id) + ) diff --git a/product_sold_by_delivery_week/models/stock_move.py b/product_sold_by_delivery_week/models/stock_move.py new file mode 100644 index 000000000..c9c9cf68a --- /dev/null +++ b/product_sold_by_delivery_week/models/stock_move.py @@ -0,0 +1,27 @@ +# Copyright 2021 Tecnativa - David Vidal +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +from odoo import models + + +class StockMove(models.Model): + _inherit = "stock.move" + + def _action_done(self, cancel_backorder=False): + """We don't need to recompute the whole string we just set the last char on as + it is the only possible truth. The cron well be in charge of reseting them every + week anyway.""" + moves_todo = super()._action_done(cancel_backorder=cancel_backorder) + products = ( + moves_todo.filtered( + lambda x: x.sale_line_id and x.picking_code == "outgoing" + ) + .mapped("product_id") + .with_company(self.company_id) + ) + for product in products.filtered( + lambda x: x.weekly_sold_delivered and x.weekly_sold_delivered[-1:] == "0" + ): + product.sudo().weekly_sold_delivered = ( + product.weekly_sold_delivered[:-1] + "1" + ) + return moves_todo diff --git a/product_sold_by_delivery_week/pyproject.toml b/product_sold_by_delivery_week/pyproject.toml new file mode 100644 index 000000000..4231d0ccc --- /dev/null +++ b/product_sold_by_delivery_week/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/product_sold_by_delivery_week/readme/CONFIGURE.md b/product_sold_by_delivery_week/readme/CONFIGURE.md new file mode 100644 index 000000000..3abe914b9 --- /dev/null +++ b/product_sold_by_delivery_week/readme/CONFIGURE.md @@ -0,0 +1,12 @@ +Some variables can be configured using config parameter keys: + +- For hint characters: + +> - For the sold char: product_sold_by_delivery_week.sold_char +> - For the not sold: product_sold_by_delivery_week.not_sold_char +> - For weeks length: product_sold_by_delivery_week.weeks_to_consider + +Assign the security group 'Weekly selling info in order lines' and/or +'Weekly selling info in product list' to users that should see weekly +sales info in product lists or in order lines. (If you want to see the +info in other views like product recommendations, this is not needed.) diff --git a/product_sold_by_delivery_week/readme/CONTRIBUTORS.md b/product_sold_by_delivery_week/readme/CONTRIBUTORS.md new file mode 100644 index 000000000..074e4d1db --- /dev/null +++ b/product_sold_by_delivery_week/readme/CONTRIBUTORS.md @@ -0,0 +1,7 @@ +- [Tecnativa](https://www.tecnativa.com): + - David Vidal + - Carlos Dauden + - César A. Sánchez + - Luis D. Lafaurie +- Jairo Llopis (\[Moduon\]()) +- [Heliconia Solutions Pvt. Ltd.](https://www.heliconia.io) diff --git a/product_sold_by_delivery_week/readme/DESCRIPTION.md b/product_sold_by_delivery_week/readme/DESCRIPTION.md new file mode 100644 index 000000000..150fc5a0a --- /dev/null +++ b/product_sold_by_delivery_week/readme/DESCRIPTION.md @@ -0,0 +1,5 @@ +This module allows to compute a graphical hint, stored in the product +itself, to display whether that product was sold and delivered in recent +weeks. + +![image](../static/description/sold_by_week_hint.png) diff --git a/product_sold_by_delivery_week/readme/ROADMAP.md b/product_sold_by_delivery_week/readme/ROADMAP.md new file mode 100644 index 000000000..2b519128a --- /dev/null +++ b/product_sold_by_delivery_week/readme/ROADMAP.md @@ -0,0 +1,7 @@ +- Services are not taken into consideration. +- A widget could be created on top of the main field to have nicer icons + and probably colors. +- For the near future, a configurable granularity would be desirable, so + behavior could be changed to years, months or days as the period of + choice. To simplify this module, only weekly periods will be + considered for now. diff --git a/product_sold_by_delivery_week/readme/USAGE.md b/product_sold_by_delivery_week/readme/USAGE.md new file mode 100644 index 000000000..069dd7080 --- /dev/null +++ b/product_sold_by_delivery_week/readme/USAGE.md @@ -0,0 +1,8 @@ +Once configured, hints will appear as an optional column in sale order +lines or in products tree. + +In the general products view, general sales info is shown. Meanwhile, in +the sale lines field we can see this field in context by simply +filtering partner sales. + +Only salespeople can access this information. diff --git a/product_sold_by_delivery_week/security/product_sold_by_delivery_week_security.xml b/product_sold_by_delivery_week/security/product_sold_by_delivery_week_security.xml new file mode 100644 index 000000000..6f8720b0e --- /dev/null +++ b/product_sold_by_delivery_week/security/product_sold_by_delivery_week_security.xml @@ -0,0 +1,11 @@ + + + + Weekly selling info in order lines + + + + Weekly selling info in product list + + + diff --git a/product_sold_by_delivery_week/static/description/icon.png b/product_sold_by_delivery_week/static/description/icon.png new file mode 100644 index 000000000..4947d4e36 Binary files /dev/null and b/product_sold_by_delivery_week/static/description/icon.png differ diff --git a/product_sold_by_delivery_week/static/description/index.html b/product_sold_by_delivery_week/static/description/index.html new file mode 100644 index 000000000..3cc25f55e --- /dev/null +++ b/product_sold_by_delivery_week/static/description/index.html @@ -0,0 +1,479 @@ + + + + + +Product weekly sales hint + + + +
+

Product weekly sales hint

+ + +

Beta License: AGPL-3 OCA/sale-reporting Translate me on Weblate Try me on Runboat

+

This module allows to compute a graphical hint, stored in the product +itself, to display whether that product was sold and delivered in recent +weeks.

+

image

+

Table of contents

+ +
+

Configuration

+

Some variables can be configured using config parameter keys:

+
    +
  • For hint characters:
  • +
+ +
+
    +
  • For the sold char: product_sold_by_delivery_week.sold_char
  • +
  • For the not sold: product_sold_by_delivery_week.not_sold_char
  • +
  • For weeks length: product_sold_by_delivery_week.weeks_to_consider
  • +
+
+

Assign the security group ‘Weekly selling info in order lines’ and/or +‘Weekly selling info in product list’ to users that should see weekly +sales info in product lists or in order lines. (If you want to see the +info in other views like product recommendations, this is not needed.)

+
+
+

Usage

+

Once configured, hints will appear as an optional column in sale order +lines or in products tree.

+

In the general products view, general sales info is shown. Meanwhile, in +the sale lines field we can see this field in context by simply +filtering partner sales.

+

Only salespeople can access this information.

+
+
+

Known issues / Roadmap

+
    +
  • Services are not taken into consideration.
  • +
  • A widget could be created on top of the main field to have nicer icons +and probably colors.
  • +
  • For the near future, a configurable granularity would be desirable, so +behavior could be changed to years, months or days as the period of +choice. To simplify this module, only weekly periods will be +considered for now.
  • +
+
+
+

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

+
    +
  • 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.

+

Current maintainer:

+

chienandalu

+

This module is part of the OCA/sale-reporting project on GitHub.

+

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

+
+
+
+ + diff --git a/product_sold_by_delivery_week/static/description/sold_by_week_hint.png b/product_sold_by_delivery_week/static/description/sold_by_week_hint.png new file mode 100644 index 000000000..1bd21c648 Binary files /dev/null and b/product_sold_by_delivery_week/static/description/sold_by_week_hint.png differ diff --git a/product_sold_by_delivery_week/tests/__init__.py b/product_sold_by_delivery_week/tests/__init__.py new file mode 100644 index 000000000..dd7691bf4 --- /dev/null +++ b/product_sold_by_delivery_week/tests/__init__.py @@ -0,0 +1 @@ +from . import test_product_sold_by_delivery_week diff --git a/product_sold_by_delivery_week/tests/test_product_sold_by_delivery_week.py b/product_sold_by_delivery_week/tests/test_product_sold_by_delivery_week.py new file mode 100644 index 000000000..e664d66e3 --- /dev/null +++ b/product_sold_by_delivery_week/tests/test_product_sold_by_delivery_week.py @@ -0,0 +1,101 @@ +# Copyright 2021 Camptocamp SA +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) +from odoo import Command +from odoo.tests import new_test_user + +from odoo.addons.base.tests.common import BaseCommon + + +class TestProductSoldByDeliveryWeek(BaseCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True)) + cls.partner = cls.env["res.partner"].create( + { + "name": "Partner for testing", + } + ) + cls.product = cls.env["product.product"].create( + { + "name": "Test product", + "detailed_type": "consu", + } + ) + cls.product_expense_product = cls.env["product.product"].create( + { + "name": "expense product for test", + "detailed_type": "service", + } + ) + cls.product.weekly_sold_delivered = "000000" + cls.product_expense_product.weekly_sold_delivered = "000000" + # Tests should pass with basic sale and stock access rights + cls.env = cls.env( + user=new_test_user( + cls.env, + login="test_user", + groups="stock.group_stock_user,sales_team.group_sale_salesman", + ) + ) + cls.order = cls.env["sale.order"].create( + { + "partner_id": cls.partner.id, + "order_line": [ + Command.create( + { + "product_id": cls.product.id, + "product_uom": cls.product.uom_id.id, + "product_uom_qty": 3.0, + }, + ), + Command.create({"display_type": "line_section", "name": "Section"}), + Command.create( + { + "product_id": cls.product_expense_product.id, + "product_uom": cls.product_expense_product.uom_id.id, + "product_uom_qty": 3.0, + }, + ), + ], + } + ) + + def test_01_check_delivered_message_without_parameters(self): + """Test the return message depending on the type of the product.""" + self.assertEqual(self.order.order_line[0].weekly_sold_delivered_shown, "◌◌◌◌◌◌") + self.assertEqual(self.order.order_line[1].weekly_sold_delivered_shown, False) + + def test_02_check_delivered_message_with_parameters(self): + """Test the definition of config parameters.""" + self.env["ir.config_parameter"].sudo().create( + [ + { + "key": "product_sold_by_delivery_week.sold_char", + "value": "R", + }, + { + "key": "product_sold_by_delivery_week.not_sold_char", + "value": "M", + }, + ] + ) + self.assertEqual(self.order.order_line[0].weekly_sold_delivered_shown, "MMMMMM") + self.assertEqual(self.order.order_line[1].weekly_sold_delivered_shown, False) + + def test_03_sale_stock_delivery_partial(self): + """Test a SO with a product on delivery.""" + # initial order + self.order.action_confirm() + self.assertTrue( + self.order.picking_ids, + "Sale Stock: no picking created for " + '"invoice on delivery" storable products', + ) + pick = self.order.picking_ids + pick.move_ids.write({"quantity": 3}) + pick.button_validate() + for line in pick.move_ids: + line._action_done() + self.assertEqual(line.product_id.weekly_sold_delivered, "000001") + self.assertEqual(line.product_id.weekly_sold_delivered_shown, "◌◌◌◌◌●") diff --git a/product_sold_by_delivery_week/views/product_views.xml b/product_sold_by_delivery_week/views/product_views.xml new file mode 100644 index 000000000..d20697b32 --- /dev/null +++ b/product_sold_by_delivery_week/views/product_views.xml @@ -0,0 +1,39 @@ + + + + + product.template + + + + + + + + + + product.product + + + + + + + + + diff --git a/product_sold_by_delivery_week/views/sale_views.xml b/product_sold_by_delivery_week/views/sale_views.xml new file mode 100644 index 000000000..3848ef201 --- /dev/null +++ b/product_sold_by_delivery_week/views/sale_views.xml @@ -0,0 +1,22 @@ + + + + + sale.order + + + + + + + + +