summaryrefslogtreecommitdiff
path: root/fixco_custom/models/automatic_purchase.py
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2025-07-08 15:24:11 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2025-07-08 15:24:11 +0700
commite46be164dc1e419cdbfd0c0cf587fadc63beef3e (patch)
treec0bd1682d84f23dafbdb66ac3e7cc9b7abc7cd10 /fixco_custom/models/automatic_purchase.py
parentb858358ffbdd14c9b56ac96f035bddccae4d872d (diff)
reordering rules and manage stock
Diffstat (limited to 'fixco_custom/models/automatic_purchase.py')
-rw-r--r--fixco_custom/models/automatic_purchase.py247
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