changes
This commit is contained in:
@@ -13,11 +13,13 @@
|
||||
"security/ir.model.access.csv",
|
||||
"data/ir_sequence_data.xml",
|
||||
"data/fusion_canada_post_data.xml",
|
||||
"data/ir_cron_data.xml",
|
||||
"views/fusion_cp_shipment_views.xml",
|
||||
"views/delivery_carrier_view.xml",
|
||||
"views/choose_delivery_carrier_views.xml",
|
||||
"views/sale_order_views.xml",
|
||||
"views/stock_picking_views.xml",
|
||||
"views/report_templates.xml",
|
||||
"views/menus.xml",
|
||||
"views/res_config_settings_views.xml",
|
||||
],
|
||||
|
||||
14
fusion_canada_post/data/ir_cron_data.xml
Normal file
14
fusion_canada_post/data/ir_cron_data.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="ir_cron_fusion_cp_refresh_tracking" model="ir.cron">
|
||||
<field name="name">Canada Post: Refresh Tracking</field>
|
||||
<field name="model_id" ref="model_fusion_cp_shipment"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._cron_refresh_tracking()</field>
|
||||
<field name="interval_number">8</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="active">True</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -1,3 +1,4 @@
|
||||
import base64
|
||||
import logging
|
||||
from datetime import datetime as dt_mod
|
||||
from lxml import etree
|
||||
@@ -66,6 +67,7 @@ class FusionCpShipment(models.Model):
|
||||
('confirmed', 'Confirmed'),
|
||||
('shipped', 'Shipped'),
|
||||
('delivered', 'Delivered'),
|
||||
('returned', 'Returned'),
|
||||
('cancelled', 'Cancelled'),
|
||||
],
|
||||
string='Status',
|
||||
@@ -95,10 +97,21 @@ class FusionCpShipment(models.Model):
|
||||
string='Commercial Invoice',
|
||||
ondelete='set null',
|
||||
)
|
||||
# Return label
|
||||
return_tracking_number = fields.Char(
|
||||
string='Return Tracking Number',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
)
|
||||
return_label_attachment_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Return Label',
|
||||
ondelete='set null',
|
||||
)
|
||||
# Shipment details
|
||||
shipping_cost = fields.Float(
|
||||
shipping_cost = fields.Monetary(
|
||||
string='Shipping Cost',
|
||||
digits='Product Price',
|
||||
currency_field='currency_id',
|
||||
readonly=True,
|
||||
)
|
||||
service_type = fields.Char(
|
||||
@@ -141,6 +154,11 @@ class FusionCpShipment(models.Model):
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
related='company_id.currency_id',
|
||||
store=True,
|
||||
)
|
||||
# Tracking history
|
||||
tracking_event_ids = fields.One2many(
|
||||
'fusion.cp.tracking.event',
|
||||
@@ -369,15 +387,31 @@ class FusionCpShipment(models.Model):
|
||||
|
||||
delivered_date = detail.get('actual-delivery-date', '')
|
||||
if delivered_date:
|
||||
self.status = 'delivered'
|
||||
if self._has_return_events():
|
||||
self.status = 'returned'
|
||||
else:
|
||||
self.status = 'delivered'
|
||||
try:
|
||||
self.delivery_date = dt_mod.strptime(
|
||||
delivered_date, '%Y-%m-%d')
|
||||
except (ValueError, TypeError):
|
||||
self.delivery_date = fields.Datetime.now()
|
||||
elif self._has_return_events():
|
||||
self.status = 'returned'
|
||||
elif self.status == 'confirmed' and self.tracking_event_ids:
|
||||
self.status = 'shipped'
|
||||
|
||||
def _has_return_events(self):
|
||||
"""Check if any tracking events indicate a return/RTS."""
|
||||
RETURN_TYPES = {'RTS', 'RETURN', 'RTS_LABEL_PROC'}
|
||||
for event in self.tracking_event_ids:
|
||||
if event.event_type in RETURN_TYPES:
|
||||
return True
|
||||
desc = (event.event_description or '').lower()
|
||||
if 'return to sender' in desc or 'item returned' in desc:
|
||||
return True
|
||||
return False
|
||||
|
||||
# ── Void & Reissue ─────────────────────────────────────
|
||||
|
||||
def action_void_shipment(self):
|
||||
@@ -504,3 +538,182 @@ class FusionCpShipment(models.Model):
|
||||
'url': '%s%s' % (base_link, self.tracking_number),
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
# ── Cron ─────────────────────────────────────────────────
|
||||
|
||||
@api.model
|
||||
def _cron_refresh_tracking(self):
|
||||
"""Auto-refresh tracking for all active (non-terminal) shipments."""
|
||||
shipments = self.search([
|
||||
('status', 'in', ('confirmed', 'shipped')),
|
||||
('tracking_number', '!=', False),
|
||||
])
|
||||
_logger.info(
|
||||
"Cron: refreshing tracking for %d shipments", len(shipments))
|
||||
for shipment in shipments:
|
||||
try:
|
||||
shipment.action_refresh_tracking()
|
||||
self.env.cr.commit()
|
||||
except Exception as e:
|
||||
self.env.cr.rollback()
|
||||
_logger.warning(
|
||||
"Cron: tracking refresh failed for %s: %s",
|
||||
shipment.name, str(e))
|
||||
|
||||
# ── Return Labels ────────────────────────────────────────
|
||||
|
||||
def action_view_return_label(self):
|
||||
return self._action_open_attachment(self.return_label_attachment_id)
|
||||
|
||||
def action_create_return_label(self):
|
||||
"""Create a prepaid return label via Canada Post Authorized
|
||||
Returns API. Bill-on-scan: charged only when the customer
|
||||
uses the label at a post office.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.return_tracking_number:
|
||||
raise ValidationError(
|
||||
_("A return label has already been created "
|
||||
"for this shipment."))
|
||||
if not self.carrier_id:
|
||||
raise ValidationError(
|
||||
_("No carrier linked to this shipment."))
|
||||
if not self.picking_id:
|
||||
raise ValidationError(
|
||||
_("No transfer linked to this shipment."))
|
||||
|
||||
carrier = self.carrier_id
|
||||
customer = carrier.customer_number
|
||||
|
||||
warehouse_partner = (
|
||||
self.picking_id.picking_type_id.warehouse_id.partner_id)
|
||||
if not warehouse_partner:
|
||||
raise ValidationError(
|
||||
_("No warehouse address found. Please configure "
|
||||
"a contact on the warehouse."))
|
||||
|
||||
if carrier.prod_environment:
|
||||
base = "https://soa-gw.canadapost.ca"
|
||||
else:
|
||||
base = "https://ct.soa-gw.canadapost.ca"
|
||||
|
||||
url = "%s/rs/%s/%s/authorizedreturn" % (base, customer, customer)
|
||||
|
||||
xml = self._build_return_label_xml(carrier, warehouse_partner)
|
||||
headers = {
|
||||
'Content-Type': 'application/vnd.cpc.authreturn-v2+xml',
|
||||
'Accept': 'application/vnd.cpc.authreturn-v2+xml',
|
||||
'Accept-language': 'en-CA',
|
||||
}
|
||||
|
||||
try:
|
||||
resp = request(
|
||||
method='POST', url=url, data=xml,
|
||||
headers=headers,
|
||||
auth=(carrier.username, carrier.password))
|
||||
_logger.info(
|
||||
"Return Label API %s → %s", url, resp.status_code)
|
||||
|
||||
if resp.status_code not in (200, 201):
|
||||
self._parse_cp_error_response(resp)
|
||||
|
||||
self._process_return_label_response(resp, carrier)
|
||||
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
_("Failed to create return label: %s") % str(e))
|
||||
|
||||
def _build_return_label_xml(self, carrier, return_to_partner):
|
||||
"""Build Authorized Return XML.
|
||||
|
||||
return_to_partner = warehouse address (where the return goes).
|
||||
"""
|
||||
ns = "http://www.canadapost.ca/ws/authreturn-v2"
|
||||
root = etree.Element("authorized-return", xmlns=ns)
|
||||
|
||||
etree.SubElement(root, "service-code").text = "DOM.EP"
|
||||
|
||||
# Receiver — warehouse where the item is returned to
|
||||
receiver = etree.SubElement(root, "receiver")
|
||||
rec_name = etree.SubElement(receiver, "name")
|
||||
rec_name.text = (return_to_partner.name or '')[:44]
|
||||
rec_company = etree.SubElement(receiver, "company")
|
||||
rec_company.text = (
|
||||
return_to_partner.commercial_company_name
|
||||
or return_to_partner.name or '')[:44]
|
||||
rec_addr = etree.SubElement(receiver, "domestic-address")
|
||||
etree.SubElement(rec_addr, "address-line-1").text = (
|
||||
return_to_partner.street or '')[:44]
|
||||
if return_to_partner.street2:
|
||||
etree.SubElement(rec_addr, "address-line-2").text = (
|
||||
return_to_partner.street2)[:44]
|
||||
etree.SubElement(rec_addr, "city").text = (
|
||||
return_to_partner.city or '')[:40]
|
||||
etree.SubElement(rec_addr, "province").text = (
|
||||
return_to_partner.state_id.code or '')
|
||||
etree.SubElement(rec_addr, "postal-code").text = (
|
||||
(return_to_partner.zip or '').replace(' ', ''))
|
||||
|
||||
# Parcel characteristics
|
||||
parcel = etree.SubElement(root, "parcel-characteristics")
|
||||
etree.SubElement(parcel, "weight").text = str(
|
||||
max(self.weight or 0.5, 0.1))
|
||||
|
||||
# Print preferences
|
||||
prefs = etree.SubElement(root, "print-preferences")
|
||||
etree.SubElement(prefs, "output-format").text = (
|
||||
carrier.fusion_cp_output_format or '8.5x11')
|
||||
|
||||
return etree.tostring(
|
||||
root, xml_declaration=True, encoding='UTF-8')
|
||||
|
||||
def _process_return_label_response(self, resp, carrier):
|
||||
"""Parse return label response, download label PDF,
|
||||
store on shipment.
|
||||
"""
|
||||
api_resp = Response(resp)
|
||||
result = api_resp.dict()
|
||||
|
||||
auth_return = result.get('authorized-return', result)
|
||||
tracking_pin = auth_return.get('tracking-pin', '')
|
||||
self.return_tracking_number = tracking_pin
|
||||
|
||||
# Get label artifact URL from links
|
||||
links = auth_return.get('links', {})
|
||||
link_list = links.get('link', [])
|
||||
if isinstance(link_list, dict):
|
||||
link_list = [link_list]
|
||||
|
||||
label_url = ''
|
||||
for link in link_list:
|
||||
if link.get('@rel') == 'labelDetails':
|
||||
label_url = link.get('@href', '')
|
||||
break
|
||||
|
||||
if label_url:
|
||||
label_resp = request(
|
||||
method='GET', url=label_url,
|
||||
headers={
|
||||
'Accept': 'application/pdf',
|
||||
'Accept-language': 'en-CA',
|
||||
},
|
||||
auth=(carrier.username, carrier.password))
|
||||
if label_resp.status_code == 200:
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': 'Return-Label-%s.pdf' % tracking_pin,
|
||||
'type': 'binary',
|
||||
'datas': base64.b64encode(
|
||||
label_resp.content).decode(),
|
||||
'res_model': self._name,
|
||||
'res_id': self.id,
|
||||
'mimetype': 'application/pdf',
|
||||
})
|
||||
self.return_label_attachment_id = attachment
|
||||
|
||||
self.message_post(
|
||||
body=_("Return label created. Tracking: %s") % tracking_pin,
|
||||
attachment_ids=(
|
||||
self.return_label_attachment_id.ids
|
||||
if self.return_label_attachment_id else []))
|
||||
|
||||
@@ -27,17 +27,20 @@ class SaleOrder(models.Model):
|
||||
fusion_cp_package_height = fields.Float(
|
||||
string='Package Height', copy=False)
|
||||
|
||||
fusion_cp_shipment_ids = fields.One2many(
|
||||
'fusion.cp.shipment',
|
||||
'sale_order_id',
|
||||
string='CP Shipments',
|
||||
)
|
||||
fusion_cp_shipment_count = fields.Integer(
|
||||
string='CP Shipments',
|
||||
compute='_compute_fusion_cp_shipment_count',
|
||||
)
|
||||
|
||||
def _compute_fusion_cp_shipment_count(self):
|
||||
Shipment = self.env['fusion.cp.shipment']
|
||||
for order in self:
|
||||
order.fusion_cp_shipment_count = Shipment.search_count(
|
||||
[('sale_order_id', '=', order.id)]
|
||||
)
|
||||
order.fusion_cp_shipment_count = len(
|
||||
order.fusion_cp_shipment_ids)
|
||||
|
||||
def action_view_fusion_cp_shipments(self):
|
||||
self.ensure_one()
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
domain="[('status','=','shipped')]"/>
|
||||
<filter name="filter_delivered" string="Delivered"
|
||||
domain="[('status','=','delivered')]"/>
|
||||
<filter name="filter_returned" string="Returned"
|
||||
domain="[('status','=','returned')]"/>
|
||||
<filter name="filter_cancelled" string="Cancelled"
|
||||
domain="[('status','=','cancelled')]"/>
|
||||
<group>
|
||||
@@ -45,6 +47,7 @@
|
||||
<field name="sale_order_id" optional="show"/>
|
||||
<field name="picking_id" optional="show"/>
|
||||
<field name="service_type"/>
|
||||
<field name="currency_id" column_invisible="1"/>
|
||||
<field name="shipping_cost" sum="Total Cost"/>
|
||||
<field name="weight" optional="hide"/>
|
||||
<field name="package_name" optional="hide"/>
|
||||
@@ -53,6 +56,7 @@
|
||||
decoration-info="status == 'draft'"
|
||||
decoration-success="status in ('confirmed','shipped')"
|
||||
decoration-bf="status == 'delivered'"
|
||||
decoration-warning="status == 'returned'"
|
||||
decoration-danger="status == 'cancelled'"
|
||||
widget="badge"/>
|
||||
</list>
|
||||
@@ -76,11 +80,17 @@
|
||||
class="btn-secondary"
|
||||
icon="fa-external-link"
|
||||
invisible="not tracking_number"/>
|
||||
<button name="action_create_return_label" type="object"
|
||||
string="Create Return Label"
|
||||
class="btn-warning"
|
||||
icon="fa-mail-reply"
|
||||
invisible="status not in ('delivered', 'shipped') or return_tracking_number"
|
||||
confirm="Return labels are a paid service (bill-on-scan). The cost will be charged to your Canada Post account when the customer uses the label. Continue?"/>
|
||||
<button name="action_void_shipment" type="object"
|
||||
string="Void Shipment"
|
||||
class="btn-danger"
|
||||
icon="fa-ban"
|
||||
invisible="status in ('cancelled', 'delivered')"
|
||||
invisible="status in ('cancelled', 'delivered', 'returned')"
|
||||
confirm="Are you sure you want to void this shipment? This cannot be undone."/>
|
||||
<button name="action_reissue_shipment" type="object"
|
||||
string="Reissue Shipment"
|
||||
@@ -132,6 +142,8 @@
|
||||
<field name="weight"/>
|
||||
<field name="package_name"
|
||||
invisible="not package_name"/>
|
||||
<field name="return_tracking_number"
|
||||
invisible="not return_tracking_number"/>
|
||||
</group>
|
||||
<group string="Links">
|
||||
<field name="sale_order_id"/>
|
||||
@@ -155,6 +167,7 @@
|
||||
<field name="full_label_attachment_id" invisible="1"/>
|
||||
<field name="receipt_attachment_id" invisible="1"/>
|
||||
<field name="commercial_invoice_attachment_id" invisible="1"/>
|
||||
<field name="return_label_attachment_id" invisible="1"/>
|
||||
<group string="Labels">
|
||||
<span class="o_form_label" invisible="not label_attachment_id">Printable Label (4x6)</span>
|
||||
<button name="action_view_label" type="object"
|
||||
@@ -168,6 +181,12 @@
|
||||
icon="fa-file-pdf-o"
|
||||
string="Open"
|
||||
invisible="not full_label_attachment_id"/>
|
||||
<span class="o_form_label" invisible="not return_label_attachment_id">Return Label</span>
|
||||
<button name="action_view_return_label" type="object"
|
||||
class="btn btn-link p-0"
|
||||
icon="fa-file-pdf-o"
|
||||
string="Open"
|
||||
invisible="not return_label_attachment_id"/>
|
||||
</group>
|
||||
<group string="Documents">
|
||||
<span class="o_form_label" invisible="not receipt_attachment_id">Receipt</span>
|
||||
@@ -191,7 +210,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="delivery_date"
|
||||
invisible="status != 'delivered'"/>
|
||||
invisible="status not in ('delivered', 'returned')"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="tracking_event_ids" readonly="1" nolabel="1">
|
||||
@@ -248,9 +267,9 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Window Action -->
|
||||
<!-- Window Actions -->
|
||||
<record id="action_fusion_cp_shipment" model="ir.actions.act_window">
|
||||
<field name="name">Shipments</field>
|
||||
<field name="name">All Shipments</field>
|
||||
<field name="res_model">fusion.cp.shipment</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="search_view_id" ref="view_fusion_cp_shipment_search"/>
|
||||
@@ -263,4 +282,36 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_cp_shipment_confirmed" model="ir.actions.act_window">
|
||||
<field name="name">Confirmed</field>
|
||||
<field name="res_model">fusion.cp.shipment</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('status', '=', 'confirmed')]</field>
|
||||
<field name="search_view_id" ref="view_fusion_cp_shipment_search"/>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_cp_shipment_shipped" model="ir.actions.act_window">
|
||||
<field name="name">Shipped</field>
|
||||
<field name="res_model">fusion.cp.shipment</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('status', '=', 'shipped')]</field>
|
||||
<field name="search_view_id" ref="view_fusion_cp_shipment_search"/>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_cp_shipment_delivered" model="ir.actions.act_window">
|
||||
<field name="name">Delivered</field>
|
||||
<field name="res_model">fusion.cp.shipment</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('status', '=', 'delivered')]</field>
|
||||
<field name="search_view_id" ref="view_fusion_cp_shipment_search"/>
|
||||
</record>
|
||||
|
||||
<record id="action_fusion_cp_shipment_returned" model="ir.actions.act_window">
|
||||
<field name="name">Returned</field>
|
||||
<field name="res_model">fusion.cp.shipment</field>
|
||||
<field name="view_mode">list,form,kanban</field>
|
||||
<field name="domain">[('status', '=', 'returned')]</field>
|
||||
<field name="search_view_id" ref="view_fusion_cp_shipment_search"/>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -6,13 +6,43 @@
|
||||
web_icon="fusion_canada_post,static/description/icon.png"
|
||||
sequence="85"/>
|
||||
|
||||
<!-- Shipments -->
|
||||
<!-- Shipments (parent — no action) -->
|
||||
<menuitem id="menu_fusion_cp_shipments"
|
||||
name="Shipments"
|
||||
parent="menu_fusion_cp_root"
|
||||
action="action_fusion_cp_shipment"
|
||||
sequence="10"/>
|
||||
|
||||
<!-- Shipments sub-menus -->
|
||||
<menuitem id="menu_fusion_cp_shipments_all"
|
||||
name="All Shipments"
|
||||
parent="menu_fusion_cp_shipments"
|
||||
action="action_fusion_cp_shipment"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem id="menu_fusion_cp_shipments_confirmed"
|
||||
name="Confirmed"
|
||||
parent="menu_fusion_cp_shipments"
|
||||
action="action_fusion_cp_shipment_confirmed"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem id="menu_fusion_cp_shipments_shipped"
|
||||
name="Shipped"
|
||||
parent="menu_fusion_cp_shipments"
|
||||
action="action_fusion_cp_shipment_shipped"
|
||||
sequence="20"/>
|
||||
|
||||
<menuitem id="menu_fusion_cp_shipments_delivered"
|
||||
name="Delivered"
|
||||
parent="menu_fusion_cp_shipments"
|
||||
action="action_fusion_cp_shipment_delivered"
|
||||
sequence="30"/>
|
||||
|
||||
<menuitem id="menu_fusion_cp_shipments_returned"
|
||||
name="Returned"
|
||||
parent="menu_fusion_cp_shipments"
|
||||
action="action_fusion_cp_shipment_returned"
|
||||
sequence="40"/>
|
||||
|
||||
<!-- Configuration parent -->
|
||||
<menuitem id="menu_fusion_cp_config"
|
||||
name="Configuration"
|
||||
|
||||
81
fusion_canada_post/views/report_templates.xml
Normal file
81
fusion_canada_post/views/report_templates.xml
Normal file
@@ -0,0 +1,81 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════
|
||||
Sale Order PDF — Shipping Information
|
||||
════════════════════════════════════════════════════════ -->
|
||||
<template id="report_saleorder_shipping_info"
|
||||
inherit_id="sale.report_saleorder_document">
|
||||
<xpath expr="//div[@name='so_total_summary']" position="after">
|
||||
<t t-set="cp_shipments" t-value="doc.fusion_cp_shipment_ids.filtered(lambda s: s.status != 'cancelled')"/>
|
||||
<div t-if="cp_shipments" class="mt-4">
|
||||
<h5><strong>Shipping Information</strong></h5>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tracking Number</th>
|
||||
<th>Service</th>
|
||||
<th class="text-end">Weight</th>
|
||||
<th class="text-end">Cost</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="cp_shipments" t-as="shipment">
|
||||
<td><span t-field="shipment.tracking_number"/></td>
|
||||
<td><span t-field="shipment.service_type"/></td>
|
||||
<td class="text-end">
|
||||
<span t-field="shipment.weight"/> kg
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="shipment.shipping_cost"
|
||||
t-options="{'widget': 'monetary', 'display_currency': shipment.currency_id}"/>
|
||||
</td>
|
||||
<td><span t-field="shipment.status"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════
|
||||
Invoice PDF — Shipping Information
|
||||
════════════════════════════════════════════════════════ -->
|
||||
<template id="report_invoice_shipping_info"
|
||||
inherit_id="account.report_invoice_document">
|
||||
<xpath expr="//div[@id='payment_term']" position="before">
|
||||
<t t-set="sale_orders" t-value="o.line_ids.sale_line_ids.order_id"/>
|
||||
<t t-set="cp_shipments" t-value="sale_orders.mapped('fusion_cp_shipment_ids').filtered(lambda s: s.status != 'cancelled')" t-if="sale_orders"/>
|
||||
<div t-if="cp_shipments" class="mt-4">
|
||||
<h5><strong>Shipping Information</strong></h5>
|
||||
<table class="table table-sm table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tracking Number</th>
|
||||
<th>Service</th>
|
||||
<th class="text-end">Weight</th>
|
||||
<th class="text-end">Cost</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="cp_shipments" t-as="shipment">
|
||||
<td><span t-field="shipment.tracking_number"/></td>
|
||||
<td><span t-field="shipment.service_type"/></td>
|
||||
<td class="text-end">
|
||||
<span t-field="shipment.weight"/> kg
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<span t-field="shipment.shipping_cost"
|
||||
t-options="{'widget': 'monetary', 'display_currency': shipment.currency_id}"/>
|
||||
</td>
|
||||
<td><span t-field="shipment.status"/></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user