447 lines
17 KiB
Python
447 lines
17 KiB
Python
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
|