mejora el stock picking batch de odoo desde OCA/stock_logistics_workflow
This commit is contained in:
parent
8a755662d7
commit
448f889f12
41 changed files with 6541 additions and 0 deletions
7
stock_picking_batch_extended/models/__init__.py
Normal file
7
stock_picking_batch_extended/models/__init__.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from . import res_config_settings
|
||||
from . import stock_batch_picking
|
||||
from . import product_product
|
||||
from . import stock_picking
|
||||
from . import stock_warehouse
|
||||
11
stock_picking_batch_extended/models/product_product.py
Normal file
11
stock_picking_batch_extended/models/product_product.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright 2012-2014 Alexandre Fayolle, Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
_inherit = "product.product"
|
||||
# TODO: Integrate in existent field
|
||||
description_warehouse = fields.Text('Warehouse Description',
|
||||
translate=True)
|
||||
20
stock_picking_batch_extended/models/res_config_settings.py
Normal file
20
stock_picking_batch_extended/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright 2019 Camptocamp - Iryna Vyshnevska
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
use_oca_batch_validation = fields.Boolean(
|
||||
string='Use OCA approach to validate Picking Batch',
|
||||
related="company_id.use_oca_batch_validation",
|
||||
readonly=False,
|
||||
)
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
use_oca_batch_validation = fields.Boolean()
|
||||
261
stock_picking_batch_extended/models/stock_batch_picking.py
Normal file
261
stock_picking_batch_extended/models/stock_batch_picking.py
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
# Copyright 2012-2014 Alexandre Fayolle, Camptocamp SA
|
||||
# Copyright 2018-2020 Tecnativa - Carlos Dauden
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockBatchPicking(models.Model):
|
||||
""" This object allow to manage multiple stock.picking at the same time.
|
||||
"""
|
||||
# renamed stock.batch.picking -> stock.picking.batch
|
||||
_inherit = ['stock.picking.batch', 'mail.thread', 'mail.activity.mixin']
|
||||
_name = 'stock.picking.batch'
|
||||
|
||||
name = fields.Char(
|
||||
index=True,
|
||||
unique=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
default=lambda self: self.env['ir.sequence'].next_by_code(
|
||||
'stock.picking.batch'
|
||||
),
|
||||
)
|
||||
|
||||
# added state to be compatible with picking_ids
|
||||
state = fields.Selection(
|
||||
selection_add=[
|
||||
('assigned', 'Available'),
|
||||
],
|
||||
readonly=True, index=True,
|
||||
help='the state of the batch picking. '
|
||||
'Workflow is draft -> in_progress/assigned -> done or cancel',
|
||||
)
|
||||
|
||||
date = fields.Date(
|
||||
string='Date',
|
||||
required=True, readonly=True, index=True,
|
||||
states={
|
||||
'draft': [('readonly', False)],
|
||||
'in_progress': [('readonly', False)]
|
||||
},
|
||||
default=fields.Date.context_today,
|
||||
help='date on which the batch picking is to be processed',
|
||||
)
|
||||
|
||||
user_id = fields.Many2one(
|
||||
comodel_name='res.users',
|
||||
string='Picker',
|
||||
readonly=True, index=True,
|
||||
states={
|
||||
'draft': [('readonly', False)],
|
||||
'in_progress': [('readonly', False)]
|
||||
},
|
||||
help='the user to which the pickings are assigned',
|
||||
old_name='picker_id',
|
||||
)
|
||||
|
||||
use_oca_batch_validation = fields.Boolean(
|
||||
default=lambda self: self.env.user.company_id.use_oca_batch_validation,
|
||||
copy=False,
|
||||
)
|
||||
|
||||
picking_ids = fields.One2many(
|
||||
string='Pickings',
|
||||
readonly=True,
|
||||
states={'draft': [('readonly', False)]},
|
||||
help='List of picking managed by this batch.',
|
||||
)
|
||||
# TODO add comment to this field
|
||||
active_picking_ids = fields.One2many(
|
||||
string="Active Pickings",
|
||||
comodel_name='stock.picking',
|
||||
inverse_name='batch_id',
|
||||
readonly=True,
|
||||
domain=[('state', 'not in', ('cancel', 'done'))],
|
||||
)
|
||||
|
||||
notes = fields.Text('Notes', help='free form remarks')
|
||||
|
||||
move_lines = fields.Many2many(
|
||||
comodel_name='stock.move',
|
||||
readonly=True,
|
||||
string='Operations',
|
||||
compute='_compute_move_lines',
|
||||
)
|
||||
|
||||
move_line_ids = fields.Many2many(
|
||||
comodel_name='stock.move.line',
|
||||
string='Detailed operations',
|
||||
compute='_compute_move_line_ids',
|
||||
# HACK: Allow to write sml fields from this model
|
||||
inverse=lambda self: self,
|
||||
)
|
||||
|
||||
entire_package_ids = fields.Many2many(
|
||||
comodel_name='stock.quant.package',
|
||||
compute='_compute_entire_package_ids',
|
||||
help='Those are the entire packages of a picking shown in the view of '
|
||||
'operations',
|
||||
)
|
||||
|
||||
entire_package_detail_ids = fields.Many2many(
|
||||
comodel_name='stock.quant.package',
|
||||
compute='_compute_entire_package_ids',
|
||||
help='Those are the entire packages of a picking shown in the view of '
|
||||
'detailed operations',
|
||||
)
|
||||
|
||||
picking_count = fields.Integer(
|
||||
string='# Pickings',
|
||||
compute='_compute_picking_count',
|
||||
)
|
||||
|
||||
@api.depends('picking_ids')
|
||||
def _compute_move_lines(self):
|
||||
for batch in self:
|
||||
if batch.use_oca_batch_validation:
|
||||
batch.move_lines = batch.picking_ids.mapped("move_lines")
|
||||
|
||||
@api.depends('picking_ids')
|
||||
def _compute_move_line_ids(self):
|
||||
for batch in self:
|
||||
if batch.use_oca_batch_validation:
|
||||
batch.move_line_ids = batch.picking_ids.mapped(
|
||||
'move_line_ids'
|
||||
)
|
||||
|
||||
@api.depends('picking_ids')
|
||||
def _compute_entire_package_ids(self):
|
||||
for batch in self:
|
||||
if batch.use_oca_batch_validation:
|
||||
batch.update({
|
||||
'entire_package_ids': batch.picking_ids.mapped(
|
||||
'entire_package_ids'),
|
||||
'entire_package_detail_ids': batch.picking_ids.mapped(
|
||||
'entire_package_detail_ids'),
|
||||
})
|
||||
|
||||
def _compute_picking_count(self):
|
||||
"""Calculate number of pickings."""
|
||||
groups = self.env['stock.picking'].read_group(
|
||||
domain=[('batch_id', 'in', self.ids)],
|
||||
fields=['batch_id'],
|
||||
groupby=['batch_id'],
|
||||
)
|
||||
counts = {g['batch_id'][0]: g['batch_id_count'] for g in groups}
|
||||
for batch in self:
|
||||
batch.picking_count = counts.get(batch.id, 0)
|
||||
|
||||
def get_not_empties(self):
|
||||
""" Return all batches in this recordset
|
||||
for which picking_ids is not empty.
|
||||
|
||||
:raise UserError: If all batches are empty.
|
||||
"""
|
||||
if not self.mapped('picking_ids'):
|
||||
if len(self) == 1:
|
||||
message = _('This Batch has no pickings')
|
||||
else:
|
||||
message = _('These Batches have no pickings')
|
||||
|
||||
raise UserError(message)
|
||||
|
||||
return self.filtered(lambda b: len(b.picking_ids) != 0)
|
||||
|
||||
def verify_state(self, expected_state=None):
|
||||
""" Check if batches states must be changed based on pickings states.
|
||||
|
||||
If all pickings are canceled, batch must be canceled.
|
||||
If all pickings are canceled or done, batch must be done.
|
||||
If all pickings are canceled or done or *expected_state*,
|
||||
batch must be *expected_state*.
|
||||
|
||||
:return: True if batches states has been changed.
|
||||
"""
|
||||
expected_states = {'done', 'cancel'}
|
||||
if expected_state is not None:
|
||||
expected_states.add(expected_state)
|
||||
|
||||
all_good = True
|
||||
for batch in self.filtered(lambda b: b.state not in expected_states):
|
||||
states = set(batch.mapped('picking_ids.state'))
|
||||
if not states or states == {'cancel'}:
|
||||
batch.state = 'cancel'
|
||||
elif states == {'done'} or states == {'done', 'cancel'}:
|
||||
batch.state = 'done'
|
||||
|
||||
elif states.issubset(expected_states):
|
||||
batch.state = expected_state
|
||||
|
||||
else:
|
||||
all_good = False
|
||||
|
||||
return all_good
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
""" Call action_cancel for all batches pickings
|
||||
and set batches states to cancel too.
|
||||
"""
|
||||
for batch in self:
|
||||
if not batch.picking_ids:
|
||||
batch.write({'state': 'cancel'})
|
||||
else:
|
||||
if not batch.verify_state():
|
||||
batch.picking_ids.action_cancel()
|
||||
|
||||
@api.multi
|
||||
def action_assign(self):
|
||||
""" Check if batches pickings are available.
|
||||
"""
|
||||
batches = self.get_not_empties()
|
||||
if not batches.verify_state('in_progress'):
|
||||
mass_wiz = self.env['stock.picking.mass.action'].create({
|
||||
'check_availability': True,
|
||||
'picking_ids': [
|
||||
(6, 0, batches.mapped('active_picking_ids').ids)
|
||||
]
|
||||
})
|
||||
return mass_wiz.mass_action()
|
||||
|
||||
@api.multi
|
||||
def action_transfer(self):
|
||||
""" Create wizard to process all active pickings in these batches
|
||||
"""
|
||||
batches = self.get_not_empties()
|
||||
if not batches.verify_state():
|
||||
mass_wiz = self.env['stock.picking.mass.action'].create({
|
||||
'transfer': True,
|
||||
'picking_ids': [
|
||||
(6, 0, batches.mapped('active_picking_ids').ids)
|
||||
],
|
||||
})
|
||||
return mass_wiz.mass_action()
|
||||
|
||||
@api.multi
|
||||
def action_print_picking(self):
|
||||
pickings = self.mapped('picking_ids')
|
||||
if not pickings:
|
||||
raise UserError(_('Nothing to print.'))
|
||||
return self.env.ref(
|
||||
'stock_picking_batch_extended.action_report_batch_picking'
|
||||
).report_action(self)
|
||||
|
||||
@api.multi
|
||||
def remove_undone_pickings(self):
|
||||
""" Remove of this batch all pickings which state is not done / cancel.
|
||||
"""
|
||||
self.mapped('active_picking_ids').write({'batch_id': False})
|
||||
self.verify_state()
|
||||
|
||||
@api.multi
|
||||
def action_view_stock_picking(self):
|
||||
"""This function returns an action that display existing pickings of
|
||||
given batch picking.
|
||||
"""
|
||||
self.ensure_one()
|
||||
pickings = self.mapped('picking_ids')
|
||||
action = self.env.ref('stock.action_picking_tree_all').read([])[0]
|
||||
action['domain'] = [('id', 'in', pickings.ids)]
|
||||
return action
|
||||
79
stock_picking_batch_extended/models/stock_picking.py
Normal file
79
stock_picking_batch_extended/models/stock_picking.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# Copyright 2016 Cyril Gaudin, Camptocamp SA
|
||||
# Copyright 2018 Tecnativa - Carlos Dauden
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.tools import float_is_zero
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = "stock.picking"
|
||||
|
||||
batch_id = fields.Many2one(
|
||||
string='Batch',
|
||||
domain="[('state', '=', 'draft')]",
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def action_cancel(self):
|
||||
"""In addition to what the method in the parent class does,
|
||||
cancel the batches for which all pickings are cancelled
|
||||
"""
|
||||
result = super(StockPicking, self).action_cancel()
|
||||
self.mapped('batch_id').verify_state()
|
||||
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def action_assign(self):
|
||||
"""In addition to what the method in the parent class does,
|
||||
Changed batches states to assigned if all picking are assigned.
|
||||
"""
|
||||
result = super(StockPicking, self).action_assign()
|
||||
self.mapped('batch_id').verify_state('assigned')
|
||||
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def action_done(self):
|
||||
"""In addition to what the method in the parent class does,
|
||||
Changed batches states to done if all picking are done.
|
||||
"""
|
||||
result = super(StockPicking, self).action_done()
|
||||
self.mapped('batch_id').verify_state()
|
||||
|
||||
return result
|
||||
|
||||
def force_transfer(self, force_qty=True):
|
||||
""" Do the picking transfer (by calling action_done)
|
||||
|
||||
If *force_qty* is True, force the transfer for all product_uom_qty
|
||||
when qty_done is 0.
|
||||
|
||||
Otherwise, process only pack operation with qty_done.
|
||||
If a picking has no qty_done filled, we released it from his batch
|
||||
"""
|
||||
for pick in self:
|
||||
if pick.state != 'assigned':
|
||||
pick.action_assign()
|
||||
if pick.state != 'assigned':
|
||||
continue
|
||||
|
||||
if force_qty:
|
||||
for pack in pick.move_line_ids:
|
||||
pack.qty_done = pack.product_uom_qty
|
||||
else:
|
||||
if all(
|
||||
float_is_zero(
|
||||
pack.qty_done,
|
||||
precision_rounding=pack.product_uom_id.rounding)
|
||||
for pack in pick.move_line_ids):
|
||||
# No qties to process, release out of the batch
|
||||
pick.batch_id = False
|
||||
continue
|
||||
else:
|
||||
for pack in pick.move_line_ids:
|
||||
if not pack.qty_done:
|
||||
pack.unlink()
|
||||
|
||||
pick.action_done()
|
||||
14
stock_picking_batch_extended/models/stock_warehouse.py
Normal file
14
stock_picking_batch_extended/models/stock_warehouse.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2016 Camptocamp SA
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
|
||||
default_user_id = fields.Many2one(
|
||||
'res.users', 'Default Picker',
|
||||
help='the user to which the batch pickings are assigned by default',
|
||||
index=True,
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue