# -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import datetime from odoo import _, api, fields, models from odoo.exceptions import UserError from odoo.osv import expression from odoo.tools.misc import clean_context class ProductReplenish(models.TransientModel): _name = 'product.replenish' _description = 'Product Replenish' _check_company_auto = True product_id = fields.Many2one('product.product', string='Product', required=True) product_tmpl_id = fields.Many2one('product.template', string='Product Template', required=True) product_has_variants = fields.Boolean('Has variants', default=False, required=True) product_uom_category_id = fields.Many2one('uom.category', related='product_id.uom_id.category_id', readonly=True, required=True) product_uom_id = fields.Many2one('uom.uom', string='Unity of measure', required=True) forecast_uom_id = fields.Many2one(related='product_id.uom_id') quantity = fields.Float('Quantity', default=1, required=True) date_planned = fields.Datetime('Scheduled Date', required=True, compute="_compute_date_planned", readonly=False, help="Date at which the replenishment should take place.", store=True, precompute=True) warehouse_id = fields.Many2one( 'stock.warehouse', string='Warehouse', required=True, check_company=True, ) route_id = fields.Many2one( 'stock.route', string='Preferred Route', help="Apply specific route for the replenishment instead of product's default routes.", check_company=True, ) company_id = fields.Many2one('res.company') forecasted_quantity = fields.Float(string="Forecasted Quantity", compute="_compute_forecasted_quantity") allowed_route_ids = fields.Many2many("stock.route", compute="_compute_allowed_route_ids") @api.onchange('product_id', 'warehouse_id') def _onchange_product_id(self): if not self.env.context.get('default_quantity'): self.quantity = abs(self.forecasted_quantity) if self.forecasted_quantity < 0 else 1 @api.depends('warehouse_id', 'product_id') def _compute_forecasted_quantity(self): for rec in self: rec.forecasted_quantity = rec.product_id.with_context(warehouse=rec.warehouse_id.id).virtual_available @api.depends('product_id', 'product_tmpl_id') def _compute_allowed_route_ids(self): domain = self._get_allowed_route_domain() route_ids = self.env['stock.route'].search(domain) self.allowed_route_ids = route_ids @api.depends('route_id') def _compute_date_planned(self): for rec in self: rec.date_planned = rec._get_date_planned(rec.route_id) @api.model def default_get(self, fields): res = super(ProductReplenish, self).default_get(fields) product_tmpl_id = self.env['product.template'] if self.env.context.get('default_product_id'): product_id = self.env['product.product'].browse(self.env.context['default_product_id']) product_tmpl_id = product_id.product_tmpl_id if 'product_id' in fields: res['product_tmpl_id'] = product_id.product_tmpl_id.id res['product_id'] = product_id.id elif self.env.context.get('default_product_tmpl_id'): product_tmpl_id = self.env['product.template'].browse(self.env.context['default_product_tmpl_id']) if 'product_id' in fields: res['product_tmpl_id'] = product_tmpl_id.id res['product_id'] = product_tmpl_id.product_variant_id.id if len(product_tmpl_id.product_variant_ids) > 1: res['product_has_variants'] = True company = product_tmpl_id.company_id or self.env.company if 'product_uom_id' in fields: res['product_uom_id'] = product_tmpl_id.uom_id.id if 'company_id' in fields: res['company_id'] = company.id if 'warehouse_id' in fields and 'warehouse_id' not in res: warehouse = self.env['stock.warehouse'].search([('company_id', '=', company.id)], limit=1) res['warehouse_id'] = warehouse.id if 'route_id' in fields and 'route_id' not in res and product_tmpl_id: res['route_id'] = self.env['stock.route'].search(self._get_route_domain(product_tmpl_id), limit=1).id if not res['route_id']: if product_tmpl_id.route_ids: res['route_id'] = product_tmpl_id.route_ids.filtered(lambda r: r.company_id == self.env.company or not r.company_id)[0].id return res def _get_date_planned(self, route_id, **kwargs): now = fields.Datetime.now() delay = 0 if route_id: delay = sum([rule.delay for rule in route_id.rule_ids]) return fields.Datetime.add(now, days=delay) def launch_replenishment(self): if not self.route_id: raise UserError(_("You need to select a route to replenish your products")) uom_reference = self.product_id.uom_id self.quantity = self.product_uom_id._compute_quantity(self.quantity, uom_reference, rounding_method='HALF-UP') try: now = self.env.cr.now() self.env['procurement.group'].with_context(clean_context(self.env.context)).run([ self.env['procurement.group'].Procurement( self.product_id, self.quantity, uom_reference, self.warehouse_id.lot_stock_id, # Location _("Manual Replenishment"), # Name _("Manual Replenishment"), # Origin self.warehouse_id.company_id, self._prepare_run_values() # Values ) ]) move = self._get_record_to_notify(now) notification = self._get_replenishment_order_notification(move) act_window_close = { 'type': 'ir.actions.act_window_close', 'infos': {'done': True}, } if notification: notification['params']['next'] = act_window_close return notification return act_window_close except UserError as error: raise UserError(error) # TODO: to remove in master def _prepare_orderpoint_values(self): values = { 'location_id': self.warehouse_id.lot_stock_id.id, 'product_id': self.product_id.id, 'qty_to_order': self.quantity, } if self.route_id: values['route_id'] = self.route_id.id return values def _prepare_run_values(self): replenishment = self.env['procurement.group'].create({}) values = { 'warehouse_id': self.warehouse_id, 'route_ids': self.route_id, 'date_planned': self.date_planned, 'group_id': replenishment, } return values def _get_record_to_notify(self, date): return self.env['stock.move'].search([('write_date', '>=', date)], limit=1) def _get_replenishment_order_notification_link(self, move): if move.picking_id: action = self.env.ref('stock.stock_picking_action_picking_type') return [{ 'label': move.picking_id.name, 'url': f'#action={action.id}&id={move.picking_id.id}&model=stock.picking&view_type=form' }] return False def _get_replenishment_order_notification(self, move): link = self._get_replenishment_order_notification_link(move) if not link: return False return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('The following replenishment order have been generated'), 'message': '%s', 'links': link, 'sticky': False, } } # OVERWRITE in 'Drop Shipping', 'Dropship and Subcontracting Management' and 'Dropship and Subcontracting Management' to hide it def _get_allowed_route_domain(self): stock_location_inter_wh_id = self.env.ref('stock.stock_location_inter_wh').id return [ ('product_selectable', '=', True), ('rule_ids.location_src_id', '!=', stock_location_inter_wh_id), ('rule_ids.location_dest_id', '!=', stock_location_inter_wh_id) ] def _get_route_domain(self, product_tmpl_id): company = product_tmpl_id.company_id or self.env.company domain = expression.AND([self._get_allowed_route_domain(), self.env['stock.route']._check_company_domain(company)]) if product_tmpl_id.route_ids: domain = expression.AND([domain, [('product_ids', '=', product_tmpl_id.id)]]) return domain