cron better exception management. show supplier commercial name in product card template. linters config

This commit is contained in:
GitHub Copilot 2026-06-02 10:43:54 +02:00
parent a812b4385d
commit 87dcb4e350
17 changed files with 104 additions and 100 deletions

View file

@ -4,7 +4,7 @@
[flake8]
max-line-length = 88
max-complexity = 30
max-complexity = 16
# B = bugbear
# B9 = bugbear opinionated (incl line length)
select = C,E,F,W,B,B9
@ -13,6 +13,12 @@ select = C,E,F,W,B,B9
# W503: line break before binary operator (black-compatible)
# B950: line too long (soft limit, complements B)
ignore = E203,E501,W503,B950
# OCA standard per-file ignores:
# - __init__.py imports submodules that are intentionally "unused" (F401)
# - __manifest__.py is a bare dict literal, not a useless expression (B018)
per-file-ignores =
__init__.py: F401
__manifest__.py: B018
exclude =
.git,
__pycache__,

View file

@ -110,22 +110,11 @@ repos:
- id: flake8
name: flake8
additional_dependencies: ["flake8-bugbear==23.12.2"]
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.19.1
hooks:
- id: mypy
# do not run on test files or __init__ files (mypy does not support
# namespace packages)
exclude: (/tests/|/__init__\.py$|^scripts/)
# Exclude migrations explicitly to avoid duplicate-module errors
args: ["--exclude", "(?i).*/migrations/.*"]
additional_dependencies:
- "lxml"
- "odoo-stubs"
- "types-python-dateutil"
- "types-pytz"
- "types-requests"
- "types-setuptools"
# NOTE: mypy is intentionally not configured here. It is not part of the
# OCA pre-commit standard and breaks on Odoo's migration folder layout
# (e.g. ".../migrations/18.0.2.1.0/post-migrate.py" -> not a valid package
# name), because mypy's exclude does not apply to files passed explicitly
# by pre-commit.
- repo: https://github.com/PyCQA/pylint
rev: v4.0.4
hooks:

View file

@ -42,7 +42,6 @@ enable=anomalous-backslash-in-string,
manifest-deprecated-key,
manifest-external-assets,
manifest-maintainers-list,
manifest-required-author,
manifest-required-key,
manifest-required-key-app,
manifest-superfluous-key,
@ -53,7 +52,6 @@ enable=anomalous-backslash-in-string,
method-search,
missing-odoo-file,
missing-odoo-file-app,
missing-readme,
missing-return,
no-raise-unlink,
no-search-all,

View file

@ -26,7 +26,6 @@ enable=anomalous-backslash-in-string,
manifest-data-duplicated,
manifest-deprecated-key,
manifest-maintainers-list,
manifest-required-author,
manifest-required-key,
manifest-superfluous-key,
manifest-version-format,
@ -34,7 +33,6 @@ enable=anomalous-backslash-in-string,
method-inverse,
method-required-super,
method-search,
missing-readme,
missing-return,
no-raise-unlink,
no-write-in-compute,

View file

@ -13,5 +13,4 @@
"excludes": ["account_invoice_fixed_discount"],
"post_init_hook": "post_init_hook",
"data": ["report/invoice.xml", "views/account_move.xml"],
"installable": True,
}

View file

@ -13,5 +13,4 @@
"data/ir_cron.xml",
"views/res_config_settings_views.xml",
],
"installable": True,
}

View file

@ -1,18 +0,0 @@
[mypy]
# Exclude migration scripts (post-migrate.py etc.) from mypy checks to avoid
# duplicate module name errors when multiple addons include scripts with the
# same filename.
# Exclude scripts/ directory from mypy checks (utility scripts, not Odoo code)
exclude = .*/migrations/.*|^scripts/.*
# Ignore missing imports from Odoo modules
ignore_missing_imports = True
[mypy-odoo]
ignore_missing_imports = True
[mypy-odoo.*]
ignore_missing_imports = True
[mypy-odoo.addons.*]
ignore_missing_imports = True

View file

@ -11,8 +11,6 @@
"data": [
"security/security.xml",
"security/ir.model.access.csv",
"security/ir.model.access.csv",
"views/portal_templates.xml",
],
"installable": True,
}

View file

@ -10,6 +10,5 @@
"author": "ACSONE SA/NV,Odoo Community Association (OCA)",
"website": "https://github.com/OCA/product-attribute",
"depends": ["account", "product"],
"data": [],
"demo": ["demo/account.xml", "demo/pricelist.xml"],
}

