feat(logistics): auto-generate packing slip on local delivery dispatch

fusion.plating.delivery already had packing_list_attachment_id + a viewer
action, but nothing populated it — shipping a local delivery produced no
packing slip. Add _fp_generate_packing_slip(): renders
fusion_plating_reports.action_report_fp_packing_slip_portrait and stores
it on packing_list_attachment_id. Hooked into action_start_route (the
dispatch / loaded-on-vehicle moment, so it travels with the goods) and as
a generate-if-missing catch-all on action_mark_delivered. Idempotent
(skips deliveries that already have one unless force=True) and best-effort
(a report glitch logs + continues, never blocks shipping). Report action
resolved at runtime so logistics keeps no hard dep on
fusion_plating_reports. Deployed + verified on entech (12.8KB PDF for
DLV-30097, rolled back).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 14:49:30 -04:00
parent 86e9fdead8
commit e6bbf566ca

View File

@@ -3,11 +3,16 @@
# License OPL-1 (Odoo Proprietary License v1.0) # License OPL-1 (Odoo Proprietary License v1.0)
# Part of the Fusion Plating product family. # Part of the Fusion Plating product family.
import base64
import logging
from markupsafe import Markup from markupsafe import Markup
from odoo import _, api, fields, models from odoo import _, api, fields, models
from odoo.exceptions import UserError from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class FpDelivery(models.Model): class FpDelivery(models.Model):
"""Scheduled delivery of finished parts back to a customer. """Scheduled delivery of finished parts back to a customer.
@@ -480,6 +485,51 @@ class FpDelivery(models.Model):
or rec.vehicle_id.display_name or rec.vehicle_id.display_name
or 'Driver'), or 'Driver'),
) )
# Packing slip travels with the shipment — render + attach it on
# dispatch so the driver/customer get it and it's on the record.
self._fp_generate_packing_slip()
def _fp_generate_packing_slip(self, force=False):
"""Render each delivery's packing slip and store it on
packing_list_attachment_id so it ships with the goods.
Fired on dispatch (action_start_route) and as a catch-all on
action_mark_delivered. Idempotent + best-effort: skips deliveries
that already carry a slip (unless force=True) and never raises —
a report glitch must not block shipping. The report action is
resolved at runtime so this module needs no hard dependency on
fusion_plating_reports.
"""
report_xmlid = (
'fusion_plating_reports.action_report_fp_packing_slip_portrait'
)
report = self.env.ref(report_xmlid, raise_if_not_found=False)
if not report:
return
for rec in self:
if 'packing_list_attachment_id' not in rec._fields:
return
if rec.packing_list_attachment_id and not force:
continue
try:
report_model = self.env['ir.actions.report'].sudo()
pdf_bytes, _fmt = report_model._render_qweb_pdf(
report_xmlid, res_ids=rec.ids)
att = self.env['ir.attachment'].sudo().create({
'name': _('Packing Slip - %s.pdf') % rec.display_name,
'type': 'binary',
'datas': base64.b64encode(pdf_bytes),
'mimetype': 'application/pdf',
'res_model': rec._name,
'res_id': rec.id,
})
rec.packing_list_attachment_id = att.id
rec.message_post(
body=_('Packing slip generated and attached.'))
except Exception as exc:
_logger.warning(
'Packing slip render failed for delivery %s: %s',
rec.display_name, exc)
def action_mark_delivered(self): def action_mark_delivered(self):
"""Block "delivered" until a Proof of Delivery exists. """Block "delivered" until a Proof of Delivery exists.
@@ -511,6 +561,9 @@ class FpDelivery(models.Model):
# Sub 8 — box-parity warning. Non-blocking; just posts to # Sub 8 — box-parity warning. Non-blocking; just posts to
# chatter so the shipping supervisor sees it on the record. # chatter so the shipping supervisor sees it on the record.
rec._fp_check_box_parity() rec._fp_check_box_parity()
# Catch-all: ensure a slip exists even if dispatch was skipped
# (generate-if-missing — won't overwrite the dispatch-time one).
self._fp_generate_packing_slip()
def _fp_check_box_parity(self): def _fp_check_box_parity(self):
"""Compare this delivery's boxes-out count to the boxes-in count """Compare this delivery's boxes-out count to the boxes-in count