Files
Odoo-Modules/fusion_shipping/wizard/choose_delivery_fusion_rate.py
Nexa Admin 431052920e feat: separate fusion field service and LTC into standalone modules, update core modules
- fusion_claims: separated field service logic, updated controllers/views
- fusion_tasks: updated task views and map integration
- fusion_authorizer_portal: added page 11 signing, schedule booking, migrations
- fusion_shipping: new standalone shipping module (Canada Post, FedEx, DHL, Purolator)
- fusion_ltc_management: new standalone LTC management module
2026-03-11 16:19:52 +00:00

447 lines
16 KiB
Python

import json
from odoo import _, api, fields, models
from odoo.exceptions import UserError
class ChooseDeliveryFusionPackage(models.TransientModel):
"""One package row in the Add Shipping wizard."""
_name = 'choose.delivery.fusion.package'
_description = 'Shipping 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 ChooseDeliveryFusionRate(models.TransientModel):
_name = 'choose.delivery.fusion.rate'
_description = 'Shipping 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_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.fusion.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_selected_service': self.service_code,
'fusion_selected_service_name': self.service_name,
'fusion_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_rate_ids = fields.One2many(
'choose.delivery.fusion.rate',
'wizard_id',
string='Available Services',
)
fusion_selected_service = fields.Char(
string='Selected Service Code',
)
fusion_selected_service_name = fields.Char(
string='Selected Service Name',
)
fusion_selected_expected_delivery = fields.Char(
string='Selected Expected Delivery',
)
# -- Package list --
fusion_package_ids = fields.One2many(
'choose.delivery.fusion.package',
'wizard_id',
string='Packages',
)
# -- Unit labels --
fusion_dimension_unit_label = fields.Char(
string='Dimension Unit',
compute='_compute_fusion_dimension_unit_label',
)
fusion_weight_unit_label = fields.Char(
string='Weight Unit',
compute='_compute_fusion_weight_unit_label',
)
@api.depends('carrier_id')
def _compute_fusion_dimension_unit_label(self):
for rec in self:
if (rec.carrier_id
and rec.carrier_id.delivery_type == 'fusion_canada_post'):
rec.fusion_dimension_unit_label = (
rec.carrier_id.fusion_cp_dimension_unit or 'cm')
else:
rec.fusion_dimension_unit_label = ''
@api.depends('carrier_id')
def _compute_fusion_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_weight_unit_label = uom.name if uom else 'kg'
else:
rec.fusion_weight_unit_label = ''
@api.onchange('carrier_id')
def _onchange_carrier_id_fusion_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_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_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_fusion_rates()
return super().update_price()
def _get_fusion_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_fusion_rates(self):
"""Fetch shipping service rates for every package and aggregate."""
carrier = self.carrier_id
packages = self.fusion_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 x W x H) "
"for all packages."))
if not pkg.weight:
raise UserError(_(
"Please enter weight for all packages."))
package_info = self._get_fusion_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_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_fusion_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.fusion.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_selected_service': cheapest_code,
'fusion_selected_service_name': (
cheapest_data['service_name']),
'fusion_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_package_ids.unlink()
selected_code = self.fusion_selected_service or ''
selected_name = self.fusion_selected_service_name or ''
selected_delivery = (
self.fusion_selected_expected_delivery or '')
pkg_vals = []
for idx, pkg in enumerate(
self.fusion_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_package_ids': pkg_vals,
'fusion_cp_service_code': selected_code,
}
# Backward compat: first-package dims in legacy fields
first_pkg = self.fusion_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_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_selected_service_name)
num_pkgs = len(self.fusion_package_ids)
if num_pkgs > 1:
parts.append("Packages: %d" % num_pkgs)
if self.fusion_selected_expected_delivery:
parts.append(
"Expected Delivery: %s"
% self.fusion_selected_expected_delivery)
line.name = '\n'.join(parts)
return res