mirror of
https://github.com/OCA/stock-logistics-warehouse.git
synced 2025-01-21 14:27:28 +02:00
@@ -17,6 +17,7 @@
|
||||
"category": "Stock",
|
||||
"data": [
|
||||
'data/stock_quant_view.xml',
|
||||
'views/stock_picking_type_views.xml',
|
||||
'wizard/stock_move_location.xml',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -2,3 +2,4 @@
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
from . import stock_move
|
||||
from . import stock_picking_type
|
||||
|
||||
25
stock_move_location/models/stock_picking_type.py
Normal file
25
stock_move_location/models/stock_picking_type.py
Normal file
@@ -0,0 +1,25 @@
|
||||
# Copyright 2019 Sergio Teruel <sergio.teruel@tecnativa.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockPickingType(models.Model):
|
||||
_inherit = "stock.picking.type"
|
||||
|
||||
show_move_onhand = fields.Boolean(
|
||||
string='Show Move On hand stock',
|
||||
help="Show a button 'Move On Hand' in the Inventory Dashboard "
|
||||
"to initiate the process to move the products in stock "
|
||||
"at the origin location.")
|
||||
|
||||
def action_move_location(self):
|
||||
action = self.env.ref(
|
||||
'stock_move_location.wiz_stock_move_location_action').read()[0]
|
||||
action['context'] = {
|
||||
'default_origin_location_id': self.default_location_src_id.id,
|
||||
'default_destination_location_id':
|
||||
self.default_location_dest_id.id,
|
||||
'default_picking_type_id': self.id,
|
||||
'default_edit_locations': False,
|
||||
}
|
||||
return action
|
||||
@@ -1,4 +1,6 @@
|
||||
* Mathieu Vatel <mathieu@julius.fr>
|
||||
* Mykhailo Panarin <m.panarin@mobilunity.com>
|
||||
* Sergio Teruel <sergio.teruel@tecnativa.com>
|
||||
* Joan Sisquella <joan.sisquella@eficent.com>
|
||||
* Joan Sisquella <joan.sisquella@eficent.com>
|
||||
* Jordi Ballester Alomar <jordi.ballester@eficent.com>
|
||||
* Lois Rilo <lois.rilo@eficent.com>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
* A new menuitem Stock > Move from location... opens a wizard
|
||||
where 2 location ca be specified.
|
||||
* A new menu item Stock > Move from location... opens a wizard
|
||||
where 2 location can be specified.
|
||||
* Select origin and destination locations and press "IMMEDIATE TRANSFER" or "PLANNED TRANSFER"
|
||||
* Press `ADD ALL` button to add all products available
|
||||
* Those lines can be edited. Move quantity can't be more than a max available quantity
|
||||
@@ -16,3 +16,7 @@ If you want to transfer a full quant:
|
||||
opened.
|
||||
|
||||
* Select the quants which you want move to another location
|
||||
|
||||
If you go to the Inventory Dashboard you can see the button "Move from location"
|
||||
in each of the picking types (only applicable to internal transfers). Press it
|
||||
and you will be directed to the wizard.
|
||||
@@ -16,6 +16,12 @@ class TestsCommon(common.SavepointCase):
|
||||
cls.wizard_obj = cls.env["wiz.stock.move.location"]
|
||||
cls.quant_obj = cls.env["stock.quant"]
|
||||
|
||||
# Enable multi-locations:
|
||||
wizard = cls.env['res.config.settings'].create({
|
||||
'group_stock_multi_locations': True,
|
||||
})
|
||||
wizard.execute()
|
||||
|
||||
cls.internal_loc_1 = cls.location_obj.create({
|
||||
"name": "INT_1",
|
||||
"usage": "internal",
|
||||
|
||||
@@ -100,13 +100,14 @@ class TestMoveLocation(TestsCommon):
|
||||
"""Test planned transfer."""
|
||||
wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2)
|
||||
wizard.onchange_origin_location()
|
||||
wizard.with_context({'planned': True}).action_move_location()
|
||||
wizard = wizard.with_context({'planned': True})
|
||||
wizard.action_move_location()
|
||||
picking = wizard.picking_id
|
||||
self.assertEqual(picking.state, 'draft')
|
||||
self.assertEqual(picking.state, 'assigned')
|
||||
self.assertEqual(len(picking.move_line_ids), 4)
|
||||
self.assertEqual(
|
||||
sorted(picking.move_line_ids.mapped("qty_done")),
|
||||
[0.0, 0.0, 0.0, 0.0],
|
||||
sorted(picking.move_line_ids.mapped("product_uom_qty")),
|
||||
[1, 1, 1, 123],
|
||||
)
|
||||
|
||||
def test_quant_transfer(self):
|
||||
@@ -129,3 +130,20 @@ class TestMoveLocation(TestsCommon):
|
||||
wizard.origin_location_id = self.internal_loc_2
|
||||
wizard._onchange_destination_location_id()
|
||||
self.assertEqual(len(lines), 3)
|
||||
|
||||
def test_readonly_location_computation(self):
|
||||
"""Test that origin_location_disable and destination_location_disable
|
||||
are computed correctly."""
|
||||
wizard = self._create_wizard(self.internal_loc_1, self.internal_loc_2)
|
||||
# locations are editable.
|
||||
self.assertFalse(wizard.origin_location_disable)
|
||||
self.assertFalse(wizard.destination_location_disable)
|
||||
# Disable edit mode:
|
||||
wizard.edit_locations = False
|
||||
self.assertTrue(wizard.origin_location_disable)
|
||||
self.assertTrue(wizard.destination_location_disable)
|
||||
|
||||
def test_picking_type_action_dummy(self):
|
||||
"""Test that no error is raised from actions."""
|
||||
pick_type = self.env.ref("stock.picking_type_internal")
|
||||
pick_type.action_move_location()
|
||||
|
||||
32
stock_move_location/views/stock_picking_type_views.xml
Normal file
32
stock_move_location/views/stock_picking_type_views.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record model="ir.ui.view" id="view_picking_type_form">
|
||||
<field name="name">Operation Types</field>
|
||||
<field name="model">stock.picking.type</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_type_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="show_operations" position="after">
|
||||
<field name="show_move_onhand" attrs='{"invisible": [("code", "not in", ["internal"])]}'/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="stock_picking_type_kanban" model="ir.ui.view">
|
||||
<field name="model">stock.picking.type</field>
|
||||
<field name="inherit_id" ref="stock.stock_picking_type_kanban"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="code" position="after">
|
||||
<field name="show_move_onhand"/>
|
||||
</field>
|
||||
<xpath expr="//div[hasclass('o_kanban_primary_left')]" position="inside">
|
||||
<div t-if="record.show_move_onhand.raw_value">
|
||||
<button name="action_move_location" type="object" class="btn btn-info" style="margin-top: 5px;">
|
||||
Move On Hand
|
||||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
@@ -11,12 +11,28 @@ class StockMoveLocationWizard(models.TransientModel):
|
||||
_name = "wiz.stock.move.location"
|
||||
_description = 'Wizard move location'
|
||||
|
||||
@api.multi
|
||||
def _get_default_picking_type_id(self):
|
||||
company_id = self.env.context.get('company_id') or \
|
||||
self.env.user.company_id.id
|
||||
return self.env['stock.picking.type'].search(
|
||||
[('code', '=', 'internal'),
|
||||
('warehouse_id.company_id', '=', company_id)], limit=1).id
|
||||
|
||||
origin_location_disable = fields.Boolean(
|
||||
compute="_compute_readonly_locations",
|
||||
help="technical field to disable the edition of origin location."
|
||||
)
|
||||
origin_location_id = fields.Many2one(
|
||||
string='Origin Location',
|
||||
comodel_name='stock.location',
|
||||
required=True,
|
||||
domain=lambda self: self._get_locations_domain(),
|
||||
)
|
||||
destination_location_disable = fields.Boolean(
|
||||
compute="_compute_readonly_locations",
|
||||
help="technical field to disable the edition of destination location."
|
||||
)
|
||||
destination_location_id = fields.Many2one(
|
||||
string='Destination Location',
|
||||
comodel_name='stock.location',
|
||||
@@ -27,10 +43,25 @@ class StockMoveLocationWizard(models.TransientModel):
|
||||
string="Move Location lines",
|
||||
comodel_name="wiz.stock.move.location.line",
|
||||
)
|
||||
picking_type_id = fields.Many2one(
|
||||
comodel_name='stock.picking.type',
|
||||
default=_get_default_picking_type_id,
|
||||
)
|
||||
picking_id = fields.Many2one(
|
||||
string="Connected Picking",
|
||||
comodel_name="stock.picking",
|
||||
)
|
||||
edit_locations = fields.Boolean(string='Edit Locations',
|
||||
default=True)
|
||||
|
||||
@api.depends('edit_locations')
|
||||
def _compute_readonly_locations(self):
|
||||
for rec in self:
|
||||
rec.origin_location_disable = self.env.context.get(
|
||||
'origin_location_disable', False)
|
||||
if not rec.edit_locations:
|
||||
rec.origin_location_disable = True
|
||||
rec.destination_location_disable = True
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
@@ -70,7 +101,7 @@ class StockMoveLocationWizard(models.TransientModel):
|
||||
|
||||
def _create_picking(self):
|
||||
return self.env['stock.picking'].create({
|
||||
'picking_type_id': self.env.ref('stock.picking_type_internal').id,
|
||||
'picking_type_id': self.picking_type_id.id,
|
||||
'location_id': self.origin_location_id.id,
|
||||
'location_dest_id': self.destination_location_id.id,
|
||||
})
|
||||
@@ -120,8 +151,9 @@ class StockMoveLocationWizard(models.TransientModel):
|
||||
move = self.env["stock.move"].create(
|
||||
self._get_move_values(picking, lines),
|
||||
)
|
||||
for line in lines:
|
||||
line.create_move_lines(picking, move)
|
||||
if not self.env.context.get("planned"):
|
||||
for line in lines:
|
||||
line.create_move_lines(picking, move)
|
||||
return move
|
||||
|
||||
@api.multi
|
||||
@@ -131,6 +163,9 @@ class StockMoveLocationWizard(models.TransientModel):
|
||||
self._create_moves(picking)
|
||||
if not self.env.context.get("planned"):
|
||||
picking.button_validate()
|
||||
else:
|
||||
picking.action_confirm()
|
||||
picking.action_assign()
|
||||
self.picking_id = picking
|
||||
return self._get_picking_action(picking.id)
|
||||
|
||||
@@ -166,12 +201,16 @@ class StockMoveLocationWizard(models.TransientModel):
|
||||
product_data = []
|
||||
for group in self._get_group_quants():
|
||||
product = product_obj.browse(group.get("product_id")).exists()
|
||||
# Apply the putaway strategy
|
||||
location_dest_id = \
|
||||
self.destination_location_id.get_putaway_strategy(
|
||||
product).id or self.destination_location_id.id
|
||||
product_data.append({
|
||||
'product_id': product.id,
|
||||
'move_quantity': group.get("sum"),
|
||||
'max_quantity': group.get("sum"),
|
||||
'origin_location_id': self.origin_location_id.id,
|
||||
'destination_location_id': self.destination_location_id.id,
|
||||
'destination_location_id': location_dest_id,
|
||||
# cursor returns None instead of False
|
||||
'lot_id': group.get("lot_id") or False,
|
||||
'product_uom_id': product.uom_id.id,
|
||||
|
||||
@@ -7,12 +7,24 @@
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box"/>
|
||||
<div>
|
||||
<label for="edit_locations">
|
||||
Edit Locations
|
||||
</label>
|
||||
<field name="edit_locations" widget="boolean_toggle"/>
|
||||
</div>
|
||||
<group name="picking_type">
|
||||
<field name="picking_type_id"/>
|
||||
</group>
|
||||
<group name="main">
|
||||
<field name="origin_location_id" invisible="context.get('origin_location_disable', False)"/>
|
||||
<field name="destination_location_id"/>
|
||||
<field name="origin_location_disable" invisible="True"/>
|
||||
<field name="origin_location_id" attrs="{'readonly': [('origin_location_disable', '=', True)]}"/>
|
||||
<field name="destination_location_disable" invisible="True"/>
|
||||
<field name="destination_location_id" attrs="{'readonly': [('destination_location_disable', '=', True)]}"/>
|
||||
</group>
|
||||
<group name="lines">
|
||||
<field name="stock_move_location_line_ids" nolabel="1" >
|
||||
<field name="stock_move_location_line_ids" nolabel="1" widget="one2many_list" mode="tree,kanban">
|
||||
<tree string="Inventory Details" editable="bottom" decoration-info="move_quantity != max_quantity" decoration-danger="(move_quantity < 0) or (move_quantity > max_quantity)" create="0">
|
||||
<field name="product_id" domain="[('type','=','product')]"/>
|
||||
<field name="product_uom_id" string="UoM" groups="uom.group_uom"/>
|
||||
@@ -23,6 +35,26 @@
|
||||
<field name="custom" invisible="1" />
|
||||
<field name="max_quantity" attrs="{'readonly': [('custom', '!=', True)]}" force_save="1"/>
|
||||
</tree>
|
||||
<kanban class="o_kanban_mobile">
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_global_click">
|
||||
<div class="o_kanban_record_body">
|
||||
<field name="product_id"/>
|
||||
<field name="product_uom_id"/>
|
||||
<field name="lot_id" groups="stock.group_production_lot"/>
|
||||
<field name="origin_location_id"/>
|
||||
<field name="destination_location_id"/>
|
||||
</div>
|
||||
<div class="o_kanban_record_bottom">
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<span><field name="move_quantity"/></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
|
||||
@@ -96,12 +96,14 @@ class StockMoveLocationWizardLine(models.TransientModel):
|
||||
self.ensure_one()
|
||||
location_dest_id = self.destination_location_id.get_putaway_strategy(
|
||||
self.product_id).id or self.destination_location_id.id
|
||||
qty_todo, qty_done = self._get_available_quantity()
|
||||
return {
|
||||
"product_id": self.product_id.id,
|
||||
"lot_id": self.lot_id.id,
|
||||
"location_id": self.origin_location_id.id,
|
||||
"location_dest_id": location_dest_id,
|
||||
"qty_done": self._get_available_quantity(),
|
||||
"product_uom_qty": qty_todo,
|
||||
"qty_done": qty_done,
|
||||
"product_uom_id": self.product_uom_id.id,
|
||||
"picking_id": picking.id,
|
||||
"move_id": move.id,
|
||||
@@ -117,7 +119,7 @@ class StockMoveLocationWizardLine(models.TransientModel):
|
||||
return 0
|
||||
if self.env.context.get("planned"):
|
||||
# for planned transfer we don't care about the amounts at all
|
||||
return 0.0
|
||||
return self.move_quantity, 0
|
||||
search_args = [
|
||||
('location_id', '=', self.origin_location_id.id),
|
||||
('product_id', '=', self.product_id.id),
|
||||
@@ -137,4 +139,4 @@ class StockMoveLocationWizardLine(models.TransientModel):
|
||||
available_qty, self.move_quantity, rounding) == -1
|
||||
if available_qty_lt_move_qty:
|
||||
return available_qty
|
||||
return self.move_quantity
|
||||
return 0, self.move_quantity
|
||||
|
||||
Reference in New Issue
Block a user