openerp-community-reviewer team mailing list archive
-
openerp-community-reviewer team
-
Mailing list archive
-
Message #05847
[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:
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-10 10:49:06 +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-10 10:49:06 +0000
@@ -0,0 +1,47 @@
+# -*- 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'],
+ "author": "Camptocamp",
+ "license": "AGPL-3",
+ "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 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, to deliver, the user uses "Related Picking" tab
+ - the "Transfer Products" wizard ("Deliver" button) displays only moves linked to a done picking dispatch.
+ """,
+ "website": "http://www.camptocamp.com",
+ "category": "Warehouse Management",
+ "demo": [],
+ "data": ['dispatch_view.xml'],
+ "test": [],
+ "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-10 10:49:06 +0000
@@ -0,0 +1,285 @@
+# -*- 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.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
+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"
+
+ def default_get(self, cr, uid, fields, context=None):
+ 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))
+ return res
+
+
+class stock_partial_move(orm.TransientModel):
+ _inherit = 'stock.partial.move'
+
+ def do_partial(self, cr, uid, ids, context=None):
+ # 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):
+ """Open the partial picking wizard"""
+ 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 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 do_partial(self, cr, uid, ids, partial_datas, context=None):
+ """ Makes partial picking and moves done.
+
+ 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-10 10:49:06 +0000
@@ -0,0 +1,45 @@
+<?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_state"/>
+ </field>
+ </field>
+ </record>
+
+ </data>
+</openerp>
Follow ups