View file

@ -16,6 +16,5 @@
"views/view_product_product.xml",
"views/view_product_template.xml",
],
"installable": True,
"pre_init_hook": "pre_init_hook",
}

View file

@ -18,5 +18,4 @@
"views/product_template.xml",
],
"maintainers": ["sbejaoui"],
"installable": True,
}

View file

@ -18,5 +18,4 @@
"views/res_partner_view.xml",
],
"post_init_hook": "post_init_hook",
"installable": True,
}

View file

@ -17,24 +17,4 @@ extend-exclude = '''
)/
'''
[tool.isort]
profile = "black"
force_single_line = true
line_length = 88
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
skip_glob = ["*/__init__.py"]
known_odoo = ["odoo"]
known_odoo_addons = ["odoo.addons"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "ODOO", "ODOO_ADDONS", "FIRSTPARTY", "LOCALFOLDER"]
default_section = "THIRDPARTY"
[tool.mypy]
# Excluir carpetas de migraciones y archivos de post-migrate.py que usan guiones
# (evita errores de "Duplicate module" en mypy cuando múltiples addons contienen
# archivos con el mismo nombre como `post-migrate.py`). Usamos una expresión
# regular que coincide con cualquier ruta que contenga `/migrations/`.
exclude = "(?i).*/migrations/.*"
# isort config lives in .isort.cfg (single source of truth).

View file

@ -1,21 +0,0 @@
[flake8]
max-line-length = 88
max-complexity = 16
select = C,E,F,W,B,B9
ignore = E203,E501,W503,B950
exclude = scripts/, .venv/
[isort]
profile = black
force_single_line = True
line_length = 88
multi_line_output = 3
include_trailing_comma = True
force_grid_wrap = 0
use_parentheses = True
ensure_newline_before_comments = True
skip_glob = */__init__.py
known_odoo=odoo
known_odoo_addons=odoo.addons
sections=FUTURE,STDLIB,THIRDPARTY,ODOO,ODOO_ADDONS,FIRSTPARTY,LOCALFOLDER
default_section=THIRDPARTY

View file

@ -246,7 +246,7 @@ def _get_product_supplier_info(self, products):
supplier_name = ""
if product.seller_ids:
partner = product.seller_ids[0].partner_id.sudo()
supplier_name = partner.name or ""
supplier_name = partner.company_name or partner.name or ""
if partner.city:
supplier_name += f" ({partner.city})"
product_supplier_info[product.id] = supplier_name

View file

