diff options
| author | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-08 15:24:11 +0700 |
|---|---|---|
| committer | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-08 15:24:11 +0700 |
| commit | e46be164dc1e419cdbfd0c0cf587fadc63beef3e (patch) | |
| tree | c0bd1682d84f23dafbdb66ac3e7cc9b7abc7cd10 /fixco_custom/models/automatic_purchase.py | |
| parent | b858358ffbdd14c9b56ac96f035bddccae4d872d (diff) | |
reordering rules and manage stock
Diffstat (limited to 'fixco_custom/models/automatic_purchase.py')
| -rw-r--r-- | fixco_custom/models/automatic_purchase.py | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/fixco_custom/models/automatic_purchase.py b/fixco_custom/models/automatic_purchase.py new file mode 100644 index 0000000..7612f4a --- /dev/null +++ b/fixco_custom/models/automatic_purchase.py @@ -0,0 +1,247 @@ +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([ + ('regular', 'Regular Fulfill SO'), + ('reordering', 'Reordering Rule'), + ], 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' + ) + + 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 create_purchase_orders(self): + self.ensure_one() + if not self.automatic_purchase_lines: + raise UserError(_("No purchase lines to process!")) + + if self.is_po: + raise UserError(_("Purchase order already created!")) + + vendor_lines = {} + for line in self.automatic_purchase_lines: + if line.partner_id.id not in vendor_lines: + vendor_lines[line.partner_id.id] = [] + vendor_lines[line.partner_id.id].append(line) + + created_orders = self.env['purchase.order'] + + for vendor_id, lines in vendor_lines.items(): + vendor = self.env['res.partner'].browse(vendor_id) + + chunk_size = 20 + 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)) + 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): + """Membuat purchase order untuk vendor""" + order_name = f"{self.number} - {vendor.name}" + if total_chunks > 1: + order_name += f" ({sequence}/{total_chunks})" + + 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': 'reordering', + }) + + def _create_purchase_order_line(self, order, line): + """Membuat purchase order line dari automatic purchase 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() + # Hapus lines yang sudah ada + self.automatic_purchase_lines.unlink() + + manage_stocks = self.env['manage.stock'].search([]) + location_id = 55 # Lokasi gudang ID 55 + + lines_to_create = [] + for stock in manage_stocks: + # Cari semua stock.quant untuk produk ini di lokasi 55 + quant_records = self.env['stock.quant'].search([ + ('product_id', '=', stock.product_id.id), + ('location_id', '=', location_id) + ]) + + # Hitung total qty_available (quantity - reserved_quantity) untuk lokasi tersebut + total_available = quant_records.available_quantity or 0.0 + + # Log nilai untuk debugging + _logger.info( + "Product %s: Available=%.4f, Min=%.4f, Buffer=%.4f", + stock.product_id.display_name, + total_available, + stock.min_stock, + stock.buffer_stock + ) + + # Gunakan float_compare untuk perbandingan yang akurat + comparison = float_compare(total_available, stock.min_stock, precision_rounding=0.0001) + + if comparison <= 0: # Termasuk saat sama dengan min_stock + # Hitung qty yang perlu dipesan + qty_purchase = stock.buffer_stock - total_available + + # Pastikan qty_purchase positif + 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 + + # Dapatkan harga dari purchase.pricelist + 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 + + # Log penambahan produk + _logger.info( + "Adding product %s: Available=%.4f, Min=%.4f, Purchase=%.4f", + stock.product_id.display_name, + total_available, + stock.min_stock, + qty_purchase + ) + + # Kumpulkan data untuk pembuatan lines + 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, + 'qty_available': total_available, + 'partner_id': stock.vendor_id.id, + 'taxes_id': stock.vendor_id.tax_id.id, + 'price': price, + 'subtotal': subtotal, + }) + else: + _logger.info( + "Skipping product %s: Available=%.4f > Min=%.4f", + stock.product_id.display_name, + total_available, + stock.min_stock + ) + + # Buat lines sekaligus untuk efisiensi + 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') + partner_id = fields.Many2one('res.partner', string='Vendor') + price = fields.Float(string='Price') + subtotal = fields.Float(string='Subtotal') + last_order_id = fields.Many2one('purchase.order', string='Last Order') + last_orderline_id = fields.Many2one('purchase.order.line', string='Last Order Line') + is_po = fields.Boolean(String='Is PO') + current_po_id = fields.Many2one('purchase.order', string='Current') + current_po_line_id = fields.Many2one('purchase.order.line', string='Current Line') + taxes_id = fields.Many2one('account.tax', string='Taxes')
\ No newline at end of file |
