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
 |