It is not possible for a user to add a carrier directly on a picking if
the invoicing policy of that carrier is set to "Real Cost"
To reproduce the issue:
1. Enable a carrier C
2. Setup a shipping method SM:
- Carrier: C
- Invoicing Policy: Real Cost
3. Create and confirm a SO with a product
4. Edit the related picking P:
- Carrier: SM
5. Validate P
Error: a Validation error is raised "The operation cannot be completed
[...] Model: Sales Order Line (sale.order.line), Field: Description
(name)"
When validating the delivery, we try to create a new SOL with the
shipping cost. We then update its description with the carrier name.
However, since the carrier has been directly added on the picking, the
sale order does not have that information. We should rather get this
information from the delivery.
OPW-2862306
closes odoo/odoo#103986
X-original-commit: 611bd008075f5b8f8080281ae877ada3076a9e7a
Signed-off-by: Adrien Widart <awt@odoo.com>
Signed-off-by: Tiffany Chang <tic@odoo.com>
286 lines
14 KiB
Python
286 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
|
|
class StockQuantPackage(models.Model):
|
|
_inherit = "stock.quant.package"
|
|
|
|
@api.depends('quant_ids', 'package_type_id')
|
|
def _compute_weight(self):
|
|
for package in self:
|
|
# set initial weight to package type base weight
|
|
weight = package.package_type_id.base_weight or 0.0
|
|
if self.env.context.get('picking_id'):
|
|
# TODO: potential bottleneck: N packages = N queries, use groupby ?
|
|
current_picking_move_line_ids = self.env['stock.move.line'].search([
|
|
('result_package_id', '=', package.id),
|
|
('picking_id', '=', self.env.context['picking_id'])
|
|
])
|
|
for ml in current_picking_move_line_ids:
|
|
weight += ml.product_uom_id._compute_quantity(
|
|
ml.qty_done, ml.product_id.uom_id) * ml.product_id.weight
|
|
else:
|
|
for quant in package.quant_ids:
|
|
weight += quant.quantity * quant.product_id.weight
|
|
package.weight = weight
|
|
|
|
def _get_default_weight_uom(self):
|
|
return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
|
|
|
|
def _compute_weight_uom_name(self):
|
|
for package in self:
|
|
package.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
|
|
|
|
def _compute_weight_is_kg(self):
|
|
self.weight_is_kg = False
|
|
uom_id = self.env['product.template']._get_weight_uom_id_from_ir_config_parameter()
|
|
if uom_id == self.env.ref('uom.product_uom_kgm'):
|
|
self.weight_is_kg = True
|
|
self.weight_uom_rounding = uom_id.rounding
|
|
|
|
weight = fields.Float(compute='_compute_weight', digits='Stock Weight', help="Total weight of all the products contained in the package.")
|
|
weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom)
|
|
weight_is_kg = fields.Boolean("Technical field indicating whether weight uom is kg or not (i.e. lb)", compute="_compute_weight_is_kg")
|
|
weight_uom_rounding = fields.Float("Technical field indicating weight's number of decimal places", compute="_compute_weight_is_kg")
|
|
shipping_weight = fields.Float(string='Shipping Weight', help="Total weight of the package.")
|
|
|
|
|
|
class StockPicking(models.Model):
|
|
_inherit = 'stock.picking'
|
|
|
|
|
|
@api.depends('move_line_ids', 'move_line_ids.result_package_id')
|
|
def _compute_packages(self):
|
|
for package in self:
|
|
packs = set()
|
|
for move_line in package.move_line_ids:
|
|
if move_line.result_package_id:
|
|
packs.add(move_line.result_package_id.id)
|
|
package.package_ids = list(packs)
|
|
|
|
@api.depends('move_line_ids', 'move_line_ids.result_package_id', 'move_line_ids.product_uom_id', 'move_line_ids.qty_done')
|
|
def _compute_bulk_weight(self):
|
|
for picking in self:
|
|
weight = 0.0
|
|
for move_line in picking.move_line_ids:
|
|
if move_line.product_id and not move_line.result_package_id:
|
|
weight += move_line.product_uom_id._compute_quantity(move_line.qty_done, move_line.product_id.uom_id) * move_line.product_id.weight
|
|
picking.weight_bulk = weight
|
|
|
|
@api.depends('move_line_ids.result_package_id', 'move_line_ids.result_package_id.shipping_weight', 'weight_bulk')
|
|
def _compute_shipping_weight(self):
|
|
for picking in self:
|
|
# if shipping weight is not assigned => default to calculated product weight
|
|
picking.shipping_weight = picking.weight_bulk + sum([pack.shipping_weight or pack.weight for pack in picking.package_ids])
|
|
|
|
def _get_default_weight_uom(self):
|
|
return self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
|
|
|
|
def _compute_weight_uom_name(self):
|
|
for package in self:
|
|
package.weight_uom_name = self.env['product.template']._get_weight_uom_name_from_ir_config_parameter()
|
|
|
|
carrier_price = fields.Float(string="Shipping Cost")
|
|
delivery_type = fields.Selection(related='carrier_id.delivery_type', readonly=True)
|
|
carrier_id = fields.Many2one("delivery.carrier", string="Carrier", check_company=True)
|
|
weight = fields.Float(compute='_cal_weight', digits='Stock Weight', store=True, help="Total weight of the products in the picking.", compute_sudo=True)
|
|
carrier_tracking_ref = fields.Char(string='Tracking Reference', copy=False)
|
|
carrier_tracking_url = fields.Char(string='Tracking URL', compute='_compute_carrier_tracking_url')
|
|
weight_uom_name = fields.Char(string='Weight unit of measure label', compute='_compute_weight_uom_name', readonly=True, default=_get_default_weight_uom)
|
|
package_ids = fields.Many2many('stock.quant.package', compute='_compute_packages', string='Packages')
|
|
weight_bulk = fields.Float('Bulk Weight', compute='_compute_bulk_weight', help="Total weight of products which are not in a package.")
|
|
shipping_weight = fields.Float("Weight for Shipping", compute='_compute_shipping_weight',
|
|
help="Total weight of packages and products not in a package. Packages with no shipping weight specified will default to their products' total weight. This is the weight used to compute the cost of the shipping.")
|
|
is_return_picking = fields.Boolean(compute='_compute_return_picking')
|
|
return_label_ids = fields.One2many('ir.attachment', compute='_compute_return_label')
|
|
destination_country_code = fields.Char(related='partner_id.country_id.code', string="Destination Country")
|
|
|
|
@api.depends('carrier_id', 'carrier_tracking_ref')
|
|
def _compute_carrier_tracking_url(self):
|
|
for picking in self:
|
|
picking.carrier_tracking_url = picking.carrier_id.get_tracking_link(picking) if picking.carrier_id and picking.carrier_tracking_ref else False
|
|
|
|
@api.depends('carrier_id', 'move_ids_without_package')
|
|
def _compute_return_picking(self):
|
|
for picking in self:
|
|
if picking.carrier_id and picking.carrier_id.can_generate_return:
|
|
picking.is_return_picking = any(m.origin_returned_move_id for m in picking.move_ids_without_package)
|
|
else:
|
|
picking.is_return_picking = False
|
|
|
|
def _compute_return_label(self):
|
|
for picking in self:
|
|
if picking.carrier_id:
|
|
picking.return_label_ids = self.env['ir.attachment'].search([('res_model', '=', 'stock.picking'), ('res_id', '=', picking.id), ('name', 'like', '%s%%' % picking.carrier_id.get_return_label_prefix())])
|
|
else:
|
|
picking.return_label_ids = False
|
|
|
|
def get_multiple_carrier_tracking(self):
|
|
self.ensure_one()
|
|
try:
|
|
return json.loads(self.carrier_tracking_url)
|
|
except (ValueError, TypeError):
|
|
return False
|
|
|
|
@api.depends('move_ids')
|
|
def _cal_weight(self):
|
|
for picking in self:
|
|
picking.weight = sum(move.weight for move in picking.move_ids if move.state != 'cancel')
|
|
|
|
def _send_confirmation_email(self):
|
|
for pick in self:
|
|
if pick.carrier_id and pick.carrier_id.integration_level == 'rate_and_ship' and pick.picking_type_code != 'incoming' and not pick.carrier_tracking_ref and pick.picking_type_id.print_label:
|
|
pick.sudo().send_to_shipper()
|
|
pick._check_carrier_details_compliance()
|
|
return super(StockPicking, self)._send_confirmation_email()
|
|
|
|
def _pre_put_in_pack_hook(self, move_line_ids):
|
|
res = super(StockPicking, self)._pre_put_in_pack_hook(move_line_ids)
|
|
if not res:
|
|
if self.carrier_id:
|
|
return self._set_delivery_package_type()
|
|
else:
|
|
return res
|
|
|
|
def _set_delivery_package_type(self):
|
|
""" This method returns an action allowing to set the package type and the shipping weight
|
|
on the stock.quant.package.
|
|
"""
|
|
self.ensure_one()
|
|
view_id = self.env.ref('delivery.choose_delivery_package_view_form').id
|
|
context = dict(
|
|
self.env.context,
|
|
current_package_carrier_type=self.carrier_id.delivery_type,
|
|
default_picking_id=self.id
|
|
)
|
|
# As we pass the `delivery_type` ('fixed' or 'base_on_rule' by default) in a key who
|
|
# correspond to the `package_carrier_type` ('none' to default), we make a conversion.
|
|
# No need conversion for other carriers as the `delivery_type` and
|
|
#`package_carrier_type` will be the same in these cases.
|
|
if context['current_package_carrier_type'] in ['fixed', 'base_on_rule']:
|
|
context['current_package_carrier_type'] = 'none'
|
|
return {
|
|
'name': _('Package Details'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_mode': 'form',
|
|
'res_model': 'choose.delivery.package',
|
|
'view_id': view_id,
|
|
'views': [(view_id, 'form')],
|
|
'target': 'new',
|
|
'context': context,
|
|
}
|
|
|
|
def send_to_shipper(self):
|
|
self.ensure_one()
|
|
res = self.carrier_id.send_shipping(self)[0]
|
|
if self.carrier_id.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= self.carrier_id.amount:
|
|
res['exact_price'] = 0.0
|
|
self.carrier_price = res['exact_price'] * (1.0 + (self.carrier_id.margin / 100.0))
|
|
if res['tracking_number']:
|
|
previous_pickings = self.env['stock.picking']
|
|
previous_moves = self.move_ids.move_orig_ids
|
|
while previous_moves:
|
|
previous_pickings |= previous_moves.picking_id
|
|
previous_moves = previous_moves.move_orig_ids
|
|
without_tracking = previous_pickings.filtered(lambda p: not p.carrier_tracking_ref)
|
|
(self + without_tracking).carrier_tracking_ref = res['tracking_number']
|
|
for p in previous_pickings - without_tracking:
|
|
p.carrier_tracking_ref += "," + res['tracking_number']
|
|
order_currency = self.sale_id.currency_id or self.company_id.currency_id
|
|
msg = _(
|
|
"Shipment sent to carrier %(carrier_name)s for shipping with tracking number %(ref)s<br/>Cost: %(price).2f %(currency)s",
|
|
carrier_name=self.carrier_id.name,
|
|
ref=self.carrier_tracking_ref,
|
|
price=self.carrier_price,
|
|
currency=order_currency.name
|
|
)
|
|
self.message_post(body=msg)
|
|
self._add_delivery_cost_to_so()
|
|
|
|
def _check_carrier_details_compliance(self):
|
|
"""Hook to check if a delivery is compliant in regard of the carrier.
|
|
"""
|
|
return
|
|
|
|
def print_return_label(self):
|
|
self.ensure_one()
|
|
self.carrier_id.get_return_label(self)
|
|
|
|
def _add_delivery_cost_to_so(self):
|
|
self.ensure_one()
|
|
sale_order = self.sale_id
|
|
if sale_order and self.carrier_id.invoice_policy == 'real' and self.carrier_price:
|
|
delivery_lines = sale_order.order_line.filtered(lambda l: l.is_delivery and l.currency_id.is_zero(l.price_unit) and l.product_id == self.carrier_id.product_id)
|
|
carrier_price = self.carrier_price * (1.0 + (float(self.carrier_id.margin) / 100.0))
|
|
if not delivery_lines:
|
|
sale_order._create_delivery_line(self.carrier_id, carrier_price)
|
|
else:
|
|
delivery_line = delivery_lines[0]
|
|
delivery_line[0].write({
|
|
'price_unit': carrier_price,
|
|
# remove the estimated price from the description
|
|
'name': self.carrier_id.with_context(lang=self.partner_id.lang).name,
|
|
})
|
|
|
|
def open_website_url(self):
|
|
self.ensure_one()
|
|
if not self.carrier_tracking_url:
|
|
raise UserError(_("Your delivery method has no redirect on courier provider's website to track this order."))
|
|
|
|
carrier_trackers = []
|
|
try:
|
|
carrier_trackers = json.loads(self.carrier_tracking_url)
|
|
except ValueError:
|
|
carrier_trackers = self.carrier_tracking_url
|
|
else:
|
|
msg = "Tracking links for shipment: <br/>"
|
|
for tracker in carrier_trackers:
|
|
msg += '<a href=' + tracker[1] + '>' + tracker[0] + '</a><br/>'
|
|
self.message_post(body=msg)
|
|
return self.env["ir.actions.actions"]._for_xml_id("delivery.act_delivery_trackers_url")
|
|
|
|
client_action = {
|
|
'type': 'ir.actions.act_url',
|
|
'name': "Shipment Tracking Page",
|
|
'target': 'new',
|
|
'url': self.carrier_tracking_url,
|
|
}
|
|
return client_action
|
|
|
|
def cancel_shipment(self):
|
|
for picking in self:
|
|
picking.carrier_id.cancel_shipment(self)
|
|
msg = "Shipment %s cancelled" % picking.carrier_tracking_ref
|
|
picking.message_post(body=msg)
|
|
picking.carrier_tracking_ref = False
|
|
|
|
def _get_estimated_weight(self):
|
|
self.ensure_one()
|
|
weight = 0.0
|
|
for move in self.move_ids:
|
|
weight += move.product_qty * move.product_id.weight
|
|
return weight
|
|
|
|
def _should_generate_commercial_invoice(self):
|
|
self.ensure_one()
|
|
return self.picking_type_id.warehouse_id.partner_id.country_id != self.partner_id.country_id
|
|
|
|
|
|
class StockReturnPicking(models.TransientModel):
|
|
_inherit = 'stock.return.picking'
|
|
|
|
def _create_returns(self):
|
|
# Prevent copy of the carrier and carrier price when generating return picking
|
|
# (we have no integration of returns for now)
|
|
new_picking, pick_type_id = super(StockReturnPicking, self)._create_returns()
|
|
picking = self.env['stock.picking'].browse(new_picking)
|
|
picking.write({'carrier_id': False,
|
|
'carrier_price': 0.0})
|
|
return new_picking, pick_type_id
|