summaryrefslogtreecommitdiff
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
parentb858358ffbdd14c9b56ac96f035bddccae4d872d (diff)
reordering rules and manage stock
-rwxr-xr-xfixco_custom/__manifest__.py2
-rwxr-xr-xfixco_custom/models/__init__.py2
-rw-r--r--fixco_custom/models/automatic_purchase.py247
-rw-r--r--fixco_custom/models/manage_stock.py21
-rwxr-xr-xfixco_custom/models/partner.py1
-rw-r--r--fixco_custom/models/purchase_order.py6
-rw-r--r--fixco_custom/models/purchase_order_line.py10
-rwxr-xr-xfixco_custom/security/ir.model.access.csv5
-rw-r--r--fixco_custom/views/automatic_purchase.xml98
-rw-r--r--fixco_custom/views/ir_sequence.xml11
-rw-r--r--fixco_custom/views/manage_stock.xml53
-rwxr-xr-xfixco_custom/views/res_partner.xml3
12 files changed, 456 insertions, 3 deletions
diff --git a/fixco_custom/__manifest__.py b/fixco_custom/__manifest__.py
index 9c943e0..5697dc2 100755
--- a/fixco_custom/__manifest__.py
+++ b/fixco_custom/__manifest__.py
@@ -34,6 +34,8 @@
'views/purchase_order.xml',
'views/requisition.xml',
'views/shipment_line.xml',
+ 'views/manage_stock.xml',
+ 'views/automatic_purchase.xml',
],
'demo': [],
'css': [],
diff --git a/fixco_custom/models/__init__.py b/fixco_custom/models/__init__.py
index c12e9a7..df56efb 100755
--- a/fixco_custom/models/__init__.py
+++ b/fixco_custom/models/__init__.py
@@ -20,3 +20,5 @@ from . import purchase_order_line
from . import purchase_order
from . import requisition
from . import account_move_line
+from . import manage_stock
+from . import automatic_purchase
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
diff --git a/fixco_custom/models/manage_stock.py b/fixco_custom/models/manage_stock.py
new file mode 100644
index 0000000..4375ad2
--- /dev/null
+++ b/fixco_custom/models/manage_stock.py
@@ -0,0 +1,21 @@
+from odoo import models, api, fields
+from odoo.exceptions import AccessError, UserError, ValidationError
+from datetime import timedelta, date
+import logging
+
+_logger = logging.getLogger(__name__)
+
+class ManageStock(models.Model):
+ _name = "manage.stock"
+ _description = "Manage Stock"
+ _inherit = ['mail.thread']
+ _rec_name = 'product_id'
+
+ product_id = fields.Many2one('product.product', string="Product", required=True)
+ min_stock = fields.Float(string='Min Stock', required=True)
+ buffer_stock = fields.Float(string='Buffer Stock', required=True)
+ vendor_id = fields.Many2one('res.partner', string="Vendor", required=True)
+
+ _sql_constraints = [
+ ('product_unique', 'unique (product_id)', 'This product already has a stock management rule!'),
+ ] \ No newline at end of file
diff --git a/fixco_custom/models/partner.py b/fixco_custom/models/partner.py
index 17d2fd0..bd17ded 100755
--- a/fixco_custom/models/partner.py
+++ b/fixco_custom/models/partner.py
@@ -5,6 +5,7 @@ class Partner(models.Model):
ginee_shop_id = fields.Char(string='Ginee Shop ID')
ginee_journal_id = fields.Many2one('account.journal', string='Ginee Journal ID')
+ tax_id = fields.Many2one('account.tax', string='Tax')
transaction_type = fields.Selection(
[('digunggung', 'Digunggung'),
('difaktur', 'Faktur Pajak')],
diff --git a/fixco_custom/models/purchase_order.py b/fixco_custom/models/purchase_order.py
index 06f4ef1..75263b1 100644
--- a/fixco_custom/models/purchase_order.py
+++ b/fixco_custom/models/purchase_order.py
@@ -17,6 +17,12 @@ _logger = logging.getLogger(__name__)
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
+ automatic_purchase_id = fields.Many2one(
+ 'automatic.purchase',
+ string='Automatic Purchase Reference',
+ ondelete='set null',
+ index=True
+ )
sale_order_id = fields.Many2one('sale.order', string='Sales Order')
amount_discount = fields.Monetary(
string='Total Discount',
diff --git a/fixco_custom/models/purchase_order_line.py b/fixco_custom/models/purchase_order_line.py
index 8cac3d1..eee5a7a 100644
--- a/fixco_custom/models/purchase_order_line.py
+++ b/fixco_custom/models/purchase_order_line.py
@@ -2,10 +2,16 @@ from odoo import models, fields, api
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
-
+
+ automatic_purchase_line_id = fields.Many2one(
+ 'automatic.purchase.line',
+ string='Automatic Purchase Line Reference',
+ ondelete='set null',
+ index=True
+ )
discount = fields.Float(
string='Discount (%)',
- digits='Discount',
+ digits='Discount',
default=0.0
)
discount_amount = fields.Float(
diff --git a/fixco_custom/security/ir.model.access.csv b/fixco_custom/security/ir.model.access.csv
index 14a8780..4f20dfe 100755
--- a/fixco_custom/security/ir.model.access.csv
+++ b/fixco_custom/security/ir.model.access.csv
@@ -26,4 +26,7 @@ access_requisition,access.requisition,model_requisition,,1,1,1,1
access_requisition_line,access.requisition.line,model_requisition_line,,1,1,1,1
access_requisition_purchase_match,access.requisition.purchase.match,model_requisition_purchase_match,,1,1,1,1
access_v_requisition_match_po,access.v.requisition.match.po,model_v_requisition_match_po,,1,1,1,1
-access_product_shipment_line,access.product.shipment.line,model_product_shipment_line,,1,1,1,1 \ No newline at end of file
+access_product_shipment_line,access.product.shipment.line,model_product_shipment_line,,1,1,1,1
+access_manage_stock,access.manage.stock,model_manage_stock,,1,1,1,1
+access_automatic_purchase_line,access.automatic.purchase.line,model_automatic_purchase_line,,1,1,1,1
+access_automatic_purchase,access.automatic.purchase,model_automatic_purchase,,1,1,1,1 \ No newline at end of file
diff --git a/fixco_custom/views/automatic_purchase.xml b/fixco_custom/views/automatic_purchase.xml
new file mode 100644
index 0000000..704a5e3
--- /dev/null
+++ b/fixco_custom/views/automatic_purchase.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<odoo>
+ <record id="automatic_purchase_tree" model="ir.ui.view">
+ <field name="name">automatic.purchase.tree</field>
+ <field name="model">automatic.purchase</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="number"/>
+ <field name="date_doc" readonly="1"/>
+ <field name="apo_type"/>
+ <field name="is_po" readonly="1"/>
+ <field name="responsible_id" readonly="1"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="automatic_purchase_line_tree" model="ir.ui.view">
+ <field name="name">automatic.purchase.line.tree</field>
+ <field name="model">automatic.purchase.line</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom">
+ <field name="product_id"/>
+ <field name="partner_id" required="1"/>
+ <field name="taxes_id" domain="[('type_tax_use','=','purchase')]"/>
+ <field name="qty_purchase"/>
+ <field name="qty_min"/>
+ <field name="qty_buffer"/>
+ <field name="qty_available"/>
+ <field name="price"/>
+ <field name="subtotal"/>
+ <field name="last_order_id" readonly="1" optional="hide"/>
+ <field name="current_po_line_id" readonly="1" optional="hide"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="automatic_purchase_form" model="ir.ui.view">
+ <field name="name">automatic.purchase.form</field>
+ <field name="model">automatic.purchase</field>
+ <field name="arch" type="xml">
+ <form>
+ <header>
+ <button name="generate_automatic_lines"
+ string="Generate Lines"
+ type="object"
+ class="mr-2 oe_highlight"
+ />
+ <button name="create_purchase_orders"
+ string="Create PO"
+ type="object"
+ class="mr-2 oe_highlight"
+ />
+ </header>
+ <sheet string="Purchase">
+ <div class="oe_button_box" name="button_box">
+ <button type="object" name="action_view_related_po"
+ class="oe_stat_button"
+ icon="fa-pencil-square-o">
+ <field name="purchase_order_count" widget="statinfo" string="Related PO"/>
+ </button>
+ </div>
+ <group>
+ <group>
+ <field name="number"/>
+ <field name="apo_type" required="1"/>
+ </group>
+ <group>
+ <field name="date_doc"/>
+ <field name="responsible_id"/>
+ </group>
+ </group>
+ <notebook>
+ <page string="Lines">
+ <field name="automatic_purchase_lines"/>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="automatic_purchase_action" model="ir.actions.act_window">
+ <field name="name">Automatic Purchase</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">automatic.purchase</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem id="menu_automatic_purchase"
+ name="Automatic Purchase"
+ action="automatic_purchase_action"
+ parent="purchase.menu_procurement_management"
+ sequence="200"/>
+</odoo> \ No newline at end of file
diff --git a/fixco_custom/views/ir_sequence.xml b/fixco_custom/views/ir_sequence.xml
index de2188b..6e0e42a 100644
--- a/fixco_custom/views/ir_sequence.xml
+++ b/fixco_custom/views/ir_sequence.xml
@@ -44,5 +44,16 @@
<field name="number_increment">1</field>
<field name="company_id">4</field>
</record>
+
+ <record id="sequence_automatic_purchase" model="ir.sequence">
+ <field name="name">Automatic Purchase</field>
+ <field name="code">automatic.purchase</field>
+ <field name="active">TRUE</field>
+ <field name="prefix">APO/%(year)s/</field>
+ <field name="padding">5</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ <field name="company_id">4</field>
+ </record>
</data>
</odoo> \ No newline at end of file
diff --git a/fixco_custom/views/manage_stock.xml b/fixco_custom/views/manage_stock.xml
new file mode 100644
index 0000000..ee24706
--- /dev/null
+++ b/fixco_custom/views/manage_stock.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <record id="manage_stock_tree" model="ir.ui.view">
+ <field name="name">manage.stock.tree</field>
+ <field name="model">manage.stock</field>
+ <field name="arch" type="xml">
+ <tree default_order="create_date desc">
+ <field name="product_id"/>
+ <field name="min_stock"/>
+ <field name="buffer_stock"/>
+ <field name="vendor_id"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="manage_stock_form" model="ir.ui.view">
+ <field name="name">manage.stock.form</field>
+ <field name="model">manage.stock</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <group>
+ <group>
+ <field name="product_id"/>
+ <field name="min_stock"/>
+ <field name="buffer_stock"/>
+ <field name="vendor_id"/>
+ </group>
+ </group>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+
+ <record id="manage_stock_action" model="ir.actions.act_window">
+ <field name="name">Manage Stock</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">manage.stock</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem
+ action="manage_stock_action"
+ id="manage_stock"
+ parent="stock.menu_stock_warehouse_mgmt"
+ name="Manage Stock"
+ sequence="1"
+ />
+</odoo>
diff --git a/fixco_custom/views/res_partner.xml b/fixco_custom/views/res_partner.xml
index 89e2bc0..1085518 100755
--- a/fixco_custom/views/res_partner.xml
+++ b/fixco_custom/views/res_partner.xml
@@ -14,6 +14,9 @@
<field name="transaction_type"/>
<field name="customer_type"/>
</field>
+ <group name="purchase" position="inside">
+ <field name="tax_id" domain="[('type_tax_use','=','purchase')]"/>
+ </group>
</field>
</record>
</data>