changes
This commit is contained in:
8
fusion_canada_post/models/__init__.py
Normal file
8
fusion_canada_post/models/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from . import delivery_carrier
|
||||
from . import product_packaging
|
||||
from . import res_company
|
||||
from . import fusion_cp_shipment
|
||||
from . import fusion_cp_tracking_event
|
||||
from . import fusion_cp_order_package
|
||||
from . import sale_order
|
||||
from . import stock_picking
|
||||
1185
fusion_canada_post/models/delivery_carrier.py
Normal file
1185
fusion_canada_post/models/delivery_carrier.py
Normal file
File diff suppressed because it is too large
Load Diff
41
fusion_canada_post/models/fusion_cp_order_package.py
Normal file
41
fusion_canada_post/models/fusion_cp_order_package.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class FusionCPOrderPackage(models.Model):
|
||||
"""Stores per-package dimensions and service info on a sale order.
|
||||
|
||||
Created when the user confirms the "Add Shipping" wizard after
|
||||
defining one or more packages with dimensions.
|
||||
Used by ``send_shipping`` to apply the correct dimensions
|
||||
to each physical package on the delivery order.
|
||||
"""
|
||||
|
||||
_name = 'fusion.cp.order.package'
|
||||
_description = 'Canada Post Order Package'
|
||||
_order = 'sequence, id'
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
ondelete='cascade',
|
||||
required=True,
|
||||
index=True,
|
||||
)
|
||||
sequence = fields.Integer(default=10)
|
||||
package_type_id = fields.Many2one(
|
||||
'stock.package.type',
|
||||
string='Box Type',
|
||||
domain="[('package_carrier_type', '=', 'fusion_canada_post')]",
|
||||
)
|
||||
package_length = fields.Float(string='Length')
|
||||
package_width = fields.Float(string='Width')
|
||||
package_height = fields.Float(string='Height')
|
||||
weight = fields.Float(string='Weight')
|
||||
service_code = fields.Char(string='Service Code')
|
||||
service_name = fields.Char(string='Service')
|
||||
price = fields.Float(string='Shipping Cost', digits='Product Price')
|
||||
expected_delivery = fields.Char(string='Expected Delivery')
|
||||
currency_id = fields.Many2one(
|
||||
'res.currency',
|
||||
related='sale_order_id.currency_id',
|
||||
)
|
||||
506
fusion_canada_post/models/fusion_cp_shipment.py
Normal file
506
fusion_canada_post/models/fusion_cp_shipment.py
Normal file
@@ -0,0 +1,506 @@
|
||||
import logging
|
||||
from datetime import datetime as dt_mod
|
||||
from lxml import etree
|
||||
from requests import request
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.addons.fusion_canada_post.fusion_cp_api.fusion_cp_response import Response
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FusionCpShipment(models.Model):
|
||||
_name = 'fusion.cp.shipment'
|
||||
_description = 'Canada Post Shipment'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
_order = 'shipment_date desc, id desc'
|
||||
_rec_name = 'name'
|
||||
|
||||
name = fields.Char(
|
||||
string='Reference',
|
||||
required=True,
|
||||
readonly=True,
|
||||
default=lambda self: _('New'),
|
||||
copy=False,
|
||||
)
|
||||
tracking_number = fields.Char(
|
||||
string='Tracking Number',
|
||||
index=True,
|
||||
readonly=True,
|
||||
copy=False,
|
||||
tracking=True,
|
||||
)
|
||||
shipment_id = fields.Char(
|
||||
string='CP Shipment ID',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help='Canada Post internal shipment identifier',
|
||||
)
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order',
|
||||
string='Sale Order',
|
||||
index=True,
|
||||
ondelete='set null',
|
||||
tracking=True,
|
||||
)
|
||||
picking_id = fields.Many2one(
|
||||
'stock.picking',
|
||||
string='Transfer',
|
||||
index=True,
|
||||
ondelete='set null',
|
||||
tracking=True,
|
||||
)
|
||||
carrier_id = fields.Many2one(
|
||||
'delivery.carrier',
|
||||
string='Carrier',
|
||||
ondelete='set null',
|
||||
)
|
||||
shipment_date = fields.Datetime(
|
||||
string='Shipment Date',
|
||||
default=fields.Datetime.now,
|
||||
tracking=True,
|
||||
)
|
||||
status = fields.Selection(
|
||||
[
|
||||
('draft', 'Draft'),
|
||||
('confirmed', 'Confirmed'),
|
||||
('shipped', 'Shipped'),
|
||||
('delivered', 'Delivered'),
|
||||
('cancelled', 'Cancelled'),
|
||||
],
|
||||
string='Status',
|
||||
default='confirmed',
|
||||
tracking=True,
|
||||
)
|
||||
# Label attachments (Many2one for storage)
|
||||
label_attachment_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Printable Label (4x6)',
|
||||
ondelete='set null',
|
||||
help='The thermal printer label (4x6 format)',
|
||||
)
|
||||
full_label_attachment_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Full Label (8.5x11)',
|
||||
ondelete='set null',
|
||||
help='Full page label with instructions and receipt',
|
||||
)
|
||||
receipt_attachment_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Receipt',
|
||||
ondelete='set null',
|
||||
)
|
||||
commercial_invoice_attachment_id = fields.Many2one(
|
||||
'ir.attachment',
|
||||
string='Commercial Invoice',
|
||||
ondelete='set null',
|
||||
)
|
||||
# Shipment details
|
||||
shipping_cost = fields.Float(
|
||||
string='Shipping Cost',
|
||||
digits='Product Price',
|
||||
readonly=True,
|
||||
)
|
||||
service_type = fields.Char(
|
||||
string='Service Type',
|
||||
readonly=True,
|
||||
)
|
||||
weight = fields.Float(
|
||||
string='Weight',
|
||||
digits='Stock Weight',
|
||||
readonly=True,
|
||||
)
|
||||
package_name = fields.Char(
|
||||
string='Package',
|
||||
readonly=True,
|
||||
help='Name of the package this shipment covers',
|
||||
)
|
||||
# Address fields (computed)
|
||||
sender_name = fields.Char(
|
||||
string='Sender',
|
||||
compute='_compute_sender_fields',
|
||||
store=True,
|
||||
)
|
||||
sender_address = fields.Char(
|
||||
string='Sender Address',
|
||||
compute='_compute_sender_fields',
|
||||
store=True,
|
||||
)
|
||||
recipient_name = fields.Char(
|
||||
string='Recipient',
|
||||
compute='_compute_recipient_fields',
|
||||
store=True,
|
||||
)
|
||||
recipient_address = fields.Char(
|
||||
string='Recipient Address',
|
||||
compute='_compute_recipient_fields',
|
||||
store=True,
|
||||
)
|
||||
company_id = fields.Many2one(
|
||||
'res.company',
|
||||
string='Company',
|
||||
default=lambda self: self.env.company,
|
||||
)
|
||||
# Tracking history
|
||||
tracking_event_ids = fields.One2many(
|
||||
'fusion.cp.tracking.event',
|
||||
'shipment_id',
|
||||
string='Tracking Events',
|
||||
)
|
||||
tracking_event_count = fields.Integer(
|
||||
string='Tracking Events',
|
||||
compute='_compute_tracking_event_count',
|
||||
)
|
||||
last_tracking_update = fields.Datetime(
|
||||
string='Last Tracking Update',
|
||||
readonly=True,
|
||||
)
|
||||
delivery_date = fields.Datetime(
|
||||
string='Delivery Date',
|
||||
readonly=True,
|
||||
)
|
||||
|
||||
def _compute_tracking_event_count(self):
|
||||
for rec in self:
|
||||
rec.tracking_event_count = len(rec.tracking_event_ids)
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('name', _('New')) == _('New'):
|
||||
vals['name'] = self.env['ir.sequence'].next_by_code(
|
||||
'fusion.cp.shipment'
|
||||
) or _('New')
|
||||
return super().create(vals_list)
|
||||
|
||||
@api.depends('picking_id', 'picking_id.picking_type_id.warehouse_id.partner_id')
|
||||
def _compute_sender_fields(self):
|
||||
for rec in self:
|
||||
partner = (
|
||||
rec.picking_id.picking_type_id.warehouse_id.partner_id
|
||||
if rec.picking_id
|
||||
else False
|
||||
)
|
||||
if partner:
|
||||
rec.sender_name = partner.name or ''
|
||||
parts = filter(None, [
|
||||
partner.street,
|
||||
partner.city,
|
||||
partner.state_id.name if partner.state_id else '',
|
||||
partner.zip,
|
||||
])
|
||||
rec.sender_address = ', '.join(parts)
|
||||
else:
|
||||
rec.sender_name = ''
|
||||
rec.sender_address = ''
|
||||
|
||||
@api.depends('picking_id', 'picking_id.partner_id')
|
||||
def _compute_recipient_fields(self):
|
||||
for rec in self:
|
||||
partner = rec.picking_id.partner_id if rec.picking_id else False
|
||||
if partner:
|
||||
rec.recipient_name = partner.name or ''
|
||||
parts = filter(None, [
|
||||
partner.street,
|
||||
partner.city,
|
||||
partner.state_id.name if partner.state_id else '',
|
||||
partner.zip,
|
||||
])
|
||||
rec.recipient_address = ', '.join(parts)
|
||||
else:
|
||||
rec.recipient_name = ''
|
||||
rec.recipient_address = ''
|
||||
|
||||
def action_open_sale_order(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Sale Order',
|
||||
'res_model': 'sale.order',
|
||||
'res_id': self.sale_order_id.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def action_open_picking(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Transfer',
|
||||
'res_model': 'stock.picking',
|
||||
'res_id': self.picking_id.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
def _action_open_attachment(self, attachment):
|
||||
"""Open an attachment PDF in the browser viewer (new tab)."""
|
||||
self.ensure_one()
|
||||
if not attachment:
|
||||
return False
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/web/content/%s?download=false' % attachment.id,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_view_label(self):
|
||||
return self._action_open_attachment(self.label_attachment_id)
|
||||
|
||||
def action_view_full_label(self):
|
||||
return self._action_open_attachment(self.full_label_attachment_id)
|
||||
|
||||
def action_view_receipt(self):
|
||||
return self._action_open_attachment(self.receipt_attachment_id)
|
||||
|
||||
def action_view_commercial_invoice(self):
|
||||
return self._action_open_attachment(self.commercial_invoice_attachment_id)
|
||||
|
||||
# ── Tracking ──────────────────────────────────────────────
|
||||
|
||||
def action_refresh_tracking(self):
|
||||
"""Fetch latest tracking events from Canada Post VIS API."""
|
||||
self.ensure_one()
|
||||
if not self.tracking_number:
|
||||
raise ValidationError(
|
||||
_("No tracking number available for this shipment."))
|
||||
carrier = self.carrier_id
|
||||
if not carrier:
|
||||
raise ValidationError(
|
||||
_("No carrier linked to this shipment."))
|
||||
|
||||
# VIS tracking uses /vis/ path, not /rs/
|
||||
if carrier.prod_environment:
|
||||
base = "https://soa-gw.canadapost.ca"
|
||||
else:
|
||||
base = "https://ct.soa-gw.canadapost.ca"
|
||||
|
||||
url = "%s/vis/track/pin/%s/detail" % (base, self.tracking_number)
|
||||
headers = {
|
||||
'Accept': 'application/vnd.cpc.track-v2+xml',
|
||||
'Accept-language': 'en-CA',
|
||||
}
|
||||
|
||||
try:
|
||||
resp = request(
|
||||
method='GET', url=url, headers=headers,
|
||||
auth=(carrier.username, carrier.password))
|
||||
_logger.info("Tracking API %s → %s", url, resp.status_code)
|
||||
|
||||
if resp.status_code == 404:
|
||||
# "No Pin History" — normal for new/sandbox shipments
|
||||
self.last_tracking_update = fields.Datetime.now()
|
||||
self.message_post(
|
||||
body=_("No tracking history available yet for %s.") %
|
||||
self.tracking_number)
|
||||
return
|
||||
|
||||
if resp.status_code != 200:
|
||||
raise ValidationError(
|
||||
_("Canada Post tracking error: %s %s\n%s") % (
|
||||
resp.status_code, resp.reason,
|
||||
resp.text[:500] if resp.text else ''))
|
||||
api_resp = Response(resp)
|
||||
result = api_resp.dict()
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
_("Failed to fetch tracking: %s") % str(e))
|
||||
|
||||
self._process_tracking_events(result)
|
||||
|
||||
def _process_tracking_events(self, result):
|
||||
"""Parse CP tracking detail response and store events."""
|
||||
detail = result.get('tracking-detail', result)
|
||||
sig_events = detail.get('significant-events', {})
|
||||
occurrences = sig_events.get('occurrence', [])
|
||||
|
||||
# Single event returns dict, multiple returns list
|
||||
if isinstance(occurrences, dict):
|
||||
occurrences = [occurrences]
|
||||
|
||||
# Replace existing events
|
||||
self.tracking_event_ids.unlink()
|
||||
|
||||
vals_list = []
|
||||
for occ in occurrences:
|
||||
event_date_str = occ.get('event-date', '')
|
||||
event_time_str = occ.get('event-time', '')
|
||||
|
||||
# Build combined datetime for sorting
|
||||
event_datetime = False
|
||||
if event_date_str:
|
||||
try:
|
||||
if event_time_str:
|
||||
event_datetime = dt_mod.strptime(
|
||||
'%s %s' % (event_date_str, event_time_str),
|
||||
'%Y-%m-%d %H:%M:%S')
|
||||
else:
|
||||
event_datetime = dt_mod.strptime(
|
||||
event_date_str, '%Y-%m-%d')
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
vals_list.append({
|
||||
'shipment_id': self.id,
|
||||
'event_date': event_date_str or False,
|
||||
'event_time': event_time_str or '',
|
||||
'event_datetime': event_datetime,
|
||||
'event_description': occ.get('event-description', ''),
|
||||
'event_type': occ.get('event-type', ''),
|
||||
'event_site': occ.get('event-site', ''),
|
||||
'event_province': occ.get('event-province', ''),
|
||||
'signatory_name': occ.get('signatory-name', ''),
|
||||
})
|
||||
|
||||
if vals_list:
|
||||
self.env['fusion.cp.tracking.event'].create(vals_list)
|
||||
|
||||
self.last_tracking_update = fields.Datetime.now()
|
||||
self._update_status_from_tracking(detail)
|
||||
self.message_post(
|
||||
body=_("Tracking refreshed: %d events loaded.") % len(vals_list))
|
||||
|
||||
def _update_status_from_tracking(self, detail):
|
||||
"""Auto-update shipment status based on tracking data."""
|
||||
if self.status == 'cancelled':
|
||||
return
|
||||
|
||||
delivered_date = detail.get('actual-delivery-date', '')
|
||||
if delivered_date:
|
||||
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.status == 'confirmed' and self.tracking_event_ids:
|
||||
self.status = 'shipped'
|
||||
|
||||
# ── Void & Reissue ─────────────────────────────────────
|
||||
|
||||
def action_void_shipment(self):
|
||||
"""Void this shipment via Canada Post API (DELETE endpoint)."""
|
||||
self.ensure_one()
|
||||
if self.status == 'cancelled':
|
||||
raise ValidationError(
|
||||
_("This shipment is already cancelled."))
|
||||
if not self.shipment_id:
|
||||
raise ValidationError(
|
||||
_("No CP shipment ID — cannot void."))
|
||||
|
||||
carrier = self.carrier_id
|
||||
if not carrier:
|
||||
raise ValidationError(
|
||||
_("No carrier linked to this shipment."))
|
||||
|
||||
if carrier.prod_environment:
|
||||
base = "https://soa-gw.canadapost.ca"
|
||||
else:
|
||||
base = "https://ct.soa-gw.canadapost.ca"
|
||||
customer = carrier.customer_number
|
||||
|
||||
if carrier.fusion_cp_type == 'commercial':
|
||||
url = "%s/rs/%s/%s/shipment/%s" % (
|
||||
base, customer, customer, self.shipment_id)
|
||||
accept_header = 'application/vnd.cpc.shipment-v8+xml'
|
||||
else:
|
||||
url = "%s/rs/%s/ncshipment/%s" % (
|
||||
base, customer, self.shipment_id)
|
||||
accept_header = 'application/vnd.cpc.ncshipment-v4+xml'
|
||||
|
||||
try:
|
||||
resp = request(
|
||||
method='DELETE', url=url,
|
||||
headers={
|
||||
'Accept': accept_header,
|
||||
'Accept-language': 'en-CA',
|
||||
},
|
||||
auth=(carrier.username, carrier.password))
|
||||
_logger.info(
|
||||
"Void Shipment %s → %s", url, resp.status_code)
|
||||
|
||||
if resp.status_code == 204:
|
||||
self.status = 'cancelled'
|
||||
self.message_post(
|
||||
body=_("Shipment voided successfully via "
|
||||
"Canada Post API."))
|
||||
else:
|
||||
# Parse CP error XML for a clean message
|
||||
error_msg = self._parse_cp_error_response(resp)
|
||||
except ValidationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
_("Failed to void shipment: %s") % str(e))
|
||||
|
||||
def _parse_cp_error_response(self, resp):
|
||||
"""Parse a Canada Post error XML response and raise a clean
|
||||
ValidationError with the human-readable description.
|
||||
|
||||
If the response cannot be parsed, falls back to the raw text.
|
||||
"""
|
||||
description = ''
|
||||
try:
|
||||
root = etree.fromstring(resp.content)
|
||||
# Strip namespace for easy tag matching
|
||||
ns = {'cp': root.nsmap.get(None, '')}
|
||||
if ns['cp']:
|
||||
msgs = root.findall('.//cp:message', ns)
|
||||
else:
|
||||
msgs = root.findall('.//message')
|
||||
parts = []
|
||||
for msg in msgs:
|
||||
desc_el = msg.find(
|
||||
'cp:description', ns) if ns['cp'] else msg.find(
|
||||
'description')
|
||||
if desc_el is not None and desc_el.text:
|
||||
parts.append(desc_el.text.strip())
|
||||
if parts:
|
||||
description = '\n'.join(parts)
|
||||
except Exception:
|
||||
description = ''
|
||||
|
||||
if not description:
|
||||
description = (resp.text[:500]
|
||||
if resp.text else 'Unknown error')
|
||||
|
||||
raise ValidationError(
|
||||
_("Canada Post: %s") % description)
|
||||
|
||||
def action_reissue_shipment(self):
|
||||
"""Void current shipment and create a new one."""
|
||||
self.ensure_one()
|
||||
if self.status != 'cancelled':
|
||||
self.action_void_shipment()
|
||||
if not self.picking_id:
|
||||
raise ValidationError(
|
||||
_("No transfer linked — cannot reissue."))
|
||||
picking = self.picking_id
|
||||
carrier = picking.carrier_id or self.carrier_id
|
||||
if not carrier:
|
||||
raise ValidationError(
|
||||
_("No carrier found for reissue."))
|
||||
result = carrier.send_shipping(picking)
|
||||
if result:
|
||||
picking.carrier_tracking_ref = result[0].get(
|
||||
'tracking_number', '')
|
||||
self.message_post(
|
||||
body=_("Shipment reissued. See new shipment record."))
|
||||
|
||||
def action_track_on_canada_post(self):
|
||||
"""Open the Canada Post tracking website in a new tab."""
|
||||
self.ensure_one()
|
||||
if not self.tracking_number:
|
||||
raise ValidationError(
|
||||
_("No tracking number available for this shipment."))
|
||||
base_link = (
|
||||
self.carrier_id.tracking_link
|
||||
if self.carrier_id and self.carrier_id.tracking_link
|
||||
else 'https://www.canadapost.ca/trackweb/en#/resultList?searchFor=')
|
||||
return {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '%s%s' % (base_link, self.tracking_number),
|
||||
'target': 'new',
|
||||
}
|
||||
44
fusion_canada_post/models/fusion_cp_tracking_event.py
Normal file
44
fusion_canada_post/models/fusion_cp_tracking_event.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class FusionCpTrackingEvent(models.Model):
|
||||
_name = 'fusion.cp.tracking.event'
|
||||
_description = 'Canada Post Tracking Event'
|
||||
_order = 'event_datetime desc, id desc'
|
||||
_rec_name = 'event_description'
|
||||
|
||||
shipment_id = fields.Many2one(
|
||||
'fusion.cp.shipment',
|
||||
string='Shipment',
|
||||
required=True,
|
||||
ondelete='cascade',
|
||||
index=True,
|
||||
)
|
||||
event_date = fields.Date(
|
||||
string='Event Date',
|
||||
)
|
||||
event_time = fields.Char(
|
||||
string='Event Time',
|
||||
help='Time from Canada Post (HH:MM:SS)',
|
||||
)
|
||||
event_datetime = fields.Datetime(
|
||||
string='Date/Time',
|
||||
help='Combined date and time for sorting',
|
||||
)
|
||||
event_description = fields.Char(
|
||||
string='Description',
|
||||
)
|
||||
event_type = fields.Char(
|
||||
string='Event Type',
|
||||
help='Canada Post event type code',
|
||||
)
|
||||
event_site = fields.Char(
|
||||
string='Location',
|
||||
)
|
||||
event_province = fields.Char(
|
||||
string='Province',
|
||||
)
|
||||
signatory_name = fields.Char(
|
||||
string='Signatory',
|
||||
help='Name of person who signed for delivery',
|
||||
)
|
||||
7
fusion_canada_post/models/product_packaging.py
Normal file
7
fusion_canada_post/models/product_packaging.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PackageType(models.Model):
|
||||
_inherit = "stock.package.type"
|
||||
|
||||
package_carrier_type = fields.Selection([('fusion_canada_post', 'Canada Post')])
|
||||
15
fusion_canada_post/models/res_company.py
Normal file
15
fusion_canada_post/models/res_company.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
def _default_uom_setting(self):
|
||||
weight_uom_id = self.env.ref('uom.product_uom_kgm', raise_if_not_found=False)
|
||||
if not weight_uom_id:
|
||||
uom_categ_id = self.env.ref('uom.product_uom_categ_kgm').id
|
||||
weight_uom_id = self.env['uom.uom'].search([('category_id', '=', uom_categ_id), ('factor', '=', 1)], limit=1)
|
||||
return weight_uom_id
|
||||
|
||||
|
||||
weight_unit_of_measurement_id = fields.Many2one('uom.uom',string="Weight Unit Of Measurement",help="Unit of measure for item weight",default=_default_uom_setting)
|
||||
57
fusion_canada_post/models/sale_order.py
Normal file
57
fusion_canada_post/models/sale_order.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
fusion_cp_service_code = fields.Char(
|
||||
string='CP Service Code',
|
||||
copy=False,
|
||||
help='Canada Post service code selected during shipping method selection',
|
||||
)
|
||||
# Per-package dimensions & service info (replaces single-dim fields).
|
||||
fusion_cp_package_ids = fields.One2many(
|
||||
'fusion.cp.order.package',
|
||||
'sale_order_id',
|
||||
string='CP Packages',
|
||||
copy=False,
|
||||
)
|
||||
|
||||
# Legacy single-package dimension fields (kept for backward
|
||||
# compatibility with existing orders — new orders populate the
|
||||
# One2many above and fill these from the first package).
|
||||
fusion_cp_package_length = fields.Float(
|
||||
string='Package Length', copy=False)
|
||||
fusion_cp_package_width = fields.Float(
|
||||
string='Package Width', copy=False)
|
||||
fusion_cp_package_height = fields.Float(
|
||||
string='Package Height', copy=False)
|
||||
|
||||
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)]
|
||||
)
|
||||
|
||||
def action_view_fusion_cp_shipments(self):
|
||||
self.ensure_one()
|
||||
shipments = self.env['fusion.cp.shipment'].search(
|
||||
[('sale_order_id', '=', self.id)]
|
||||
)
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Canada Post Shipments',
|
||||
'res_model': 'fusion.cp.shipment',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('sale_order_id', '=', self.id)],
|
||||
}
|
||||
if len(shipments) == 1:
|
||||
action['view_mode'] = 'form'
|
||||
action['res_id'] = shipments.id
|
||||
return action
|
||||
34
fusion_canada_post/models/stock_picking.py
Normal file
34
fusion_canada_post/models/stock_picking.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
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 picking in self:
|
||||
picking.fusion_cp_shipment_count = Shipment.search_count(
|
||||
[('picking_id', '=', picking.id)]
|
||||
)
|
||||
|
||||
def action_view_fusion_cp_shipments(self):
|
||||
self.ensure_one()
|
||||
shipments = self.env['fusion.cp.shipment'].search(
|
||||
[('picking_id', '=', self.id)]
|
||||
)
|
||||
action = {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Canada Post Shipments',
|
||||
'res_model': 'fusion.cp.shipment',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('picking_id', '=', self.id)],
|
||||
}
|
||||
if len(shipments) == 1:
|
||||
action['view_mode'] = 'form'
|
||||
action['res_id'] = shipments.id
|
||||
return action
|
||||
Reference in New Issue
Block a user