Files
odoo_source/addons/purchase_stock/models/purchase.py
T

370 lines
18 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools.float_utils import float_compare
from odoo.exceptions import UserError
from odoo.addons.purchase.models.purchase import PurchaseOrder as Purchase
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
@api.model
def _default_picking_type(self):
type_obj = self.env['stock.picking.type']
company_id = self.env.context.get('company_id') or self.env.user.company_id.id
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)])
if not types:
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
return types[:1]
incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', states={'done': [('readonly', True)]}, help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
picking_count = fields.Integer(compute='_compute_picking', string='Picking count', default=0, store=True)
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True)
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=Purchase.READONLY_STATES, required=True, default=_default_picking_type,
help="This will determine operation type of incoming shipment")
default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',
help="Technical field used to display the Drop Ship Address", readonly=True)
group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False)
is_shipped = fields.Boolean(compute="_compute_is_shipped")
@api.depends('order_line.move_ids.returned_move_ids',
'order_line.move_ids.state',
'order_line.move_ids.picking_id')
def _compute_picking(self):
for order in self:
pickings = self.env['stock.picking']
for line in order.order_line:
# We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
# do some recursive search, but that could be prohibitive if not done correctly.
moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
pickings |= moves.mapped('picking_id')
order.picking_ids = pickings
order.picking_count = len(pickings)
@api.depends('picking_ids', 'picking_ids.state')
def _compute_is_shipped(self):
for order in self:
if order.picking_ids and all([x.state in ['done', 'cancel'] for x in order.picking_ids]):
order.is_shipped = True
@api.onchange('picking_type_id')
def _onchange_picking_type_id(self):
if self.picking_type_id.default_location_dest_id.usage != 'customer':
self.dest_address_id = False
# --------------------------------------------------
# CRUD
# --------------------------------------------------
def write(self, vals):
if vals.get('order_line') and self.state == 'purchase':
for order in self:
pre_order_line_qty = {order_line: order_line.product_qty for order_line in order.mapped('order_line')}
res = super(PurchaseOrder, self).write(vals)
if vals.get('order_line') and self.state == 'purchase':
for order in self:
to_log = {}
for order_line in order.order_line:
if pre_order_line_qty.get(order_line, False) and float_compare(pre_order_line_qty[order_line], order_line.product_qty, precision_rounding=order_line.product_uom.rounding) > 0:
to_log[order_line] = (order_line.product_qty, pre_order_line_qty[order_line])
if to_log:
order._log_decrease_ordered_quantity(to_log)
return res
# --------------------------------------------------
# Actions
# --------------------------------------------------
@api.multi
def button_approve(self, force=False):
result = super(PurchaseOrder, self).button_approve(force=force)
self._create_picking()
return result
@api.multi
def button_cancel(self):
for order in self:
for pick in order.picking_ids:
if pick.state == 'done':
raise UserError(_('Unable to cancel purchase order %s as some receptions have already been done.') % (order.name))
# If the product is MTO, change the procure_method of the the closest move to purchase to MTS.
# The purpose is to link the po that the user will manually generate to the existing moves's chain.
if order.state in ('draft', 'sent', 'to approve'):
for order_line in order.order_line:
if order_line.move_dest_ids:
siblings_states = (order_line.move_dest_ids.mapped('move_orig_ids')).mapped('state')
if all(state in ('done', 'cancel') for state in siblings_states):
order_line.move_dest_ids.write({'procure_method': 'make_to_stock'})
order_line.move_dest_ids._recompute_state()
for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'):
pick.action_cancel()
return super(PurchaseOrder, self).button_cancel()
@api.multi
def action_view_picking(self):
""" This function returns an action that display existing picking orders of given purchase order ids. When only one found, show the picking immediately.
"""
action = self.env.ref('stock.action_picking_tree')
result = action.read()[0]
# override the context to get rid of the default filtering on operation type
result['context'] = {}
pick_ids = self.mapped('picking_ids')
# choose the view_mode accordingly
if len(pick_ids) > 1:
result['domain'] = "[('id','in',%s)]" % (pick_ids.ids)
elif len(pick_ids) == 1:
res = self.env.ref('stock.view_picking_form', False)
result['views'] = [(res and res.id or False, 'form')]
result['res_id'] = pick_ids.id
return result
# --------------------------------------------------
# Business methods
# --------------------------------------------------
def _log_decrease_ordered_quantity(self, purchase_order_lines_quantities):
def _keys_in_sorted(move):
""" sort by picking and the responsible for the product the
move.
"""
return (move.picking_id.id, move.product_id.responsible_id.id)
def _keys_in_groupby(move):
""" group by picking and the responsible for the product the
move.
"""
return (move.picking_id, move.product_id.responsible_id)
def _render_note_exception_quantity_po(order_exceptions):
order_line_ids = self.env['purchase.order.line'].browse([order_line.id for order in order_exceptions.values() for order_line in order[0]])
purchase_order_ids = order_line_ids.mapped('order_id')
move_ids = self.env['stock.move'].concat(*rendering_context.keys())
impacted_pickings = move_ids.mapped('picking_id')._get_impacted_pickings(move_ids) - move_ids.mapped('picking_id')
values = {
'purchase_order_ids': purchase_order_ids,
'order_exceptions': order_exceptions.values(),
'impacted_pickings': impacted_pickings,
}
return self.env.ref('purchase_stock.exception_on_po').render(values=values)
documents = self.env['stock.picking']._log_activity_get_documents(purchase_order_lines_quantities, 'move_ids', 'DOWN', _keys_in_sorted, _keys_in_groupby)
filtered_documents = {}
for (parent, responsible), rendering_context in documents.items():
if parent._name == 'stock.picking':
if parent.state == 'cancel':
continue
filtered_documents[(parent, responsible)] = rendering_context
self.env['stock.picking']._log_activity(_render_note_exception_quantity_po, filtered_documents)
@api.multi
def _get_destination_location(self):
self.ensure_one()
if self.dest_address_id:
return self.dest_address_id.property_stock_customer.id
return self.picking_type_id.default_location_dest_id.id
@api.model
def _prepare_picking(self):
if not self.group_id:
self.group_id = self.group_id.create({
'name': self.name,
'partner_id': self.partner_id.id
})
if not self.partner_id.property_stock_supplier.id:
raise UserError(_("You must set a Vendor Location for this partner %s") % self.partner_id.name)
return {
'picking_type_id': self.picking_type_id.id,
'partner_id': self.partner_id.id,
'date': self.date_order,
'origin': self.name,
'location_dest_id': self._get_destination_location(),
'location_id': self.partner_id.property_stock_supplier.id,
'company_id': self.company_id.id,
}
@api.multi
def _create_picking(self):
StockPicking = self.env['stock.picking']
for order in self:
if any([ptype in ['product', 'consu'] for ptype in order.order_line.mapped('product_id.type')]):
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
if not pickings:
res = order._prepare_picking()
picking = StockPicking.create(res)
else:
picking = pickings[0]
moves = order.order_line._create_stock_moves(picking)
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm()
seq = 0
for move in sorted(moves, key=lambda move: move.date_expected):
seq += 5
move.sequence = seq
moves._action_assign()
picking.message_post_with_view('mail.message_origin_link',
values={'self': picking, 'origin': order},
subtype_id=self.env.ref('mail.mt_note').id)
return True
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False)
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint')
move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves')
@api.model
def create(self, values):
line = super(PurchaseOrderLine, self).create(values)
if line.order_id.state == 'purchase':
line._create_or_update_picking()
return line
@api.multi
def write(self, values):
result = super(PurchaseOrderLine, self).write(values)
# Update expected date of corresponding moves
if 'date_planned' in values:
self.env['stock.move'].search([
('purchase_line_id', 'in', self.ids), ('state', '!=', 'done')
]).write({'date_expected': values['date_planned']})
if 'product_qty' in values:
self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
return result
# --------------------------------------------------
# Business methods
# --------------------------------------------------
@api.multi
def _create_or_update_picking(self):
for line in self:
if line.product_id.type in ('product', 'consu'):
# Prevent decreasing below received quantity
if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0:
raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n'
'Create a return first.'))
if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1:
# If the quantity is now below the invoiced quantity, create an activity on the vendor bill
# inviting the user to create a refund.
activity = self.env['mail.activity'].sudo().create({
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'note': _('The quantities on your purchase order indicate less than billed. You should ask for a refund. '),
'res_id': line.invoice_lines[0].invoice_id.id,
'res_model_id': self.env.ref('account.model_account_invoice').id,
})
activity._onchange_activity_type_id()
# If the user increased quantity of existing line or created a new line
pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit'))
picking = pickings and pickings[0] or False
if not picking:
res = line.order_id._prepare_picking()
picking = self.env['stock.picking'].create(res)
move_vals = line._prepare_stock_moves(picking)
for move_val in move_vals:
self.env['stock.move']\
.create(move_val)\
._action_confirm()\
._action_assign()
@api.multi
def _get_stock_move_price_unit(self):
self.ensure_one()
line = self[0]
order = line.order_id
price_unit = line.price_unit
if line.taxes_id:
price_unit = line.taxes_id.with_context(round=False).compute_all(
price_unit, currency=line.order_id.currency_id, quantity=1.0, product=line.product_id, partner=line.order_id.partner_id
)['total_excluded']
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
price_unit = order.currency_id._convert(
price_unit, order.company_id.currency_id, self.company_id, self.date_order or fields.Date.today(), round=False)
return price_unit
@api.multi
def _prepare_stock_moves(self, picking):
""" Prepare the stock moves data for one order line. This function returns a list of
dictionary ready to be used in stock.move's create()
"""
self.ensure_one()
res = []
if self.product_id.type not in ['product', 'consu']:
return res
qty = 0.0
price_unit = self._get_stock_move_price_unit()
for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
template = {
'name': self.name or '',
'product_id': self.product_id.id,
'product_uom': self.product_uom.id,
'date': self.order_id.date_order,
'date_expected': self.date_planned,
'location_id': self.order_id.partner_id.property_stock_supplier.id,
'location_dest_id': self.order_id._get_destination_location(),
'picking_id': picking.id,
'partner_id': self.order_id.dest_address_id.id,
'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids],
'state': 'draft',
'purchase_line_id': self.id,
'company_id': self.order_id.company_id.id,
'price_unit': price_unit,
'picking_type_id': self.order_id.picking_type_id.id,
'group_id': self.order_id.group_id.id,
'origin': self.order_id.name,
'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [],
'warehouse_id': self.order_id.picking_type_id.warehouse_id.id,
}
diff_quantity = self.product_qty - qty
if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0:
quant_uom = self.product_id.uom_id
get_param = self.env['ir.config_parameter'].sudo().get_param
if self.product_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
product_qty = self.product_uom._compute_quantity(diff_quantity, quant_uom, rounding_method='HALF-UP')
template['product_uom'] = quant_uom.id
template['product_uom_qty'] = product_qty
else:
template['product_uom_qty'] = diff_quantity
res.append(template)
return res
@api.multi
def _create_stock_moves(self, picking):
moves = self.env['stock.move']
done = self.env['stock.move'].browse()
for line in self:
for val in line._prepare_stock_moves(picking):
done += moves.create(val)
return done
def _update_received_qty(self):
for line in self:
total = 0.0
for move in line.move_ids:
if move.state == 'done':
if move.location_dest_id.usage == "supplier":
if move.to_refund:
total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
else:
total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
line.qty_received = total
def _merge_in_existing_line(self, product_id, product_qty, product_uom, location_id, name, origin, values):
""" This function purpose is to be override with the purpose to forbide _run_buy method
to merge a new po line in an existing one.
"""
return True