from odoo import models, api, fields, tools, _ from odoo.exceptions import UserError from datetime import datetime import logging, math from odoo.tools import float_compare, float_round _logger = logging.getLogger(__name__) class AutomaticPurchase(models.Model): _name = 'automatic.purchase' _order = 'id desc' _inherit = ['mail.thread'] _rec_name = 'number' number = fields.Char(string='Document No', index=True, copy=False, readonly=True) date_doc = fields.Date(string='Date', readonly=True, help='Isi tanggal hari ini', default=fields.Date.context_today, tracking=True) automatic_purchase_lines = fields.One2many('automatic.purchase.line', 'automatic_purchase_id', string='Lines', auto_join=True) is_po = fields.Boolean(string='Is PO', tracking=True) responsible_id = fields.Many2one('res.users', string='Responsible', tracking=True, default=lambda self: self.env.user) apo_type = fields.Selection([ ('purchasing_job', 'Purchasing Job'), ('reordering', 'Reordering Rule'), ('requisition', 'Requisition'), ], string='Type', tracking=True) purchase_order_ids = fields.One2many( 'purchase.order', 'automatic_purchase_id', string='Generated Purchase Orders' ) purchase_order_count = fields.Integer( string='Purchase Orders', compute='_compute_purchase_order_count' ) sale_order_id = fields.Many2one('sale.order', string='Sales Order') def action_generate_lines_from_so(self): self.ensure_one() self.automatic_purchase_lines.unlink() if not self.sale_order_id: raise UserError("Please select a Sales Order first.") lines = [] for line in self.sale_order_id.order_line: product = line.product_id manage_stock = self.env['manage.stock'].search([ ('product_id', '=', product.id) ], limit=1) qty_min = manage_stock.min_stock if manage_stock else 0.0 qty_buffer = manage_stock.buffer_stock if manage_stock else 0.0 quant = self.env['stock.quant'].search([ ('product_id', '=', product.id) ], limit=1) qty_available = quant.available_quantity if quant else 0.0 pricelist = self.env['purchase.pricelist'].search([ ('product_id', '=', product.id) ], limit=1) vendor = pricelist.vendor_id if pricelist else False price = pricelist.price if pricelist else 0.0 subtotal = price * line.product_uom_qty lines.append({ 'automatic_purchase_id': self.id, 'product_id': product.id, 'qty_purchase': line.product_uom_qty, 'qty_min': qty_min, 'qty_buffer': qty_buffer, 'qty_available': qty_available, 'partner_id': vendor.id if vendor else False, 'taxes_id': vendor.tax_id.id if vendor else False, 'price': price, 'product_public_category_id': product.product_public_category_id.id, 'brand_id': product.brand_id.id, }) self.env['automatic.purchase.line'].create(lines) def action_view_related_po(self): self.ensure_one() return { 'name': 'Related', 'type': 'ir.actions.act_window', 'res_model': 'purchase.order', 'view_mode': 'tree,form', 'target': 'current', 'domain': [('id', 'in', list(self.purchase_order_ids.ids))], } def _compute_purchase_order_count(self): for record in self: record.purchase_order_count = len(record.purchase_order_ids) def action_view_purchase_orders(self): self.ensure_one() action = self.env.ref('purchase.purchase_form_action').read()[0] if len(self.purchase_order_ids) > 1: action['domain'] = [('id', 'in', self.purchase_order_ids.ids)] action['view_mode'] = 'tree,form' elif self.purchase_order_ids: action['views'] = [(self.env.ref('purchase.purchase_order_form').id, 'form')] action['res_id'] = self.purchase_order_ids[0].id return action def delete_note_pj(self): self.env['purchasing.job.note'].search([ ('product_id', 'in', self.automatic_purchase_lines.mapped('product_id').ids) ]).unlink() def create_purchase_orders(self): self.ensure_one() if not self.automatic_purchase_lines: raise UserError(_("No purchase lines to process!")) self.delete_note_pj() if self.is_po: raise UserError(_("Purchase order already created!")) vendor_lines = {} for line in self.automatic_purchase_lines: partner_id = line.partner_id.id brand_id = line.brand_id.id if line.brand_id else 0 category_id = line.product_public_category_id.id if line.product_public_category_id else 0 if partner_id != 270: key = (partner_id,) elif brand_id == 3: # ryu key = (partner_id, brand_id, category_id) else: key = (partner_id, brand_id) vendor_lines.setdefault(key, []).append(line) created_orders = self.env['purchase.order'] for key, lines in vendor_lines.items(): partner_id = key[0] brand_id = key[1] if len(key) > 1 else None category_id = key[2] if len(key) > 2 else None vendor = self.env['res.partner'].browse(partner_id) chunk_size = 10 line_chunks = [lines[i:i + chunk_size] for i in range(0, len(lines), chunk_size)] for index, chunk in enumerate(line_chunks): order = self._create_purchase_order( vendor, index + 1, len(line_chunks), brand_id=brand_id, category_id=category_id ) created_orders += order for line in chunk: self._create_purchase_order_line(order, line) self.purchase_order_ids = [(6, 0, created_orders.ids)] self.is_po = True return self.action_view_purchase_orders() def _create_purchase_order(self, vendor, sequence, total_chunks, brand_id=None, category_id=None): return self.env['purchase.order'].create({ 'partner_id': vendor.id, 'origin': self.number, 'date_order': fields.Datetime.now(), 'company_id': self.env.company.id, 'currency_id': vendor.property_purchase_currency_id.id or self.env.company.currency_id.id, 'automatic_purchase_id': self.id, 'source': self.apo_type, }) def _create_purchase_order_line(self, order, line): product = line.product_id return self.env['purchase.order.line'].create({ 'order_id': order.id, 'product_id': product.id, 'name': product.name, 'product_qty': line.qty_purchase, 'product_uom': product.uom_po_id.id, 'price_unit': line.price, 'date_planned': fields.Datetime.now(), 'taxes_id': [(6, 0, [line.taxes_id.id])] if line.taxes_id else False, 'automatic_purchase_line_id': line.id, }) def generate_automatic_lines(self): self.ensure_one() self.automatic_purchase_lines.unlink() manage_stocks = self.env['manage.stock'].search([]) location_id = 55 lines_to_create = [] for stock in manage_stocks: quant_records = self.env['stock.quant'].search([ ('product_id', '=', stock.product_id.id), ('location_id', '=', location_id) ]) total_available = quant_records.quantity or 0.0 _logger.info( "Product %s: Available=%.4f, Min=%.4f, Buffer=%.4f", stock.product_id.display_name, total_available, stock.min_stock, stock.buffer_stock ) comparison = float_compare(total_available, stock.min_stock, precision_rounding=0.0001) if comparison <= 0: qty_purchase = stock.buffer_stock - total_available if float_compare(qty_purchase, 0.0, precision_rounding=0.0001) <= 0: _logger.warning( "Negative purchase quantity for %s: Available=%.4f, Buffer=%.4f, Purchase=%.4f", stock.product_id.display_name, total_available, stock.buffer_stock, qty_purchase ) continue pricelist = self.env['purchase.pricelist'].search([ ('product_id', '=', stock.product_id.id), ('vendor_id', '=', stock.vendor_id.id) ], limit=1) price = pricelist.price if pricelist else 0.0 subtotal = qty_purchase * price _logger.info( "Adding product %s: Available=%.4f, Min=%.4f, Purchase=%.4f", stock.product_id.display_name, total_available, stock.min_stock, qty_purchase ) lines_to_create.append({ 'automatic_purchase_id': self.id, 'product_id': stock.product_id.id, 'qty_purchase': qty_purchase, 'qty_min': stock.min_stock, 'qty_buffer': stock.buffer_stock, 'partner_id': stock.vendor_id.id, 'taxes_id': stock.vendor_id.tax_id.id, 'price': price, 'product_public_category_id': stock.product_id.product_public_category_id.id, 'brand_id': stock.product_id.brand_id.id, }) else: _logger.info( "Skipping product %s: Available=%.4f > Min=%.4f", stock.product_id.display_name, total_available, stock.min_stock ) if lines_to_create: self.env['automatic.purchase.line'].create(lines_to_create) _logger.info("Created %d purchase lines", len(lines_to_create)) else: _logger.warning("No products need to be purchased") raise UserError(_("No products need to be purchased based on current stock levels.")) @api.model def create(self, vals): vals['number'] = self.env['ir.sequence'].next_by_code('automatic.purchase') or '0' result = super(AutomaticPurchase, self).create(vals) return result class AutomaticPurchaseLine(models.Model): _name = 'automatic.purchase.line' _description = 'Automatic Purchase Line' _order = 'automatic_purchase_id, id' automatic_purchase_id = fields.Many2one('automatic.purchase', string='Ref', required=True, ondelete='cascade', index=True, copy=False) product_id = fields.Many2one('product.product', string='Product') qty_purchase = fields.Float(string='Qty Purchase') qty_min = fields.Float(string='Qty Min') qty_buffer = fields.Float(string='Qty Buffer') qty_available = fields.Float(string='Qty Available', compute='compute_qty_available') qty_onhand = fields.Float(string='Qty On Hand', compute='compute_qty_onhand') qty_incoming = fields.Float(string='Qty Incoming', compute='compute_qty_incoming') qty_outgoing = fields.Float(string='Qty Outgoing', compute='compute_qty_outgoing') partner_id = fields.Many2one('res.partner', string='Vendor') price = fields.Float(string='Price') subtotal = fields.Float(string='Subtotal', compute='compute_subtotal') is_po = fields.Boolean(String='Is PO') taxes_id = fields.Many2one('account.tax', string='Taxes') brand_id = fields.Many2one('brands', string='Brand') product_public_category_id = fields.Many2one('product.public.category', string='Public Category') def compute_subtotal(self): for line in self: line.subtotal = line.qty_purchase * line.price @api.onchange('product_id') def _onchange_product_id(self): if self.product_id: manage_stock = self.env['manage.stock'].search([ ('product_id', '=', self.product_id.id) ], limit=1) if manage_stock: self.qty_min = manage_stock.min_stock self.qty_buffer = manage_stock.buffer_stock self.taxes_id = manage_stock.vendor_id.tax_id.id self.partner_id = manage_stock.vendor_id.id # pricelist = self.env['purchase.pricelist'].search([ # ('product_id', '=', self.product_id.id), # ('vendor_id', '=', manage_stock.vendor_id.id) # ], limit=1) # if pricelist: # self.price = pricelist.price def compute_qty_available(self): for line in self: if line.product_id: stock_quant = self.env['stock.quant'].search([ ('product_id', '=', line.product_id.id), ('location_id', '=', 55) ], limit=1) line.qty_available = stock_quant.available_quantity if stock_quant else 0.0 def compute_qty_onhand(self): for line in self: if line.product_id: stock_quant = self.env['stock.quant'].search([ ('product_id', '=', line.product_id.id), ('location_id', '=', 55) ], limit=1) line.qty_onhand = stock_quant.quantity if stock_quant else 0.0 def compute_qty_incoming(self): for line in self: if line.product_id: stock_move = self.env['stock.move'].search([ ('product_id', '=', line.product_id.id), ('location_dest_id', '=', 55), ('location_id', '=', 4), ('state', 'in', ['waiting', 'confirmed', 'assigned', 'partially_available']) ]) line.qty_incoming = sum(move.product_uom_qty for move in stock_move) def compute_qty_outgoing(self): for line in self: if line.product_id: stock_move = self.env['stock.move'].search([ ('product_id', '=', line.product_id.id), ('location_dest_id', '=', 5), ('location_id', '=', 55), ('state', 'in', ['waiting', 'confirmed', 'assigned', 'partially_available']) ]) line.qty_outgoing = sum(move.product_uom_qty for move in stock_move)