diff --git a/addons/purchase/models/purchase.py b/addons/purchase/models/purchase.py
index 1c5ec88759f..8b8cc4c250e 100644
--- a/addons/purchase/models/purchase.py
+++ b/addons/purchase/models/purchase.py
@@ -552,40 +552,21 @@ class PurchaseOrderLine(models.Model):
def create(self, values):
line = super(PurchaseOrderLine, self).create(values)
if line.order_id.state == 'purchase':
- line.order_id._create_picking()
+ line._create_or_update_picking(values)
msg = _("Extra line with %s ") % (line.product_id.display_name,)
line.order_id.message_post(body=msg)
return line
@api.multi
def write(self, values):
- orders = False
if 'product_qty' in values:
- changed_lines = self.filtered(lambda x: x.order_id.state == 'purchase')
- if changed_lines:
- orders = changed_lines.mapped('order_id')
- for order in orders:
- order_lines = changed_lines.filtered(lambda x: x.order_id == order)
- msg = ""
- if any([values['product_qty'] < x.product_qty for x in order_lines]):
- msg += "" + _('The ordered quantity has been decreased. Do not forget to take it into account on your bills and receipts.') + '
'
- msg += "
"
- for line in order_lines:
- msg += "- %s:" % (line.product_id.display_name,)
- msg += "
" + _("Ordered Quantity") + ": %s -> %s
" % (line.product_qty, float(values['product_qty']),)
- if line.product_id.type in ('product', 'consu'):
- msg += _("Received Quantity") + ": %s
" % (line.qty_received,)
- msg += _("Billed Quantity") + ": %s
" % (line.qty_invoiced,)
- msg += "
"
- order.message_post(body=msg)
+ self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking(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']})
- result = super(PurchaseOrderLine, self).write(values)
- if orders:
- orders._create_picking()
return result
name = fields.Text(string='Description', required=True)
@@ -619,6 +600,59 @@ class PurchaseOrderLine(models.Model):
date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True)
procurement_ids = fields.One2many('procurement.order', 'purchase_line_id', string='Associated Procurements', copy=False)
+ @api.multi
+ def _create_or_update_picking(self, vals):
+ for line in self:
+ if line.product_id.type in ('product', 'consu'):
+ if float_compare(vals['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()
+ diff_purchase_uom_qty = vals['product_qty'] - line.product_qty
+ diff_qty = line.product_uom._compute_quantity(diff_purchase_uom_qty, line.product_id.uom_id)
+ if diff_purchase_uom_qty > 0 or not line.move_ids:
+ # 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._get_move_template(picking)
+ if not line.move_ids:
+ move_vals['product_uom_qty'] = vals['product_qty']
+ else:
+ move_vals['product_uom_qty'] = diff_purchase_uom_qty
+ self.env['stock.move']\
+ .create(move_vals)\
+ .action_confirm()\
+ .action_assign()
+ else:
+ # Prevent decreasing below received quantity
+ if float_compare(vals['product_qty'], line.qty_received, line.product_uom.rounding) >= 0:
+ open_moves = line.move_ids.filtered(lambda m: m.state not in ('done', 'cancel')).sorted(lambda m: m.procurement_id.id)
+ # We sort by procurement id to decrease first the moves without procurement id,
+ # the idea is to get rid of the extra moves in priority
+ for move in open_moves:
+ if float_compare(move.product_qty, -diff_qty, line.product_id.uom_id.rounding) > 0:
+ qty_for_move = line.product_uom._compute_quantity(diff_purchase_uom_qty, move.product_uom)
+ move.product_uom_qty += qty_for_move
+ move.action_assign()
+ break
+ else:
+ diff_qty += move.product_qty
+ move.do_unreserve()
+ move.state = 'draft'
+ move.unlink()
+ else:
+ raise UserError('You cannot decrease the ordered quantity below your receipt.\n'
+ 'Create a return first.')
+
@api.multi
def _get_stock_move_price_unit(self):
self.ensure_one()
@@ -634,18 +668,8 @@ class PurchaseOrderLine(models.Model):
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()
- """
+ def _get_move_template(self, picking):
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_qty
template = {
'name': self.name or '',
'product_id': self.product_id.id,
@@ -660,14 +684,30 @@ class PurchaseOrderLine(models.Model):
'state': 'draft',
'purchase_line_id': self.id,
'company_id': self.order_id.company_id.id,
- 'price_unit': price_unit,
+ 'price_unit': self._get_stock_move_price_unit(),
'picking_type_id': self.order_id.picking_type_id.id,
'group_id': self.order_id.group_id.id,
'procurement_id': False,
'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 [],
+ '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,
}
+ return template
+
+ @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
+ for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
+ qty += move.product_qty
+ template = self._get_move_template(picking)
# Fullfill all related procurements with this po line
diff_quantity = self.product_qty - qty
for procurement in self.procurement_ids.filtered(lambda p: p.state != 'cancel'):
@@ -806,15 +846,6 @@ class PurchaseOrderLine(models.Model):
self.price_unit = price_unit
- @api.onchange('product_qty')
- def _onchange_product_qty(self):
- if (self.state == 'purchase' or self.state == 'to approve') and self.product_id.type in ['product', 'consu'] and self.product_qty < self._origin.product_qty:
- warning_mess = {
- 'title': _('Ordered quantity decreased!'),
- 'message' : _('You are decreasing the ordered quantity!\nYou must update the quantities on the reception and/or bills.'),
- }
- return {'warning': warning_mess}
-
def _suggest_quantity(self):
'''
Suggest a minimal quantity based on the seller
diff --git a/addons/purchase/models/stock.py b/addons/purchase/models/stock.py
index 3bfc0877f74..83db902f1ac 100644
--- a/addons/purchase/models/stock.py
+++ b/addons/purchase/models/stock.py
@@ -11,15 +11,6 @@ class StockPicking(models.Model):
purchase_id = fields.Many2one('purchase.order', related='move_lines.purchase_line_id.order_id',
string="Purchase Orders", readonly=True)
- @api.model
- def _prepare_values_extra_move(self, op, product, remaining_qty):
- res = super(StockPicking, self)._prepare_values_extra_move(op, product, remaining_qty)
- for m in op.linked_move_operation_ids:
- if m.move_id.purchase_line_id and m.move_id.product_id == product:
- res['purchase_line_id'] = m.move_id.purchase_line_id.id
- break
- return res
-
@api.model
def _create_backorder(self, backorder_moves=[]):
res = super(StockPicking, self)._create_backorder(backorder_moves)
@@ -36,7 +27,7 @@ class StockMove(models.Model):
_inherit = 'stock.move'
purchase_line_id = fields.Many2one('purchase.order.line',
- 'Purchase Order Line', ondelete='set null', index=True, readonly=True)
+ 'Purchase Order Line', ondelete='set null', index=True, readonly=True, copy=False)
@api.multi
def get_price_unit(self):
@@ -56,17 +47,15 @@ class StockMove(models.Model):
return self.price_unit
return super(StockMove, self).get_price_unit()
- @api.multi
- def copy(self, default=None):
- self.ensure_one()
- default = default or {}
- # we don't want to propagate the link to the purchase order line on the move copied,
- # except when it's a split or a returned move
-
- if not default.get('split_from') and not default.get('origin_returned_move_id'):
- default['purchase_line_id'] = False
- return super(StockMove, self).copy(default)
+ def _prepare_extra_move_vals(self, qty):
+ vals = super(StockMove, self)._prepare_extra_move_vals(qty)
+ vals['purchase_line_id'] = self.purchase_line_id.id
+ return vals
+ def _prepare_move_split_vals(self, uom_qty):
+ vals = super(StockMove, self)._prepare_move_split_vals(uom_qty)
+ vals['purchase_line_id'] = self.purchase_line_id.id
+ return vals
class StockWarehouse(models.Model):
_inherit = 'stock.warehouse'
@@ -141,3 +130,11 @@ class StockWarehouse(models.Model):
if warehouse.in_type_id.default_location_dest_id != warehouse.buy_pull_id.location_id:
warehouse.buy_pull_id.write({'location_id': warehouse.in_type_id.default_location_dest_id.id})
return res
+
+class ReturnPicking(models.TransientModel):
+ _inherit = "stock.return.picking"
+
+ def _prepare_move_default_values(self, return_line, new_picking):
+ vals = super(ReturnPicking, self)._prepare_move_default_values(return_line, new_picking)
+ vals['purchase_line_id'] = return_line.move_id.purchase_line_id.id
+ return vals
diff --git a/addons/purchase/tests/test_create_picking.py b/addons/purchase/tests/test_create_picking.py
index 383b250907a..0d8de789006 100644
--- a/addons/purchase/tests/test_create_picking.py
+++ b/addons/purchase/tests/test_create_picking.py
@@ -47,7 +47,6 @@ class TestCreatePicking(common.TestProductCommon):
self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase')
self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created')
self.assertEqual(len(self.po.order_line.move_ids), 1, 'One move should be created')
-
# Change purchase order line product quantity
self.po.order_line.write({'product_qty': 7.0})
self.assertEqual(len(self.po.order_line.move_ids), 2, 'Two move should be created')
@@ -55,9 +54,11 @@ class TestCreatePicking(common.TestProductCommon):
# Validate first shipment
self.picking = self.po.picking_ids[0]
self.picking.force_assign()
- self.picking.pack_operation_product_ids.write({'qty_done': 7.0})
- self.picking.do_new_transfer()
+ for ml in self.picking.pack_operation_ids:
+ ml.qty_done = ml.product_uom_qty
+ self.picking.action_done()
self.assertEqual(self.po.order_line.mapped('qty_received'), [7.0], 'Purchase: all products should be received')
+
# create new order line
self.po.write({'order_line': [
@@ -71,7 +72,7 @@ class TestCreatePicking(common.TestProductCommon):
})]})
self.assertEqual(self.po.picking_count, 2, 'New picking should be created')
moves = self.po.order_line.mapped('move_ids').filtered(lambda x: x.state not in ('done', 'cancel'))
- self.assertEqual(len(moves), 1, 'One move should be created')
+ self.assertEqual(len(moves), 1, 'One moves should have been created')
def test_01_check_double_validation(self):
diff --git a/addons/purchase/tests/test_purchase_order.py b/addons/purchase/tests/test_purchase_order.py
index 42c67a62b63..6d63545b138 100644
--- a/addons/purchase/tests/test_purchase_order.py
+++ b/addons/purchase/tests/test_purchase_order.py
@@ -75,8 +75,8 @@ class TestPurchaseOrder(AccountingTestCase):
self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created"')
self.picking = self.po.picking_ids[0]
self.picking.force_assign()
- self.picking.pack_operation_product_ids.write({'qty_done': 5.0})
- self.picking.do_new_transfer()
+ self.picking.pack_operation_ids.write({'qty_done': 5.0})
+ self.picking.button_validate()
self.assertEqual(self.po.order_line.mapped('qty_received'), [5.0, 5.0], 'Purchase: all products should be received"')
self.invoice = self.AccountInvoice.create({
@@ -109,8 +109,8 @@ class TestPurchaseOrder(AccountingTestCase):
self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created"')
self.picking = self.po.picking_ids[0]
self.picking.force_assign()
- self.picking.pack_operation_product_ids.write({'qty_done': 5.0})
- self.picking.do_new_transfer()
+ self.picking.pack_operation_ids.write({'qty_done': 5.0})
+ self.picking.button_validate()
self.assertEqual(self.po.order_line.mapped('qty_received'), [5.0, 5.0], 'Purchase: all products should be received"')
#After Receiving all products create vendor bill.
@@ -139,8 +139,9 @@ class TestPurchaseOrder(AccountingTestCase):
# Validate picking
return_pick.force_assign()
- return_pick.pack_operation_product_ids.write({'qty_done': 2})
- return_pick.do_new_transfer()
+ return_pick.pack_operation_ids.write({'qty_done': 2})
+
+ return_pick.button_validate()
# Check Received quantity
self.assertEqual(self.po.order_line[0].qty_received, 3.0, 'Purchase: delivered quantity should be 3.0 instead of "%s" after picking return' % self.po.order_line[0].qty_received)
diff --git a/addons/stock/models/stock_move.py b/addons/stock/models/stock_move.py
index 27d44b3d81a..28b527cd672 100644
--- a/addons/stock/models/stock_move.py
+++ b/addons/stock/models/stock_move.py
@@ -811,6 +811,13 @@ class StockMove(models.Model):
self.mapped('procurement_id').check()
return True
+ def _prepare_extra_move_vals(self, qty):
+ vals = {
+ 'product_uom_qty': qty,
+ 'picking_id': self.picking_id.id
+ }
+ return vals
+
@api.multi
def _create_extra_move(self):
""" If the quantity done on a move exceeds its quantity todo, this method will create an
@@ -830,9 +837,7 @@ class StockMove(models.Model):
self.quantity_done - self.product_uom_qty,
precision_rounding=self.product_uom.rounding,
rounding_method ='UP')
- extra_move_vals = {
- 'product_uom_qty': extra_move_quantity,
- 'picking_id': self.picking_id.id}
+ extra_move_vals = self._prepare_extra_move_vals(extra_move_quantity)
extra_move = self.copy(default=extra_move_vals).action_confirm()
# link it to some move lines
@@ -928,6 +933,17 @@ class StockMove(models.Model):
raise UserError(_('You can only delete draft moves.'))
return super(StockMove, self).unlink()
+ def _prepare_move_split_vals(self, uom_qty):
+ vals = {
+ 'product_uom_qty': uom_qty,
+ 'procure_method': 'make_to_stock',
+ 'procurement_id': self.procurement_id.id,
+ 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if x.state not in ('done', 'cancel')],
+ 'move_orig_ids': [(4, x.id) for x in self.move_orig_ids],
+ 'origin_returned_move_id': self.origin_returned_move_id.id,
+ }
+ return vals
+
@api.multi
def split(self, qty, restrict_lot_id=False, restrict_partner_id=False):
""" Splits qty from move move into a new move
@@ -948,15 +964,7 @@ class StockMove(models.Model):
return self.id
# HALF-UP rounding as only rounding errors will be because of propagation of error from default UoM
uom_qty = self.product_id.uom_id._compute_quantity(qty, self.product_uom, rounding_method='HALF-UP')
- defaults = {
- 'product_uom_qty': uom_qty,
- 'procure_method': 'make_to_stock',
- 'restrict_lot_id': restrict_lot_id,
- 'procurement_id': self.procurement_id.id,
- 'move_dest_ids': [(4, x.id) for x in self.move_dest_ids if x.state not in ('done', 'cancel')],
- 'move_orig_ids': [(4, x.id) for x in self.move_orig_ids],
- 'origin_returned_move_id': self.origin_returned_move_id.id,
- }
+ defaults = self._prepare_move_split_vals(uom_qty)
if restrict_partner_id:
defaults['restrict_partner_id'] = restrict_partner_id
diff --git a/addons/stock/wizard/stock_picking_return.py b/addons/stock/wizard/stock_picking_return.py
index 19d9343f615..adb520f68f8 100644
--- a/addons/stock/wizard/stock_picking_return.py
+++ b/addons/stock/wizard/stock_picking_return.py
@@ -68,6 +68,21 @@ class ReturnPicking(models.TransientModel):
res['location_id'] = location_id
return res
+ def _prepare_move_default_values(self, return_line, new_picking):
+ vals = {
+ 'product_id': return_line.product_id.id,
+ 'product_uom_qty': return_line.quantity,
+ 'picking_id': new_picking.id,
+ 'state': 'draft',
+ 'location_id': return_line.move_id.location_dest_id.id,
+ 'location_dest_id': self.location_id.id or return_line.move_id.location_id.id,
+ 'picking_type_id': new_picking.picking_type_id.id,
+ 'warehouse_id': self.picking_id.picking_type_id.warehouse_id.id,
+ 'origin_returned_move_id': return_line.move_id.id,
+ 'procure_method': 'make_to_stock',
+ }
+ return vals
+
@api.multi
def _create_returns(self):
# TODO sle: the unreserve of the next moves could be less brutal
@@ -90,22 +105,11 @@ class ReturnPicking(models.TransientModel):
for return_line in self.product_return_moves:
if not return_line.move_id:
raise UserError(_("You have manually created product lines, please delete them to proceed"))
- new_qty = return_line.quantity
# TODO sle: float_is_zero?
- if new_qty:
+ if return_line.quantity:
returned_lines += 1
- r = return_line.move_id.copy({
- 'product_id': return_line.product_id.id,
- 'product_uom_qty': new_qty,
- 'picking_id': new_picking.id,
- 'state': 'draft',
- 'location_id': return_line.move_id.location_dest_id.id,
- 'location_dest_id': self.location_id.id or return_line.move_id.location_id.id,
- 'picking_type_id': picking_type_id,
- 'warehouse_id': self.picking_id.picking_type_id.warehouse_id.id,
- 'origin_returned_move_id': return_line.move_id.id,
- 'procure_method': 'make_to_stock',
- })
+ vals = self._prepare_move_default_values(return_line, new_picking)
+ r = return_line.move_id.copy(vals)
r.write({'move_orig_ids': [(4, return_line.move_id.id, False)]})
if not returned_lines: