From d4652e344279bf953409cfba31faf87f93f0d3ba Mon Sep 17 00:00:00 2001 From: Wolfgang Pichler Date: Thu, 21 Nov 2019 19:31:44 +0100 Subject: [PATCH] [ADD] Added functionality to create, reserve, confirm, unreserver, cancel stock moves using task stages --- .../models/project_task.py | 223 +++++++++++++++--- .../views/project_task_view.xml | 10 +- 2 files changed, 204 insertions(+), 29 deletions(-) diff --git a/project_task_material_stock/models/project_task.py b/project_task_material_stock/models/project_task.py index 66fa35e7e1..4d399e6e51 100644 --- a/project_task_material_stock/models/project_task.py +++ b/project_task_material_stock/models/project_task.py @@ -4,15 +4,51 @@ # Copyright 2019 Valentin Vinagre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl) from odoo import _, api, exceptions, fields, models +from odoo.exceptions import UserError class ProjectTaskType(models.Model): _inherit = 'project.task.type' - consume_material = fields.Boolean( + pick_material = fields.Boolean( help="If you mark this check, when a task goes to this state, " - "it will consume the associated materials", + "it will create a stock picking for the associated materials", ) + reserve_material = fields.Boolean( + help="If you mark this check, when a task goes to this state, " + "it will reserve materials in associated stock.pickings", + ) + unreserve_material = fields.Boolean( + help="If you mark this check, when a task goes to this state, " + "it will unreserve materials in associated stock.pickings", + ) + done_material = fields.Boolean( + help="If you mark this check, when a task goes to this state, " + "it will validate the associated stock.pickings if possible", + ) + abort_material = fields.Boolean( + help="If you mark this check, when a task goes to this state, " + "it will try to unreserve and cancel stock.pickings, if picking is already done, then it will return goods", + ) + pick_type_id = fields.Many2one( + comodel_name='stock.picking.type', + string='Picktype', + default=lambda self: self.env.ref('project_task_material_stock.project_task_material_picking_type').id + ) + location_id = fields.Many2one( + comodel_name='stock.location', + string='Source Location', + ) + location_dest_id = fields.Many2one( + comodel_name='stock.location', + string='Destination Location', + ) + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + task_id = fields.Many2one('project.task', string="Task") class Task(models.Model): @@ -22,7 +58,7 @@ class Task(models.Model): @api.depends('material_ids.stock_move_id') def _compute_stock_move(self): for task in self: - task.stock_move_ids = task.mapped('material_ids.stock_move_id') + task.stock_move_ids = task.mapped('material_ids.stock_move_ids') @api.multi @api.depends('material_ids.analytic_line_id') @@ -44,9 +80,24 @@ def _compute_stock_state(self): task.stock_state = state break + @api.multi + def _compute_matching_picking(self): + default_pick_type = self.env.ref( + 'project_task_material_stock.project_task_material_picking_type') + for task in self: + pick_type = task.pick_type_id or default_pick_type + pickings = task.picking_ids.filtered(lambda p: p.picking_type_id.id == pick_type.id) + task.picking_id = pickings[0].id if pickings else None + + picking_ids = fields.One2many( + "stock.picking", + "task_id", + string="Stock Pickings" + ) picking_id = fields.Many2one( "stock.picking", - related="stock_move_ids.picking_id", + compute="_compute_matching_picking", + string="Current Picking" ) stock_move_ids = fields.Many2many( comodel_name='stock.move', @@ -63,8 +114,29 @@ def _compute_stock_state(self): compute='_compute_analytic_line', string='Analytic Lines', ) - consume_material = fields.Boolean( - related='stage_id.consume_material', + pick_material = fields.Boolean( + related='stage_id.pick_material', + ) + reserve_material = fields.Boolean( + related='stage_id.reserve_material', + ) + unreserve_material = fields.Boolean( + related='stage_id.unreserve_material', + ) + done_material = fields.Boolean( + related='stage_id.done_material', + ) + abort_material = fields.Boolean( + related='stage_id.abort_material', + ) + pick_type_id = fields.Many2one( + related='stage_id.pick_type_id', + ) + stage_location_id = fields.Many2one( + related='stage_id.location_id', + ) + stage_location_dest_id = fields.Many2one( + related='stage_id.location_dest_id', ) stock_state = fields.Selection( selection=[ @@ -102,26 +174,83 @@ def unlink_stock_move(self): res = moves.unlink() return res + @api.multi + def _pick_material(self): + self.ensure_one() + if not self.picking_id or (self.picking_id and self.picking_id.state == 'draft'): + todo_lines = self.material_ids.filtered( + lambda m: not m.stock_move_id + ) + if todo_lines: + todo_lines.create_stock_move() + todo_lines.create_analytic_line() + + @api.multi + def _reserve_material(self): + self.ensure_one() + if not self.picking_id or self.picking_id.state not in ['waiting','confirmed']: + return + self.picking_id.action_assign() + + @api.multi + def _unreserve_material(self): + self.ensure_one() + if not self.picking_id or self.picking_id.state not in ['assigned']: + return + self.picking_id.do_unreserve() + + @api.multi + def _abort_material(self): + self.ensure_one() + for picking in self.picking_ids: + if picking.state == 'done': + # Return the picking + stock_return_picking = self.env['stock.return.picking'] \ + .with_context(active_id=picking.id) \ + .create({}) + stock_return_picking_action = stock_return_picking.create_returns() + return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id']) + return_pick.action_assign() + return_pick.move_lines.quantity_done = 1 + return_pick.action_done() + else: + picking.action_cancel() + + @api.multi + def _validate_material(self): + self.ensure_one() + if not self.picking_id: + return + if self.picking_id.state == 'done': + return + res = self.picking_id.button_validate() + if res and res['res_model'] == 'stock.immediate.transfer': + # if we get back the immediate transfer wizard - then process it + wizard = self.env['stock.immediate.transfer'].browse(res['res_id']) + wizard.process() + elif res and res['res_model'] == 'stock.overprocessed.transfer': + raise UserError(_('There are overprocessed stock moves. You have to manually resolv this !')) + @api.multi def write(self, vals): res = super(Task, self).write(vals) for task in self: if 'stage_id' in vals or 'material_ids' in vals: - if task.consume_material: - todo_lines = task.material_ids.filtered( - lambda m: not m.stock_move_id - ) - if todo_lines: - todo_lines.create_stock_move() - todo_lines.create_analytic_line() - else: - if task.unlink_stock_move() and task.material_ids.mapped( - 'analytic_line_id'): - raise exceptions.Warning( - _("You can't move to a not consume stage if " - "there are already analytic lines") - ) - task.material_ids.mapped('analytic_line_id').unlink() + if task.done_material: + task._pick_material() + task.refresh() + task._reserve_material() + task._validate_material() + elif task.reserve_material: + task._pick_material() + task.refresh() + task._reserve_material() + elif task.pick_material: + task._pick_material() + elif task.abort_material: + task._abort_material() + elif task.unreserve_material: + task._unreserve_material() return res @api.multi @@ -141,13 +270,39 @@ def action_done(self): self.mapped('stock_move_ids')._action_done() +class StockMove(models.Model): + _inherit = "stock.move" + + project_task_material_id = fields.Many2one( + comodel_name='project.task.material', + string="Task Material" + ) + + class ProjectTaskMaterial(models.Model): _inherit = "project.task.material" + @api.multi + @api.depends('stock_move_ids') + def _compute_stage_move(self): + default_pick_type = self.env.ref( + 'project_task_material_stock.project_task_material_picking_type') + for material in self: + pick_type = material.task_id.pick_type_id or default_pick_type + moves = material.stock_move_ids.filtered( + lambda m: m.picking_type_id.id == pick_type.id) + material.stock_move_id = moves[0].id if moves else None + stock_move_id = fields.Many2one( comodel_name='stock.move', + compute='_compute_stage_move', string='Stock Move', ) + stock_move_ids = fields.One2many( + comodel_name='stock.move', + inverse_name='project_task_material_id', + string='Stock Moves', + ) analytic_line_id = fields.Many2one( comodel_name='account.analytic.line', string='Analytic Line', @@ -173,15 +328,18 @@ def _prepare_stock_move(self): 'product_id': product.id, 'name': product.partner_ref, 'state': 'confirmed', + 'project_task_material_id': self.id, 'product_uom': self.product_uom_id.id or product.uom_id.id, 'product_uom_qty': self.quantity, 'origin': self.task_id.name, 'location_id': self.task_id.location_source_id.id or + self.task_id.stage_id.location_id.id or self.task_id.project_id.location_source_id.id or self.env.ref('stock.stock_location_stock').id, 'location_dest_id': self.task_id.location_dest_id.id or + self.task_id.stage_id.location_dest_id.id or self.task_id.project_id.location_dest_id.id or self.env.ref('stock.stock_location_customers').id, } @@ -189,22 +347,33 @@ def _prepare_stock_move(self): @api.multi def create_stock_move(self): - pick_type = self.env.ref( - 'project_task_material_stock.project_task_material_picking_type') task = self[0].task_id + pick_type = task.pick_type_id + if not pick_type: + pick_type = self.env.ref( + 'project_task_material_stock.project_task_material_picking_type') + # Do search for matching picking picking_id = task.picking_id or self.env['stock.picking'].create({ 'origin': "{}/{}".format(task.project_id.name, task.name), 'partner_id': task.partner_id.id, + 'task_id': task.id, 'picking_type_id': pick_type.id, - 'location_id': pick_type.default_location_src_id.id, - 'location_dest_id': pick_type.default_location_dest_id.id, + 'location_id': + task.location_source_id.id or + task.stage_id.location_id.id or + task.project_id.location_source_id.id or + self.env.ref('stock.stock_location_stock').id, + 'location_dest_id': + task.location_dest_id.id or + task.stage_id.location_dest_id.id or + task.project_id.location_dest_id.id or + self.env.ref('stock.stock_location_customers').id, }) for line in self: if not line.stock_move_id: move_vals = line._prepare_stock_move() move_vals.update({'picking_id': picking_id.id or False}) - move_id = self.env['stock.move'].create(move_vals) - line.stock_move_id = move_id.id + self.env['stock.move'].create(move_vals) def _prepare_analytic_line(self): product = self.product_id diff --git a/project_task_material_stock/views/project_task_view.xml b/project_task_material_stock/views/project_task_view.xml index cd8bdc0663..672ce07d8d 100644 --- a/project_task_material_stock/views/project_task_view.xml +++ b/project_task_material_stock/views/project_task_view.xml @@ -13,7 +13,14 @@ - + + + + + + + + @@ -38,7 +45,6 @@ -