From e685346d85e0d998778c164ab78aa7c3d65c6f4b Mon Sep 17 00:00:00 2001 From: kanda999 Date: Thu, 19 Mar 2026 04:58:53 +0000 Subject: [PATCH] [RM] stock_svl_remaining_repair --- .../static/description/index.html | 7 +- .../static/description/index.html | 7 +- .../static/description/index.html | 7 +- .../static/description/index.html | 7 +- .../static/description/index.html | 7 +- .../static/description/index.html | 7 +- stock_svl_remaining_repair/README.rst | 15 - stock_svl_remaining_repair/__init__.py | 2 - stock_svl_remaining_repair/__manifest__.py | 14 - stock_svl_remaining_repair/hooks.py | 10 - stock_svl_remaining_repair/models/__init__.py | 1 - .../models/stock_svl_remaining_repair.py | 1030 ----------------- stock_svl_remaining_repair/pyproject.toml | 3 - .../readme/DESCRIPTION.md | 8 - 14 files changed, 18 insertions(+), 1107 deletions(-) delete mode 100644 stock_svl_remaining_repair/README.rst delete mode 100644 stock_svl_remaining_repair/__init__.py delete mode 100644 stock_svl_remaining_repair/__manifest__.py delete mode 100644 stock_svl_remaining_repair/hooks.py delete mode 100644 stock_svl_remaining_repair/models/__init__.py delete mode 100644 stock_svl_remaining_repair/models/stock_svl_remaining_repair.py delete mode 100644 stock_svl_remaining_repair/pyproject.toml delete mode 100644 stock_svl_remaining_repair/readme/DESCRIPTION.md diff --git a/account_billing_dispatch_option/static/description/index.html b/account_billing_dispatch_option/static/description/index.html index 4cde8834..b98925b5 100644 --- a/account_billing_dispatch_option/static/description/index.html +++ b/account_billing_dispatch_option/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { diff --git a/account_move_delivery_slip_required/static/description/index.html b/account_move_delivery_slip_required/static/description/index.html index d9290f3d..3535a167 100644 --- a/account_move_delivery_slip_required/static/description/index.html +++ b/account_move_delivery_slip_required/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { diff --git a/l10n_jp_summary_invoice_stock_secondary_unit/static/description/index.html b/l10n_jp_summary_invoice_stock_secondary_unit/static/description/index.html index 27d87abb..0e401140 100644 --- a/l10n_jp_summary_invoice_stock_secondary_unit/static/description/index.html +++ b/l10n_jp_summary_invoice_stock_secondary_unit/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { diff --git a/partner_customer_code/static/description/index.html b/partner_customer_code/static/description/index.html index 5a931d9c..59d8a708 100644 --- a/partner_customer_code/static/description/index.html +++ b/partner_customer_code/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { diff --git a/sale_order_dispatch_date/static/description/index.html b/sale_order_dispatch_date/static/description/index.html index 2282af05..932cbf7b 100644 --- a/sale_order_dispatch_date/static/description/index.html +++ b/sale_order_dispatch_date/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { diff --git a/stock_lot_removal_date/static/description/index.html b/stock_lot_removal_date/static/description/index.html index 1966c3be..c0cca776 100644 --- a/stock_lot_removal_date/static/description/index.html +++ b/stock_lot_removal_date/static/description/index.html @@ -8,11 +8,10 @@ /* :Author: David Goodger (goodger@python.org) -:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ +:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. -Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. @@ -275,7 +274,7 @@ margin-left: 2em ; margin-right: 2em } -pre.code .ln { color: gray; } /* line numbers */ +pre.code .ln { color: grey; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } @@ -301,7 +300,7 @@ span.pre { white-space: pre } -span.problematic, pre.problematic { +span.problematic { color: red } span.section-subtitle { diff --git a/stock_svl_remaining_repair/README.rst b/stock_svl_remaining_repair/README.rst deleted file mode 100644 index bfeafd26..00000000 --- a/stock_svl_remaining_repair/README.rst +++ /dev/null @@ -1,15 +0,0 @@ -========================== -Stock SVL Remaining Repair -========================== - -This module is intended for one-time installation on the target database. - -When the module is installed, it automatically: - -* applies the known quantity pre-fix for internal reference ``86406`` -* repairs stock valuation layer ``remaining_qty`` and ``remaining_value`` -* creates adjustment SVLs and journal entries when required -* updates ``standard_price`` where needed -* creates backup tables for all changes - -No menu or manual wizard is provided. The repair runs from ``post_init_hook``. diff --git a/stock_svl_remaining_repair/__init__.py b/stock_svl_remaining_repair/__init__.py deleted file mode 100644 index cc6b6354..00000000 --- a/stock_svl_remaining_repair/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import models -from .hooks import post_init_hook diff --git a/stock_svl_remaining_repair/__manifest__.py b/stock_svl_remaining_repair/__manifest__.py deleted file mode 100644 index 74027d2d..00000000 --- a/stock_svl_remaining_repair/__manifest__.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright 2026 Quartile -# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl). -{ - "name": "Stock SVL Remaining Repair", - "summary": "One-off SVL remaining repair for Odoo.sh deployment", - "version": "18.0.1.0.0", - "category": "Stock", - "website": "https://www.quartile.co", - "author": "Quartile", - "license": "LGPL-3", - "installable": True, - "depends": ["stock_account"], - "post_init_hook": "post_init_hook", -} diff --git a/stock_svl_remaining_repair/hooks.py b/stock_svl_remaining_repair/hooks.py deleted file mode 100644 index c24dd91e..00000000 --- a/stock_svl_remaining_repair/hooks.py +++ /dev/null @@ -1,10 +0,0 @@ -import json -import logging - - -_logger = logging.getLogger(__name__) - - -def post_init_hook(env): - summary = env["stock.svl.remaining.repair.service"].sudo().run_post_init_repair() - _logger.warning("stock_svl_remaining_repair summary: %s", json.dumps(summary, ensure_ascii=False, default=str)) diff --git a/stock_svl_remaining_repair/models/__init__.py b/stock_svl_remaining_repair/models/__init__.py deleted file mode 100644 index a72f4e58..00000000 --- a/stock_svl_remaining_repair/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import stock_svl_remaining_repair diff --git a/stock_svl_remaining_repair/models/stock_svl_remaining_repair.py b/stock_svl_remaining_repair/models/stock_svl_remaining_repair.py deleted file mode 100644 index c14a39e7..00000000 --- a/stock_svl_remaining_repair/models/stock_svl_remaining_repair.py +++ /dev/null @@ -1,1030 +0,0 @@ -import collections -import datetime as dt -import json -import logging - -from odoo import SUPERUSER_ID, fields, models -from odoo.exceptions import UserError -from odoo.tools import float_compare, float_is_zero - - -_logger = logging.getLogger(__name__) - - -BAD_QTY_SQL = """ - SELECT DISTINCT company_id, product_id - FROM stock_valuation_layer - WHERE - (COALESCE(quantity, 0) = 0 AND COALESCE(remaining_qty, 0) <> 0) - OR (ABS(COALESCE(remaining_qty, 0)) - ABS(COALESCE(quantity, 0)) > 1e-9) -""" - -QTY_GAP_SQL = """ - WITH svl AS ( - SELECT - company_id, - product_id, - COALESCE(SUM(remaining_qty), 0) AS rem_qty_sum - FROM stock_valuation_layer - GROUP BY company_id, product_id - ), - quant AS ( - SELECT - sq.company_id, - sq.product_id, - COALESCE(SUM(sq.quantity), 0) AS quant_qty - FROM stock_quant sq - JOIN stock_location sl ON sl.id = sq.location_id - WHERE - sl.usage = 'internal' - OR (sl.usage = 'transit' AND sl.company_id IS NOT NULL) - GROUP BY sq.company_id, sq.product_id - ) - SELECT svl.company_id, svl.product_id - FROM svl - LEFT JOIN quant - ON quant.company_id = svl.company_id - AND quant.product_id = svl.product_id - WHERE ABS(COALESCE(quant.quant_qty, 0) - svl.rem_qty_sum) > 1e-9 -""" - - -class StockSvlRemainingRepairService(models.AbstractModel): - _name = "stock.svl.remaining.repair.service" - _description = "Stock SVL Remaining Repair Service" - - _DONE_PARAM = "stock_svl_remaining_repair.done_at" - _SUMMARY_PARAM = "stock_svl_remaining_repair.summary" - _DEFAULT_REASON = "SVL remaining repair" - _DEFAULT_ADJUSTMENT_ACCOUNT_ID = 83 - _EXCLUDED_DEFAULT_CODES = {"9999", "9901"} - _KNOWN_QUANTITY_FIXES = ( - { - "default_code": "86406", - "description": "INV:2022年6月末棚卸_ヒューテック臨海第2", - "return_description": "INV:2022年6月末棚卸_ヒューテック臨海第2_戻し", - "note": "Fix broken inventory-adjustment SVL quantity before remaining repair.", - }, - ) - - def run_post_init_repair(self): - params = self.env["ir.config_parameter"].sudo() - if params.get_param(self._DONE_PARAM): - return { - "skipped": True, - "reason": "already_done", - "done_at": params.get_param(self._DONE_PARAM), - } - - report = self.run_repair( - adjustment_account_id=self._DEFAULT_ADJUSTMENT_ACCOUNT_ID, - reason=self._DEFAULT_REASON, - excluded_default_codes=self._EXCLUDED_DEFAULT_CODES, - ) - summary = self._compact_summary(report) - params.set_param(self._DONE_PARAM, fields.Datetime.now()) - params.set_param(self._SUMMARY_PARAM, json.dumps(summary, ensure_ascii=False, default=str)) - return summary - - def run_repair(self, adjustment_account_id, reason, excluded_default_codes=None): - excluded_default_codes = set(excluded_default_codes or []) - stamp = dt.datetime.utcnow().strftime("%Y%m%d%H%M%S") - quantity_fix_info, forced_keys = self._apply_known_quantity_fixes(stamp) - - keys = set(self._fetch_target_keys()) - keys.update(forced_keys) - keys = self._filter_keys(keys, excluded_default_codes=excluded_default_codes) - - plans, svl_updates, adjustments, price_updates, unsupported = self._build_plan(sorted(keys), reason=reason) - if unsupported: - raise UserError( - "Unsupported products found during stock_svl_remaining_repair:\n%s" - % json.dumps(unsupported, ensure_ascii=False, indent=2) - ) - - self._validate_apply_requirements( - adjustments, - adjustment_account_id=adjustment_account_id, - ) - backup_tables = self._create_backup_tables(stamp, svl_updates, price_updates) - if quantity_fix_info["backup_table"]: - backup_tables["quantity_fix_backup_table"] = quantity_fix_info["backup_table"] - - self._apply_svl_updates(svl_updates) - created_adjustments = self._apply_adjustments( - adjustments, - adjustment_account_id=adjustment_account_id, - move_date=fields.Date.today(), - ) - self._log_created_adjustments(backup_tables["adjustment_log_table"], created_adjustments) - self._apply_price_updates(price_updates) - - report = self._build_report( - keys=sorted(keys), - plans=plans, - svl_updates=svl_updates, - adjustments=adjustments, - price_updates=price_updates, - apply=True, - backup_tables=backup_tables, - created_adjustments=created_adjustments, - ) - report["known_quantity_fixes"] = quantity_fix_info["rows"] - return report - - def _compact_summary(self, report): - return { - "db": self.env.cr.dbname, - "target_count": report["target_count"], - "svl_update_count": report["svl_update_count"], - "adjustment_count": report["adjustment_count"], - "price_update_count": report["price_update_count"], - "backup_tables": report["backup_tables"], - "known_quantity_fixes": report.get("known_quantity_fixes", []), - "adjustments": [ - { - "product_id": row["product_id"], - "created_svl_id": row["created_svl_id"], - "created_account_move_id": row["created_account_move_id"], - } - for row in report.get("created_adjustments", []) - ], - } - - def _normalize_qty(self, product, qty): - rounding = product.uom_id.rounding or 0.0001 - if float_is_zero(qty, precision_rounding=rounding): - return 0.0 - return qty - - def _normalize_value(self, company, value): - rounding = company.currency_id.rounding or 0.01 - value = company.currency_id.round(value) - if float_is_zero(value, precision_rounding=rounding): - return 0.0 - return value - - def _qty_row_is_bad(self, product, quantity, remaining_qty): - rounding = product.uom_id.rounding or 0.0001 - if float_is_zero(quantity, precision_rounding=rounding): - return not float_is_zero(remaining_qty, precision_rounding=rounding) - return float_compare(abs(remaining_qty), abs(quantity), precision_rounding=rounding) > 0 - - def _get_product_metadata(self, company_id, product_id): - product = self.env["product.product"].browse(product_id).with_company(company_id) - company = self.env["res.company"].browse(company_id) - return product, company - - def _fetch_sql_keys(self, sql, product_ids=None): - query = "SELECT company_id, product_id FROM (%s) AS target_keys" % sql - params = [] - if product_ids: - query += " WHERE product_id = ANY(%s)" - params.append(product_ids) - query += " ORDER BY company_id, product_id" - self.env.cr.execute(query, tuple(params)) - return [(company_id, product_id) for company_id, product_id in self.env.cr.fetchall()] - - def _fetch_target_keys(self, product_ids=None): - if product_ids: - self.env.cr.execute( - """ - SELECT DISTINCT company_id, product_id - FROM stock_valuation_layer - WHERE product_id = ANY(%s) - ORDER BY company_id, product_id - """, - (product_ids,), - ) - return [(company_id, product_id) for company_id, product_id in self.env.cr.fetchall()] - keys = set(self._fetch_sql_keys(BAD_QTY_SQL, product_ids=product_ids)) - for company_id, product_id in self._fetch_sql_keys(QTY_GAP_SQL, product_ids=product_ids): - product, _company = self._get_product_metadata(company_id, product_id) - if product.cost_method == "fifo": - keys.add((company_id, product_id)) - return sorted(keys) - - def _filter_keys(self, keys, excluded_default_codes=None): - excluded_default_codes = set(excluded_default_codes or []) - filtered = [] - for company_id, product_id in keys: - product, _company = self._get_product_metadata(company_id, product_id) - if product.default_code in excluded_default_codes: - continue - filtered.append((company_id, product_id)) - return filtered - - def _fetch_quant_qty_map(self, keys): - if not keys: - return {} - product_ids = sorted({product_id for _, product_id in keys}) - company_ids = sorted({company_id for company_id, _ in keys}) - self.env.cr.execute( - """ - SELECT - sq.company_id, - sq.product_id, - COALESCE(SUM(sq.quantity), 0) - FROM stock_quant sq - JOIN stock_location sl ON sl.id = sq.location_id - WHERE sq.product_id = ANY(%s) - AND sq.company_id = ANY(%s) - AND ( - sl.usage = 'internal' - OR (sl.usage = 'transit' AND sl.company_id IS NOT NULL) - ) - GROUP BY sq.company_id, sq.product_id - """, - (product_ids, company_ids), - ) - return { - (company_id, product_id): qty - for company_id, product_id, qty in self.env.cr.fetchall() - } - - def _fetch_svl_rows(self, company_id, product_id): - self.env.cr.execute( - """ - SELECT - id, - quantity, - value, - COALESCE(remaining_qty, 0), - COALESCE(remaining_value, 0), - create_date, - description - FROM stock_valuation_layer - WHERE company_id = %s - AND product_id = %s - ORDER BY create_date, id - """, - (company_id, product_id), - ) - rows = [] - for svl_id, quantity, value, remaining_qty, remaining_value, create_date, description in self.env.cr.fetchall(): - rows.append( - { - "id": svl_id, - "quantity": quantity or 0.0, - "value": value or 0.0, - "remaining_qty": remaining_qty or 0.0, - "remaining_value": remaining_value or 0.0, - "create_date": create_date, - "description": description or "", - } - ) - return rows - - def _summarize_key(self, company_id, product_id, quant_qty=None): - product, company = self._get_product_metadata(company_id, product_id) - rows = self._fetch_svl_rows(company_id, product_id) - qty_sum = 0.0 - value_sum = 0.0 - rem_qty_sum = 0.0 - rem_value_sum = 0.0 - bad_qty_rows = 0 - nonzero_remaining_rows = 0 - for row in rows: - qty_sum += row["quantity"] - value_sum += row["value"] - rem_qty_sum += row["remaining_qty"] - rem_value_sum += row["remaining_value"] - if self._qty_row_is_bad(product, row["quantity"], row["remaining_qty"]): - bad_qty_rows += 1 - if ( - not float_is_zero(row["remaining_qty"], precision_rounding=product.uom_id.rounding) - or not float_is_zero(row["remaining_value"], precision_rounding=company.currency_id.rounding) - ): - nonzero_remaining_rows += 1 - if quant_qty is None: - quant_qty = self._fetch_quant_qty_map([(company_id, product_id)]).get((company_id, product_id), 0.0) - return { - "company_id": company_id, - "product_id": product_id, - "default_code": product.default_code, - "name": product.display_name, - "is_storable": bool(product.is_storable), - "cost_method": product.cost_method, - "valuation": product.valuation, - "quant_qty": quant_qty, - "quantity_sum": self._normalize_qty(product, qty_sum), - "value_sum": self._normalize_value(company, value_sum), - "remaining_qty_sum": self._normalize_qty(product, rem_qty_sum), - "remaining_value_sum": self._normalize_value(company, rem_value_sum), - "qty_gap": self._normalize_qty(product, quant_qty - rem_qty_sum), - "bad_qty_rows": bad_qty_rows, - "nonzero_remaining_rows": nonzero_remaining_rows, - "standard_price": product.standard_price, - } - - def _check_supported(self, company_id, product_id, before): - product, company = self._get_product_metadata(company_id, product_id) - svl_model = self.env["stock.valuation.layer"] - has_landed_cost = "stock_landed_cost_id" in svl_model._fields - - if product.cost_method == "fifo": - if product.lot_valuated: - return "lot_valuated product is not supported" - if float_compare(before["quantity_sum"], before["quant_qty"], precision_rounding=product.uom_id.rounding) != 0: - return "sum(quantity) and quant qty differ" - domains = [ - [ - ("company_id", "=", company_id), - ("product_id", "=", product_id), - ("stock_valuation_layer_id", "!=", False), - ], - [ - ("company_id", "=", company_id), - ("product_id", "=", product_id), - ("quantity", "=", 0), - ("description", "like", "Manual Stock Valuation:%"), - ], - [ - ("company_id", "=", company_id), - ("product_id", "=", product_id), - ("remaining_qty", "<", 0), - ], - ] - if has_landed_cost: - domains.append( - [ - ("company_id", "=", company_id), - ("product_id", "=", product_id), - ("stock_landed_cost_id", "!=", False), - ] - ) - if any(svl_model.sudo().search_count(domain) for domain in domains): - return "linked adjustments, landed costs, or open negative stock detected" - return None - - rows = self._fetch_svl_rows(company_id, product_id) - if not float_is_zero(before["quant_qty"], precision_rounding=product.uom_id.rounding): - return "non-fifo residual cleanup requires zero quant quantity" - if any(not float_is_zero(row["value"], precision_rounding=company.currency_id.rounding) for row in rows): - return "non-fifo residual cleanup requires all SVL values to be zero" - return None - - def _plan_fifo_product(self, company_id, product_id, value_sum_target=None): - product, company = self._get_product_metadata(company_id, product_id) - rows = self._fetch_svl_rows(company_id, product_id) - queue = collections.deque() - states = {} - - for row in rows: - quantity = row["quantity"] - if float_compare(quantity, 0.0, precision_rounding=product.uom_id.rounding) > 0: - state = { - "remaining_qty": self._normalize_qty(product, quantity), - "remaining_value": self._normalize_value(company, row["value"]), - } - states[row["id"]] = state - queue.append(state) - continue - - if float_compare(quantity, 0.0, precision_rounding=product.uom_id.rounding) < 0: - qty_to_take = self._normalize_qty(product, abs(quantity)) - while qty_to_take and queue: - candidate = queue[0] - take = min(candidate["remaining_qty"], qty_to_take) - candidate_unit_cost = ( - candidate["remaining_value"] / candidate["remaining_qty"] if candidate["remaining_qty"] else 0.0 - ) - taken_value = company.currency_id.round(take * candidate_unit_cost) - candidate["remaining_qty"] = self._normalize_qty(product, candidate["remaining_qty"] - take) - candidate["remaining_value"] = self._normalize_value(company, candidate["remaining_value"] - taken_value) - qty_to_take = self._normalize_qty(product, qty_to_take - take) - if float_is_zero(candidate["remaining_qty"], precision_rounding=product.uom_id.rounding): - queue.popleft() - if qty_to_take: - raise ValueError( - "open negative stock is not supported for %s (svl %s, qty %s)" - % (product.default_code, row["id"], qty_to_take) - ) - - rem_qty_after = 0.0 - rem_value_after = 0.0 - for row in rows: - if float_compare(row["quantity"], 0.0, precision_rounding=product.uom_id.rounding) > 0: - rem_qty_after += states[row["id"]]["remaining_qty"] - rem_value_after += states[row["id"]]["remaining_value"] - - rem_qty_after = self._normalize_qty(product, rem_qty_after) - rem_value_after = self._normalize_value(company, rem_value_after) - if value_sum_target is not None and float_compare(rem_qty_after, 0.0, precision_rounding=product.uom_id.rounding) > 0: - absorption = self._normalize_value(company, value_sum_target - rem_value_after) - if not float_is_zero(absorption, precision_rounding=company.currency_id.rounding): - for row in rows: - if float_compare(row["quantity"], 0.0, precision_rounding=product.uom_id.rounding) <= 0: - continue - target_qty = states[row["id"]]["remaining_qty"] - if float_is_zero(target_qty, precision_rounding=product.uom_id.rounding): - continue - states[row["id"]]["remaining_value"] = self._normalize_value( - company, - states[row["id"]]["remaining_value"] + absorption, - ) - rem_value_after = self._normalize_value(company, rem_value_after + absorption) - break - - updates = [] - for row in rows: - current_qty = row["remaining_qty"] - current_value = row["remaining_value"] - if float_compare(row["quantity"], 0.0, precision_rounding=product.uom_id.rounding) > 0: - target_qty = states[row["id"]]["remaining_qty"] - target_value = states[row["id"]]["remaining_value"] - else: - target_qty = 0.0 - target_value = 0.0 - - qty_changed = not float_is_zero(current_qty - target_qty, precision_rounding=product.uom_id.rounding) - value_changed = abs(current_value - target_value) > 1e-9 - if qty_changed or value_changed: - updates.append( - { - "svl_id": row["id"], - "old_remaining_qty": current_qty, - "old_remaining_value": current_value, - "new_remaining_qty": target_qty, - "new_remaining_value": target_value, - } - ) - - new_standard_price = None - if float_compare(rem_qty_after, 0.0, precision_rounding=product.uom_id.rounding) > 0: - new_standard_price = rem_value_after / rem_qty_after - - return updates, rem_qty_after, rem_value_after, new_standard_price - - def _plan_zero_residual_product(self, company_id, product_id): - product, company = self._get_product_metadata(company_id, product_id) - rows = self._fetch_svl_rows(company_id, product_id) - updates = [] - for row in rows: - qty_changed = not float_is_zero(row["remaining_qty"], precision_rounding=product.uom_id.rounding) - value_changed = abs(row["remaining_value"]) > 1e-9 - if qty_changed or value_changed: - updates.append( - { - "svl_id": row["id"], - "old_remaining_qty": row["remaining_qty"], - "old_remaining_value": row["remaining_value"], - "new_remaining_qty": 0.0, - "new_remaining_value": 0.0, - } - ) - return updates, 0.0, 0.0, None - - def _build_plan(self, keys, reason): - quant_qty_map = self._fetch_quant_qty_map(keys) - plans = [] - svl_updates = [] - adjustments = [] - price_updates = [] - unsupported = [] - - for company_id, product_id in keys: - before = self._summarize_key(company_id, product_id, quant_qty=quant_qty_map.get((company_id, product_id), 0.0)) - product, company = self._get_product_metadata(company_id, product_id) - reason_unsupported = self._check_supported(company_id, product_id, before) - if reason_unsupported: - unsupported.append( - { - "company_id": company_id, - "product_id": product_id, - "default_code": before["default_code"], - "reason": reason_unsupported, - } - ) - continue - - needs_value_alignment_adjustment = product.cost_method == "fifo" and not float_is_zero( - before["qty_gap"], - precision_rounding=product.uom_id.rounding, - ) - - try: - if product.cost_method == "fifo": - value_sum_target = before["value_sum"] if not needs_value_alignment_adjustment else None - updates, rem_qty_after, rem_value_after, new_standard_price = self._plan_fifo_product( - company_id, - product_id, - value_sum_target=value_sum_target, - ) - else: - updates, rem_qty_after, rem_value_after, new_standard_price = self._plan_zero_residual_product( - company_id, product_id - ) - except ValueError as exc: - unsupported.append( - { - "company_id": company_id, - "product_id": product_id, - "default_code": before["default_code"], - "reason": str(exc), - } - ) - continue - - adjustment = None - if needs_value_alignment_adjustment: - value_delta = self._normalize_value(company, rem_value_after - before["value_sum"]) - if not float_is_zero(value_delta, precision_rounding=company.currency_id.rounding): - adjustment = { - "company_id": company_id, - "product_id": product_id, - "default_code": before["default_code"], - "name": before["name"], - "value_delta": value_delta, - "description": "Manual Stock Valuation: %s (%s)." % (reason, before["default_code"] or before["name"]), - "needs_account_move": before["valuation"] == "real_time", - } - adjustments.append(adjustment) - - price_update = None - if new_standard_price is not None: - old_standard_price = product.standard_price - if not float_is_zero(old_standard_price - new_standard_price, precision_rounding=company.currency_id.rounding): - price_update = { - "company_id": company_id, - "product_id": product_id, - "old_standard_price": old_standard_price, - "new_standard_price": new_standard_price, - } - price_updates.append(price_update) - - after_value_sum = self._normalize_value(company, before["value_sum"] + (adjustment["value_delta"] if adjustment else 0.0)) - plans.append( - { - "company_id": company_id, - "product_id": product_id, - "default_code": before["default_code"], - "name": before["name"], - "before": before, - "after": { - "remaining_qty_sum": rem_qty_after, - "remaining_value_sum": rem_value_after, - "qty_gap": self._normalize_qty(product, before["quant_qty"] - rem_qty_after), - "value_sum": after_value_sum, - "value_gap": self._normalize_value(company, rem_value_after - after_value_sum), - "bad_qty_rows": 0, - "standard_price": new_standard_price if new_standard_price is not None else before["standard_price"], - }, - "update_count": len(updates), - "adjustment": adjustment, - "price_update": price_update, - } - ) - svl_updates.extend(updates) - - return plans, svl_updates, adjustments, price_updates, unsupported - - def _validate_apply_requirements(self, adjustments, adjustment_account_id=None): - needs_moves = [row for row in adjustments if row["needs_account_move"]] - if needs_moves and not adjustment_account_id: - raise UserError("adjustment account is required to apply real_time valuation adjustments") - - adjustment_account = None - if adjustment_account_id: - adjustment_account = self.env["account.account"].browse(adjustment_account_id) - if not adjustment_account.exists(): - raise UserError("adjustment account %s not found" % adjustment_account_id) - - for row in needs_moves: - product, company = self._get_product_metadata(row["company_id"], row["product_id"]) - accounts = product.product_tmpl_id.get_product_accounts() - if not accounts.get("stock_valuation"): - raise UserError("stock valuation account is missing for %s" % (row["default_code"] or row["product_id"])) - journal = accounts.get("stock_journal") - if not journal: - raise UserError("stock journal is missing for %s" % (row["default_code"] or row["product_id"])) - if journal.company_id and journal.company_id != company: - raise UserError("stock journal %s does not belong to company %s" % (journal.display_name, company.display_name)) - if adjustment_account: - if "company_id" in adjustment_account._fields: - account_company_ok = not adjustment_account.company_id or adjustment_account.company_id == company - else: - account_company_ok = not adjustment_account.company_ids or company in adjustment_account.company_ids - if not account_company_ok: - raise UserError( - "adjustment account %s does not belong to company %s" - % (adjustment_account.display_name, company.display_name) - ) - - def _create_backup_tables(self, stamp, svl_updates, price_updates): - tables = {} - - if svl_updates: - svl_table = "x_svl_remaining_fix_%s" % stamp - self.env.cr.execute( - """ - CREATE TABLE %s ( - svl_id integer PRIMARY KEY, - old_remaining_qty numeric, - old_remaining_value numeric, - new_remaining_qty numeric, - new_remaining_value numeric, - backup_at timestamp without time zone - ) - """ - % svl_table - ) - self.env.cr.executemany( - f""" - INSERT INTO {svl_table} ( - svl_id, - old_remaining_qty, - old_remaining_value, - new_remaining_qty, - new_remaining_value, - backup_at - ) VALUES (%s, %s, %s, %s, %s, now()) - """, - [ - ( - row["svl_id"], - row["old_remaining_qty"], - row["old_remaining_value"], - row["new_remaining_qty"], - row["new_remaining_value"], - ) - for row in svl_updates - ], - ) - tables["svl_backup_table"] = svl_table - - adjustment_table = "x_svl_value_alignment_%s" % stamp - self.env.cr.execute( - """ - CREATE TABLE %s ( - company_id integer, - product_id integer, - value_delta numeric, - description text, - created_svl_id integer, - created_account_move_id integer, - backup_at timestamp without time zone - ) - """ - % adjustment_table - ) - tables["adjustment_log_table"] = adjustment_table - - if price_updates: - price_table = "x_svl_price_fix_%s" % stamp - self.env.cr.execute( - """ - CREATE TABLE %s ( - company_id integer, - product_id integer, - old_standard_price numeric, - new_standard_price numeric, - backup_at timestamp without time zone - ) - """ - % price_table - ) - self.env.cr.executemany( - f""" - INSERT INTO {price_table} ( - company_id, - product_id, - old_standard_price, - new_standard_price, - backup_at - ) VALUES (%s, %s, %s, %s, now()) - """, - [ - ( - row["company_id"], - row["product_id"], - row["old_standard_price"], - row["new_standard_price"], - ) - for row in price_updates - ], - ) - tables["price_backup_table"] = price_table - - return tables - - def _apply_svl_updates(self, svl_updates): - if not svl_updates: - return - self.env.cr.executemany( - """ - UPDATE stock_valuation_layer - SET remaining_qty = %s, - remaining_value = %s, - write_date = now(), - write_uid = %s - WHERE id = %s - """, - [ - ( - row["new_remaining_qty"], - row["new_remaining_value"], - SUPERUSER_ID, - row["svl_id"], - ) - for row in svl_updates - ], - ) - - def _create_adjustment_move(self, svl, value_delta, adjustment_account_id, move_date): - product = svl.product_id.with_company(svl.company_id) - accounts = product.product_tmpl_id.get_product_accounts() - stock_valuation_account = accounts.get("stock_valuation") - if not stock_valuation_account: - raise UserError("stock valuation account is missing for %s" % (product.default_code or product.display_name)) - - journal = accounts.get("stock_journal") - if not journal: - raise UserError("stock journal is missing for %s" % (product.default_code or product.display_name)) - - counterpart_account = self.env["account.account"].browse(adjustment_account_id) - amount = abs(value_delta) - if value_delta < 0: - debit_account = counterpart_account - credit_account = stock_valuation_account - else: - debit_account = stock_valuation_account - credit_account = counterpart_account - - line_name = "%s\nSVL remaining repair alignment" % svl.description - move = self.env["account.move"].create( - { - "journal_id": journal.id, - "company_id": svl.company_id.id, - "ref": "SVL remaining repair %s" % (product.default_code or product.display_name), - "stock_valuation_layer_ids": [(6, None, [svl.id])], - "date": move_date, - "move_type": "entry", - "line_ids": [ - ( - 0, - 0, - { - "name": line_name, - "account_id": debit_account.id, - "debit": amount, - "credit": 0, - "product_id": product.id, - }, - ), - ( - 0, - 0, - { - "name": line_name, - "account_id": credit_account.id, - "debit": 0, - "credit": amount, - "product_id": product.id, - }, - ), - ], - } - ) - move._post() - return move - - def _apply_adjustments(self, adjustments, adjustment_account_id, move_date): - created_rows = [] - for row in adjustments: - svl = self.env["stock.valuation.layer"].create( - { - "company_id": row["company_id"], - "product_id": row["product_id"], - "description": row["description"], - "value": row["value_delta"], - "unit_cost": 0.0, - "quantity": 0.0, - "remaining_qty": 0.0, - "remaining_value": 0.0, - } - ) - account_move = None - if row["needs_account_move"]: - account_move = self._create_adjustment_move( - svl, - row["value_delta"], - adjustment_account_id=adjustment_account_id, - move_date=move_date, - ) - created_rows.append( - { - "company_id": row["company_id"], - "product_id": row["product_id"], - "value_delta": row["value_delta"], - "description": row["description"], - "created_svl_id": svl.id, - "created_account_move_id": account_move.id if account_move else None, - } - ) - return created_rows - - def _log_created_adjustments(self, table_name, created_rows): - if not created_rows: - return - self.env.cr.executemany( - f""" - INSERT INTO {table_name} ( - company_id, - product_id, - value_delta, - description, - created_svl_id, - created_account_move_id, - backup_at - ) VALUES (%s, %s, %s, %s, %s, %s, now()) - """, - [ - ( - row["company_id"], - row["product_id"], - row["value_delta"], - row["description"], - row["created_svl_id"], - row["created_account_move_id"], - ) - for row in created_rows - ], - ) - - def _apply_price_updates(self, price_updates): - for row in price_updates: - product = ( - self.env["product.product"] - .browse(row["product_id"]) - .with_company(row["company_id"]) - .with_context(disable_auto_svl=True) - ) - product.standard_price = row["new_standard_price"] - - def _build_report( - self, - keys, - plans, - svl_updates, - adjustments, - price_updates, - apply=False, - backup_tables=None, - created_adjustments=None, - ): - quant_qty_map = self._fetch_quant_qty_map(keys) - verification = [ - self._summarize_key(company_id, product_id, quant_qty=quant_qty_map.get((company_id, product_id), 0.0)) - for company_id, product_id in keys - ] - needs_adjustment_account = any(row["needs_account_move"] for row in adjustments) - return { - "db": self.env.cr.dbname, - "apply": apply, - "target_count": len(keys), - "svl_update_count": len(svl_updates), - "adjustment_count": len(adjustments), - "price_update_count": len(price_updates), - "apply_requirements": { - "adjustment_account_required": needs_adjustment_account, - }, - "backup_tables": backup_tables or {}, - "plan": plans, - "created_adjustments": created_adjustments or [], - "verification": verification, - } - - def _apply_known_quantity_fixes(self, stamp): - patch_rows = [] - forced_keys = set() - table_name = None - - for patch in self._KNOWN_QUANTITY_FIXES: - products = self.env["product.product"].with_context(active_test=False).search( - [("default_code", "=", patch["default_code"])] - ) - if not products: - continue - - self.env.cr.execute( - """ - SELECT company_id, product_id - FROM stock_valuation_layer - WHERE product_id = ANY(%s) - GROUP BY company_id, product_id - ORDER BY company_id, product_id - """, - (products.ids,), - ) - forced_keys.update((company_id, product_id) for company_id, product_id in self.env.cr.fetchall()) - - self.env.cr.execute( - """ - SELECT id, company_id, product_id, quantity, value - FROM stock_valuation_layer - WHERE product_id = ANY(%s) - AND description = %s - ORDER BY create_date, id - """, - (products.ids, patch["description"]), - ) - candidates = self.env.cr.fetchall() - if not candidates: - continue - if len(candidates) != 1: - raise UserError("Quantity fix for %s is ambiguous" % patch["default_code"]) - - svl_id, company_id, product_id, current_quantity, current_value = candidates[0] - self.env.cr.execute( - """ - SELECT COALESCE(SUM(quantity), 0), COALESCE(SUM(value), 0) - FROM stock_valuation_layer - WHERE product_id = %s - AND company_id = %s - AND description = %s - """, - (product_id, company_id, patch["return_description"]), - ) - return_qty_sum, return_value_sum = self.env.cr.fetchone() - target_quantity = -(return_qty_sum or 0.0) - target_value = -(return_value_sum or 0.0) - if float_is_zero(target_quantity, precision_rounding=0.0001): - raise UserError("Quantity fix for %s did not find return rows" % patch["default_code"]) - if abs((current_value or 0.0) - target_value) > 1e-9: - raise UserError("Quantity fix for %s failed value validation" % patch["default_code"]) - if abs((current_quantity or 0.0) - target_quantity) <= 1e-9: - continue - - if table_name is None: - table_name = "x_svl_quantity_fix_%s" % stamp - self.env.cr.execute( - """ - CREATE TABLE %s ( - svl_id integer PRIMARY KEY, - company_id integer, - product_id integer, - default_code varchar, - description text, - old_quantity numeric, - new_quantity numeric, - note text, - backup_at timestamp without time zone - ) - """ - % table_name - ) - - self.env.cr.execute( - f""" - INSERT INTO {table_name} ( - svl_id, - company_id, - product_id, - default_code, - description, - old_quantity, - new_quantity, - note, - backup_at - ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, now()) - """, - ( - svl_id, - company_id, - product_id, - patch["default_code"], - patch["description"], - current_quantity, - target_quantity, - patch["note"], - ), - ) - self.env.cr.execute( - """ - UPDATE stock_valuation_layer - SET quantity = %s, - write_date = now(), - write_uid = %s - WHERE id = %s - """, - (target_quantity, SUPERUSER_ID, svl_id), - ) - patch_rows.append( - { - "svl_id": svl_id, - "company_id": company_id, - "product_id": product_id, - "default_code": patch["default_code"], - "old_quantity": current_quantity, - "new_quantity": target_quantity, - "description": patch["description"], - } - ) - - if patch_rows: - _logger.warning( - "Applied known SVL quantity fixes: %s", - json.dumps(patch_rows, ensure_ascii=False, default=str), - ) - return {"rows": patch_rows, "backup_table": table_name}, forced_keys diff --git a/stock_svl_remaining_repair/pyproject.toml b/stock_svl_remaining_repair/pyproject.toml deleted file mode 100644 index 4231d0cc..00000000 --- a/stock_svl_remaining_repair/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build-system] -requires = ["whool"] -build-backend = "whool.buildapi" diff --git a/stock_svl_remaining_repair/readme/DESCRIPTION.md b/stock_svl_remaining_repair/readme/DESCRIPTION.md deleted file mode 100644 index 5fb4f072..00000000 --- a/stock_svl_remaining_repair/readme/DESCRIPTION.md +++ /dev/null @@ -1,8 +0,0 @@ -Auto-applies the one-off stock valuation layer remaining repair during module installation. - -This addon includes: - -- the FIFO remaining quantity/value repair logic used for the HLS data fix -- a built-in quantity pre-fix for internal reference 86406 -- automatic adjustment entry creation with account 83 when quantity gaps require value alignment -- backup tables for all changed SVL rows, created adjustments, and standard price updates