cron better exception management. show supplier commercial name in product card template. linters config
This commit is contained in:
parent
a812b4385d
commit
87dcb4e350
17 changed files with 104 additions and 100 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
confirmed_sale_orders |= sale_order
|
||||
except Exception:
|
||||
# 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 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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue