diff --git a/stock_vertical_lift/__manifest__.py b/stock_vertical_lift/__manifest__.py index 580fc0706..7d97d1741 100644 --- a/stock_vertical_lift/__manifest__.py +++ b/stock_vertical_lift/__manifest__.py @@ -26,6 +26,10 @@ 'views/stock_location_views.xml', 'views/stock_move_line_views.xml', 'views/vertical_lift_shuttle_views.xml', + 'views/vertical_lift_operation_base_views.xml', + 'views/vertical_lift_operation_pick_views.xml', + 'views/vertical_lift_operation_put_views.xml', + 'views/vertical_lift_operation_inventory_views.xml', 'views/stock_vertical_lift_templates.xml', 'views/shuttle_screen_templates.xml', 'security/ir.model.access.csv', diff --git a/stock_vertical_lift/models/__init__.py b/stock_vertical_lift/models/__init__.py index 80b7355db..839f04888 100644 --- a/stock_vertical_lift/models/__init__.py +++ b/stock_vertical_lift/models/__init__.py @@ -1,4 +1,8 @@ from . import vertical_lift_shuttle +from . import vertical_lift_operation_base +from . import vertical_lift_operation_pick +from . import vertical_lift_operation_put +from . import vertical_lift_operation_inventory from . import stock_location from . import stock_move from . import stock_move_line diff --git a/stock_vertical_lift/models/stock_move.py b/stock_vertical_lift/models/stock_move.py index 1210d6b09..42222e716 100644 --- a/stock_vertical_lift/models/stock_move.py +++ b/stock_vertical_lift/models/stock_move.py @@ -5,19 +5,24 @@ from odoo import api, models class StockMove(models.Model): - _inherit = 'stock.move' + _inherit = "stock.move" @api.multi def write(self, vals): result = super().write(vals) - if 'state' in vals: + if "state" in vals: # We cannot have fields to depends on to invalidate these computed - # fields on vertical.lift.shuttle. But we know that when the state - # of any move line changes, we can invalidate them as the count of - # assigned move lines may change (and we track this in stock.move, - # not stock.move.line, becaus the state of the lines is a related - # to this one). - self.env['vertical.lift.shuttle'].invalidate_cache( - ['number_of_ops', 'number_of_ops_all'] + # fields on vertical.lift.operation.*. But we know that when the + # state of any move line changes, we can invalidate them as the + # count of assigned move lines may change (and we track this in + # stock.move, not stock.move.line, because the state of the lines + # is a related to this one). + models = ( + "vertical.lift.operation.pick", + "vertical.lift.operation.put", ) + for model in models: + self.env[model].invalidate_cache( + ["number_of_ops", "number_of_ops_all"] + ) return result diff --git a/stock_vertical_lift/models/stock_quant.py b/stock_vertical_lift/models/stock_quant.py index 905484139..5a5ba4f86 100644 --- a/stock_vertical_lift/models/stock_quant.py +++ b/stock_vertical_lift/models/stock_quant.py @@ -10,7 +10,14 @@ class StockQuant(models.Model): def _update_available_quantity(self, *args, **kwargs): result = super()._update_available_quantity(*args, **kwargs) # We cannot have fields to depends on to invalidate this computed - # fields on vertical.lift.shuttle. But we know that when the quantity - # of quant changes, we can invalidate the field on the shuttles. - self.env['vertical.lift.shuttle'].invalidate_cache(['tray_qty']) + # fields on vertical.lift.operation.* models. But we know that when the + # quantity of quant changes, we can invalidate the field + models = ( + "vertical.lift.operation.pick", + "vertical.lift.operation.put", + ) + for model in models: + self.env[model].invalidate_cache( + ["tray_qty"] + ) return result diff --git a/stock_vertical_lift/models/vertical_lift_operation_base.py b/stock_vertical_lift/models/vertical_lift_operation_base.py new file mode 100644 index 000000000..a23cdc2ec --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_base.py @@ -0,0 +1,264 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.addons.base_sparse_field.models.fields import Serialized + + +class VerticalLiftOperationBase(models.AbstractModel): + """Base model for shuttle operations (pick, put, inventory)""" + + _name = "vertical.lift.operation.base" + _inherit = "barcodes.barcode_events_mixin" + _description = "Vertical Lift Operation - Base" + + name = fields.Char(related="shuttle_id.name", readonly=True) + shuttle_id = fields.Many2one( + comodel_name="vertical.lift.shuttle", required=True, readonly=True + ) + location_id = fields.Many2one( + related="shuttle_id.location_id", readonly=True + ) + mode = fields.Selection(related="shuttle_id.mode", readonly=True) + operation_descr = fields.Char( + string="Operation", default="...", readonly=True + ) + + _sql_constraints = [ + ( + "shuttle_id_unique", + "UNIQUE(shuttle_id)", + "One pick can be run at a time for a shuttle.", + ) + ] + + def on_barcode_scanned(self, barcode): + self.ensure_one() + # to implement in sub-classes + + def on_screen_open(self): + """Called when the screen is open + + To implement in sub-classes. + """ + + def action_open_screen(self): + return self.shuttle_id.action_open_screen() + + def action_menu(self): + return self.shuttle_id.action_menu() + + def action_manual_barcode(self): + return self.shuttle_id.action_manual_barcode() + + +class VerticalLiftOperationTransfer(models.AbstractModel): + """Base model for shuttle pick and put operations""" + + _name = "vertical.lift.operation.transfer" + _inherit = "vertical.lift.operation.base" + _description = "Vertical Lift Operation - Transfer" + + current_move_line_id = fields.Many2one(comodel_name="stock.move.line") + + number_of_ops = fields.Integer( + compute="_compute_number_of_ops", string="Number of Operations" + ) + number_of_ops_all = fields.Integer( + compute="_compute_number_of_ops_all", + string="Number of Operations in all shuttles", + ) + + tray_location_id = fields.Many2one( + comodel_name="stock.location", + compute="_compute_tray_data", + string="Tray Location", + ) + tray_name = fields.Char(compute="_compute_tray_data", string="Tray Name") + tray_type_id = fields.Many2one( + comodel_name="stock.location.tray.type", + compute="_compute_tray_data", + string="Tray Type", + ) + tray_type_code = fields.Char( + compute="_compute_tray_data", string="Tray Code" + ) + tray_x = fields.Integer(string="X", compute="_compute_tray_data") + tray_y = fields.Integer(string="Y", compute="_compute_tray_data") + tray_matrix = Serialized(string="Cells", compute="_compute_tray_data") + tray_qty = fields.Float( + string="Stock Quantity", compute="_compute_tray_qty" + ) + + # current operation information + picking_id = fields.Many2one( + related="current_move_line_id.picking_id", readonly=True + ) + picking_origin = fields.Char( + related="current_move_line_id.picking_id.origin", readonly=True + ) + picking_partner_id = fields.Many2one( + related="current_move_line_id.picking_id.partner_id", readonly=True + ) + product_id = fields.Many2one( + related="current_move_line_id.product_id", readonly=True + ) + product_uom_id = fields.Many2one( + related="current_move_line_id.product_uom_id", readonly=True + ) + product_uom_qty = fields.Float( + related="current_move_line_id.product_uom_qty", readonly=True + ) + product_packagings = fields.Html( + string="Packaging", compute="_compute_product_packagings" + ) + qty_done = fields.Float( + related="current_move_line_id.qty_done", readonly=True + ) + lot_id = fields.Many2one( + related="current_move_line_id.lot_id", readonly=True + ) + location_dest_id = fields.Many2one( + string="Destination", + related="current_move_line_id.location_dest_id", + readonly=True, + ) + # TODO add a glue addon with product_expiry to add the field + + def on_barcode_scanned(self, barcode): + self.ensure_one() + self.env.user.notify_info( + "Scanned barcode: {}. Not implemented.".format(barcode) + ) + + @api.depends("current_move_line_id.product_id.packaging_ids") + def _compute_product_packagings(self): + for record in self: + if not record.current_move_line_id: + continue + product = record.current_move_line_id.product_id + values = { + "packagings": [ + { + "name": pkg.name, + "qty": pkg.qty, + "unit": product.uom_id.name, + } + for pkg in product.packaging_ids + ] + } + content = self.env["ir.qweb"].render( + "stock_vertical_lift.packagings", values + ) + record.product_packagings = content + + @api.depends() + def _compute_number_of_ops(self): + for record in self: + record.number_of_ops = record.count_move_lines_to_do() + + @api.depends() + def _compute_number_of_ops_all(self): + for record in self: + record.number_of_ops_all = record.count_move_lines_to_do_all() + + @api.depends("tray_location_id", "current_move_line_id.product_id") + def _compute_tray_qty(self): + for record in self: + if not (record.tray_location_id and record.current_move_line_id): + continue + product = record.current_move_line_id.product_id + quants = self.env["stock.quant"].search( + [ + ("location_id", "=", record.tray_location_id.id), + ("product_id", "=", product.id), + ] + ) + record.tray_qty = sum(quants.mapped("quantity")) + + # depends of the quantity so we can't have all triggers + @api.depends("current_move_line_id") + def _compute_tray_data(self): + for record in self: + modes = {"pick": "location_id", "put": "location_dest_id"} + location = record.current_move_line_id[modes[record.mode]] + tray_type = location.location_id.tray_type_id + # this is the current cell + record.tray_location_id = location.id + # name of the tray where the cell is + record.tray_name = location.location_id.name + record.tray_type_id = tray_type.id + record.tray_type_code = tray_type.code + record.tray_x = location.posx + record.tray_y = location.posy + record.tray_matrix = location.tray_matrix + + def _domain_move_lines_to_do(self): + # to implement in sub-classes + return [("id", "=", 0)] + + def _domain_move_lines_to_do_all(self): + # to implement in sub-classes + return [("id", "=", 0)] + + def count_move_lines_to_do(self): + """Count move lines to process in current shuttles""" + self.ensure_one() + return self.env["stock.move.line"].search_count( + self._domain_move_lines_to_do() + ) + + def count_move_lines_to_do_all(self): + """Count move lines to process in all shuttles""" + self.ensure_one() + return self.env["stock.move.line"].search_count( + self._domain_move_lines_to_do_all() + ) + + def on_screen_open(self): + """Called when the screen is open""" + self.select_next_move_line() + + def button_release(self): + """Release the operation, go to the next""" + self.select_next_move_line() + if not self.current_move_line_id: + # sorry not sorry + return { + "effect": { + "fadeout": "slow", + "message": _("Congrats, you cleared the queue!"), + "img_url": "/web/static/src/img/smile.svg", + "type": "rainbow_man", + } + } + + def process_current(self): + raise NotImplementedError + + def button_save(self): + if not (self and self.current_move_line_id): + return + self.ensure_one() + self.process_current() + self.operation_descr = _("Release") + + def fetch_tray(self): + return + + def select_next_move_line(self): + self.ensure_one() + next_move_line = self.env["stock.move.line"].search( + self._domain_move_lines_to_do(), limit=1 + ) + self.current_move_line_id = next_move_line + # TODO use a state machine to define next steps and + # description? + descr = ( + _("Scan New Destination Location") + if next_move_line + else _("No operations") + ) + self.operation_descr = descr + if next_move_line: + self.fetch_tray() diff --git a/stock_vertical_lift/models/vertical_lift_operation_inventory.py b/stock_vertical_lift/models/vertical_lift_operation_inventory.py new file mode 100644 index 000000000..17fd732cd --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_inventory.py @@ -0,0 +1,10 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models + + +class VerticalLiftOperationInventory(models.Model): + _name = 'vertical.lift.operation.inventory' + _inherit = 'vertical.lift.operation.base' + _description = 'Vertical Lift Operation Inventory' diff --git a/stock_vertical_lift/models/vertical_lift_operation_pick.py b/stock_vertical_lift/models/vertical_lift_operation_pick.py new file mode 100644 index 000000000..25bd40173 --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_pick.py @@ -0,0 +1,53 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, models + + +class VerticalLiftOperationPick(models.Model): + _name = "vertical.lift.operation.pick" + _inherit = "vertical.lift.operation.transfer" + _description = "Vertical Lift Operation Pick" + + def on_barcode_scanned(self, barcode): + self.ensure_one() + location = self.env["stock.location"].search( + [("barcode", "=", barcode)] + ) + if location: + self.current_move_line_id.location_dest_id = location + self.operation_descr = _("Save") + else: + self.env.user.notify_warning( + _("No location found for barcode {}").format(barcode) + ) + + def _domain_move_lines_to_do(self): + # TODO check domain + domain = [ + ("state", "=", "assigned"), + ("location_id", "child_of", self.location_id.id), + ] + return domain + + def _domain_move_lines_to_do_all(self): + shuttle_locations = self.env["stock.location"].search( + [("vertical_lift_kind", "=", "view")] + ) + # TODO check domain + domain = [ + ("state", "=", "assigned"), + ("location_id", "child_of", shuttle_locations.ids), + ] + return domain + + def fetch_tray(self): + self.current_move_line_id.fetch_vertical_lift_tray_source() + + def process_current(self): + # test code, TODO the smart one + # (scan of barcode increments qty, save calls action_done?) + line = self.current_move_line_id + if line.state != "done": + line.qty_done = line.product_qty + line.move_id._action_done() diff --git a/stock_vertical_lift/models/vertical_lift_operation_put.py b/stock_vertical_lift/models/vertical_lift_operation_put.py new file mode 100644 index 000000000..92903fb01 --- /dev/null +++ b/stock_vertical_lift/models/vertical_lift_operation_put.py @@ -0,0 +1,35 @@ +# Copyright 2019 Camptocamp SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import _, exceptions, models + + +class VerticalLiftOperationPut(models.Model): + _name = "vertical.lift.operation.put" + _inherit = "vertical.lift.operation.transfer" + _description = "Vertical Lift Operation Put" + + def _domain_move_lines_to_do(self): + # TODO check domain + domain = [ + ("state", "=", "assigned"), + ("location_dest_id", "child_of", self.location_id.id), + ] + return domain + + def _domain_move_lines_to_do_all(self): + shuttle_locations = self.env["stock.location"].search( + [("vertical_lift_kind", "=", "view")] + ) + domain = [ + # TODO check state + ("state", "=", "assigned"), + ("location_dest_id", "child_of", shuttle_locations.ids), + ] + return domain + + def fetch_tray(self): + self.current_move_line_id.fetch_vertical_lift_tray_dest() + + def process_current(self): + raise exceptions.UserError(_("Put workflow not implemented")) diff --git a/stock_vertical_lift/models/vertical_lift_shuttle.py b/stock_vertical_lift/models/vertical_lift_shuttle.py index fcdb4a3bd..d3271acb2 100644 --- a/stock_vertical_lift/models/vertical_lift_shuttle.py +++ b/stock_vertical_lift/models/vertical_lift_shuttle.py @@ -1,384 +1,141 @@ # Copyright 2019 Camptocamp SA # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import _, api, exceptions, fields, models -from odoo.addons.base_sparse_field.models.fields import Serialized +from odoo import _, api, fields, models class VerticalLiftShuttle(models.Model): - _name = 'vertical.lift.shuttle' - _inherit = 'barcodes.barcode_events_mixin' - _description = 'Vertical Lift Shuttle' + _name = "vertical.lift.shuttle" + _inherit = "barcodes.barcode_events_mixin" + _description = "Vertical Lift Shuttle" name = fields.Char() mode = fields.Selection( - [('pick', 'Pick'), ('put', 'Put'), ('inventory', 'Inventory')], - default='pick', + [("pick", "Pick"), ("put", "Put"), ("inventory", "Inventory")], + default="pick", required=True, ) location_id = fields.Many2one( - comodel_name='stock.location', + comodel_name="stock.location", required=True, domain="[('vertical_lift_kind', '=', 'shuttle')]", - ondelete='restrict', + ondelete="restrict", help="The Shuttle source location for Pick operations " "and destination location for Put operations.", ) hardware = fields.Selection( - selection='_selection_hardware', default='simulation', required=True - ) - current_move_line_id = fields.Many2one(comodel_name='stock.move.line') - - number_of_ops = fields.Integer( - compute='_compute_number_of_ops', string='Number of Operations' - ) - number_of_ops_all = fields.Integer( - compute='_compute_number_of_ops_all', - string='Number of Operations in all shuttles', - ) - - operation_descr = fields.Char( - string="Operation", - default="Scan New Destination Location", - readonly=True, - ) - - # tray information (will come from stock.location or a new tray model) - tray_location_id = fields.Many2one( - comodel_name='stock.location', - compute='_compute_tray_matrix', - string='Tray Location', - ) - tray_name = fields.Char(compute='_compute_tray_matrix', string='Tray Name') - tray_type_id = fields.Many2one( - comodel_name='stock.location.tray.type', - compute='_compute_tray_matrix', - string='Tray Type', - ) - tray_type_code = fields.Char( - compute='_compute_tray_matrix', string='Tray Code' - ) - tray_x = fields.Integer(string='X', compute='_compute_tray_matrix') - tray_y = fields.Integer(string='Y', compute='_compute_tray_matrix') - tray_matrix = Serialized(string='Cells', compute='_compute_tray_matrix') - tray_qty = fields.Float( - string='Stock Quantity', compute='_compute_tray_qty' - ) - - # current operation information - picking_id = fields.Many2one( - related='current_move_line_id.picking_id', readonly=True - ) - picking_origin = fields.Char( - related='current_move_line_id.picking_id.origin', readonly=True - ) - picking_partner_id = fields.Many2one( - related='current_move_line_id.picking_id.partner_id', readonly=True - ) - product_id = fields.Many2one( - related='current_move_line_id.product_id', readonly=True - ) - product_uom_id = fields.Many2one( - related='current_move_line_id.product_uom_id', readonly=True - ) - product_uom_qty = fields.Float( - related='current_move_line_id.product_uom_qty', readonly=True - ) - product_packagings = fields.Html( - string='Packaging', compute='_compute_product_packagings' - ) - qty_done = fields.Float( - related='current_move_line_id.qty_done', readonly=True - ) - lot_id = fields.Many2one( - related='current_move_line_id.lot_id', readonly=True - ) - location_dest_id = fields.Many2one( - string="Destination", - related='current_move_line_id.location_dest_id', - readonly=True, - ) - - # TODO add a glue addon with product_expiry to add the field - - _barcode_scanned = fields.Char( - "Barcode Scanned", - help="Value of the last barcode scanned.", - store=False, + selection="_selection_hardware", default="simulation", required=True ) _sql_constraints = [ - ('location_id_unique', 'UNIQUE(location_id)', - 'You cannot have two shuttles using the same location.'), - ] - - def on_barcode_scanned(self, barcode): - self.ensure_one() - # FIXME notify_info is only for the demo - self.env.user.notify_info('Scanned barcode: {}'.format(barcode)) - method = 'on_barcode_scanned_{}'.format(self.mode) - getattr(self, method)(barcode) - - def on_barcode_scanned_pick(self, barcode): - location = self.env['stock.location'].search( - [('barcode', '=', barcode)] + ( + "location_id_unique", + "UNIQUE(location_id)", + "You cannot have two shuttles using the same location.", ) - if location: - self.current_move_line_id.location_dest_id = location - self.operation_descr = _('Save') - else: - self.env.user.notify_warning( - _('No location found for barcode {}').format(barcode) - ) - - def on_barcode_scanned_put(self, barcode): - pass - - def on_barcode_scanned_inventory(self, barcode): - pass + ] @api.model def _selection_hardware(self): - return [('simulation', 'Simulation')] + return [("simulation", "Simulation")] - @api.depends('current_move_line_id.product_id.packaging_ids') - def _compute_product_packagings(self): - for record in self: - if not record.current_move_line_id: - continue - product = record.current_move_line_id.product_id - values = { - 'packagings': [ - { - 'name': pkg.name, - 'qty': pkg.qty, - 'unit': product.uom_id.name, - } - for pkg in product.packaging_ids - ] - } - content = self.env['ir.qweb'].render( - 'stock_vertical_lift.packagings', values - ) - record.product_packagings = content - - @api.depends() - def _compute_number_of_ops(self): - for record in self: - record.number_of_ops = record.count_move_lines_to_do() - - @api.depends() - def _compute_number_of_ops_all(self): - for record in self: - record.number_of_ops_all = record.count_move_lines_to_do_all() - - @api.depends('tray_location_id', 'current_move_line_id.product_id') - def _compute_tray_qty(self): - for record in self: - if not (record.tray_location_id and record.current_move_line_id): - continue - product = record.current_move_line_id.product_id - quants = self.env['stock.quant'].search( - [ - ('location_id', '=', record.tray_location_id.id), - ('product_id', '=', product.id), - ] - ) - record.tray_qty = sum(quants.mapped('quantity')) - - @api.depends() - def _compute_tray_matrix(self): - for record in self: - modes = { - 'pick': 'location_id', - 'put': 'location_dest_id', - # TODO what to do for inventory? - 'inventory': 'location_id', - } - location = record.current_move_line_id[modes[record.mode]] - tray_type = location.location_id.tray_type_id - selected = [] - cells = [] - if location: - selected = location._tray_cell_coords() - cells = location._tray_cell_matrix() - - # this is the current cell - record.tray_location_id = location.id - # name of the tray where the cell is - record.tray_name = location.location_id.name - record.tray_type_id = tray_type.id - record.tray_type_code = tray_type.code - record.tray_x = location.posx - record.tray_y = location.posy - record.tray_matrix = { - # x, y: position of the selected cell - 'selected': selected, - # 0 is empty, 1 is not - 'cells': cells, - } - - def _domain_move_lines_to_do(self): - domain = [ - # TODO check state - ('state', '=', 'assigned') - ] - domain_extensions = { - 'pick': [('location_id', 'child_of', self.location_id.id)], - # TODO ensure that we cannot have the same ml in 2 shuttles (cannot - # happen with 'pick' as they are in the shuttle's location) - 'put': [('location_dest_id', 'child_of', self.location_id.id)], - # TODO - 'inventory': [('id', '=', 0)], + @property + def _model_for_mode(self): + return { + "pick": "vertical.lift.operation.pick", + "put": "vertical.lift.operation.put", + "inventory": "vertical.lift.operation.inventory", } - return domain + domain_extensions[self.mode] - def _domain_move_lines_to_do_all(self): - domain = [ - # TODO check state - ('state', '=', 'assigned') - ] - # TODO search only in the view being a parent of shuttle's location - shuttle_locations = self.env['stock.location'].search( - [('vertical_lift_kind', '=', 'view')] - ) - domain_extensions = { - 'pick': [('location_id', 'child_of', shuttle_locations.ids)], - 'put': [('location_dest_id', 'child_of', shuttle_locations.ids)], - # TODO - 'inventory': [('id', '=', 0)], + @property + def _screen_view_for_mode(self): + return { + "pick": ( + "stock_vertical_lift." + "vertical_lift_operation_pick_screen_view" + ), + "put": ( + "stock_vertical_lift." + "vertical_lift_operation_put_screen_view" + ), + "inventory": ( + "stock_vertical_lift." + "vertical_lift_operation_inventory_screen_view" + ), } - return domain + domain_extensions[self.mode] - def count_move_lines_to_do(self): - self.ensure_one() - return self.env['stock.move.line'].search_count( - self._domain_move_lines_to_do() - ) - - def count_move_lines_to_do_all(self): - self.ensure_one() - return self.env['stock.move.line'].search_count( - self._domain_move_lines_to_do_all() - ) - - def button_release(self): - self.select_next_move_line() - if not self.current_move_line_id: - # sorry not sorry - return { - 'effect': { - 'fadeout': 'slow', - 'message': _('Congrats, you cleared the queue!'), - 'img_url': '/web/static/src/img/smile.svg', - 'type': 'rainbow_man', - } - } - - def process_current_pick(self): - # test code, TODO the smart one - # (scan of barcode increments qty, save calls action_done?) - line = self.current_move_line_id - if line.state != 'done': - line.qty_done = line.product_qty - line.move_id._action_done() - - def process_current_put(self): - raise exceptions.UserError(_('Put workflow not implemented')) - - def process_current_inventory(self): - raise exceptions.UserError(_('Inventory workflow not implemented')) - - def button_save(self): - if not (self and self.current_move_line_id): - return - self.ensure_one() - method = 'process_current_{}'.format(self.mode) - getattr(self, method)() - self.operation_descr = _('Release') - - def select_next_move_line(self): - self.ensure_one() - next_move_line = self.env['stock.move.line'].search( - self._domain_move_lines_to_do(), limit=1 - ) - self.current_move_line_id = next_move_line - # TODO use a state machine to define next steps and - # description? - descr = ( - _('Scan New Destination Location') - if next_move_line - else _('No operations') - ) - self.operation_descr = descr - if next_move_line: - # TODO different method (source vs dest) on pick/put scenario - next_move_line.fetch_vertical_lift_tray_source() + def _operation_for_mode(self): + model = self._model_for_mode[self.mode] + record = self.env[model].search([("shuttle_id", "=", self.id)]) + if not record: + record = self.env[model].create({"shuttle_id": self.id}) + return record def action_open_screen(self): - self.select_next_move_line() self.ensure_one() - screen_xmlid = ( - 'stock_vertical_lift.vertical_lift_shuttle_view_form_screen' - ) + assert self.mode in ("pick", "put", "inventory") + screen_xmlid = self._screen_view_for_mode[self.mode] + operation = self._operation_for_mode() + operation.on_screen_open() return { - 'type': 'ir.actions.act_window', - 'res_model': self._name, - 'views': [[self.env.ref(screen_xmlid).id, 'form']], - 'res_id': self.id, - 'target': 'fullscreen', - 'flags': { - 'headless': True, - 'form_view_initial_mode': 'edit', - 'no_breadcrumbs': True, + "type": "ir.actions.act_window", + "res_model": operation._name, + "views": [[self.env.ref(screen_xmlid).id, "form"]], + "res_id": operation.id, + "target": "fullscreen", + "flags": { + "headless": True, + "form_view_initial_mode": "edit", + "no_breadcrumbs": True, }, } def action_menu(self): - menu_xmlid = 'stock_vertical_lift.vertical_lift_shuttle_form_menu' + menu_xmlid = "stock_vertical_lift.vertical_lift_shuttle_form_menu" return { - 'type': 'ir.actions.act_window', - 'res_model': 'vertical.lift.shuttle', - 'views': [[self.env.ref(menu_xmlid).id, 'form']], - 'name': _('Menu'), - 'target': 'new', - 'res_id': self.id, + "type": "ir.actions.act_window", + "res_model": "vertical.lift.shuttle", + "views": [[self.env.ref(menu_xmlid).id, "form"]], + "name": _("Menu"), + "target": "new", + "res_id": self.id, } def action_manual_barcode(self): return { - 'type': 'ir.actions.act_window', - 'res_model': 'vertical.lift.shuttle.manual.barcode', - 'view_mode': 'form', - 'name': _('Barcode'), - 'target': 'new', + "type": "ir.actions.act_window", + "res_model": "vertical.lift.shuttle.manual.barcode", + "view_mode": "form", + "name": _("Barcode"), + "target": "new", } # TODO: should the mode be changed on all the shuttles at the same time? def switch_pick(self): - self.mode = 'pick' - self.select_next_move_line() + self.mode = "pick" + return self.action_open_screen() def switch_put(self): - self.mode = 'put' - self.select_next_move_line() + self.mode = "put" + return self.action_open_screen() def switch_inventory(self): - self.mode = 'inventory' - self.select_next_move_line() + self.mode = "inventory" + return self.action_open_screen() class VerticalLiftShuttleManualBarcode(models.TransientModel): - _name = 'vertical.lift.shuttle.manual.barcode' - _description = 'Action to input a barcode' + _name = "vertical.lift.shuttle.manual.barcode" + _description = "Action to input a barcode" barcode = fields.Char(string="Barcode") @api.multi def button_save(self): - shuttle_id = self.env.context.get('active_id') - shuttle = self.env['vertical.lift.shuttle'].browse(shuttle_id).exists() - if not shuttle: + active_id = self.env.context.get("active_id") + model = self.env.context.get("active_model") + record = self.env[model].browse(active_id).exists() + if not record: return if self.barcode: - shuttle.on_barcode_scanned(self.barcode) + record.on_barcode_scanned(self.barcode) diff --git a/stock_vertical_lift/security/ir.model.access.csv b/stock_vertical_lift/security/ir.model.access.csv index dc7cb4b87..2a69468e4 100644 --- a/stock_vertical_lift/security/ir.model.access.csv +++ b/stock_vertical_lift/security/ir.model.access.csv @@ -1,3 +1,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_vertical_lift_shuttle_stock_user,access_vertical_lift_shuttle stock user,model_vertical_lift_shuttle,stock.group_stock_user,1,0,0,0 access_vertical_lift_shuttle_manager,access_vertical_lift_shuttle stock manager,model_vertical_lift_shuttle,stock.group_stock_manager,1,1,1,1 +access_vertical_lift_operation_pick_stock_user,access_vertical_lift_operation_pick stock user,model_vertical_lift_operation_pick,stock.group_stock_user,1,1,1,1 +access_vertical_lift_operation_put_stock_user,access_vertical_lift_operation_put stock user,model_vertical_lift_operation_put,stock.group_stock_user,1,1,1,1 +access_vertical_lift_operation_inventory_stock_user,access_vertical_lift_operation_inventory stock user,model_vertical_lift_operation_inventory,stock.group_stock_user,1,1,1,1 diff --git a/stock_vertical_lift/tests/test_vertical_lift_shuttle.py b/stock_vertical_lift/tests/test_vertical_lift_shuttle.py index 1575051cb..b80e46c1e 100644 --- a/stock_vertical_lift/tests/test_vertical_lift_shuttle.py +++ b/stock_vertical_lift/tests/test_vertical_lift_shuttle.py @@ -13,7 +13,7 @@ class TestVerticalLiftTrayType(VerticalLiftCase): def setUpClass(cls): super().setUpClass() cls.picking_out = cls.env.ref( - 'stock_vertical_lift.stock_picking_out_demo_vertical_lift_1' + "stock_vertical_lift.stock_picking_out_demo_vertical_lift_1" ) # we have a move line to pick created by demo picking # stock_picking_out_demo_vertical_lift_1 @@ -21,114 +21,112 @@ class TestVerticalLiftTrayType(VerticalLiftCase): def test_switch_pick(self): self.shuttle.switch_pick() - self.assertEqual(self.shuttle.mode, 'pick') - self.assertEqual(self.shuttle.current_move_line_id, self.out_move_line) + self.assertEqual(self.shuttle.mode, "pick") + self.assertEqual( + self.shuttle._operation_for_mode().current_move_line_id, + self.out_move_line, + ) def test_switch_put(self): self.shuttle.switch_put() - self.assertEqual(self.shuttle.mode, 'put') + self.assertEqual(self.shuttle.mode, "put") # TODO check that we have an incoming move when switching self.assertEqual( - self.shuttle.current_move_line_id, - self.env['stock.move.line'].browse(), + self.shuttle._operation_for_mode().current_move_line_id, + self.env["stock.move.line"].browse(), ) def test_switch_inventory(self): self.shuttle.switch_inventory() - self.assertEqual(self.shuttle.mode, 'inventory') - # TODO check that we have what we should (what?) - self.assertEqual( - self.shuttle.current_move_line_id, - self.env['stock.move.line'].browse(), - ) + self.assertEqual(self.shuttle.mode, "inventory") def test_pick_action_open_screen(self): self.shuttle.switch_pick() action = self.shuttle.action_open_screen() - self.assertTrue(self.shuttle.current_move_line_id) - self.assertEqual(action['type'], 'ir.actions.act_window') - self.assertEqual(action['res_model'], 'vertical.lift.shuttle') - self.assertEqual(action['res_id'], self.shuttle.id) + operation = self.shuttle._operation_for_mode() + self.assertTrue(operation.current_move_line_id) + self.assertEqual(action["type"], "ir.actions.act_window") + self.assertEqual(action["res_model"], "vertical.lift.operation.pick") + self.assertEqual(action["res_id"], operation.id) def test_pick_select_next_move_line(self): - self.shuttle.switch_pick() - self.shuttle.select_next_move_line() - self.assertEqual(self.shuttle.current_move_line_id, self.out_move_line) + self.shuttle.mode = "pick" + operation = self.shuttle._operation_for_mode() + operation.select_next_move_line() + self.assertEqual(operation.current_move_line_id, self.out_move_line) self.assertEqual( - self.shuttle.operation_descr, - _('Scan New Destination Location') + operation.operation_descr, _("Scan New Destination Location") ) def test_pick_save(self): self.shuttle.switch_pick() - self.shuttle.current_move_line_id = self.out_move_line - self.shuttle.button_save() - self.assertEqual( - self.shuttle.current_move_line_id.state, - 'done' - ) - self.assertEqual(self.shuttle.operation_descr, _('Release')) + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line + operation.button_save() + self.assertEqual(operation.current_move_line_id.state, "done") + self.assertEqual(operation.operation_descr, _("Release")) def test_pick_related_fields(self): self.shuttle.switch_pick() - ml = self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + ml = operation.current_move_line_id = self.out_move_line # Trays related fields # For pick, this is the source location, which is the cell where the # product is. - self.assertEqual(self.shuttle.tray_location_id, ml.location_id) + self.assertEqual(operation.tray_location_id, ml.location_id) self.assertEqual( - self.shuttle.tray_name, + operation.tray_name, # parent = tray ml.location_id.location_id.name, ) self.assertEqual( - self.shuttle.tray_type_id, + operation.tray_type_id, # the tray type is on the parent of the cell (on the tray) ml.location_id.location_id.tray_type_id, ) self.assertEqual( - self.shuttle.tray_type_code, + operation.tray_type_code, ml.location_id.location_id.tray_type_id.code, ) - self.assertEqual(self.shuttle.tray_x, ml.location_id.posx) - self.assertEqual(self.shuttle.tray_y, ml.location_id.posy) + self.assertEqual(operation.tray_x, ml.location_id.posx) + self.assertEqual(operation.tray_y, ml.location_id.posy) # Move line related fields - self.assertEqual(self.shuttle.picking_id, ml.picking_id) - self.assertEqual(self.shuttle.picking_origin, ml.picking_id.origin) + self.assertEqual(operation.picking_id, ml.picking_id) + self.assertEqual(operation.picking_origin, ml.picking_id.origin) self.assertEqual( - self.shuttle.picking_partner_id, ml.picking_id.partner_id + operation.picking_partner_id, ml.picking_id.partner_id ) - self.assertEqual(self.shuttle.product_id, ml.product_id) - self.assertEqual(self.shuttle.product_uom_id, ml.product_uom_id) - self.assertEqual(self.shuttle.product_uom_qty, ml.product_uom_qty) - self.assertEqual(self.shuttle.qty_done, ml.qty_done) - self.assertEqual(self.shuttle.lot_id, ml.lot_id) + self.assertEqual(operation.product_id, ml.product_id) + self.assertEqual(operation.product_uom_id, ml.product_uom_id) + self.assertEqual(operation.product_uom_qty, ml.product_uom_qty) + self.assertEqual(operation.qty_done, ml.qty_done) + self.assertEqual(operation.lot_id, ml.lot_id) def test_pick_count_move_lines(self): - product1 = self.env.ref('stock_vertical_lift.product_running_socks') - product2 = self.env.ref('stock_vertical_lift.product_recovery_socks') + product1 = self.env.ref("stock_vertical_lift.product_running_socks") + product2 = self.env.ref("stock_vertical_lift.product_recovery_socks") # cancel the picking from demo data to start from a clean state self.env.ref( - 'stock_vertical_lift.stock_picking_out_demo_vertical_lift_1' + "stock_vertical_lift.stock_picking_out_demo_vertical_lift_1" ).action_cancel() # ensure that we have stock in some cells, we'll put product1 # in the first Shuttle and product2 in the second cell1 = self.env.ref( - 'stock_vertical_lift.' - 'stock_location_vertical_lift_demo_tray_1a_x3y2' + "stock_vertical_lift." + "stock_location_vertical_lift_demo_tray_1a_x3y2" ) self._update_quantity_in_cell(cell1, product1, 50) cell2 = self.env.ref( - 'stock_vertical_lift.' - 'stock_location_vertical_lift_demo_tray_2a_x1y1' + "stock_vertical_lift." + "stock_location_vertical_lift_demo_tray_2a_x1y1" ) self._update_quantity_in_cell(cell2, product2, 50) # create pickings (we already have an existing one from demo data) - pickings = self.env['stock.picking'].browse() + pickings = self.env["stock.picking"].browse() pickings |= self._create_simple_picking_out(product1, 1) pickings |= self._create_simple_picking_out(product1, 1) pickings |= self._create_simple_picking_out(product1, 1) @@ -144,41 +142,43 @@ class TestVerticalLiftTrayType(VerticalLiftCase): pickings.action_assign() shuttle1 = self.shuttle + operation1 = shuttle1._operation_for_mode() shuttle2 = self.env.ref( - 'stock_vertical_lift.stock_vertical_lift_demo_shuttle_2' + "stock_vertical_lift.stock_vertical_lift_demo_shuttle_2" ) + operation2 = shuttle2._operation_for_mode() - self.assertEqual(shuttle1.number_of_ops, 4) - self.assertEqual(shuttle2.number_of_ops, 2) - self.assertEqual(shuttle1.number_of_ops_all, 6) - self.assertEqual(shuttle2.number_of_ops_all, 6) + self.assertEqual(operation1.number_of_ops, 4) + self.assertEqual(operation2.number_of_ops, 2) + self.assertEqual(operation1.number_of_ops_all, 6) + self.assertEqual(operation2.number_of_ops_all, 6) # Process a line, should change the numbers. - shuttle1.select_next_move_line() - shuttle1.process_current_pick() - self.assertEqual(shuttle1.number_of_ops, 3) - self.assertEqual(shuttle2.number_of_ops, 2) - self.assertEqual(shuttle1.number_of_ops_all, 5) - self.assertEqual(shuttle2.number_of_ops_all, 5) + operation1.select_next_move_line() + operation1.process_current() + self.assertEqual(operation1.number_of_ops, 3) + self.assertEqual(operation2.number_of_ops, 2) + self.assertEqual(operation1.number_of_ops_all, 5) + self.assertEqual(operation2.number_of_ops_all, 5) # add stock and make the last one assigned to check the number is # updated self._update_quantity_in_cell(cell2, product2, 10) unassigned.action_assign() - self.assertEqual(shuttle1.number_of_ops, 3) - self.assertEqual(shuttle2.number_of_ops, 3) - self.assertEqual(shuttle1.number_of_ops_all, 6) - self.assertEqual(shuttle2.number_of_ops_all, 6) + self.assertEqual(operation1.number_of_ops, 3) + self.assertEqual(operation2.number_of_ops, 3) + self.assertEqual(operation1.number_of_ops_all, 6) + self.assertEqual(operation2.number_of_ops_all, 6) - @unittest.skip('Not implemented') + @unittest.skip("Not implemented") def test_put_count_move_lines(self): pass - @unittest.skip('Not implemented') + @unittest.skip("Not implemented") def test_inventory_count_move_lines(self): pass - @unittest.skip('Not implemented') + @unittest.skip("Not implemented") def test_on_barcode_scanned(self): # test to implement when the code is implemented pass @@ -188,48 +188,52 @@ class TestVerticalLiftTrayType(VerticalLiftCase): self.out_move_line.qty_done = self.out_move_line.product_qty self.out_move_line.move_id._action_done() # release, no further operation in queue - result = self.shuttle.button_release() - self.assertFalse(self.shuttle.current_move_line_id) - self.assertEqual(self.shuttle.operation_descr, _('No operations')) + operation = self.shuttle._operation_for_mode() + result = operation.button_release() + self.assertFalse(operation.current_move_line_id) + self.assertEqual(operation.operation_descr, _("No operations")) expected_result = { - 'effect': { - 'fadeout': 'slow', - 'message': _('Congrats, you cleared the queue!'), - 'img_url': '/web/static/src/img/smile.svg', - 'type': 'rainbow_man', + "effect": { + "fadeout": "slow", + "message": _("Congrats, you cleared the queue!"), + "img_url": "/web/static/src/img/smile.svg", + "type": "rainbow_man", } } self.assertEqual(result, expected_result) def test_process_current_pick(self): self.shuttle.switch_pick() - self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line qty_to_process = self.out_move_line.product_qty - self.shuttle.process_current_pick() - self.assertEqual(self.out_move_line.state, 'done') + operation.process_current() + self.assertEqual(self.out_move_line.state, "done") self.assertEqual(self.out_move_line.qty_done, qty_to_process) def test_process_current_put(self): # test to implement when the code is implemented + self.shuttle.switch_put() + operation = self.shuttle._operation_for_mode() with self.assertRaises(exceptions.UserError): - self.shuttle.process_current_put() + operation.process_current() def test_process_current_inventory(self): # test to implement when the code is implemented - with self.assertRaises(exceptions.UserError): - self.shuttle.process_current_inventory() + self.shuttle.switch_inventory() def test_matrix(self): self.shuttle.switch_pick() - self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line location = self.out_move_line.location_id # offset by -1 because the fields are for humans expected_x = location.posx - 1 expected_y = location.posy - 1 self.assertEqual( - self.shuttle.tray_matrix, + operation.tray_matrix, { - 'selected': [expected_x, expected_y], + "selected": [expected_x, expected_y], # fmt: off 'cells': [ [0, 0, 0, 0, 0, 0, 0, 0], @@ -241,12 +245,13 @@ class TestVerticalLiftTrayType(VerticalLiftCase): def test_tray_qty(self): cell = self.env.ref( - 'stock_vertical_lift.' - 'stock_location_vertical_lift_demo_tray_1a_x3y2' + "stock_vertical_lift." + "stock_location_vertical_lift_demo_tray_1a_x3y2" ) self.out_move_line.location_id = cell - self.shuttle.current_move_line_id = self.out_move_line + operation = self.shuttle._operation_for_mode() + operation.current_move_line_id = self.out_move_line self._update_quantity_in_cell(cell, self.out_move_line.product_id, 50) - self.assertEqual(self.shuttle.tray_qty, 50) + self.assertEqual(operation.tray_qty, 50) self._update_quantity_in_cell(cell, self.out_move_line.product_id, -20) - self.assertEqual(self.shuttle.tray_qty, 30) + self.assertEqual(operation.tray_qty, 30) diff --git a/stock_vertical_lift/views/vertical_lift_operation_base_views.xml b/stock_vertical_lift/views/vertical_lift_operation_base_views.xml new file mode 100644 index 000000000..752dd5940 --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_base_views.xml @@ -0,0 +1,147 @@ + + + + + vertical.lift.operation.base.screen.view + vertical.lift.operation.base + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+ +
+
+
+
+ + +
+
+ + + vertical.lift.operation.transfer.screen.view + vertical.lift.operation.transfer + + primary + + + + + {'invisible': [('current_move_line_id', '=', False)]} + + + + +
+
+ + + +
+
+ +
+ + + + + + + +
+ +
+
+
+
+
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_operation_inventory_views.xml b/stock_vertical_lift/views/vertical_lift_operation_inventory_views.xml new file mode 100644 index 000000000..509b471c8 --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_inventory_views.xml @@ -0,0 +1,19 @@ + + + + + vertical.lift.operation.inventory.screen.view + vertical.lift.operation.inventory + + primary + +
+ Inventory Screen +
+ + Not implemented + +
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml b/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml new file mode 100644 index 000000000..2fc407f9e --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_pick_views.xml @@ -0,0 +1,16 @@ + + + + + vertical.lift.operation.pick.screen.view + vertical.lift.operation.pick + + primary + +
+ Pick Screen +
+
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_operation_put_views.xml b/stock_vertical_lift/views/vertical_lift_operation_put_views.xml new file mode 100644 index 000000000..4a891d985 --- /dev/null +++ b/stock_vertical_lift/views/vertical_lift_operation_put_views.xml @@ -0,0 +1,16 @@ + + + + + vertical.lift.operation.put.screen.view + vertical.lift.operation.put + + primary + +
+ Put Screen +
+
+
+ +
diff --git a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml index 6d5bb6d53..97169a43c 100644 --- a/stock_vertical_lift/views/vertical_lift_shuttle_views.xml +++ b/stock_vertical_lift/views/vertical_lift_shuttle_views.xml @@ -1,133 +1,6 @@ - - vertical.lift.shuttle.view.form.screen - vertical.lift.shuttle - 99 - -
- -
-
- -
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
- -
-
-
- -
-
- - - -
-
- -
- - - - - - - -
- -
-
-
-
- - -
-
- vertical.lift.shuttle.view.form.menu vertical.lift.shuttle @@ -190,9 +63,6 @@ - - @@ -205,8 +75,8 @@ remove later --> - - + +
@@ -224,14 +94,14 @@ remove later --> Mode:
-
- Operations: - -
-
- All Operations: - -
+ + + + + + + +