Files
Odoo-Modules/fusion_canada_post/wizard/choose_delivery_cp_rate.py
gsinghpal f81e0cd918 changes
2026-03-09 23:45:00 -04:00

447 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class ChooseDeliveryCPPackage(models.TransientModel):
"""One package row in the Add Shipping wizard for Canada Post."""
_name = 'choose.delivery.cp.package'
_description = 'Canada Post Package (Wizard)'
_order = 'sequence, id'
wizard_id = fields.Many2one(
'choose.delivery.carrier',
string='Wizard',
ondelete='cascade',
)
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')
# Per-package cost for the selected service (updated on service select)
selected_price = fields.Float(
string='Cost', digits='Product Price', readonly=True)
currency_id = fields.Many2one(
'res.currency',
related='wizard_id.currency_id',
)
@api.onchange('package_type_id')
def _onchange_package_type_id(self):
"""Pre-fill dimensions from selected box type."""
if self.package_type_id:
self.package_length = self.package_type_id.packaging_length
self.package_width = self.package_type_id.width
self.package_height = self.package_type_id.height
class ChooseDeliveryCPRate(models.TransientModel):
_name = 'choose.delivery.cp.rate'
_description = 'Canada Post Rate Option'
wizard_id = fields.Many2one(
'choose.delivery.carrier',
string='Wizard',
ondelete='cascade',
)
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 Date')
is_selected = fields.Boolean(string='Selected', default=False)
currency_id = fields.Many2one(
'res.currency',
related='wizard_id.currency_id',
)
# JSON: [{"pkg_id": <int>, "price": <float>}, ...]
per_package_prices = fields.Text(string='Per-Package Prices')
def action_select(self):
"""Select this rate and deselect others. Update per-package costs."""
self.ensure_one()
# Deselect all, then select this one
self.wizard_id.fusion_cp_rate_ids.write({'is_selected': False})
self.is_selected = True
# Update per-package costs from stored JSON
if self.per_package_prices:
try:
pkg_prices = json.loads(self.per_package_prices)
for pp in pkg_prices:
pkg = self.env['choose.delivery.cp.package'].browse(
pp['pkg_id'])
if pkg.exists():
pkg.selected_price = pp['price']
except (json.JSONDecodeError, KeyError):
pass
# Apply margin from the carrier
carrier = self.wizard_id.carrier_id
price = self.price
if carrier:
price = carrier._apply_margins(price, self.wizard_id.order_id)
# Check free_over
if carrier.free_over:
order = self.wizard_id.order_id
amount = order.currency_id._convert(
order.amount_untaxed,
order.company_id.currency_id,
order.company_id,
fields.Date.today(),
)
if amount >= carrier.amount:
price = 0.0
self.wizard_id.write({
'delivery_price': price,
'display_price': price,
'fusion_cp_selected_service': self.service_code,
'fusion_cp_selected_service_name': self.service_name,
'fusion_cp_selected_expected_delivery': self.expected_delivery,
})
# Re-open the wizard to show updated selection
return {
'name': _('Add a shipping method'),
'type': 'ir.actions.act_window',
'res_model': 'choose.delivery.carrier',
'res_id': self.wizard_id.id,
'view_mode': 'form',
'target': 'new',
}
class ChooseDeliveryCarrier(models.TransientModel):
_inherit = 'choose.delivery.carrier'
fusion_cp_rate_ids = fields.One2many(
'choose.delivery.cp.rate',
'wizard_id',
string='Available Services',
)
fusion_cp_selected_service = fields.Char(
string='Selected CP Service Code',
)
fusion_cp_selected_service_name = fields.Char(
string='Selected CP Service Name',
)
fusion_cp_selected_expected_delivery = fields.Char(
string='Selected CP Expected Delivery',
)
# ── Package list ──
fusion_cp_package_ids = fields.One2many(
'choose.delivery.cp.package',
'wizard_id',
string='Packages',
)
# ── Unit labels ──
fusion_cp_dimension_unit_label = fields.Char(
string='Dimension Unit',
compute='_compute_fusion_cp_dimension_unit_label',
)
fusion_cp_weight_unit_label = fields.Char(
string='Weight Unit',
compute='_compute_fusion_cp_weight_unit_label',
)
@api.depends('carrier_id')
def _compute_fusion_cp_dimension_unit_label(self):
for rec in self:
if (rec.carrier_id
and rec.carrier_id.delivery_type == 'fusion_canada_post'):
rec.fusion_cp_dimension_unit_label = (
rec.carrier_id.fusion_cp_dimension_unit or 'cm')
else:
rec.fusion_cp_dimension_unit_label = ''
@api.depends('carrier_id')
def _compute_fusion_cp_weight_unit_label(self):
for rec in self:
if (rec.carrier_id
and rec.carrier_id.delivery_type == 'fusion_canada_post'):
uom = (rec.order_id.company_id
.weight_unit_of_measurement_id)
rec.fusion_cp_weight_unit_label = uom.name if uom else 'kg'
else:
rec.fusion_cp_weight_unit_label = ''
@api.onchange('carrier_id')
def _onchange_carrier_id_cp_packages(self):
"""When a CP carrier is selected, create one default package."""
if (self.carrier_id
and self.carrier_id.delivery_type == 'fusion_canada_post'
and not self.fusion_cp_package_ids):
vals = {
'sequence': 10,
'weight': self.total_weight or 0.0,
}
# Pre-fill from default package type if set on carrier
if self.carrier_id.product_packaging_id:
pkg = self.carrier_id.product_packaging_id
vals['package_type_id'] = pkg.id
vals['package_length'] = pkg.packaging_length
vals['package_width'] = pkg.width
vals['package_height'] = pkg.height
self.fusion_cp_package_ids = [(5, 0, 0), (0, 0, vals)]
# ── Rate fetching ──
def update_price(self):
"""Override: for Canada Post, fetch all rates for all packages."""
if self.carrier_id.delivery_type == 'fusion_canada_post':
return self._update_cp_rates()
return super().update_price()
def _get_cp_package_info_for_pkg(self, pkg):
"""Build package_info dict for a single package, converted to cm."""
carrier = self.carrier_id
return {
'length': round(carrier._fusion_cp_convert_dimension_to_cm(
pkg.package_length), 1),
'width': round(carrier._fusion_cp_convert_dimension_to_cm(
pkg.package_width), 1),
'height': round(carrier._fusion_cp_convert_dimension_to_cm(
pkg.package_height), 1),
}
def _update_cp_rates(self):
"""Fetch CP service rates for every package and aggregate."""
carrier = self.carrier_id
packages = self.fusion_cp_package_ids
if not packages:
raise UserError(_(
"Please add at least one package with dimensions."))
from_unit = (self.order_id.company_id
.weight_unit_of_measurement_id)
# ── Validate every package ──
for pkg in packages:
if not (pkg.package_length and pkg.package_width
and pkg.package_height):
raise UserError(_(
"Please enter dimensions (L × W × H) "
"for all packages."))
if not pkg.weight:
raise UserError(_(
"Please enter weight for all packages."))
package_info = self._get_cp_package_info_for_pkg(pkg)
weight_kg = pkg.weight
if from_unit:
weight_kg = round(carrier.convert_weight(
from_unit, carrier.weight_uom_id, weight_kg), 2)
carrier._fusion_cp_validate_package(weight_kg, package_info)
# ── Clear old rates ──
self.fusion_cp_rate_ids.unlink()
# ── Fetch rates per package ──
# {service_code: {service_name, packages: [{pkg_id, price, exp}]}}
all_service_rates = {}
for pkg in packages:
package_info = self._get_cp_package_info_for_pkg(pkg)
carrier_ctx = carrier.with_context(
order_weight=pkg.weight, # raw, in company UOM
cp_package_info=package_info,
)
rates = carrier_ctx.fusion_canada_post_rate_shipment_all(
self.order_id)
if isinstance(rates, dict) and rates.get('error_message'):
raise UserError(rates['error_message'])
if not rates:
raise UserError(_(
"No shipping prices available for this order."))
for rate in rates:
code = rate['service_code']
if code not in all_service_rates:
all_service_rates[code] = {
'service_name': rate['service_name'],
'packages': [],
}
all_service_rates[code]['packages'].append({
'pkg_id': pkg.id,
'price': rate['price'],
'expected_delivery': rate.get(
'expected_delivery', ''),
})
# ── Keep only services available for ALL packages ──
num_packages = len(packages)
available = {
code: data
for code, data in all_service_rates.items()
if len(data['packages']) == num_packages
}
if not available:
raise UserError(_(
"No single shipping service covers all packages. "
"Try adjusting package dimensions or weight."))
# ── Find cheapest service ──
cheapest_code = min(
available.keys(),
key=lambda c: sum(
p['price'] for p in available[c]['packages']))
# ── Create combined rate lines ──
vals_list = []
for code, data in available.items():
total_price = sum(p['price'] for p in data['packages'])
expected_dates = [
p['expected_delivery'] for p in data['packages']
if p['expected_delivery']
]
expected = max(expected_dates) if expected_dates else ''
is_sel = (code == cheapest_code)
vals_list.append({
'wizard_id': self.id,
'service_code': code,
'service_name': data['service_name'],
'price': total_price,
'expected_delivery': expected,
'is_selected': is_sel,
'per_package_prices': json.dumps(data['packages']),
})
self.env['choose.delivery.cp.rate'].create(vals_list)
# ── Auto-select cheapest: update packages and wizard ──
cheapest_data = available[cheapest_code]
selected_price = sum(
p['price'] for p in cheapest_data['packages'])
for pkg_data in cheapest_data['packages']:
pkg_rec = packages.filtered(
lambda p, pid=pkg_data['pkg_id']: p.id == pid)
if pkg_rec:
pkg_rec.selected_price = pkg_data['price']
# Apply carrier margins
price = selected_price
if self.carrier_id:
price = self.carrier_id._apply_margins(
price, self.order_id)
if self.carrier_id.free_over:
amount = self.order_id.currency_id._convert(
self.order_id.amount_untaxed,
self.order_id.company_id.currency_id,
self.order_id.company_id,
fields.Date.today(),
)
if amount >= self.carrier_id.amount:
price = 0.0
expected_dates = [
p['expected_delivery'] for p in cheapest_data['packages']
if p['expected_delivery']
]
self.write({
'delivery_price': price,
'display_price': price,
'fusion_cp_selected_service': cheapest_code,
'fusion_cp_selected_service_name': (
cheapest_data['service_name']),
'fusion_cp_selected_expected_delivery': (
max(expected_dates) if expected_dates else ''),
})
return {
'name': _('Add a shipping method'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'choose.delivery.carrier',
'res_id': self.id,
'target': 'new',
}
# ── Confirm ──
def button_confirm(self):
"""Override: store per-package info on the sale order and enhance
the delivery line description."""
if self.carrier_id.delivery_type == 'fusion_canada_post':
order = self.order_id
# Clear previous package records
order.fusion_cp_package_ids.unlink()
selected_code = self.fusion_cp_selected_service or ''
selected_name = self.fusion_cp_selected_service_name or ''
selected_delivery = (
self.fusion_cp_selected_expected_delivery or '')
pkg_vals = []
for idx, pkg in enumerate(
self.fusion_cp_package_ids.sorted('sequence')):
pkg_vals.append((0, 0, {
'sequence': (idx + 1) * 10,
'package_type_id': (
pkg.package_type_id.id
if pkg.package_type_id else False),
'package_length': pkg.package_length,
'package_width': pkg.package_width,
'package_height': pkg.package_height,
'weight': pkg.weight,
'service_code': selected_code,
'service_name': selected_name,
'price': pkg.selected_price,
'expected_delivery': selected_delivery,
}))
write_vals = {
'fusion_cp_package_ids': pkg_vals,
'fusion_cp_service_code': selected_code,
}
# Backward compat: first-package dims in legacy fields
first_pkg = self.fusion_cp_package_ids.sorted('sequence')[:1]
if first_pkg:
write_vals['fusion_cp_package_length'] = (
first_pkg.package_length)
write_vals['fusion_cp_package_width'] = (
first_pkg.package_width)
write_vals['fusion_cp_package_height'] = (
first_pkg.package_height)
order.write(write_vals)
res = super().button_confirm()
# Enhance delivery line description with service details
if (self.carrier_id.delivery_type == 'fusion_canada_post'
and self.fusion_cp_selected_service_name):
delivery_line = self.order_id.order_line.filtered(
'is_delivery')
if delivery_line:
line = delivery_line[-1]
parts = [line.name]
parts.append(
"Service: %s"
% self.fusion_cp_selected_service_name)
num_pkgs = len(self.fusion_cp_package_ids)
if num_pkgs > 1:
parts.append("Packages: %d" % num_pkgs)
if self.fusion_cp_selected_expected_delivery:
parts.append(
"Expected Delivery: %s"
% self.fusion_cp_selected_expected_delivery)
line.name = '\n'.join(parts)
return res