241 lines
11 KiB
Python
241 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
|
|
from odoo.exceptions import UserError
|
|
|
|
|
|
class SaleOrder(models.Model):
|
|
_inherit = "sale.order"
|
|
|
|
@api.model
|
|
def _default_warehouse_id(self):
|
|
company = self.env.user.company_id.id
|
|
warehouse_ids = self.env['stock.warehouse'].search([('company_id', '=', company)], limit=1)
|
|
return warehouse_ids
|
|
|
|
incoterm = fields.Many2one(
|
|
'stock.incoterms', 'Incoterms',
|
|
help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
|
|
picking_policy = fields.Selection([
|
|
('direct', 'Deliver each product when available'),
|
|
('one', 'Deliver all products at once')],
|
|
string='Shipping Policy', required=True, readonly=True, default='direct',
|
|
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
|
|
warehouse_id = fields.Many2one(
|
|
'stock.warehouse', string='Warehouse',
|
|
required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
|
default=_default_warehouse_id)
|
|
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this sale')
|
|
delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids')
|
|
|
|
@api.multi
|
|
@api.depends('procurement_group_id')
|
|
def _compute_picking_ids(self):
|
|
for order in self:
|
|
order.picking_ids = self.env['stock.picking'].search([('group_id', '=', order.procurement_group_id.id)]) if order.procurement_group_id else []
|
|
order.delivery_count = len(order.picking_ids)
|
|
|
|
@api.onchange('warehouse_id')
|
|
def _onchange_warehouse_id(self):
|
|
if self.warehouse_id.company_id:
|
|
self.company_id = self.warehouse_id.company_id.id
|
|
|
|
@api.multi
|
|
def action_view_delivery(self):
|
|
'''
|
|
This function returns an action that display existing delivery orders
|
|
of given sales order ids. It can either be a in a list or in a form
|
|
view, if there is only one delivery order to show.
|
|
'''
|
|
action = self.env.ref('stock.action_picking_tree_all').read()[0]
|
|
|
|
pickings = self.mapped('picking_ids')
|
|
if len(pickings) > 1:
|
|
action['domain'] = [('id', 'in', pickings.ids)]
|
|
elif pickings:
|
|
action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')]
|
|
action['res_id'] = pickings.id
|
|
return action
|
|
|
|
@api.multi
|
|
def action_cancel(self):
|
|
self.mapped('order_line').mapped('procurement_ids').cancel()
|
|
return super(SaleOrder, self).action_cancel()
|
|
|
|
@api.multi
|
|
def _prepare_invoice(self):
|
|
invoice_vals = super(SaleOrder, self)._prepare_invoice()
|
|
invoice_vals['incoterms_id'] = self.incoterm.id or False
|
|
return invoice_vals
|
|
|
|
def _prepare_procurement_group(self):
|
|
res = super(SaleOrder, self)._prepare_procurement_group()
|
|
res.update({'move_type': self.picking_policy, 'partner_id': self.partner_shipping_id.id})
|
|
return res
|
|
|
|
@api.model
|
|
def _get_customer_lead(self, product_tmpl_id):
|
|
super(SaleOrder, self)._get_customer_lead(product_tmpl_id)
|
|
return product_tmpl_id.sale_delay
|
|
|
|
|
|
class SaleOrderLine(models.Model):
|
|
_inherit = 'sale.order.line'
|
|
|
|
product_packaging = fields.Many2one('product.packaging', string='Packaging', default=False)
|
|
route_id = fields.Many2one('stock.location.route', string='Route', domain=[('sale_selectable', '=', True)])
|
|
product_tmpl_id = fields.Many2one('product.template', related='product_id.product_tmpl_id', string='Product Template', readonly=True)
|
|
|
|
@api.depends('order_id.state')
|
|
def _compute_invoice_status(self):
|
|
super(SaleOrderLine, self)._compute_invoice_status()
|
|
for line in self:
|
|
# We handle the following specific situation: a physical product is partially delivered,
|
|
# but we would like to set its invoice status to 'Fully Invoiced'. The use case is for
|
|
# products sold by weight, where the delivered quantity rarely matches exactly the
|
|
# quantity ordered.
|
|
if line.order_id.state == 'done'\
|
|
and line.invoice_status == 'no'\
|
|
and line.product_id.type in ['consu', 'product']\
|
|
and line.product_id.invoice_policy == 'delivery'\
|
|
and line.procurement_ids.mapped('move_ids')\
|
|
and all(move.state in ['done', 'cancel'] for move in line.procurement_ids.mapped('move_ids')):
|
|
line.invoice_status = 'invoiced'
|
|
|
|
@api.multi
|
|
@api.depends('product_id')
|
|
def _compute_qty_delivered_updateable(self):
|
|
# prefetch field before filtering
|
|
self.mapped('product_id')
|
|
# on consumable or stockable products, qty_delivered_updateable defaults
|
|
# to False; on other lines use the original computation
|
|
lines = self.filtered(lambda line: line.product_id.type not in ('consu', 'product'))
|
|
lines = lines.with_prefetch(self._prefetch)
|
|
super(SaleOrderLine, lines)._compute_qty_delivered_updateable()
|
|
|
|
@api.onchange('product_id')
|
|
def _onchange_product_id_set_customer_lead(self):
|
|
self.customer_lead = self.product_id.sale_delay
|
|
|
|
@api.onchange('product_packaging')
|
|
def _onchange_product_packaging(self):
|
|
if self.product_packaging:
|
|
return self._check_package()
|
|
|
|
@api.onchange('product_id')
|
|
def _onchange_product_id_uom_check_availability(self):
|
|
if not self.product_uom or (self.product_id.uom_id.category_id.id != self.product_uom.category_id.id):
|
|
self.product_uom = self.product_id.uom_id
|
|
self._onchange_product_id_check_availability()
|
|
|
|
@api.onchange('product_uom_qty', 'product_uom', 'route_id')
|
|
def _onchange_product_id_check_availability(self):
|
|
if not self.product_id or not self.product_uom_qty or not self.product_uom:
|
|
self.product_packaging = False
|
|
return {}
|
|
if self.product_id.type == 'product':
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
product_qty = self.product_uom._compute_quantity(self.product_uom_qty, self.product_id.uom_id)
|
|
if float_compare(self.product_id.virtual_available, product_qty, precision_digits=precision) == -1:
|
|
is_available = self._check_routing()
|
|
if not is_available:
|
|
warning_mess = {
|
|
'title': _('Not enough inventory!'),
|
|
'message' : _('You plan to sell %s %s but you only have %s %s available!\nThe stock on hand is %s %s.') % \
|
|
(self.product_uom_qty, self.product_uom.name, self.product_id.virtual_available, self.product_id.uom_id.name, self.product_id.qty_available, self.product_id.uom_id.name)
|
|
}
|
|
return {'warning': warning_mess}
|
|
return {}
|
|
|
|
@api.onchange('product_uom_qty')
|
|
def _onchange_product_uom_qty(self):
|
|
if self.state == 'sale' and self.product_id.type in ['product', 'consu'] and self.product_uom_qty < self._origin.product_uom_qty:
|
|
warning_mess = {
|
|
'title': _('Ordered quantity decreased!'),
|
|
'message' : _('You are decreasing the ordered quantity! Do not forget to manually update the delivery order if needed.'),
|
|
}
|
|
return {'warning': warning_mess}
|
|
return {}
|
|
|
|
@api.multi
|
|
def _prepare_order_line_procurement(self, group_id=False):
|
|
vals = super(SaleOrderLine, self)._prepare_order_line_procurement(group_id=group_id)
|
|
date_planned = datetime.strptime(self.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT)\
|
|
+ timedelta(days=self.customer_lead or 0.0) - timedelta(days=self.order_id.company_id.security_lead)
|
|
vals.update({
|
|
'date_planned': date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
|
'location_id': self.order_id.partner_shipping_id.property_stock_customer.id,
|
|
'route_ids': self.route_id and [(4, self.route_id.id)] or [],
|
|
'warehouse_id': self.order_id.warehouse_id and self.order_id.warehouse_id.id or False,
|
|
'partner_dest_id': self.order_id.partner_shipping_id.id,
|
|
'sale_line_id': self.id,
|
|
})
|
|
return vals
|
|
|
|
@api.multi
|
|
def _get_delivered_qty(self):
|
|
"""Computes the delivered quantity on sale order lines, based on done stock moves related to its procurements
|
|
"""
|
|
self.ensure_one()
|
|
super(SaleOrderLine, self)._get_delivered_qty()
|
|
qty = 0.0
|
|
for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped):
|
|
if move.location_dest_id.usage == "customer":
|
|
if not move.origin_returned_move_id:
|
|
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom)
|
|
elif move.location_dest_id.usage != "customer" and move.to_refund_so:
|
|
qty -= move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom)
|
|
return qty
|
|
|
|
@api.multi
|
|
def _check_package(self):
|
|
default_uom = self.product_id.uom_id
|
|
pack = self.product_packaging
|
|
qty = self.product_uom_qty
|
|
q = default_uom._compute_quantity(pack.qty, self.product_uom)
|
|
if qty and q and (qty % q):
|
|
newqty = qty - (qty % q) + q
|
|
return {
|
|
'warning': {
|
|
'title': _('Warning'),
|
|
'message': _("This product is packaged by %.2f %s. You should sell %.2f %s.") % (pack.qty, default_uom.name, newqty, self.product_uom.name),
|
|
},
|
|
}
|
|
return {}
|
|
|
|
def _check_routing(self):
|
|
""" Verify the route of the product based on the warehouse
|
|
return True if the product availibility in stock does not need to be verified,
|
|
which is the case in MTO, Cross-Dock or Drop-Shipping
|
|
"""
|
|
is_available = False
|
|
product_routes = self.route_id or (self.product_id.route_ids + self.product_id.categ_id.total_route_ids)
|
|
|
|
# Check MTO
|
|
wh_mto_route = self.order_id.warehouse_id.mto_pull_id.route_id
|
|
if wh_mto_route and wh_mto_route <= product_routes:
|
|
is_available = True
|
|
else:
|
|
mto_route = False
|
|
try:
|
|
mto_route = self.env['stock.warehouse']._get_mto_route()
|
|
except UserError:
|
|
# if route MTO not found in ir_model_data, we treat the product as in MTS
|
|
pass
|
|
if mto_route and mto_route in product_routes:
|
|
is_available = True
|
|
|
|
# Check Drop-Shipping
|
|
if not is_available:
|
|
for pull_rule in product_routes.mapped('pull_ids'):
|
|
if pull_rule.picking_type_id.sudo().default_location_src_id.usage == 'supplier' and\
|
|
pull_rule.picking_type_id.sudo().default_location_dest_id.usage == 'customer':
|
|
is_available = True
|
|
break
|
|
|
|
return is_available
|