@ -10,9 +10,6 @@ from odoo import models
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
# Pylint: explicit 'string' attributes are intentional for readable labels in views.
# Some linters flag these as redundant; disable that specific check here.
# pylint: disable=attribute-string-redundant
class GroupOrder(models.Model):
@ -945,6 +942,7 @@ class GroupOrder(models.Model):
# route configuration doesn't block all remaining orders.
confirmed_sale_orders = SaleOrder.browse()
failed_sale_orders = SaleOrder.browse()
failure_reasons = {}
for sale_order in sale_orders:
try:
@ -953,9 +951,18 @@ class GroupOrder(models.Model):
# route issues. This cron confirms business orders first
# and handles stock exceptions operationally.
sale_order.with_context(from_orderpoint=True).action_confirm()
# Flush INSIDE the savepoint. Odoo raises many
# ValidationErrors lazily at flush/recompute time, which
# would otherwise escape this savepoint (it has already
# been released on block exit) and poison the
# transaction, aborting every remaining sale order. By
# forcing the flush here, a failing order is rolled back
# to its savepoint and the loop continues cleanly.
self.env.flush_all()
confirmed_sale_orders |= sale_order
except Exception:
except Exception as exc:
failed_sale_orders |= sale_order
failure_reasons[sale_order.id] = str(exc) or exc.__class__.__name__
_logger.exception(
"Cron: Error confirming sale order %s (%s) for group order %s (%s). "
"Order skipped; remaining orders will continue.",
@ -965,9 +972,12 @@ class GroupOrder(models.Model):
self.name,
)
batches = self.env["stock.picking.batch"]
if confirmed_sale_orders:
# Create picking batches only for confirmed sale orders
self._create_picking_batches_for_sale_orders(confirmed_sale_orders)
batches = self._create_picking_batches_for_sale_orders(
confirmed_sale_orders
)
self._log_missing_procurement_warnings(confirmed_sale_orders)
if failed_sale_orders:
@ -980,6 +990,9 @@ class GroupOrder(models.Model):
self.name,
failed_sale_orders.ids,
)
self._post_failed_sale_orders_message(
batches, failed_sale_orders, failure_reasons
)
except Exception:
_logger.exception(
"Cron: Error confirming sale orders for group order %s (%s)",
@ -987,6 +1000,66 @@ class GroupOrder(models.Model):
self.name,
)
def _post_failed_sale_orders_message(
self, batches, failed_sale_orders, failure_reasons=None
):
"""Post a note on the picking batch wall about sale orders that failed.
Operators read the batch chatter to follow up manually on orders that
could not be confirmed (and are therefore NOT part of the batch).
Args:
batches: stock.picking.batch recordset to notify (may be empty).
failed_sale_orders: sale.order recordset that failed to confirm.
failure_reasons: optional {sale_order_id: error message} mapping.
"""
self.ensure_one()
if not failed_sale_orders:
return
failure_reasons = failure_reasons or {}
from markupsafe import Markup
from markupsafe import escape
items = Markup()
for sale_order in failed_sale_orders:
reason = failure_reasons.get(sale_order.id)
label = sale_order.name or self.env._("(unnamed)")
partner = sale_order.partner_id.display_name or ""
item = escape(f"{label} ({partner})") if partner else escape(label)
if reason:
item += Markup(": ") + escape(reason)
items += Markup("<li>%s</li>") % item
body = (
escape(
self.env._(
"%(count)d sale order(s) linked to group order "
"%(name)s” failed to confirm and are NOT part of this "
"batch. Please review and confirm them manually:",
count=len(failed_sale_orders),
name=self.name,
)
)
+ Markup("<ul>%s</ul>") % items
)
if not batches:
# No batch was created (all confirmations failed), so there is no
# wall to post on. The warning logged by the caller is the record.
_logger.warning(
"Cron: No picking batch available to report %d failed sale "
"orders for group order %s (%s): ids=%s",
len(failed_sale_orders),
self.id,
self.name,
failed_sale_orders.ids,
)
return
for batch in batches:
batch.message_post(body=body, subtype_xmlid="mail.mt_note")
def _log_missing_procurement_warnings(self, sale_orders):
"""Log warnings for confirmed orders with stockable lines lacking moves.
@ -1028,9 +1101,13 @@ class GroupOrder(models.Model):
Args:
sale_orders: Recordset of confirmed sale.order
Returns:
stock.picking.batch recordset with the batches created here.
"""
self.ensure_one()
StockPickingBatch = self.env["stock.picking.batch"].sudo()
created_batches = StockPickingBatch.browse()
# Create batches per group order, not per consumer group.
# If multiple picking types exist, keep one batch per picking type.
@ -1067,6 +1144,7 @@ class GroupOrder(models.Model):
)
pickings.write({"batch_id": batch.id})
created_batches |= batch
_logger.info(
"Cron: Created batch %s with %d pickings for group order %s",
@ -1074,3 +1152,5 @@ class GroupOrder(models.Model):
len(pickings),
self.name,
)
return created_batches

View file

@ -638,14 +638,14 @@
<i class="fa fa-image fa-3x text-muted" />
</div>
</t>
<t t-set="user_ribbon" t-value="product.sudo().variant_ribbon_id or product.sudo().website_ribbon_id" />
<t t-if="user_ribbon">
<span t-attf-class="o_ribbon {{ user_ribbon._get_position_class() }} z-1" t-attf-style="color: {{ user_ribbon.text_color }}; background-color: {{ user_ribbon.bg_color }};" t-esc="user_ribbon.name" />
</t>
<t t-if="product.dynamic_ribbon_id">
<t t-set="ribbon" t-value="product.dynamic_ribbon_id" />
<span t-attf-class="o_ribbon {{ ribbon._get_position_class() }} z-1" t-attf-style="color: {{ ribbon.text_color }}; background-color: {{ ribbon.bg_color }};" t-esc="ribbon.name" />
</t>
<t t-set="user_ribbon" t-value="product.sudo().variant_ribbon_id or product.sudo().website_ribbon_id" />
<t t-if="user_ribbon">
<span t-attf-class="o_ribbon {{ user_ribbon._get_position_class() }} z-1" t-attf-style="color: {{ user_ribbon.text_color }}; background-color: {{ user_ribbon.bg_color }};" t-esc="user_ribbon.name" />
</t>
<div class="card-body d-flex flex-column">
<h6 class="card-title" t-esc="product.name" />
<t t-if="product.product_tag_ids">