openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #05980
[Merge] lp:~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde into lp:stock-logistic-flows
Romain Deheele - Camptocamp has proposed merging lp:~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde into lp:stock-logistic-flows.
Requested reviews:
Leonardo Pistone - camptocamp (lpistone): code review
Stock and Logistic Core Editors (stock-logistic-core-editors)
For more details, see:
https://code.launchpad.net/~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde/+merge/215147
Hi,
It adds a picking dispatch extension, with a picking_oriented use.
Description:
The picking_dispatch addon is stock move-oriented.
This addon changes it for a picking-oriented use.
On "Done" button, a wizard is displayed (same as picking_dispatch), but:
- moves are not passed to "done" state, but splitted between picked quantity and remains.
- unpicked moves are moved in a new backorder.
Then, when the picking dispatch state is done :
- the picking dispatch hides the "Stock Moves" tab, to deliver, the user uses "Related Picking" tab
- the "Transfer Products" wizard ("Deliver" button) displays only moves linked to a done picking dispatch.
The process is the next:
- A picking dispatch is created with several moves from several pickings.
- When the user
I'm not necessarily satisfied about addon's name, I'm ok to change it if we find.
Regards,
Romain
--
https://code.launchpad.net/~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde/+merge/215147
Your team Stock and Logistic Core Editors is requested to review the proposed merge of lp:~camptocamp/stock-logistic-flows/7.0-picking_dispatch_picking_oriented_use-rde into lp:stock-logistic-flows.
=== added directory 'picking_dispatch_picking_oriented'
=== added file 'picking_dispatch_picking_oriented/__init__.py'
--- picking_dispatch_picking_oriented/__init__.py 1970-01-01 00:00:00 +0000
+++ picking_dispatch_picking_oriented/__init__.py 2014-04-16 11:35:27 +0000
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Alexandre Fayolle, Romain Deheele
+# Copyright 2014 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from . import dispatch # noqa
=== added file 'picking_dispatch_picking_oriented/__openerp__.py'
--- picking_dispatch_picking_oriented/__openerp__.py 1970-01-01 00:00:00 +0000
+++ picking_dispatch_picking_oriented/__openerp__.py 2014-04-16 11:35:27 +0000
@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Alexandre Fayolle, Romain Deheele
+# Copyright 2014 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+
+{
+ "name": "Picking Dispatch picking-oriented",
+ "version": "0.1",
+ "depends": ['picking_dispatch','delivery'],
+ "author": "Camptocamp",
+ "license": "AGPL-3",
+ "description": """picking_dispatch addon is stock move-oriented.
+This addon changes it for a picking-oriented use.
+
+On "Done" button, a wizard is displayed (same as picking_dispatch), but:
+
+* moves are not passed to "done" state, but split between picked quantity and remains.
+
+* unpicked moves are moved in a new backorder.
+
+Then, when the picking dispatch state is done:
+
+* the picking dispatch hides the "Stock Moves" tab, the user uses "Related Picking" tab to deliver pickings one after the other.
+
+* the "Transfer Products" wizard ("Deliver" button) displays only moves linked to a done picking dispatch.
+
+* on "Transfer Products" wizard, a carrier field is displayed to give possility to check and change it if it's necessary.
+ """,
+ "website": "http://www.camptocamp.com",
+ "category": "Warehouse Management",
+ "demo": [],
+ "data": ['dispatch_view.xml'],
+ "test": ['test/dispatch_picking_oriented.yml'],
+ "installable": True,
+}
=== added file 'picking_dispatch_picking_oriented/dispatch.py'
--- picking_dispatch_picking_oriented/dispatch.py 1970-01-01 00:00:00 +0000
+++ picking_dispatch_picking_oriented/dispatch.py 2014-04-16 11:35:27 +0000
@@ -0,0 +1,566 @@
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# Author: Alexandre Fayolle, Romain Deheele
+# Copyright 2014 Camptocamp SA
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#############################################################################
+import logging
+import time
+from openerp import netsvc
+from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
+from openerp.tools.float_utils import float_compare
+from openerp.osv import orm, fields
+from openerp.tools.translate import _
+_logger = logging.getLogger(__name__)
+
+
+class stock_partial_picking(orm.TransientModel):
+ _inherit = "stock.partial.picking"
+
+ _columns = {
+ 'carrier_id': fields.many2one('delivery.carrier','Carrier'),
+ }
+
+ def default_get(self, cr, uid, fields, context=None):
+ """override to:
+ - add filter on showed moves
+ - fill carrier_id"""
+ if context is None:
+ context = {}
+ res = super(stock_partial_picking, self).default_get(cr, uid, fields,
+ context=context)
+ picking_ids = context.get('active_ids', [])
+ active_model = context.get('active_model')
+
+ if not picking_ids or len(picking_ids) != 1:
+ """ Partial Picking Processing may only be done
+ for one picking at a time"""
+ return res
+ assert active_model in ('stock.picking', 'stock.picking.in', 'stock.picking.out'), 'Bad context propagation'
+ picking_id, = picking_ids
+ if 'picking_id' in fields:
+ res.update(picking_id=picking_id)
+ if 'move_ids' in fields:
+ picking = self.pool.get('stock.picking').browse(cr, uid, picking_id, context=context)
+ moves = [self._partial_move_for(cr, uid, m)
+ for m in picking.move_lines
+ if (m.state not in ('done', 'cancel')
+ and not (m.dispatch_id and m.dispatch_id.state != 'done'))]
+ res.update(move_ids=moves)
+ if 'date' in fields:
+ res.update(date=time.strftime(DEFAULT_SERVER_DATETIME_FORMAT))
+ if 'carrier_id' in fields:
+ picking = self.pool.get('stock.picking').browse(cr, uid, picking_id, context=context)
+ res.update(carrier_id=picking.carrier_id.id)
+ return res
+
+ def do_partial(self, cr, uid, ids, context=None):
+ """override to just add carrier_id in partial_data"""
+ assert len(ids) == 1, 'Partial picking processing may only be done one at a time.'
+ stock_picking = self.pool.get('stock.picking')
+ stock_move = self.pool.get('stock.move')
+ uom_obj = self.pool.get('product.uom')
+ partial = self.browse(cr, uid, ids[0], context=context)
+ partial_data = {
+ 'delivery_date' : partial.date,
+ 'carrier_id': partial.carrier_id and partial.carrier_id.id or False
+ }
+ picking_type = partial.picking_id.type
+
+ for wizard_line in partial.move_ids:
+ line_uom = wizard_line.product_uom
+ move_id = wizard_line.move_id.id
+
+ #Quantiny must be Positive
+ if wizard_line.quantity < 0:
+ raise osv.except_osv(_('Warning!'), _('Please provide proper Quantity.'))
+
+ #Compute the quantity for respective wizard_line in the line uom (this jsut do the rounding if necessary)
+ qty_in_line_uom = uom_obj._compute_qty(cr, uid, line_uom.id, wizard_line.quantity, line_uom.id)
+
+ if line_uom.factor and line_uom.factor <> 0:
+ if float_compare(qty_in_line_uom, wizard_line.quantity, precision_rounding=line_uom.rounding) != 0:
+ raise osv.except_osv(_('Warning!'), _('The unit of measure rounding does not allow you to ship "%s %s", only rounding of "%s %s" is accepted by the Unit of Measure.') % (wizard_line.quantity, line_uom.name, line_uom.rounding, line_uom.name))
+ if move_id:
+ #Check rounding Quantity.ex.
+ #picking: 1kg, uom kg rounding = 0.01 (rounding to 10g),
+ #partial delivery: 253g
+ #=> result= refused, as the qty left on picking would be 0.747kg and only 0.75 is accepted by the uom.
+ initial_uom = wizard_line.move_id.product_uom
+ #Compute the quantity for respective wizard_line in the initial uom
+ qty_in_initial_uom = uom_obj._compute_qty(cr, uid, line_uom.id, wizard_line.quantity, initial_uom.id)
+ without_rounding_qty = (wizard_line.quantity / line_uom.factor) * initial_uom.factor
+ if float_compare(qty_in_initial_uom, without_rounding_qty, precision_rounding=initial_uom.rounding) != 0:
+ raise osv.except_osv(_('Warning!'), _('The rounding of the initial uom does not allow you to ship "%s %s", as it would let a quantity of "%s %s" to ship and only rounding of "%s %s" is accepted by the uom.') % (wizard_line.quantity, line_uom.name, wizard_line.move_id.product_qty - without_rounding_qty, initial_uom.name, initial_uom.rounding, initial_uom.name))
+ else:
+ seq_obj_name = 'stock.picking.' + picking_type
+ move_id = stock_move.create(cr,uid,{'name' : self.pool.get('ir.sequence').get(cr, uid, seq_obj_name),
+ 'product_id': wizard_line.product_id.id,
+ 'product_qty': wizard_line.quantity,
+ 'product_uom': wizard_line.product_uom.id,
+ 'prodlot_id': wizard_line.prodlot_id.id,
+ 'location_id' : wizard_line.location_id.id,
+ 'location_dest_id' : wizard_line.location_dest_id.id,
+ 'picking_id': partial.picking_id.id
+ },context=context)
+ stock_move.action_confirm(cr, uid, [move_id], context)
+ partial_data['move%s' % (move_id)] = {
+ 'product_id': wizard_line.product_id.id,
+ 'product_qty': wizard_line.quantity,
+ 'product_uom': wizard_line.product_uom.id,
+ 'prodlot_id': wizard_line.prodlot_id.id,
+ }
+ if (picking_type == 'in') and (wizard_line.product_id.cost_method == 'average'):
+ partial_data['move%s' % (wizard_line.move_id.id)].update(product_price=wizard_line.cost,
+ product_currency=wizard_line.currency.id)
+ stock_picking.do_partial(cr, uid, [partial.picking_id.id], partial_data, context=context)
+ return {'type': 'ir.actions.act_window_close'}
+
+
+class stock_partial_move(orm.TransientModel):
+ _inherit = 'stock.partial.move'
+
+ def do_partial(self, cr, uid, ids, context=None):
+ """override to don't close window if the context is 'partial_via_dispatch'"""
+ # no call to super!
+ assert len(ids) == 1, 'Partial move processing may only be done one form at a time.'
+ partial = self.browse(cr, uid, ids[0], context=context)
+ partial_data = {
+ 'delivery_date': partial.date
+ }
+ moves_ids = []
+ for move in partial.move_ids:
+ if not move.move_id:
+ raise orm.except_orm(_('Warning !'), _("You have manually created product lines, please delete them to proceed"))
+ move_id = move.move_id.id
+ partial_data['move%s' % (move_id)] = {
+ 'product_id': move.product_id.id,
+ 'product_qty': move.quantity,
+ 'product_uom': move.product_uom.id,
+ 'prodlot_id': move.prodlot_id.id,
+ }
+ moves_ids.append(move_id)
+ if (move.move_id.picking_id.type == 'in') and (move.product_id.cost_method == 'average'):
+ partial_data['move%s' % (move_id)].update(product_price=move.cost,
+ product_currency=move.currency.id)
+ #in classic context, we close wizard pop-up.
+ #in picking dispatch context, we need to display the new created dispatch
+ res = self.pool.get('stock.move').do_partial(cr, uid, moves_ids, partial_data, context=context)
+ if context and 'partial_via_dispatch' in context:
+ return res
+ return {'type': 'ir.actions.act_window_close'}
+
+
+class PickingDispatch(orm.Model):
+ _inherit = 'picking.dispatch'
+
+ def action_done(self, cr, uid, ids, context=None):
+ """Override to indicate 'partial_via_dispatch' context"""
+ if not ids:
+ return True
+ if context is None:
+ context = {}
+ ctx = context.copy()
+ ctx.update({
+ 'partial_via_dispatch': True,
+ })
+ move_obj = self.pool['stock.move']
+ move_ids = move_obj.search(cr, uid, [('dispatch_id', 'in', ids)], context=ctx)
+ return move_obj.action_partial_move(cr, uid, move_ids, context=ctx)
+
+
+class StockPicking(orm.Model):
+ _inherit = 'stock.picking'
+
+ def do_partial(self, cr, uid, ids, partial_datas, context=None):
+ """ Override to:
+ - pass carrier_id information on done picking
+ - in case of shortage :
+ - copies are the done moves, we need to pass them the current dispatch id
+ - undone moves are the original moves:
+ - we need to pass them the id of dispatch backorder if it exists,
+ - we need to pass False if there is no dispatch backorder
+ """
+ if context is None:
+ context = {}
+ else:
+ context = dict(context)
+ res = {}
+ move_obj = self.pool.get('stock.move')
+ product_obj = self.pool.get('product.product')
+ currency_obj = self.pool.get('res.currency')
+ uom_obj = self.pool.get('product.uom')
+ pricetype_obj = self.pool.get('product.price.type')
+ sequence_obj = self.pool.get('ir.sequence')
+ wf_service = netsvc.LocalService("workflow")
+ for pick in self.browse(cr, uid, ids, context=context):
+ new_picking = None
+ complete, too_many, too_few = [], [], []
+ move_product_qty, prodlot_ids, product_avail, partial_qty, product_uoms = {}, {}, {}, {}, {}
+ for move in pick.move_lines:
+ if move.state in ('done', 'cancel'):
+ continue
+ partial_data = partial_datas.get('move%s'%(move.id), {})
+ product_qty = partial_data.get('product_qty',0.0)
+ move_product_qty[move.id] = product_qty
+ product_uom = partial_data.get('product_uom',False)
+ product_price = partial_data.get('product_price',0.0)
+ product_currency = partial_data.get('product_currency',False)
+ prodlot_id = partial_data.get('prodlot_id')
+ prodlot_ids[move.id] = prodlot_id
+ product_uoms[move.id] = product_uom
+ partial_qty[move.id] = uom_obj._compute_qty(cr, uid, product_uoms[move.id], product_qty, move.product_uom.id)
+ if move.product_qty == partial_qty[move.id]:
+ complete.append(move)
+ elif move.product_qty > partial_qty[move.id]:
+ too_few.append(move)
+ else:
+ too_many.append(move)
+
+ # Average price computation
+ if (pick.type == 'in') and (move.product_id.cost_method == 'average'):
+ product = product_obj.browse(cr, uid, move.product_id.id)
+ move_currency_id = move.company_id.currency_id.id
+ context['currency_id'] = move_currency_id
+ qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
+ price_type_id = pricetype_obj.search(cr, uid,
+ [('field', '=', 'standard_price')],
+ context=context)[0]
+ price_type = pricetype_obj.browse(cr, uid, price_type_id, context=context)
+ price_type_currency_id = price_type.currency_id.id
+
+ if product.id not in product_avail:
+ # keep track of stock on hand including processed lines not yet marked as done
+ product_avail[product.id] = product.qty_available
+
+ if qty > 0:
+ # New price in company currency
+ new_price = currency_obj.compute(cr, uid, product_currency,
+ move_currency_id, product_price, round=False)
+ new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
+ product.uom_id.id)
+ if product_avail[product.id] <= 0:
+ product_avail[product.id] = 0
+ new_std_price = new_price
+ else:
+ # Get the standard price
+ amount_unit = product.price_get('standard_price', context=context)[product.id]
+ # Here we must convert the new price computed in the currency of the price_type
+ # of the product (e.g. company currency: EUR, price_type: USD)
+ # The current value is still in company currency at this stage
+ new_std_price = ((amount_unit * product_avail[product.id])\
+ + (new_price * qty))/(product_avail[product.id] + qty)
+ # Convert the price in price_type currency
+ new_std_price = currency_obj.compute(
+ cr, uid, move_currency_id,
+ price_type_currency_id, new_std_price)
+ # Write the field according to price type field
+ product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
+
+ # Record the values that were chosen in the wizard, so they can be
+ # used for inventory valuation if real-time valuation is enabled.
+ move_obj.write(cr, uid, [move.id],
+ {'price_unit': product_price,
+ 'price_currency_id': product_currency})
+
+ product_avail[product.id] += qty
+
+
+
+ for move in too_few:
+ product_qty = move_product_qty[move.id]
+ if not new_picking:
+ new_picking_name = pick.name
+ self.write(cr, uid, [pick.id],
+ {'name': sequence_obj.get(cr, uid,
+ 'stock.picking.%s'%(pick.type)),
+ })
+ new_picking = self.copy(cr, uid, pick.id,
+ {
+ 'name': new_picking_name,
+ 'move_lines' : [],
+ 'state':'draft',
+ 'carrier_id': 'carrier_id' in partial_datas and partial_datas['carrier_id'],
+ })
+ if product_qty != 0:
+ defaults = {
+ 'product_qty' : product_qty,
+ 'product_uos_qty': product_qty, #TODO: put correct uos_qty
+ 'picking_id' : new_picking,
+ 'state': 'assigned',
+ 'move_dest_id': move.move_dest_id.id,
+ 'price_unit': move.price_unit,
+ 'product_uom': product_uoms[move.id],
+ 'dispatch_id': move.dispatch_id and move.dispatch_id.id
+ }
+ prodlot_id = prodlot_ids[move.id]
+ if prodlot_id:
+ defaults.update(prodlot_id=prodlot_id)
+ # the copy will be a done move, it has to attached to the current dispatch
+ new_move_id = move_obj.copy(cr, uid, move.id, defaults)
+ move_vals = {
+ 'product_qty': move.product_qty - partial_qty[move.id],
+ 'product_uos_qty': move.product_qty - partial_qty[move.id], #TODO: put correct uos_qty
+ 'prodlot_id': False,
+ 'tracking_id': False,
+ }
+ # 2 cases of shortage context:
+ if move.dispatch_id:
+ dispatch_obj = self.pool['picking.dispatch']
+ new_dispatch_id = dispatch_obj.search(cr, uid, [('backorder_id', '=', move.dispatch_id.id),
+ ('state', '=', 'draft')], context=context)
+ # we have anticipated the shortage, a backorder dispatch exists, we can attach the move on it
+ if new_dispatch_id:
+ move_vals['dispatch_id'] = new_dispatch_id[0]
+ # we have not anticipated (broken products,...), the move will be not attached to a dispatch
+ else:
+ move_vals['dispatch_id'] = False
+ move_obj.write(cr, uid, [move.id], move_vals)
+
+ if new_picking:
+ move_obj.write(cr, uid, [c.id for c in complete], {'picking_id': new_picking})
+ for move in complete:
+ defaults = {'product_uom': product_uoms[move.id], 'product_qty': move_product_qty[move.id]}
+ if prodlot_ids.get(move.id):
+ defaults.update({'prodlot_id': prodlot_ids[move.id]})
+ move_obj.write(cr, uid, [move.id], defaults)
+ for move in too_many:
+ product_qty = move_product_qty[move.id]
+ defaults = {
+ 'product_qty' : product_qty,
+ 'product_uos_qty': product_qty, #TODO: put correct uos_qty
+ 'product_uom': product_uoms[move.id]
+ }
+ prodlot_id = prodlot_ids.get(move.id)
+ if prodlot_ids.get(move.id):
+ defaults.update(prodlot_id=prodlot_id)
+ if new_picking:
+ defaults.update(picking_id=new_picking)
+ move_obj.write(cr, uid, [move.id], defaults)
+
+ # At first we confirm the new picking (if necessary)
+ if new_picking:
+ wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_confirm', cr)
+ # Then we finish the good picking
+ self.write(cr, uid, [pick.id], {'backorder_id': new_picking})
+ self.action_move(cr, uid, [new_picking], context=context)
+ wf_service.trg_validate(uid, 'stock.picking', new_picking, 'button_done', cr)
+ wf_service.trg_write(uid, 'stock.picking', pick.id, cr)
+ delivered_pack_id = pick.id
+ back_order_name = self.browse(cr, uid, delivered_pack_id, context=context).name
+ self.message_post(cr, uid, new_picking, body=_("Back order <em>%s</em> has been <b>created</b>.") % (back_order_name), context=context)
+ else:
+ if 'carrier_id' in partial_datas and partial_datas['carrier_id']:
+ self.write(cr, uid, [pick.id], {'carrier_id': partial_datas['carrier_id']}, context=context)
+ self.action_move(cr, uid, [pick.id], context=context)
+ wf_service.trg_validate(uid, 'stock.picking', pick.id, 'button_done', cr)
+ delivered_pack_id = pick.id
+
+ delivered_pack = self.browse(cr, uid, delivered_pack_id, context=context)
+ res[pick.id] = {'delivered_picking': delivered_pack.id or False}
+
+ return res
+
+
+class StockMove(orm.Model):
+ _inherit = 'stock.move'
+
+ _columns = {
+ 'dispatch_state': fields.related('dispatch_id', 'state',
+ type='char',
+ relation='picking.dispatch',
+ string='Dispatch State',
+ readonly=True),
+ }
+
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ """Override because in shortage context done moves are created from copies of undone moves
+ We need dispatch_id information on done move"""
+ if default is None:
+ default = {}
+ dispatch_id = False
+ if 'dispatch_id' in default and default['dispatch_id']:
+ dispatch_id = default['dispatch_id']
+ default = default.copy()
+ res = super(StockMove, self).\
+ copy_data(cr, uid, id, default=default, context=context)
+ if dispatch_id:
+ res.update({'dispatch_id': dispatch_id})
+ return res
+
+ def do_partial(self, cr, uid, ids, partial_datas, context=None):
+ """ Inherited to allow the use of do_partial_via_dispatch()
+ instead of do_partial(), switch is done with
+ a 'partial_via_dispatch' key in the context.
+
+ @param partial_datas : Dictionary containing details of partial picking
+ like partner_id, address_id, delivery_date,
+ delivery moves with product_id, product_qty, uom
+ @return: Dictionary of values
+ """
+ if context is None:
+ context = {}
+ if context.get('partial_via_dispatch'):
+ return self.do_partial_via_dispatch(
+ cr, uid, ids, partial_datas, context=context)
+ else:
+ return super(StockMove, self).do_partial(
+ cr, uid, ids, partial_datas, context=context)
+
+ def do_partial_via_dispatch(self, cr, uid, ids, partial_datas, context=None):
+ """ Makes picking dispatch done, split moves between ok and other in backorders.
+ @param partial_datas: Dictionary containing details of partial picking
+ like partner_id, delivery_date, delivery
+ moves with product_id, product_qty, uom
+ """
+ product_obj = self.pool.get('product.product')
+ currency_obj = self.pool.get('res.currency')
+ pricetype_obj = self.pool.get('product.price.type')
+ uom_obj = self.pool.get('product.uom')
+ dispatch_obj = self.pool.get('picking.dispatch')
+
+ if context is None:
+ context = {}
+
+ complete, too_many, too_few = [], [], []
+ move_product_qty = {}
+ prodlot_ids = {}
+ for move in self.browse(cr, uid, ids, context=context):
+ if move.state in ('done', 'cancel'):
+ continue
+ partial_data = partial_datas.get('move%s' % (move.id), False)
+ assert partial_data, _('Missing partial picking data for move #%s.') % (move.id)
+ product_qty = partial_data.get('product_qty', 0.0)
+ move_product_qty[move.id] = product_qty
+ product_uom = partial_data.get('product_uom', False)
+ product_price = partial_data.get('product_price', 0.0)
+ product_currency = partial_data.get('product_currency', False)
+ prodlot_ids[move.id] = partial_data.get('prodlot_id')
+ if move.product_qty == product_qty:
+ complete.append(move)
+ elif move.product_qty > product_qty:
+ too_few.append(move)
+ else:
+ too_many.append(move)
+ # Average price computation
+ if (move.picking_id.type == 'in') and (move.product_id.cost_method == 'average'):
+ product = product_obj.browse(cr, uid, move.product_id.id)
+ move_currency_id = move.company_id.currency_id.id
+ context['currency_id'] = move_currency_id
+ qty = uom_obj._compute_qty(cr, uid, product_uom, product_qty, product.uom_id.id)
+ price_type_id = pricetype_obj.search(cr, uid,
+ [('field', '=', 'standard_price')],
+ context=context)[0]
+ price_type = pricetype_obj.browse(cr, uid, price_type_id, context=context)
+ price_type_currency_id = price_type.currency_id.id
+ if qty > 0:
+ new_price = currency_obj.compute(cr, uid, product_currency,
+ move_currency_id, product_price, round=False)
+ new_price = uom_obj._compute_price(cr, uid, product_uom, new_price,
+ product.uom_id.id)
+ if product.qty_available <= 0:
+ new_std_price = new_price
+ else:
+ # Get the standard price
+ amount_unit = product.price_get('standard_price', context=context)[product.id]
+ # Here we must convert the new price computed in the currency of the price_type
+ # of the product (e.g. company currency: EUR, price_type: USD)
+ # The current value is still in company currency at this stage
+ new_std_price = ((amount_unit * product.qty_available)
+ + (new_price * qty))/(product.qty_available + qty)
+ # Convert the price in price_type currency
+ new_std_price = currency_obj.compute(
+ cr, uid, move_currency_id,
+ price_type_currency_id, new_std_price)
+ # Write the field according to price type field
+ product_obj.write(cr, uid, [product.id], {'standard_price': new_std_price})
+
+ # Record the values that were chosen in the wizard, so they can be
+ # used for inventory valuation if real-time valuation is enabled.
+ self.write(cr, uid, [move.id],
+ {'price_unit': product_price,
+ 'price_currency_id': product_currency,
+ })
+
+ for move in too_few:
+ product_qty = move_product_qty[move.id]
+ if product_qty != 0:
+ defaults = {
+ 'product_qty': product_qty,
+ 'product_uos_qty': product_qty,
+ 'picking_id': move.picking_id.id,
+ 'state': 'assigned',
+ 'move_dest_id': move.move_dest_id.id,
+ 'price_unit': move.price_unit,
+ }
+ prodlot_id = prodlot_ids[move.id]
+ if prodlot_id:
+ defaults.update(prodlot_id=prodlot_id)
+ new_move = self.copy(cr, uid, move.id, defaults)
+ complete.append(self.browse(cr, uid, new_move))
+ self.write(cr, uid, [move.id],
+ {'product_qty': move.product_qty - product_qty,
+ 'product_uos_qty': move.product_qty - product_qty,
+ 'prodlot_id': False,
+ 'tracking_id': False,
+ })
+
+ for move in too_many:
+ self.write(cr, uid, [move.id],
+ {'product_qty': move.product_qty,
+ 'product_uos_qty': move.product_qty,
+ })
+ complete.append(move)
+
+ for move in complete:
+ if prodlot_ids.get(move.id):
+ self.write(cr, uid, [move.id], {'prodlot_id': prodlot_ids.get(move.id)})
+
+ # in complete_move_ids, we have:
+ # * moves that were fully processed
+ # * newly created moves belonging
+ # to the same dispatch as the original move
+ # so the difference between the original set of moves
+ # and the complete_moves is the set of unprocessed moves
+ dispatch_id = context['active_id']
+ ids = self.search(cr, uid,
+ [('dispatch_id', '=', context['active_id'])],
+ context=context)
+ complete_move_ids = [x.id for x in complete]
+ unprocessed_move_ids = set(ids) - set(complete_move_ids)
+ if unprocessed_move_ids:
+ new_dispatch_id = dispatch_obj.copy(cr, uid, dispatch_id,
+ {'backorder_id': dispatch_id})
+ self.write(cr, uid, complete_move_ids,
+ {'dispatch_id': dispatch_id})
+ self.write(cr, uid, list(unprocessed_move_ids),
+ {'dispatch_id': new_dispatch_id})
+ dispatch_obj.write(cr, uid, [dispatch_id], {'state': 'done'},
+ context=context)
+ return {
+ 'domain': str([('id', '=', dispatch_id)]),
+ 'view_type': 'form',
+ 'view_mode': 'form',
+ 'res_model': 'picking.dispatch',
+ 'type': 'ir.actions.act_window',
+ 'context': context,
+ 'res_id': dispatch_id,
+ }
+ else:
+ dispatch_obj.write(cr, uid, [dispatch_id], {'state': 'done'},
+ context=context)
+ return {'type': 'ir.actions.act_window_close'}
=== added file 'picking_dispatch_picking_oriented/dispatch_view.xml'
--- picking_dispatch_picking_oriented/dispatch_view.xml 1970-01-01 00:00:00 +0000
+++ picking_dispatch_picking_oriented/dispatch_view.xml 2014-04-16 11:35:27 +0000
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?>
+<openerp>
+ <data>
+
+ <!-- picking dispatch : hides stock moves tab if dispatch is done -->
+ <record id="view_dispatch_picking_oriented_form" model="ir.ui.view">
+ <field name="name">picking.dispatch.form.picking.oriented</field>
+ <field name="model">picking.dispatch</field>
+ <field name="inherit_id" ref="picking_dispatch.picking_dispatch_form"/>
+ <field name="arch" type="xml">
+ <page string="Stock Moves" position="attributes">
+ <attribute name="attrs">{'invisible':[('state','=','done')]}</attribute>
+ </page>
+ </field>
+ </record>
+
+ <!-- stock picking : up dispatch infos -->
+ <record id="view_picking_form_dispatch_picking_oriented" model="ir.ui.view">
+ <field name="name">stock.picking.form.dispatch.int.picking.oriented</field>
+ <field name="model">stock.picking</field>
+ <field name="inherit_id" ref="picking_dispatch.view_picking_form_int"/>
+ <field name="type">form</field>
+ <field name="arch" type="xml">
+ <xpath expr="//form/sheet/group" position="after">
+ <field name="related_dispatch_ids"/>
+ </xpath>
+ <page string="Related Dispatch" position="replace">
+ </page>
+ </field>
+ </record>
+
+ <!-- stock move : add dispatch state on tree view -->
+ <record id="view_move_picking_tree_dispatch_picking_oriented" model="ir.ui.view">
+ <field name="name">stock.move.picking.tree.picking.oriented</field>
+ <field name="model">stock.move</field>
+ <field name="inherit_id" ref="stock.view_move_picking_tree"/>
+ <field name="arch" type="xml">
+ <field name="product_qty" position="after">
+ <field name="dispatch_id"/>
+ <field name="dispatch_state"/>
+ </field>
+ </field>
+ </record>
+
+ <!-- stock partial picking : add carrier_id on stock partial picking wizard -->
+ <record id="add_carrier_id_on_stock_partial_picking_form" model="ir.ui.view">
+ <field name="name">add.carrier.id.on.stock.partial.picking.form</field>
+ <field name="model">stock.partial.picking</field>
+ <field name="inherit_id" ref="stock.stock_partial_picking_form"/>
+ <field name="arch" type="xml">
+ <field name="move_ids" position="before">
+ <group>
+ <field name="carrier_id"/>
+ </group>
+ </field>
+ </field>
+ </record>
+
+ </data>
+</openerp>
=== added directory 'picking_dispatch_picking_oriented/test'
=== added file 'picking_dispatch_picking_oriented/test/dispatch_picking_oriented.yml'
--- picking_dispatch_picking_oriented/test/dispatch_picking_oriented.yml 1970-01-01 00:00:00 +0000
+++ picking_dispatch_picking_oriented/test/dispatch_picking_oriented.yml 2014-04-16 11:35:27 +0000
@@ -0,0 +1,97 @@
+-
+ I create an outgoing picking with 2 moves.
+-
+ !record {model: stock.picking.out, id: ship_out_1}:
+ name: OUT_001
+-
+ !record {model: stock.move, id: move_out_a}:
+ product_id: product.product_product_11
+ product_qty: 4
+ product_uom: product.product_uom_unit
+ location_id: stock.stock_location_components
+ location_dest_id: stock.stock_location_output
+ picking_id: ship_out_1
+-
+ !record {model: stock.move, id: move_out_b}:
+ product_id: product.product_product_10
+ product_qty: 4
+ product_uom: product.product_uom_unit
+ location_id: stock.stock_location_components
+ location_dest_id: stock.stock_location_output
+ picking_id: ship_out_1
+-
+ I confirm the outgoing picking and force assign it.
+-
+ !workflow {model: stock.picking, action: button_confirm, ref: ship_out_1}
+-
+ !python {model: stock.picking}: |
+ self.force_assign(cr, uid, [ref("ship_out_1")])
+-
+ I create a dispatch and I link it with the 2 moves.
+-
+ !record {model: picking.dispatch, id: dispatch_1}:
+ name: Dispatch_1
+ picker_id: base.user_demo
+-
+ !python {model: stock.move}: |
+ self.write(cr, uid, [ref("move_out_a"),ref("move_out_b")], {'dispatch_id':ref("dispatch_1")})
+-
+ I assign the dispatch
+-
+ !python {model: picking.dispatch}: |
+ self.action_assign(cr, uid, [ref("dispatch_1")])
+-
+ I confirm the dispatch
+-
+ !python {model: picking.dispatch}: |
+ self.action_progress(cr, uid, [ref("dispatch_1")])
+-
+ I process the dispatch, it displays a wizard where I choose quantities that I pick.
+-
+ !python {model: stock.partial.move}: |
+ context.update({'active_model': 'stock.move', 'active_id': ref('dispatch_1'), 'active_ids': [ref('move_out_a'),ref('move_out_b')], 'partial_via_dispatch': True})
+-
+ !record {model: stock.partial.move, id: partial_move_dispatch}:
+ move_ids:
+ - quantity: 1
+ product_id: product.product_product_11
+ product_uom: product.product_uom_unit
+ move_id: move_out_a
+ location_id: stock.stock_location_components
+ location_dest_id: stock.stock_location_output
+ - quantity: 3
+ product_id: product.product_product_10
+ product_uom: product.product_uom_unit
+ move_id: move_out_b
+ location_id: stock.stock_location_components
+ location_dest_id: stock.stock_location_output
+-
+ !python {model: stock.partial.move }: |
+ self.do_partial(cr, uid, [ref('partial_move_dispatch')], context=context)
+-
+ I deliver outgoing shipment linked to the dispatch, only moves with
+-
+ !python {model: stock.partial.picking}: |
+ context.update({'active_model': 'stock.picking', 'active_id': ref('ship_out_1'), 'active_ids': [ref('ship_out_1')]})
+-
+ !record {model: stock.partial.picking, id: partial_outgoing}:
+ picking_id: ship_out_1
+-
+ !python {model: stock.partial.picking }: |
+ self.do_partial(cr, uid, [ref('partial_outgoing')], context=context)
+-
+ I check outgoing shipment backorder.
+-
+ !python {model: stock.picking}: |
+ shipment = self.browse(cr, uid, ref("ship_out_1"), context=context)
+ for move_line in shipment.move_lines:
+ if move_line.id == ref("move_out_a"):
+ assert move_line.product_qty == 3.0, "Move Quantity from a should be 3"
+ if move_line.id == ref("move_out_b"):
+ assert move_line.product_qty == 1.0, "Move Quantity from b should be 1"
+-
+ I check if the picking dispatch backorder exists
+-
+ !python {model: picking.dispatch}: |
+ backorder = self.search(cr, uid, [('backorder_id','=',ref("dispatch_1"))], context=context)
+ assert backorder, "the backorder exists"
Follow ups