# -*- coding: utf-8 -*- from email.policy import default from odoo import _, api, fields, models from odoo.exceptions import ValidationError, UserError class Grn(models.Model): _name = 'grn' _description = 'Goods Receipt Note' _inherit = ['mail.thread', 'mail.activity.mixin', 'product.catalog.mixin'] name = fields.Char( string='Name', copy=False, readonly=True, default=lambda self: _('New') ) vendor_id = fields.Many2one('res.partner', string='Vendor', readonly=True, ) note = fields.Text(string='Source Document / Note') date = fields.Date(string='Date', default=fields.Date.context_today, copy=False) picking_id = fields.Many2one("stock.picking", string="Stock Transfer", copy=False) state = fields.Selection( [('draft', 'Draft'), ('confirmed', 'Confirmed'), ('done', 'Done'), ('cancel', 'Cancelled')], string='State', default='draft', tracking=True ) grn_line_ids = fields.One2many('grn.lines', 'grn_id', string="GRN Lines") received_by = fields.Many2one('res.users', string='Received By') location_id = fields.Many2one( 'stock.location', string='To', domain="[('usage', '=', 'internal')]", required=True ) total_amount = fields.Float(string='Amount', compute='_compute_total_amount') company_id = fields.Many2one( 'res.company', string='Company', required=True, readonly=True, default=lambda self: self.env.company, ) def _get_product_catalog_order_data(self, products, **kwargs): res = super()._get_product_catalog_order_data(products, **kwargs) for product in products: res[product.id] |= { 'price': product.standard_price, } return res def _get_product_catalog_domain(self): """Get the domain to search for products in the catalog. """ domain = [('company_id', 'in', [self.company_id.id, False]), ('type', '=', 'consu'), ('purchase_ok', '=', True)] return domain def _update_order_line_info(self, product_id, quantity, **kwargs): self.ensure_one() pol = self.grn_line_ids.filtered(lambda line: line.product_id.id == product_id) if pol: if quantity != 0: pol.quantity = quantity else: pol.unlink() elif quantity > 0: pol = self.env['grn.lines'].create({ 'grn_id': self.id, 'product_id': product_id, 'quantity': quantity, }) else: pass return 0 @api.depends('grn_line_ids') def _compute_total_amount(self): """Compute the value of the field computed_field.""" for record in self: amount = 0 for line in record.grn_line_ids: amount += (line.quantity * line.price) record.total_amount = amount def _get_product_catalog_lines_data(self): catalog_info = { 'quantity': self.quantity, 'price': self.product_id.standard_price, } return catalog_info def button_action_confirm(self): """Confirm the GRN and assign sequence and received user""" if not self.grn_line_ids: raise UserError(_( "Cannot confirm the GRN '%s' because no products have been added in the lines. " "Please add at least one product before confirming." ) % self.name) if not self.name or self.name == _('New'): self.name = self.env['ir.sequence'].next_by_code('grn') or _('New') self.state = 'confirmed' self.received_by = self.env.user for line in self.grn_line_ids: if line.price: continue else: line.price = line.product_id.standard_price def button_action_done(self): """Set GRN state to done if picking is completed""" if not self.picking_id or self.picking_id.state != 'done': raise ValidationError(_( "Cannot mark the GRN '%s' as done because the associated stock transfer is not yet completed. " "Please complete the transfer before proceeding." ) % (self.name,)) if self.state == 'confirmed': self.state = 'done' def button_action_cancel(self): """Cancel GRN if stock transfer is not done""" if self.picking_id and self.picking_id.state == 'done': raise ValidationError(_( "Cannot cancel the GRN '%s' because the stock transfer '%s' has already been completed. " ) % (self.name, self.picking_id.name)) if self.picking_id: self.picking_id.action_cancel() self.picking_id.unlink() self.state = 'cancel' def button_action_reset_to_draft(self): self.state = 'draft' def button_create_transfer(self): """Create stock picking for confirmed GRN""" if self.picking_id: return if self.state != 'confirmed': raise UserError(_( "The GRN '%s' cannot be transferred because it is not confirmed yet. " "Please confirm the GRN first." ) % self.name) picking_type = self.env['stock.picking.type'].search([('code', '=', 'incoming')], limit=1) if not picking_type: raise UserError(_( "No Incoming Picking Type is configured in the system. " "Please create an Incoming Picking Type before transferring GRN '%s'." ) % self.name) picking = self.env['stock.picking'].create({ 'origin': self.name, 'location_dest_id': self.location_id.id, 'location_id': picking_type.default_location_src_id.id, 'picking_type_id': picking_type.id, 'partner_id': self.vendor_id.id }) moves = [] for line in self.grn_line_ids: moves.append((0, 0, { 'name': line.product_id.name, 'product_id': line.product_id.id, 'product_uom': line.product_uom_id.id, 'product_uom_qty': line.quantity, 'location_id': picking_type.default_location_src_id.id, 'location_dest_id': self.location_id.id, 'price_unit': line.price if line.price > 1 else line.product_id.standard_price, })) picking.write({'move_ids_without_package': moves}) picking.action_confirm() for move in picking: move.location_dest_id = self.location_id picking.location_dest_id = self.location_id self.picking_id = picking def action_view_transfer(self): """Return action to view related stock picking""" self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': _('Receipts'), 'res_model': 'stock.picking', 'view_mode': 'form', 'target': 'current', 'res_id': self.picking_id.id, } @api.model_create_multi def create(self, vals_list): return super().create(vals_list) def write(self, vals): return super().write(vals) def unlink(self): if any(rec.state not in ['cancel', 'draft'] for rec in self): raise UserError(_( "Unable to delete the GRN '%s' because it has already been confirmed or processed. " ) % self.name) return super().unlink() class GrnLines(models.Model): _name = 'grn.lines' _description = 'GRN Lines' grn_id = fields.Many2one('grn', string="GRN", required=True, ondelete='cascade') product_id = fields.Many2one( 'product.product', string='Product', ondelete="cascade", domain=[('type', '!=', 'service'), ('purchase_ok', '=', True)], index=True ) product_uom_id = fields.Many2one(related='product_id.uom_id', string='Unit of Measure') quantity = fields.Float('Quantity', digits='Product Unit of Measure') price = fields.Float("Unit Price") total_price = fields.Float("Total Price", compute='_compute_product_total_price', store=True) @api.depends('quantity', 'price') def _compute_product_total_price(self): for rec in self: rec.total_price = rec.quantity * rec.price if rec.quantity and rec.price else 0 @api.constrains('product_id') def _check_unique_product_grn(self): for rec in self: duplicate_lines = rec.grn_id.grn_line_ids.filtered(lambda l: l.product_id == rec.product_id) if len(duplicate_lines) > 1: raise UserError(_('The product %s already exists in the GRN.') % rec.product_id.display_name) def action_add_from_catalog(self): order = self.env['grn'].browse(self.env.context.get('order_id')) return order.with_context(child_field='grn_line_ids').action_add_from_catalog() def unlink(self): if any(rec.grn_id.state not in ['cancel', 'draft'] for rec in self): raise UserError(_( "Unable to delete the GRN '%s' because it has already been confirmed or processed. " ) % self.name) return super().unlink()