diff options
5 files changed, 292 insertions, 57 deletions
diff --git a/addons/stock_picking_batch/data/stock_picking_batch_demo.xml b/addons/stock_picking_batch/data/stock_picking_batch_demo.xml index b4f00288029..69c8edf0e76 100644 --- a/addons/stock_picking_batch/data/stock_picking_batch_demo.xml +++ b/addons/stock_picking_batch/data/stock_picking_batch_demo.xml @@ -4,10 +4,12 @@ <!-- Add batch picking --> <record id="stock_picking_batch_dry_1" model="stock.picking.batch"> <field name="state">in_progress</field> + <field name="picking_type_id" ref="stock.picking_type_internal"/> <field name="company_id" ref="base.main_company"/> </record> <record id="stock_picking_batch_freeze_1" model="stock.picking.batch"> <field name="state">in_progress</field> + <field name="picking_type_id" ref="stock.picking_type_internal"/> <field name="company_id" ref="base.main_company"/> </record> diff --git a/addons/stock_picking_batch/models/stock_picking.py b/addons/stock_picking_batch/models/stock_picking.py index b7501f41de0..dd8470bee70 100644 --- a/addons/stock_picking_batch/models/stock_picking.py +++ b/addons/stock_picking_batch/models/stock_picking.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import fields, models +from odoo import api, fields, models class StockPicking(models.Model): @@ -13,6 +13,21 @@ class StockPicking(models.Model): states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help='Batch associated to this transfer', copy=False) + @api.model + def create(self, vals): + res = super().create(vals) + if vals.get('batch_id'): + self.batch_id._sanity_check() + return res + + def write(self, vals): + res = super().write(vals) + if vals.get('batch_id'): + if not self.batch_id.picking_type_id: + self.batch_id.picking_type_id = self.picking_type_id[0] + self.batch_id._sanity_check() + return res + def _should_show_transfers(self): if len(self.batch_id) == 1 and self == self.batch_id.picking_ids: return False diff --git a/addons/stock_picking_batch/models/stock_picking_batch.py b/addons/stock_picking_batch/models/stock_picking_batch.py index 70e4c1b17f2..5e13a4649a9 100644 --- a/addons/stock_picking_batch/models/stock_picking_batch.py +++ b/addons/stock_picking_batch/models/stock_picking_batch.py @@ -17,48 +17,134 @@ class StockPickingBatch(models.Model): help='Name of the batch transfer') user_id = fields.Many2one( 'res.users', string='Responsible', tracking=True, check_company=True, + readonly=True, states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]}, help='Person responsible for this batch transfer') company_id = fields.Many2one( 'res.company', string="Company", required=True, readonly=True, index=True, default=lambda self: self.env.company) picking_ids = fields.One2many( - 'stock.picking', 'batch_id', string='Transfers', - domain="[('company_id', '=', company_id), ('state', 'not in', ('done', 'cancel'))]", + 'stock.picking', 'batch_id', string='Transfers', readonly=True, + domain="[('id', 'in', allowed_picking_ids)]", check_company=True, + states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]}, help='List of transfers associated to this batch') + show_check_availability = fields.Boolean( + compute='_compute_move_ids', + help='Technical field used to compute whether the check availability button should be shown.') + allowed_picking_ids = fields.One2many('stock.picking', compute='_compute_allowed_picking_ids') + move_ids = fields.One2many( + 'stock.move', string="Stock moves", compute='_compute_move_ids') + move_line_ids = fields.One2many( + 'stock.move.line', string='Stock move lines', + compute='_compute_move_ids', inverse='_set_move_line_ids', readonly=True, + states={'draft': [('readonly', False)], 'in_progress': [('readonly', False)]}) state = fields.Selection([ ('draft', 'Draft'), ('in_progress', 'In progress'), ('done', 'Done'), ('cancel', 'Cancelled')], default='draft', copy=False, tracking=True, required=True, readonly=True) + picking_type_id = fields.Many2one( + 'stock.picking.type', 'Operation Type', check_company=True, copy=False, + readonly=True, states={'draft': [('readonly', False)]}) + @api.depends('company_id', 'picking_type_id', 'state') + def _compute_allowed_picking_ids(self): + allowed_picking_states = ['waiting', 'confirmed', 'assigned'] + cancelled_batchs = self.env['stock.picking.batch'].search_read( + [('state', '=', 'cancel')], ['id'] + ) + cancelled_batch_ids = [batch['id'] for batch in cancelled_batchs] + + for batch in self: + domain_states = list(allowed_picking_states) + # Allows to add draft pickings only if batch is in draft as well. + if batch.state == 'draft': + domain_states.append('draft') + domain = [ + ('company_id', '=', batch.company_id.id), + ('immediate_transfer', '=', False), + ('state', 'in', domain_states), + '|', + '|', + ('batch_id', '=', False), + ('batch_id', '=', batch.id), + ('batch_id', 'in', cancelled_batch_ids), + ] + if batch.picking_type_id: + domain += [('picking_type_id', '=', batch.picking_type_id.id)] + batch.allowed_picking_ids = self.env['stock.picking'].search(domain) + + @api.depends('picking_ids', 'picking_ids.move_line_ids', 'picking_ids.move_lines', 'picking_ids.move_lines.state') + def _compute_move_ids(self): + for batch in self: + batch.move_ids = batch.picking_ids.move_lines + batch.move_line_ids = batch.picking_ids.move_line_ids + batch.show_check_availability = any(m.state != 'assigned' for m in batch.move_ids) + + + def _set_move_line_ids(self): + new_move_lines = self[0].move_line_ids + for picking in self.picking_ids: + old_move_lines = picking.move_line_ids + picking.move_line_ids = new_move_lines.filtered(lambda ml: ml.picking_id.id == picking.id) + move_lines_to_unlink = old_move_lines - new_move_lines + if move_lines_to_unlink: + move_lines_to_unlink.unlink() + + # ------------------------------------------------------------------------- + # CRUD + # ------------------------------------------------------------------------- @api.model def create(self, vals): if vals.get('name', '/') == '/': vals['name'] = self.env['ir.sequence'].next_by_code('picking.batch') or '/' - return super(StockPickingBatch, self).create(vals) + return super().create(vals) + + def write(self, vals): + res = super().write(vals) + if vals.get('picking_type_id'): + self._sanity_check() + if vals.get('picking_ids'): + batch_without_picking_type = self.filtered(lambda batch: not batch.picking_type_id) + if batch_without_picking_type: + picking = self.picking_ids and self.picking_ids[0] + batch_without_picking_type.picking_type_id = picking.picking_type_id.id + return res - def confirm_picking(self): + def unlink(self): + if any(batch.state != 'draft' for batch in self): + raise UserError(_("You can only delete draft batch transfers.")) + return super().unlink() + + # ------------------------------------------------------------------------- + # Action methods + # ------------------------------------------------------------------------- + def action_confirm(self): + """Sanity checks, confirm the pickings and mark the batch as confirmed.""" + self.ensure_one() + if not self.picking_ids: + raise UserError(_("You have to set some pickings to batch.")) + self.picking_ids.action_confirm() self._check_company() - pickings_todo = self.mapped('picking_ids') - self.write({'state': 'in_progress'}) - return pickings_todo.action_assign() - - def cancel_picking(self): - self.mapped('picking_ids').action_cancel() - return self.write({'state': 'cancel'}) - - def print_picking(self): - pickings = self.mapped('picking_ids') - if not pickings: - raise UserError(_('Nothing to print.')) + self.state = 'in_progress' + return True + + def action_cancel(self): + self.ensure_one() + self.state = 'cancel' + return True + + def action_print(self): + self.ensure_one() return self.env.ref('stock_picking_batch.action_report_picking_batch').report_action(self) - def done(self): + def action_done(self): + self.ensure_one() self._check_company() pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done')) if any(picking.state not in ('assigned') for picking in pickings): raise UserError(_('Some transfers are still waiting for goods. Please check or force their availability before setting this batch to done.')) + for picking in pickings: picking.message_post( body="<b>%s:</b> %s <a href=#id=%s&view_type=form&model=stock.picking.batch>%s</a>" % ( @@ -70,8 +156,25 @@ class StockPickingBatch(models.Model): self.write({'state': 'done'}) return self.picking_ids.button_validate() + def action_assign(self): + self.ensure_one() + self.picking_ids.action_assign() + + # ------------------------------------------------------------------------- + # Miscellaneous + # ------------------------------------------------------------------------- + def _sanity_check(self): + for batch in self: + if not batch.picking_ids <= batch.allowed_picking_ids: + erroneous_pickings = batch.picking_ids - batch.allowed_picking_ids + raise UserError(_( + "The following transfers cannot be added to batch transfer %s. " + "Please check their states and operation types, if they aren't immediate " + + "transfers or if they're not already part of another batch transfer.\n\n" + "Incompatibilities: %s") % (batch.name, ', '.join(erroneous_pickings.mapped('name')))) + def _track_subtype(self, init_values): if 'state' in init_values: return self.env.ref('stock_picking_batch.mt_batch_state') - return super(StockPickingBatch, self)._track_subtype(init_values) + return super()._track_subtype(init_values) diff --git a/addons/stock_picking_batch/tests/test_batch_picking.py b/addons/stock_picking_batch/tests/test_batch_picking.py index 1a41622bb99..f23270e003c 100644 --- a/addons/stock_picking_batch/tests/test_batch_picking.py +++ b/addons/stock_picking_batch/tests/test_batch_picking.py @@ -71,14 +71,18 @@ class TestBatchPicking(TransactionCase): self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0) self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0) - # confirm batch, picking should be assigned - self.batch.confirm_picking() - self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be reserved') - self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be reserved') + # Confirm batch, pickings should not be automatically assigned. + self.batch.action_confirm() + self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed') + self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed') + # Ask to assign, so pickings should be assigned now. + self.batch.action_assign() + self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready') + self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready') self.picking_client_1.move_lines.quantity_done = 10 self.picking_client_2.move_lines.quantity_done = 10 - self.batch.done() + self.batch.action_done() self.assertEqual(self.picking_client_1.state, 'done', 'Picking 1 should be done') self.assertEqual(self.picking_client_2.state, 'done', 'Picking 2 should be done') @@ -98,13 +102,17 @@ class TestBatchPicking(TransactionCase): self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 10.0) self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0) - # confirm batch, picking should be assigned - self.batch.confirm_picking() - self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be reserved') - self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be reserved') + # Confirm batch, pickings should not be automatically assigned. + self.batch.action_confirm() + self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed') + self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed') + # Ask to assign, so pickings should be assigned now. + self.batch.action_assign() + self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready') + self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready') # There should be a wizard asking to process picking without quantity done - immediate_transfer_wizard_dict = self.batch.done() + immediate_transfer_wizard_dict = self.batch.action_done() self.assertTrue(immediate_transfer_wizard_dict) immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save() self.assertEqual(len(immediate_transfer_wizard.pick_ids), 2) @@ -128,8 +136,12 @@ class TestBatchPicking(TransactionCase): self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0) self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0) - # confirm batch, picking should be assigned - self.batch.confirm_picking() + # Confirm batch, pickings should not be automatically assigned. + self.batch.action_confirm() + self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed') + self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed') + # Ask to assign, so pickings should be assigned now. + self.batch.action_assign() self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready') self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready') @@ -137,7 +149,7 @@ class TestBatchPicking(TransactionCase): self.picking_client_2.move_lines.quantity_done = 10 # There should be a wizard asking to process picking without quantity done - back_order_wizard_dict = self.batch.done() + back_order_wizard_dict = self.batch.action_done() self.assertTrue(back_order_wizard_dict) back_order_wizard = Form(self.env[(back_order_wizard_dict.get('res_model'))].with_context(back_order_wizard_dict['context'])).save() self.assertEqual(len(back_order_wizard.pick_ids), 1) @@ -164,13 +176,17 @@ class TestBatchPicking(TransactionCase): self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0) self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0) - # confirm batch, picking should be assigned - self.batch.confirm_picking() + # Confirm batch, pickings should not be automatically assigned. + self.batch.action_confirm() + self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed') + self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed') + # Ask to assign, so pickings should be assigned now. + self.batch.action_assign() self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready') self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready') # There should be a wizard asking to process picking without quantity done - immediate_transfer_wizard_dict = self.batch.done() + immediate_transfer_wizard_dict = self.batch.action_done() self.assertTrue(immediate_transfer_wizard_dict) immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save() self.assertEqual(len(immediate_transfer_wizard.pick_ids), 2) @@ -200,14 +216,18 @@ class TestBatchPicking(TransactionCase): self.env['stock.quant']._update_available_quantity(self.productA, self.stock_location, 5.0) self.env['stock.quant']._update_available_quantity(self.productB, self.stock_location, 10.0) - # confirm batch, picking should be assigned - self.batch.confirm_picking() + # Confirm batch, pickings should not be automatically assigned. + self.batch.action_confirm() + self.assertEqual(self.picking_client_1.state, 'confirmed', 'Picking 1 should be confirmed') + self.assertEqual(self.picking_client_2.state, 'confirmed', 'Picking 2 should be confirmed') + # Ask to assign, so pickings should be assigned now. + self.batch.action_assign() self.assertEqual(self.picking_client_1.state, 'assigned', 'Picking 1 should be ready') self.assertEqual(self.picking_client_2.state, 'assigned', 'Picking 2 should be ready') self.picking_client_1.move_lines.quantity_done = 5 # There should be a wizard asking to process picking without quantity done - immediate_transfer_wizard_dict = self.batch.done() + immediate_transfer_wizard_dict = self.batch.action_done() self.assertTrue(immediate_transfer_wizard_dict) immediate_transfer_wizard = Form(self.env[(immediate_transfer_wizard_dict.get('res_model'))].with_context(immediate_transfer_wizard_dict['context'])).save() self.assertEqual(len(immediate_transfer_wizard.pick_ids), 1) diff --git a/addons/stock_picking_batch/views/stock_picking_batch_views.xml b/addons/stock_picking_batch/views/stock_picking_batch_views.xml index a53e19c8c06..b37fd782c9f 100644 --- a/addons/stock_picking_batch/views/stock_picking_batch_views.xml +++ b/addons/stock_picking_batch/views/stock_picking_batch_views.xml @@ -1,17 +1,96 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> + <record id="view_picking_form_inherited" model="ir.ui.view"> + <field name="name">stock_picking_batch.picking.form</field> + <field name="model">stock.picking</field> + <field name="inherit_id" ref="stock.view_picking_form"/> + <field name="mode">primary</field> + <field name="priority" eval="1000"/> + <field name="arch" type="xml"> + <xpath expr="//header" position="replace"> + <header> + <field name="state" widget="statusbar" statusbar_visible="draft,confirmed,partially_available,assigned,done"/> + </header> + </xpath> + </field> + </record> + + + <record id="view_picking_move_tree_inherited" model="ir.ui.view"> + <field name="name">stock_picking_batch.picking.move.tree</field> + <field name="model">stock.move</field> + <field name="inherit_id" ref="stock.view_picking_move_tree"/> + <field name="mode">primary</field> + <field name="priority" eval="1000"/> + <field name="arch" type="xml"> + <xpath expr="//tree" position="attributes"> + <attribute name="delete">0</attribute> + </xpath> + <xpath expr="//field[@name='product_id']" position="after"> + <field name="picking_id" required="1" + attrs="{'readonly': [('id', '!=', False)]}" + domain="[('id', 'in', parent.picking_ids)]" + options="{'no_create_edit': True}"/> + </xpath> + <xpath expr="//field[@name='quantity_done']" position="attributes"> + <attribute name="readonly">1</attribute> + </xpath> + </field> + </record> + + <record id="view_move_line_tree" model="ir.ui.view"> + <field name="name">stock_picking_batch.move.line.tree</field> + <field name="model">stock.move.line</field> + <field name="arch" type="xml"> + <tree editable="top" decoration-muted="state == 'cancel'" string="Move Lines" default_order="location_id"> + <field name="tracking" invisible="1"/> + <field name="state" invisible="1"/> + <field name="product_id" required="1" attrs="{'readonly': [('id', '!=', False)]}"/> + <field name="picking_id" required="1" attrs="{'readonly': [('id', '!=', False)]}" + options="{'no_create_edit': True}" domain="[('id', 'in', parent.picking_ids)]"/> + <field name="lot_id" groups="stock.group_production_lot" attrs="{'readonly': [('tracking', 'not in', ['lot', 'serial'])]}"/> + <field name="location_id"/> + <field name="location_dest_id"/> + <field name="product_uom_qty" readonly="1"/> + <field name="product_uom_id" options="{'no_create': True}" + groups="uom.group_uom" readonly="1" force_save="1"/> + <field name="qty_done"/> + <field name="company_id" groups="base.group_multi_company" force_save="1"/> + </tree> + </field> + </record> <record id="stock_picking_batch_form" model="ir.ui.view"> <field name="name">stock.picking.batch.form</field> <field name="model">stock.picking.batch</field> <field name="arch" type="xml"> <form string="Stock Batch Transfer"> + <field name="show_check_availability" invisible="1"/> <header> - <button name="confirm_picking" states="draft" string="Confirm" type="object" class="oe_highlight"/> - <button name="print_picking" string="Print" type="object" class="oe_highlight"/> - <button name="done" states="in_progress" string="Done" type="object" class="oe_highlight"/> - <button name="cancel_picking" string="Cancel" type="object" states="in_progress"/> - <field name="state" widget="statusbar" statusbar_visible="in_progress,done"/> + <button name="action_confirm" states="draft" string="Confirm" type="object" class="oe_highlight"/> + <button name="action_done" string="Validate" type="object" class="oe_highlight" + attrs="{'invisible': [ + '|', + ('state', '!=', 'in_progress'), + ('show_check_availability', '=', True)]}"/> + <button name="action_assign" string="Check Availability" type="object" class="oe_highlight" + attrs="{'invisible': [ + '|', + ('state', '!=', 'in_progress'), + ('show_check_availability', '=', False)]}"/> + <button name="action_done" string="Validate" type="object" + attrs="{'invisible': [ + '|', + ('state', '!=', 'in_progress'), + ('show_check_availability', '=', False)]}"/> + <button name="action_assign" string="Check Availability" type="object" + attrs="{'invisible': [ + '|', + ('state', '!=', 'draft'), + ('show_check_availability', '=', False)]}"/> + <button name="action_print" states="in_progress,done" string="Print" type="object"/> + <button name="action_cancel" string="Cancel" type="object" states="in_progress"/> + <field name="state" widget="statusbar" statusbar_visible="draft,in_progress,done"/> </header> <sheet> <div class="oe_title"> @@ -20,22 +99,31 @@ <group> <group> <field name="user_id"/> + <field name="picking_type_id"/> <field name="company_id" groups="base.group_multi_company" attrs="{'readonly': [('picking_ids', '!=', [])]}" force_save="1"/> </group> </group> - <separator string="Transfers"/> - <field name="picking_ids" widget="many2many" options="{'not_delete': True}"> - <tree> - <field name="name"/> - <field name="scheduled_date"/> - <field name="location_id"/> - <field name="backorder_id"/> - <field name="origin"/> - <field name="state"/> - <button name="action_assign" string="Confirm picking" type="object" icon="fa-check text-success" attrs="{'invisible': [('state', 'in', ('done', 'cancel', 'confirmed', 'assigned'))]}"/> - <button name="action_cancel" string="Cancel picking" type="object" icon="fa-times-circle text-danger" attrs="{'invisible': [('state', 'in', ('done', 'cancel'))]}"/> - </tree> - </field> + <notebook> + <page string="Detailed Operations" attrs="{'invisible': [('state', '=', 'draft')]}"> + <field name="move_line_ids" context="{'tree_view_ref': 'stock_picking_batch.view_move_line_tree'}"/> + </page> + <page string="Operations" attrs="{'invisible': [('state', '=', 'draft')]}"> + <field name="move_ids" context="{'tree_view_ref': 'stock_picking_batch.view_picking_move_tree_inherited'}"/> + </page> + <page string="Transfers"> + <field name="allowed_picking_ids" invisible="1"/> + <field name="picking_ids" widget="many2many" context="{'form_view_ref': 'stock_picking_batch.view_picking_form_inherited'}"> + <tree> + <field name="name"/> + <field name="scheduled_date"/> + <field name="location_id"/> + <field name="backorder_id"/> + <field name="origin"/> + <field name="state"/> + </tree> + </field> + </page> + </notebook> </sheet> <div class="oe_chatter"> <field name="message_follower_ids" widget="mail_followers" groups="base.group_user"/> @@ -139,7 +227,14 @@ <field name="inherit_id" ref="stock.vpicktree"/> <field name="arch" type="xml"> <xpath expr="//field[@name='state']" position="before"> - <field name="batch_id" optional="show"/> + <field name="batch_id" optional="show" + domain="[ + ('state', 'in', ['draft', 'in_progress']), + '|', + ('picking_type_id', '=', picking_type_id), + ('picking_type_id', '=', False), + ]" + context="{'default_picking_type_id': picking_type_id}"/> </xpath> </field> </record> |
