Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14.0][IMP] stock_release_channel: refactor compute count #699

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions stock_release_channel/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Stock Release Channels
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:c70f38a0e4ec0907b099eb00dd3e99976de9b9b73f0a71bd991cb4cc17c03a1b
!! source digest: sha256:2ebcba7644a7a1c9838a864f46ff6085b1575556c8511835dbded88b965bdc92
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
Expand Down Expand Up @@ -114,6 +114,7 @@ Contributors
* Sébastien Alix <[email protected]>
* Jacques-Etienne Baudoux <[email protected]>
* Laurent Mignon <[email protected]>
* Michael Tietz (MT Software) <[email protected]>

Design
~~~~~~
Expand Down Expand Up @@ -145,10 +146,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 <https://odoo-community.org/page/maintainer-role>`__:
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-sebalix|
|maintainer-sebalix| |maintainer-mt-software-de|

This module is part of the `OCA/wms <https://github.com/OCA/wms/tree/14.0/stock_release_channel>`_ project on GitHub.

Expand Down
5 changes: 3 additions & 2 deletions stock_release_channel/__manifest__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright 2020 Camptocamp
# Copyright 2023 Michael Tietz (MT Software) <[email protected]>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)

{
Expand All @@ -7,8 +8,8 @@
"version": "14.0.2.1.1",
"development_status": "Beta",
"license": "AGPL-3",
"author": "Camptocamp, ACSONE SA/NV,Odoo Community Association (OCA)",
"maintainers": ["sebalix"],
"author": "Camptocamp, ACSONE SA/NV, Odoo Community Association (OCA)",
"maintainers": ["sebalix", "mt-software-de"],
"website": "https://github.com/OCA/wms",
"depends": [
"sale_stock",
Expand Down
196 changes: 122 additions & 74 deletions stock_release_channel/models/stock_release_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
(
Expand All @@ -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(record.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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions stock_release_channel/readme/CONTRIBUTORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Sébastien Alix <[email protected]>
* Jacques-Etienne Baudoux <[email protected]>
* Laurent Mignon <[email protected]>
* Michael Tietz (MT Software) <[email protected]>

Design
~~~~~~
Expand Down
Loading