changeset ed3cb6d538b7 in modules/stock_assign_manual:default
details: 
https://hg.tryton.org/modules/stock_assign_manual?cmd=changeset&node=ed3cb6d538b7
description:
        Add wizard to unassign quantity from inventory moves on shipment

        issue9769
        review306801011
diffstat:

 CHANGELOG                                   |    4 +-
 __init__.py                                 |    3 +
 doc/index.rst                               |    3 +
 stock.py                                    |  162 +++++++++++++++++++++++++++-
 stock.xml                                   |   37 ++++++
 tests/scenario_stock_assign_manual.rst      |   47 ++++++++
 view/shipment_assigned_move_form.xml        |   11 +
 view/shipment_assigned_move_list.xml        |    8 +
 view/shipment_unassign_manual_show_form.xml |    8 +
 9 files changed, 278 insertions(+), 5 deletions(-)

diffs (373 lines):

diff -r ac8f323710ac -r ed3cb6d538b7 CHANGELOG
--- a/CHANGELOG Tue Dec 22 18:11:05 2020 +0100
+++ b/CHANGELOG Mon Apr 12 19:43:05 2021 +0200
@@ -1,2 +1,4 @@
+* Add manual unassign wizard on shipment
+
 Version 5.8.0 - 2020-11-02
-* Initial release
\ No newline at end of file
+* Initial release
diff -r ac8f323710ac -r ed3cb6d538b7 __init__.py
--- a/__init__.py       Tue Dec 22 18:11:05 2020 +0100
+++ b/__init__.py       Mon Apr 12 19:43:05 2021 +0200
@@ -14,10 +14,13 @@
         stock.ShipmentInReturn,
         stock.ShipmentOut,
         stock.ShipmentInternal,
+        stock.ShipmentAssignedMove,
         stock.ShipmentAssignManualShow,
