addons-cm/stock_picking_batch_collect/hooks.py
GitHub Copilot ef1283be7c [REF] stock_picking_batch_collect: rename and drop the aplicoop dependency
"custom" said nothing about what the module does. It is really about collecting
goods into baskets: the extra detailed-operation columns, the is_collected flag,
the Product Summary tab, the per-company validation restrictions and the
Basket Assembly operator view all serve that one job. Rename it accordingly.

Invert the dependency while at it. A generic warehouse addon was dragging in an
entire eCommerce application, and the whole coupling was a single field:
stock.move.line.home_delivery, related to picking_id.home_delivery. Everything
else was already duck-typed. website_sale_aplicoop now depends on this module
and injects its own consumer group columns into these views.

This removes duplicated logic rather than relocating it: stock.picking
.batch_consumer_group_id re-derived from sale_id a value aplicoop already stored
as stock.picking.consumer_group_id, and the duplicate carried no @api.depends,
so it never recomputed reliably. The batch transfers list now shows the stored
field, which is sortable and groupable.

The two aplicoop tests that probed information_schema for the res_company
batch_* columns can drop that guard: a real dependency guarantees them.

Renaming an addon is not something a migrations/ script can do, since a renamed
addon is a brand new module to Odoo and its migration scripts never run. A
pre_init_hook does it instead: it fires on install after the Python is imported
but before registry.load(), which is the window where remapping ir_model_data
makes Odoo reuse the existing tables and columns. is_collected, the summary line
table and the company settings all survive untouched.

Two details the hook has to get right:

- ir_model_constraint.module and ir_model_relation.module are integer FKs with
  ON DELETE CASCADE, so they must be repointed before the old module row is
  deleted or the bookkeeping goes with it.
- Deleting an ir_model_data row does not cascade to the record it points at.
  Artifacts handed over to aplicoop only need the xmlid dropped, but artifacts
  that disappear need the record deleted too, or the field survives as an orphan
  manual field and the view as a custom view referencing it.

Verified against a restored copy of the dev database: 50 xmlids moved, 19 summary
lines and 5 collected move lines preserved, no orphans, both test suites green,
and the module installs cleanly on a database without website_sale_aplicoop.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:26:47 +02:00

117 lines
4.6 KiB
Python

# Copyright 2026 Criptomart
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
_logger = logging.getLogger(__name__)
OLD_MODULE = "stock_picking_batch_custom"
NEW_MODULE = "stock_picking_batch_collect"
# Fields handed over to `website_sale_aplicoop`, which declares the very same
# ones. Dropping just the xmlid is enough and is what we want: Odoo matches the
# existing row again by (model, name) and re-registers it under aplicoop's
# ownership later in the same run (aplicoop depends on us, so it loads after
# us). Both are non-stored, so no column is touched.
HANDED_OVER = (
"field_stock_move_line__home_delivery",
"field_stock_move_line__consumer_group_id",
)
# Artifacts that disappear for good, because aplicoop already stores the same
# value as `stock.picking.consumer_group_id`. Here the record itself has to go
# too, not only the xmlid: deleting an `ir_model_data` row does not cascade, so
# the field would survive as an orphan manual field and the view as a custom
# view still pointing at it. Mapped as table -> xmlids.
DROPPED = {
"ir_model_fields": ("field_stock_picking__batch_consumer_group_id",),
"ir_ui_view": ("view_stock_picking_batch_picking_tree_consumer_group",),
}
def pre_init_hook(env):
"""Adopt the database records of the former ``stock_picking_batch_custom``.
A renamed addon is a brand new module for Odoo, so a versioned
``migrations/`` script would never run. This hook does instead, on install,
after the Python is imported but before ``registry.load()`` and
``init_models()``: remapping ``ir_model_data`` in that window makes Odoo
reuse the existing tables and columns instead of creating new ones, so
``stock_move_line.is_collected``, the ``stock_picking_batch_summary_line``
table and the ``res_company.batch_*`` settings all survive untouched.
A no-op on a database where the old module was never installed.
"""
cr = env.cr
cr.execute("SELECT id FROM ir_module_module WHERE name = %s", (OLD_MODULE,))
old = cr.fetchone()
if not old:
return
old_id = old[0]
cr.execute("SELECT id FROM ir_module_module WHERE name = %s", (NEW_MODULE,))
new = cr.fetchone()
if not new:
# Should not happen: the module list is refreshed before installing.
_logger.warning("%s: no module row to adopt %s into", NEW_MODULE, OLD_MODULE)
return
new_id = new[0]
_logger.info(
"Adopting %s (id=%s) as %s (id=%s)", OLD_MODULE, old_id, NEW_MODULE, new_id
)
# 1. Hand every xmlid over to the new module name.
cr.execute(
"UPDATE ir_model_data SET module = %s WHERE module = %s",
(NEW_MODULE, OLD_MODULE),
)
_logger.info("Moved %s xmlids to %s", cr.rowcount, NEW_MODULE)
# 2. Give back what is no longer ours, so aplicoop can claim it.
cr.execute(
"DELETE FROM ir_model_data WHERE module = %s AND name IN %s",
(NEW_MODULE, HANDED_OVER),
)
# 3. Drop for good what neither module defines any more, record included.
for table, xmlids in DROPPED.items():
cr.execute(
"DELETE FROM %s WHERE id IN ("
" SELECT res_id FROM ir_model_data"
" WHERE module = %%s AND name IN %%s"
")" % table,
(NEW_MODULE, xmlids),
)
if cr.rowcount:
_logger.info("Dropped %s obsolete row(s) from %s", cr.rowcount, table)
cr.execute(
"DELETE FROM ir_model_data WHERE module = %s AND name IN %s",
(NEW_MODULE, tuple(name for names in DROPPED.values() for name in names)),
)
# 4. `ir_model_constraint.module` and `ir_model_relation.module` are integer
# FKs to `ir_module_module` with ON DELETE CASCADE: they must be
# repointed *before* the old row goes away, or the bookkeeping for our
# SQL constraints is silently destroyed with it.
cr.execute(
"UPDATE ir_model_constraint SET module = %s WHERE module = %s",
(new_id, old_id),
)
cr.execute(
"UPDATE ir_model_relation SET module = %s WHERE module = %s",
(new_id, old_id),
)
# 5. Drop the dependency rows, both ours and any pointing at the old name.
cr.execute(
"DELETE FROM ir_module_module_dependency WHERE module_id = %s OR name = %s",
(old_id, OLD_MODULE),
)
# 6. Finally the module row itself. We delete the *old* one and keep the
# new one: the loader already holds the new row's id in memory, so
# renaming the old row in place would leave Odoo writing to a record
# that no longer exists.
cr.execute("DELETE FROM ir_module_module WHERE id = %s", (old_id,))