# Copyright 2017 Eficent Business and IT Consulting Services, S.L. # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html). from odoo import _, api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.tools import float_compare from odoo.addons import decimal_precision as dp REQUEST_STATES = [ ("draft", "Draft"), ("open", "In progress"), ("done", "Done"), ("cancel", "Cancelled"), ] class StockRequest(models.Model): _name = "stock.request" _description = "Stock Request" _inherit = "stock.request.abstract" _order = "id desc" def __get_request_states(self): return REQUEST_STATES def _get_request_states(self): return self.__get_request_states() def _get_default_requested_by(self): return self.env["res.users"].browse(self.env.uid) @staticmethod def _get_expected_date(): return fields.Datetime.now() def _get_default_expected_date(self): if self.order_id: res = self.order_id.expected_date else: res = self._get_expected_date() return res name = fields.Char(states={"draft": [("readonly", False)]}) state = fields.Selection( selection=_get_request_states, string="Status", copy=False, default="draft", index=True, readonly=True, track_visibility="onchange", ) requested_by = fields.Many2one( "res.users", "Requested by", required=True, track_visibility="onchange", default=lambda s: s._get_default_requested_by(), ) expected_date = fields.Datetime( "Expected Date", default=lambda s: s._get_default_expected_date(), index=True, required=True, readonly=True, states={"draft": [("readonly", False)]}, help="Date when you expect to receive the goods.", ) picking_policy = fields.Selection( [ ("direct", "Receive each product when available"), ("one", "Receive all products at once"), ], string="Shipping Policy", required=True, readonly=True, states={"draft": [("readonly", False)]}, default="direct", ) move_ids = fields.One2many( comodel_name="stock.move", compute="_compute_move_ids", string="Stock Moves", readonly=True, ) picking_ids = fields.One2many( "stock.picking", compute="_compute_picking_ids", string="Pickings", readonly=True, ) qty_in_progress = fields.Float( "Qty In Progress", digits=dp.get_precision("Product Unit of Measure"), readonly=True, compute="_compute_qty", store=True, help="Quantity in progress.", ) qty_done = fields.Float( "Qty Done", digits=dp.get_precision("Product Unit of Measure"), readonly=True, compute="_compute_qty", store=True, help="Quantity completed", ) picking_count = fields.Integer( string="Delivery Orders", compute="_compute_picking_ids", readonly=True ) allocation_ids = fields.One2many( comodel_name="stock.request.allocation", inverse_name="stock_request_id", string="Stock Request Allocation", ) order_id = fields.Many2one("stock.request.order", readonly=True) warehouse_id = fields.Many2one( states={"draft": [("readonly", False)]}, readonly=True ) location_id = fields.Many2one( states={"draft": [("readonly", False)]}, readonly=True ) product_id = fields.Many2one(states={"draft": [("readonly", False)]}, readonly=True) product_uom_id = fields.Many2one( states={"draft": [("readonly", False)]}, readonly=True ) product_uom_qty = fields.Float( states={"draft": [("readonly", False)]}, readonly=True ) procurement_group_id = fields.Many2one( states={"draft": [("readonly", False)]}, readonly=True ) company_id = fields.Many2one(states={"draft": [("readonly", False)]}, readonly=True) route_id = fields.Many2one(states={"draft": [("readonly", False)]}, readonly=True) _sql_constraints = [ ("name_uniq", "unique(name, company_id)", "Stock Request name must be unique") ] @api.depends("allocation_ids") def _compute_move_ids(self): for request in self: request.move_ids = request.allocation_ids.mapped("stock_move_id") @api.depends("allocation_ids") def _compute_picking_ids(self): for request in self: request.picking_count = 0 request.picking_ids = self.env["stock.picking"] request.picking_ids = request.move_ids.filtered( lambda m: m.state != "cancel" ).mapped("picking_id") request.picking_count = len(request.picking_ids) @api.depends( "allocation_ids", "allocation_ids.stock_move_id.state", "allocation_ids.stock_move_id.move_line_ids", "allocation_ids.stock_move_id.move_line_ids.qty_done", ) def _compute_qty(self): for request in self: done_qty = sum(request.allocation_ids.mapped("allocated_product_qty")) open_qty = sum(request.allocation_ids.mapped("open_product_qty")) request.qty_done = request.product_id.uom_id._compute_quantity( done_qty, request.product_uom_id ) request.qty_in_progress = request.product_id.uom_id._compute_quantity( open_qty, request.product_uom_id ) @api.constrains("order_id", "requested_by") def check_order_requested_by(self): if self.order_id and self.order_id.requested_by != self.requested_by: raise ValidationError(_("Requested by must be equal to the order")) @api.constrains("order_id", "warehouse_id") def check_order_warehouse_id(self): if self.order_id and self.order_id.warehouse_id != self.warehouse_id: raise ValidationError(_("Warehouse must be equal to the order")) @api.constrains("order_id", "location_id") def check_order_location(self): if self.order_id and self.order_id.location_id != self.location_id: raise ValidationError(_("Location must be equal to the order")) @api.constrains("order_id", "procurement_group_id") def check_order_procurement_group(self): if ( self.order_id and self.order_id.procurement_group_id != self.procurement_group_id ): raise ValidationError(_("Procurement group must be equal to the order")) @api.constrains("order_id", "company_id") def check_order_company(self): if self.order_id and self.order_id.company_id != self.company_id: raise ValidationError(_("Company must be equal to the order")) @api.constrains("order_id", "expected_date") def check_order_expected_date(self): if self.order_id and self.order_id.expected_date != self.expected_date: raise ValidationError(_("Expected date must be equal to the order")) @api.constrains("order_id", "picking_policy") def check_order_picking_policy(self): if self.order_id and self.order_id.picking_policy != self.picking_policy: raise ValidationError(_("The picking policy must be equal to the order")) @api.multi def _action_confirm(self): self._action_launch_procurement_rule() self.state = "open" @api.multi def action_confirm(self): self._action_confirm() return True def action_draft(self): self.write({"state": "draft"}) return True def action_cancel(self): self.sudo().mapped("move_ids")._action_cancel() self.state = "cancel" return True def action_done(self): self.state = "done" if self.order_id: self.order_id.check_done() return True def check_done(self): precision = self.env["decimal.precision"].precision_get( "Product Unit of Measure" ) for request in self: allocated_qty = sum(request.allocation_ids.mapped("allocated_product_qty")) qty_done = request.product_id.uom_id._compute_quantity( allocated_qty, request.product_uom_id ) if ( float_compare( qty_done, request.product_uom_qty, precision_digits=precision ) >= 0 ): request.action_done() return True def _prepare_procurement_values(self, group_id=False): """ Prepare specific key for moves or other components that will be created from a procurement rule coming from a stock request. This method could be override in order to add other custom key that could be used in move/po creation. """ return { "date_planned": self.expected_date, "warehouse_id": self.warehouse_id, "stock_request_allocation_ids": self.id, "group_id": group_id or self.procurement_group_id.id or False, "route_ids": self.route_id, "stock_request_id": self.id, } def _skip_procurement(self): return self.state != "draft" or self.product_id.type not in ("consu", "product") @api.multi def _action_launch_procurement_rule(self): """ Launch procurement group run method with required/custom fields genrated by a stock request. procurement group will launch '_run_move', '_run_buy' or '_run_manufacture' depending on the stock request product rule. """ precision = self.env["decimal.precision"].precision_get( "Product Unit of Measure" ) errors = [] for request in self: if request._skip_procurement(): continue qty = 0.0 for move in request.move_ids.filtered(lambda r: r.state != "cancel"): qty += move.product_qty if float_compare(qty, request.product_qty, precision_digits=precision) >= 0: continue values = request._prepare_procurement_values( group_id=request.procurement_group_id ) try: # We launch with sudo because potentially we could create # objects that the user is not authorized to create, such # as PO. self.env["procurement.group"].sudo().run( request.product_id, request.product_uom_qty, request.product_uom_id, request.location_id, request.name, request.name, values, ) except UserError as error: errors.append(error.name) if errors: raise UserError("\n".join(errors)) return True @api.multi def action_view_transfer(self): action = self.env.ref("stock.action_picking_tree_all").read()[0] pickings = self.mapped("picking_ids") if len(pickings) > 1: action["domain"] = [("id", "in", pickings.ids)] elif pickings: action["views"] = [(self.env.ref("stock.view_picking_form").id, "form")] action["res_id"] = pickings.id return action @api.model def create(self, vals): upd_vals = vals.copy() if upd_vals.get("name", "/") == "/": upd_vals["name"] = self.env["ir.sequence"].next_by_code("stock.request") return super().create(upd_vals) @api.multi def unlink(self): if self.filtered(lambda r: r.state != "draft"): raise UserError(_("Only requests on draft state can be unlinked")) return super(StockRequest, self).unlink()