+        stock.ShipmentUnassignManualShow,
         module='stock_assign_manual', type_='model')
     Pool.register(
         stock.ShipmentAssignManual,
+        stock.ShipmentUnassignManual,
         module='stock_assign_manual', type_='wizard')
     Pool.register(
         production.Production,
diff -r ac8f323710ac -r ed3cb6d538b7 doc/index.rst
--- a/doc/index.rst     Tue Dec 22 18:11:05 2020 +0100
+++ b/doc/index.rst     Mon Apr 12 19:43:05 2021 +0200
@@ -3,3 +3,6 @@
 
 The Stock Assign Manual module adds a wizard on shipments and production that
 allows you to decide from which precise location to pick products.
+
+Another wizard allows either the whole amount, or a specific quantity, to be
+unassigned of each move.
diff -r ac8f323710ac -r ed3cb6d538b7 stock.py
--- a/stock.py  Tue Dec 22 18:11:05 2020 +0100
+++ b/stock.py  Mon Apr 12 19:43:05 2021 +0200
@@ -4,7 +4,8 @@
 
 from trytond.exceptions import UserError
 from trytond.i18n import gettext
-from trytond.model import Model, ModelStorage, ModelView, fields
+from trytond.model import (
+    Model, ModelStorage, ModelView, ModelSQL, fields, dualmethod)
 from trytond.model.exceptions import ValidationError
 from trytond.pool import Pool, PoolMeta
 from trytond.pyson import Eval
@@ -12,7 +13,41 @@
 from trytond.wizard import Wizard, StateTransition, StateView, Button
 
 
-class ShipmentInReturn(metaclass=PoolMeta):
+class ShipmentUnassignMixin:
+    '''Mixin to unassign quantity from assigned shipment moves'''
+
+    @dualmethod
+    def unassign(cls, shipments, moves, quantities):
+        '''
+        Unassign the quantity from the corresponding move of the shipments.
+        '''
+        pool = Pool()
+        Move = pool.get('stock.move')
+        to_unassign = []
+        if not all(m.state == 'assigned' for m in moves):
+            raise ValueError("Not assigned move")
+        Move.draft(moves)
+        for move, unassign_quantity in zip(moves, quantities):
+            if not unassign_quantity:
+                continue
+            if unassign_quantity > move.quantity:
+                raise ValueError(
+                    "Unassigned quantity greater than move quantity")
+            if unassign_quantity == move.quantity:
+                to_unassign.append(move)
+            else:
+                with Transaction().set_context(_stock_move_split=True):
+                    to_unassign.extend(Move.copy(
+                            [move],
+                            {'quantity': unassign_quantity}))
+                move.quantity -= unassign_quantity
+        Move.save(moves)
+        Move.assign(moves)
+        if to_unassign:
+            cls.wait(shipments, to_unassign)
+
+
+class ShipmentInReturn(ShipmentUnassignMixin, metaclass=PoolMeta):
     __name__ = 'stock.shipment.in.return'
 
     @classmethod
@@ -32,7 +67,7 @@
         pass
 
 
-class ShipmentOut(metaclass=PoolMeta):
+class ShipmentOut(ShipmentUnassignMixin, metaclass=PoolMeta):
     __name__ = 'stock.shipment.out'
 
     @classmethod
@@ -55,7 +90,7 @@
         pass
 
 
-class ShipmentInternal(metaclass=PoolMeta):
+class ShipmentInternal(ShipmentUnassignMixin, metaclass=PoolMeta):
     __name__ = 'stock.shipment.internal'
 
     @classmethod
@@ -277,3 +312,122 @@
         for field, value in zip(grouping, key[1:]):
             if value is not None and '.' not in field:
                 setattr(self.move, field, value)
+
+
+class ShipmentUnassignManual(Wizard):
+    "Manual Unassign Shipment"
+    __name__ = 'stock.shipment.unassign.manual'
+    start = StateTransition()
+    show = StateView('stock.shipment.unassign.manual.show',
+        'stock_assign_manual.shipment_unassign_manual_show_view_form', [
+            Button("Cancel", 'end', 'tryton-cancel'),
+            Button("Unassign", 'unassign', 'tryton-ok', default=True),
+            ])
+    unassign = StateTransition()
+
+    def transition_start(self):
+        moves = self.record.assign_moves
+        if any(m.state == 'assigned' for m in moves):
+            return 'show'
+        return 'end'
+
+    def default_show(self, fields):
+        moves = self.record.assign_moves
+        move_ids = [m.id for m in moves if m.state == 'assigned']
+        return {
+            'assigned_moves': move_ids,
+            }
+
+    def transition_unassign(self):
+        moves = []
+        quantities = []
+        for m in self.show.moves:
+            moves.append(m.move)
+            quantities.append(m.unassigned_quantity)
+        self.record.unassign(moves, quantities)
+        return 'end'
+
+
+class ShipmentAssignedMove(ModelView, ModelSQL):
+    "Shipment Assigned Move"
+    __name__ = 'stock.shipment.assigned.move'
+
+    move = fields.Many2One('stock.move', "Move", required=True)
+    unassigned_quantity = fields.Float(
+        "Unassigned Quantity", digits=(16, Eval('unit_digits', 2)),
+        domain=['OR',
+            ('unassigned_quantity', '=', None),
+            [
+                ('unassigned_quantity', '>=', 0),
+                ('unassigned_quantity', '<=', Eval('move_quantity', 0)),
+                ],
+            ],
+        depends=['unit_digits', 'move_quantity'],
+        help="The quantity to unassign")
+    assigned_quantity = fields.Float(
+        "Assigned Quantity", digits=(16, Eval('unit_digits', 2)),
+        domain=['OR',
+            ('assigned_quantity', '=', None),
+            [
+                ('assigned_quantity', '>=', 0),
+                ('assigned_quantity', '<=', Eval('move_quantity', 0)),
+                ],
+            ],
+        depends=['unit_digits', 'move_quantity'],
+        help="The quantity left assigned")
+    unit = fields.Function(
+        fields.Many2One('product.uom', "Unit"), 'on_change_with_unit')
+    unit_digits = fields.Function(
+        fields.Integer("Unit Digits"), 'on_change_with_unit_digits')
+    move_quantity = fields.Function(
+        fields.Float("Move Quantity"), 'on_change_with_move_quantity')
+
+    @staticmethod
+    def default_unassigned_quantity():
+        return 0.0
+
+    @fields.depends('move', 'unassigned_quantity', 'assigned_quantity')
+    def on_change_move(self, name=None):
+        if self.move:
+            self.assigned_quantity = self.move.quantity
+            self.unassigned_quantity = 0.0
+
+    @fields.depends('assigned_quantity', 'move', 'unassigned_quantity', 'unit')
+    def on_change_unassigned_quantity(self, name=None):
+        if self.move and self.unassigned_quantity:
+            self.assigned_quantity = self.unit.round(
+                self.move.quantity - self.unassigned_quantity)
+
+    @fields.depends('unassigned_quantity', 'move', 'assigned_quantity', 'unit')
+    def on_change_assigned_quantity(self, name=None):
+        if self.move and self.assigned_quantity:
+            self.unassigned_quantity = self.unit.round(
+                self.move.quantity - self.assigned_quantity)
+
+    @fields.depends('move')
+    def on_change_with_unit(self, name=None):
+        if self.move:
+            return self.move.uom.id
+
+    @fields.depends('move')
+    def on_change_with_unit_digits(self, name=None):
+        if self.move:
+            return self.move.uom.digits
+
+    @fields.depends('move')
+    def on_change_with_move_quantity(self, name=None):
+        if self.move:
+            return self.move.quantity
+
+
+class ShipmentUnassignManualShow(ModelView):
+    "Manually Unassign Shipment"
+    __name__ = 'stock.shipment.unassign.manual.show'
+
+    moves = fields.One2Many(
+        'stock.shipment.assigned.move', None, "Moves",
+        domain=[('move.id', 'in', Eval('assigned_moves'))],
+        depends=['assigned_moves'],
+        help="The moves to unassign.")
+    assigned_moves = fields.Many2Many(
+        'stock.move', None, None, "Assigned Moves")
diff -r ac8f323710ac -r ed3cb6d538b7 stock.xml
--- a/stock.xml Tue Dec 22 18:11:05 2020 +0100
+++ b/stock.xml Mon Apr 12 19:43:05 2021 +0200
@@ -72,5 +72,42 @@
             <field name="type">form</field>
             <field name="name">shipment_assign_manual_show_form</field>
         </record>
+
+        <record model="ir.action.wizard" id="wizard_shipment_unassign_manual">
+            <field name="name">Manually Unassign Shipment</field>
+            <field name="wiz_name">stock.shipment.unassign.manual</field>
+        </record>
+        <record model="ir.action.keyword" 
id="shipment_in_return_unassign_manual_keyword">
+            <field name="keyword">form_action</field>
+            <field name="model">stock.shipment.in.return,-1</field>
+            <field name="action" ref="wizard_shipment_unassign_manual"/>
+        </record>
+        <record model="ir.action.keyword" 
id="shipment_out_unassign_manual_keyword">
+            <field name="keyword">form_action</field>
+            <field name="model">stock.shipment.out,-1</field>
+            <field name="action" ref="wizard_shipment_unassign_manual"/>
+        </record>
+        <record model="ir.action.keyword" 
id="shipment_internal_unassign_manual_keyword">
+            <field name="keyword">form_action</field>
+            <field name="model">stock.shipment.internal,-1</field>
+            <field name="action" ref="wizard_shipment_unassign_manual"/>
+        </record>
+
+        <record model="ir.ui.view" 
id="shipment_unassign_manual_show_view_form">
+            <field name="model">stock.shipment.unassign.manual.show</field>
+            <field name="type">form</field>
+            <field name="name">shipment_unassign_manual_show_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="shipment_assigned_move_view_form">
+            <field name="model">stock.shipment.assigned.move</field>
+            <field name="type">form</field>
+            <field name="name">shipment_assigned_move_form</field>
+        </record>
+        <record model="ir.ui.view" id="shipment_assigned_move_view_list">
+            <field name="model">stock.shipment.assigned.move</field>
+            <field name="type">tree</field>
+            <field name="name">shipment_assigned_move_list</field>
+        </record>
     </data>
 </tryton>
diff -r ac8f323710ac -r ed3cb6d538b7 tests/scenario_stock_assign_manual.rst
--- a/tests/scenario_stock_assign_manual.rst    Tue Dec 22 18:11:05 2020 +0100
+++ b/tests/scenario_stock_assign_manual.rst    Mon Apr 12 19:43:05 2021 +0200
@@ -137,3 +137,50 @@
 
     >>> shipment.state
     'assigned'
+
+Unassign move::
+
+    >>> AssignedMove = Model.get('stock.shipment.assigned.move')
+    >>> sorted([m.state for m in shipment.inventory_moves])
+    ['assigned', 'assigned', 'assigned']
+    >>> move1, _, _ = shipment.inventory_moves
+    >>> unassign_manual = Wizard('stock.shipment.unassign.manual', [shipment])
+    >>> move_to_unassign = AssignedMove()
+    >>> move_to_unassign.move = StockMove(move1.id)
+    >>> move_to_unassign.unassigned_quantity = 1.0
+    >>> unassign_manual.form.moves.append(move_to_unassign)
+    >>> unassign_manual.execute('unassign')
+    >>> shipment.state
+    'waiting'
+    >>> sorted([(m.state, m.quantity) for m in shipment.inventory_moves])
+    [('assigned', 1.0), ('assigned', 3.0), ('draft', 1.0)]
+
+Unassign a second move to be merged::
+
+    >>> move2, = [i for i in shipment.inventory_moves if (
+    ...     i.quantity == 1.0 and i.state == 'assigned')]
+    >>> unassign_manual = Wizard('stock.shipment.unassign.manual', [shipment])
+    >>> move_to_unassign = AssignedMove()
+    >>> move_to_unassign.move = StockMove(move2.id)
+    >>> move_to_unassign.unassigned_quantity = 1.0
+    >>> unassign_manual.form.moves.append(move_to_unassign)
+    >>> unassign_manual.execute('unassign')
+    >>> shipment.state
+    'waiting'
+    >>> sorted([(m.state, m.quantity) for m in shipment.inventory_moves])
+    [('assigned', 3.0), ('draft', 2.0)]
+
+Unassign partially third move::
+
+    >>> move3, = [i for i in shipment.inventory_moves
+    ...     if i.quantity == 3.0 and i.state == 'assigned']
+    >>> unassign_manual = Wizard('stock.shipment.unassign.manual', [shipment])
+    >>> move_to_unassign = AssignedMove()
+    >>> move_to_unassign.move = StockMove(move3.id)
+    >>> move_to_unassign.unassigned_quantity = 2.0
+    >>> unassign_manual.form.moves.append(move_to_unassign)
+    >>> unassign_manual.execute('unassign')
+    >>> shipment.state
+    'waiting'
+    >>> sorted([(m.state, m.quantity) for m in shipment.inventory_moves])
+    [('assigned', 1.0), ('draft', 2.0), ('draft', 2.0)]
diff -r ac8f323710ac -r ed3cb6d538b7 view/shipment_assigned_move_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/shipment_assigned_move_form.xml      Mon Apr 12 19:43:05 2021 +0200
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="move"/>
+    <field name="move" colspan="3"/>
+    <label name="unassigned_quantity"/>
+    <field name="unassigned_quantity" symbol="unit"/>
+    <label name="assigned_quantity"/>
+    <field name="assigned_quantity" symbol="unit"/>
+</form>
diff -r ac8f323710ac -r ed3cb6d538b7 view/shipment_assigned_move_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/shipment_assigned_move_list.xml      Mon Apr 12 19:43:05 2021 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree editable="1">
+    <field name="move" expand="1"/>
+    <field name="unassigned_quantity" symbol="unit"/>
+    <field name="assigned_quantity" symbol="unit"/>
+</tree>
diff -r ac8f323710ac -r ed3cb6d538b7 view/shipment_unassign_manual_show_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/shipment_unassign_manual_show_form.xml       Mon Apr 12 19:43:05 
2021 +0200
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form col="2">
+    <image name="tryton-question" xexpand="0" xfill="0"/>
+    <label string="Enter the quantities to unassign:" id="fill" yalign="0.0" 
xalign="0.0" xexpand="1"/>
+    <field name="moves" product="move" colspan="2"/>
+</form>

Reply via email to