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