From ef10934cc987031d3a96a87953f03eb3d303a2cd Mon Sep 17 00:00:00 2001 From: Michael Tietz Date: Fri, 23 Jun 2023 12:32:27 +0200 Subject: [PATCH] [IMP] stock_release_channel: Make _compute_picking_count hookable --- stock_release_channel/README.rst | 15 +- stock_release_channel/__manifest__.py | 5 +- .../models/stock_release_channel.py | 196 +++++++++++------- stock_release_channel/readme/CONTRIBUTORS.rst | 1 + .../static/description/index.html | 10 +- 5 files changed, 142 insertions(+), 85 deletions(-) diff --git a/stock_release_channel/README.rst b/stock_release_channel/README.rst index ce43ecc7e83..ab9ce6d3a2d 100644 --- a/stock_release_channel/README.rst +++ b/stock_release_channel/README.rst @@ -19,9 +19,9 @@ Stock Release Channels .. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png :target: https://translation.odoo-community.org/projects/wms-14-0/wms-14-0-stock_release_channel :alt: Translate me on Weblate -.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/285/14.0 - :alt: Try me on Runbot +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/wms&target_branch=14.0 + :alt: Try me on Runboat |badge1| |badge2| |badge3| |badge4| |badge5| @@ -102,6 +102,7 @@ Authors * Camptocamp * ACSONE SA/NV +* MT Software Contributors ~~~~~~~~~~~~ @@ -111,6 +112,7 @@ Contributors * Sébastien Alix * Jacques-Etienne Baudoux * Laurent Mignon +* Michael Tietz (MT Software) Design ~~~~~~ @@ -142,10 +144,13 @@ promote its widespread use. .. |maintainer-sebalix| image:: https://github.com/sebalix.png?size=40px :target: https://github.com/sebalix :alt: sebalix +.. |maintainer-mt-software-de| image:: https://github.com/mt-software-de.png?size=40px + :target: https://github.com/mt-software-de + :alt: mt-software-de -Current `maintainer `__: +Current `maintainers `__: -|maintainer-sebalix| +|maintainer-sebalix| |maintainer-mt-software-de| This module is part of the `OCA/wms `_ project on GitHub. diff --git a/stock_release_channel/__manifest__.py b/stock_release_channel/__manifest__.py index 260138685a6..c8f65c7cfdc 100644 --- a/stock_release_channel/__manifest__.py +++ b/stock_release_channel/__manifest__.py @@ -1,4 +1,5 @@ # Copyright 2020 Camptocamp +# Copyright 2023 Michael Tietz (MT Software) # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) { @@ -7,8 +8,8 @@ "version": "14.0.2.1.0", "development_status": "Beta", "license": "AGPL-3", - "author": "Camptocamp, ACSONE SA/NV,Odoo Community Association (OCA)", - "maintainers": ["sebalix"], + "author": "Camptocamp, ACSONE SA/NV, MT Software, Odoo Community Association (OCA)", + "maintainers": ["sebalix", "mt-software-de"], "website": "https://github.com/OCA/wms", "depends": [ "sale_stock", diff --git a/stock_release_channel/models/stock_release_channel.py b/stock_release_channel/models/stock_release_channel.py index b51bf1a90d9..f3fcea55e90 100644 --- a/stock_release_channel/models/stock_release_channel.py +++ b/stock_release_channel/models/stock_release_channel.py @@ -2,6 +2,8 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html) import logging +from collections import defaultdict +from copy import deepcopy from pytz import timezone @@ -245,8 +247,8 @@ def _get_picking_to_assign_domain(self): def _field_picking_domains(self): return { - "count_picking_all": [], - "count_picking_release_ready": [ + "all": [], + "release_ready": [ ("release_ready", "=", True), # FIXME not TZ friendly ( @@ -255,91 +257,135 @@ def _field_picking_domains(self): fields.Datetime.now().replace(hour=23, minute=59), ), ], - "count_picking_released": [ + "released": [ ("last_release_date", "!=", False), ("state", "in", ("assigned", "waiting", "confirmed")), ], - "count_picking_assigned": [ + "assigned": [ ("last_release_date", "!=", False), ("state", "=", "assigned"), ], - "count_picking_waiting": [ + "waiting": [ ("last_release_date", "!=", False), ("state", "in", ("waiting", "confirmed")), ], - "count_picking_late": [ + "late": [ ("last_release_date", "!=", False), ("scheduled_date", "<", fields.Datetime.now()), ("state", "in", ("assigned", "waiting", "confirmed")), ], - "count_picking_priority": [ + "priority": [ ("last_release_date", "!=", False), ("priority", "=", "1"), ("state", "in", ("assigned", "waiting", "confirmed")), ], - "count_picking_done": [ + "done": [ ("state", "=", "done"), ("date_done", ">", fields.Datetime.now().replace(hour=0, minute=0)), ], } + @api.model + def _get_picking_read_group_fields(self): + "Additional fields to read on read_group of stock.pickings" + return [] + + @api.model + def _get_picking_compute_fields(self): + """This returns a list of tuples + the first value of the tuple represents the prefix of computed field + and the second value represents the field used to set the computed field""" + return [("count", "release_channel_id_count")] + + @api.model + def _get_move_read_group_fields(self): + "Additional fields to read on read_group of stock.moves" + return [] + + @api.model + def _get_move_compute_fields(self): + """This returns a list of tuples + the first value of the tuple represents the prefix of computed field + and the second value represents the field used to set the computed field""" + return [("count", "picking_id_count")] + + @api.model + def _get_compute_field_name(self, prefix, name, domain_name): + return f"{prefix}_{name}_{domain_name}" + + @api.model + def _get_default_aggregate_values(self): + picking_compute_fields = self._get_picking_compute_fields() + move_compute_fields = self._get_move_compute_fields() + default_values = {} + for domain_name, _d in self._field_picking_domains().items(): + for prefix, _fetch in picking_compute_fields: + field = self._get_compute_field_name(prefix, "picking", domain_name) + default_values[field] = 0 + for prefix, _fetch in move_compute_fields: + field = self._get_compute_field_name(prefix, "move", domain_name) + default_values[field] = 0 + return default_values + # TODO maybe we have to do raw SQL to include the picking + moves counts in # a single query def _compute_picking_count(self): domains = self._field_picking_domains() - picking_ids_per_field = {} - for field, domain in domains.items(): + picking_channels = defaultdict( + lambda: {"channel_id": False, "matched_domains": []} + ) + all_picking_ids = set() + channels_aggregate_values = defaultdict(lambda: defaultdict(lambda: 0)) + picking_read_fields = self._get_picking_read_group_fields() + picking_compute_fields = self._get_picking_compute_fields() + for domain_name, domain in domains.items(): data = self.env["stock.picking"].read_group( domain + [("release_channel_id", "in", self.ids)], - ["release_channel_id", "picking_ids:array_agg(id)"], + ["release_channel_id", "picking_ids:array_agg(id)"] + + picking_read_fields, ["release_channel_id"], ) - count = { - row["release_channel_id"][0]: row["release_channel_id_count"] - for row in data - if row["release_channel_id"] - } - picking_ids_per_field.update( - { - (row["release_channel_id"][0], field): row["picking_ids"] - for row in data - if row["release_channel_id"] - } - ) - - for record in self: - record[field] = count.get(record.id, 0) - - all_picking_ids = [ - pid for picking_ids in picking_ids_per_field.values() for pid in picking_ids - ] + for row in data: + channel_id = row["release_channel_id"] and row["release_channel_id"][0] + if not channel_id: + continue + picking_ids = row["picking_ids"] + all_picking_ids.update(picking_ids) + for picking_id in picking_ids: + picking_channels[picking_id]["channel_id"] = channel_id + picking_channels[picking_id]["matched_domains"].append(domain_name) + + for prefix, fetch in picking_compute_fields: + field = self._get_compute_field_name(prefix, "picking", domain_name) + channels_aggregate_values[channel_id][field] = row[fetch] + move_read_fields = self._get_move_read_group_fields() + move_compute_fields = self._get_move_compute_fields() data = self.env["stock.move"].read_group( # TODO for now we do estimates, later we may improve the domains per # field, but now we can run one sql query on stock.move for all fields - [("picking_id", "in", all_picking_ids), ("state", "!=", "cancel")], - ["picking_id"], + [("picking_id", "in", list(all_picking_ids)), ("state", "!=", "cancel")], + ["picking_id"] + move_read_fields, ["picking_id"], ) - move_count = { - row["picking_id"][0]: row["picking_id_count"] - for row in data - if row["picking_id"] - } - for field, __ in domains.items(): - move_field = field.replace("picking", "move") - for record in self: - picking_ids = picking_ids_per_field.get((record.id, field), []) - move_estimate = sum( - move_count.get(picking_id, 0) for picking_id in picking_ids - ) - record[move_field] = move_estimate - + for row in data: + picking_id = row["picking_id"][0] + for matched_domain in picking_channels[picking_id]["matched_domains"]: + for prefix, fetch in move_compute_fields: + field = self._get_compute_field_name(prefix, "move", matched_domain) + channel_id = picking_channels[picking_id]["channel_id"] + channels_aggregate_values[channel_id][field] += row[fetch] + + default_aggregate_values = self._get_default_aggregate_values() for record in self: - record.count_picking_full_progress = ( - record.count_picking_release_ready - + record.count_picking_released - + record.count_picking_done - ) + values = deepcopy(default_aggregate_values) + values.update(channels_aggregate_values.get(channel_id, {})) + for prefix, _fetch in self._get_picking_compute_fields(): + values[f"{prefix}_picking_full_progress"] = ( + values[f"{prefix}_picking_release_ready"] + + values[f"{prefix}_picking_released"] + + values[f"{prefix}_picking_done"] + ) + record.write(values) def _query_get_chain(self, pickings): """Get all stock.picking before an outgoing one @@ -380,7 +426,7 @@ def _compute_picking_chain(self): self.env["stock.move"].flush(["move_dest_ids", "move_orig_ids", "picking_id"]) self.env["stock.picking"].flush(["state"]) for channel in self: - domain = self._field_picking_domains()["count_picking_released"] + domain = self._field_picking_domains()["released"] domain += [("release_channel_id", "=", channel.id)] released = self.env["stock.picking"].search(domain) @@ -408,7 +454,7 @@ def _compute_picking_chain(self): def _compute_last_done_picking(self): for channel in self: # TODO we have one query per channel, could be better - domain = self._field_picking_domains()["count_picking_done"] + domain = self._field_picking_domains()["done"] domain += [("release_channel_id", "=", channel.id)] picking = self.env["stock.picking"].search( domain, limit=1, order="date_done DESC" @@ -535,35 +581,36 @@ def _eval_code(self, pickings): def action_picking_all(self): return self._action_picking_for_field( - "count_picking_all", context={"search_default_release_ready": 1} + "all", context={"search_default_release_ready": 1} ) def action_picking_release_ready(self): - return self._action_picking_for_field("count_picking_release_ready") + return self._action_picking_for_field("release_ready") def action_picking_released(self): - return self._action_picking_for_field("count_picking_released") + return self._action_picking_for_field("released") def action_picking_assigned(self): - return self._action_picking_for_field("count_picking_assigned") + return self._action_picking_for_field("assigned") def action_picking_waiting(self): - return self._action_picking_for_field("count_picking_waiting") + return self._action_picking_for_field("waiting") def action_picking_late(self): - return self._action_picking_for_field("count_picking_late") + return self._action_picking_for_field("late") def action_picking_priority(self): - return self._action_picking_for_field("count_picking_priority") + return self._action_picking_for_field("priority") def action_picking_done(self): - return self._action_picking_for_field("count_picking_done") + return self._action_picking_for_field("done") def _action_picking_for_field(self, field_domain, context=None): domain = self._field_picking_domains()[field_domain] domain += [("release_channel_id", "in", self.ids)] pickings = self.env["stock.picking"].search(domain) - field_descr = self._fields[field_domain]._description_string(self.env) + field = self._get_compute_field_name("count", "picking", field_domain) + field_descr = self._fields[field]._description_string(self.env) return self._build_action( "stock_available_to_promise_release.stock_picking_release_action", pickings, @@ -573,35 +620,36 @@ def _action_picking_for_field(self, field_domain, context=None): def action_move_all(self): return self._action_move_for_field( - "count_picking_all", context={"search_default_release_ready": 1} + "all", context={"search_default_release_ready": 1} ) def action_move_release_ready(self): - return self._action_move_for_field("count_picking_release_ready") + return self._action_move_for_field("release_ready") def action_move_released(self): - return self._action_move_for_field("count_picking_released") + return self._action_move_for_field("released") def action_move_assigned(self): - return self._action_move_for_field("count_picking_assigned") + return self._action_move_for_field("assigned") def action_move_waiting(self): - return self._action_move_for_field("count_picking_waiting") + return self._action_move_for_field("waiting") def action_move_late(self): - return self._action_move_for_field("count_picking_late") + return self._action_move_for_field("late") def action_move_priority(self): - return self._action_move_for_field("count_picking_priority") + return self._action_move_for_field("priority") def action_move_done(self): - return self._action_move_for_field("count_picking_done") + return self._action_move_for_field("done") def _action_move_for_field(self, field_domain, context=None): domain = self._field_picking_domains()[field_domain] domain += [("release_channel_id", "in", self.ids)] pickings = self.env["stock.picking"].search(domain) - field_descr = self._fields[field_domain]._description_string(self.env) + field = self._get_compute_field_name("count", "picking", field_domain) + field_descr = self._fields[field]._description_string(self.env) xmlid = "stock_available_to_promise_release.stock_move_release_action" action = self.env["ir.actions.act_window"]._for_xml_id(xmlid) action["display_name"] = "{} ({})".format( @@ -663,7 +711,7 @@ def _get_next_pickings_max(self): if not self.max_auto_release: raise exceptions.UserError(_("No Max transfers to release is configured.")) - waiting_domain = self._field_picking_domains()["count_picking_waiting"] + waiting_domain = self._field_picking_domains()["waiting"] waiting_domain += [("release_channel_id", "=", self.id)] released_in_progress = self.env["stock.picking"].search_count(waiting_domain) @@ -675,7 +723,7 @@ def _get_next_pickings_max(self): " progress is already at the maximum." ) ) - domain = self._field_picking_domains()["count_picking_release_ready"] + domain = self._field_picking_domains()["release_ready"] domain += [("release_channel_id", "=", self.id)] next_pickings = self.env["stock.picking"].search(domain) # We have to use a python sort and not a order + limit on the search @@ -685,7 +733,7 @@ def _get_next_pickings_max(self): return next_pickings.sorted(self._pickings_sort_key)[:release_limit] def _get_next_pickings_group_commercial_partner(self): - domain = self._field_picking_domains()["count_picking_release_ready"] + domain = self._field_picking_domains()["release_ready"] domain += [("release_channel_id", "=", self.id)] # We have to use a python sort and not a order + limit on the search # because "date_priority" is computed and not stored. If needed, we diff --git a/stock_release_channel/readme/CONTRIBUTORS.rst b/stock_release_channel/readme/CONTRIBUTORS.rst index 5a7ddcbf5ac..7674c23c505 100644 --- a/stock_release_channel/readme/CONTRIBUTORS.rst +++ b/stock_release_channel/readme/CONTRIBUTORS.rst @@ -3,6 +3,7 @@ * Sébastien Alix * Jacques-Etienne Baudoux * Laurent Mignon +* Michael Tietz (MT Software) Design ~~~~~~ diff --git a/stock_release_channel/static/description/index.html b/stock_release_channel/static/description/index.html index 59b0466a752..3f6f8b34859 100644 --- a/stock_release_channel/static/description/index.html +++ b/stock_release_channel/static/description/index.html @@ -3,7 +3,7 @@ - + Stock Release Channels