summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2025-07-12 10:40:06 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2025-07-12 10:40:06 +0700
commitad1f21b27dff6eca4eb90d2a4496cd9ff80701c4 (patch)
treed3335e6a6d1be85a903ef5699b07cc303619c954
parentf12beff2f1e4da1244e7a8e014e73e5e5023aa9d (diff)
purchasing job, requisition, reordering v2
-rwxr-xr-xfixco_custom/__manifest__.py1
-rwxr-xr-xfixco_custom/models/__init__.py1
-rw-r--r--fixco_custom/models/automatic_purchase.py106
-rw-r--r--fixco_custom/models/manage_stock.py75
-rw-r--r--fixco_custom/models/purchase_pricelist.py4
-rw-r--r--fixco_custom/models/purchasing_job.py113
-rwxr-xr-xfixco_custom/security/ir.model.access.csv3
-rw-r--r--fixco_custom/views/automatic_purchase.xml12
-rw-r--r--fixco_custom/views/manage_stock.xml14
-rw-r--r--fixco_custom/views/purchasing_job.xml57
10 files changed, 376 insertions, 10 deletions
diff --git a/fixco_custom/__manifest__.py b/fixco_custom/__manifest__.py
index 44ec3be..6c6c0df 100755
--- a/fixco_custom/__manifest__.py
+++ b/fixco_custom/__manifest__.py
@@ -35,6 +35,7 @@
'views/shipment_line.xml',
'views/manage_stock.xml',
'views/automatic_purchase.xml',
+ 'views/purchasing_job.xml',
],
'demo': [],
'css': [],
diff --git a/fixco_custom/models/__init__.py b/fixco_custom/models/__init__.py
index 150e15f..27178c6 100755
--- a/fixco_custom/models/__init__.py
+++ b/fixco_custom/models/__init__.py
@@ -21,3 +21,4 @@ from . import purchase_order
from . import account_move_line
from . import manage_stock
from . import automatic_purchase
+from . import purchasing_job
diff --git a/fixco_custom/models/automatic_purchase.py b/fixco_custom/models/automatic_purchase.py
index 7612f4a..f3f650d 100644
--- a/fixco_custom/models/automatic_purchase.py
+++ b/fixco_custom/models/automatic_purchase.py
@@ -19,8 +19,9 @@ class AutomaticPurchase(models.Model):
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'),
+ ('purchasing_job', 'Purchasing Job'),
('reordering', 'Reordering Rule'),
+ ('requisition', 'Requisition'),
], string='Type', tracking=True)
purchase_order_ids = fields.One2many(
'purchase.order',
@@ -31,6 +32,55 @@ class AutomaticPurchase(models.Model):
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,
+ 'subtotal': subtotal,
+ })
+
+ self.env['automatic.purchase.line'].create(lines)
def action_view_related_po(self):
self.ensure_one()
@@ -77,7 +127,7 @@ class AutomaticPurchase(models.Model):
for vendor_id, lines in vendor_lines.items():
vendor = self.env['res.partner'].browse(vendor_id)
- chunk_size = 20
+ 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):
@@ -105,7 +155,7 @@ class AutomaticPurchase(models.Model):
'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',
+ 'source': self.apo_type,
})
def _create_purchase_order_line(self, order, line):
@@ -141,7 +191,7 @@ class AutomaticPurchase(models.Model):
])
# Hitung total qty_available (quantity - reserved_quantity) untuk lokasi tersebut
- total_available = quant_records.available_quantity or 0.0
+ total_available = quant_records.quantity or 0.0
# Log nilai untuk debugging
_logger.info(
@@ -195,7 +245,6 @@ class AutomaticPurchase(models.Model):
'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,
@@ -235,7 +284,10 @@ class AutomaticPurchaseLine(models.Model):
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')
+ 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')
@@ -244,4 +296,44 @@ class AutomaticPurchaseLine(models.Model):
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
+ taxes_id = fields.Many2one('account.tax', string='Taxes')
+
+ 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'])
+ ], limit=1)
+ line.qty_outgoing = sum(move.product_uom_qty for move in stock_move) \ No newline at end of file
diff --git a/fixco_custom/models/manage_stock.py b/fixco_custom/models/manage_stock.py
index 4375ad2..e654b4e 100644
--- a/fixco_custom/models/manage_stock.py
+++ b/fixco_custom/models/manage_stock.py
@@ -15,7 +15,80 @@ class ManageStock(models.Model):
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)
+ qty_available = fields.Float(string='Qty Available', compute='_compute_qty_available')
+ qty_onhand = fields.Float(string='Qty Onhand', compute='_compute_qty_available')
_sql_constraints = [
('product_unique', 'unique (product_id)', 'This product already has a stock management rule!'),
- ] \ No newline at end of file
+ ]
+
+ def _compute_qty_available(self):
+ for record in self:
+ quant_records = self.env['stock.quant'].search([
+ ('product_id', '=', record.product_id.id),
+ # ('id','in', [80,81]),
+ ('location_id', '=', 55)
+ ])
+
+ total_available = quant_records.available_quantity or 0.0
+ total_onhand = quant_records.quantity or 0.0
+ record.qty_available = total_available
+ record.qty_onhand = total_onhand
+
+ def create_automatic_purchase(self):
+ if not self:
+ raise UserError("No stock records selected.")
+
+ automatic_purchase = self.env['automatic.purchase'].create({
+ 'apo_type': 'reordering',
+ })
+
+ lines_to_create = []
+
+ for stock in self:
+ location_id = 55
+
+ 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
+
+ qty_incoming = stock.product_id.incoming_qty or 0.0
+
+ qty_purchase = stock.buffer_stock - (total_available + qty_incoming)
+
+ qty_purchase = max(qty_purchase, 0.0)
+
+ 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
+
+ lines_to_create.append({
+ 'automatic_purchase_id': automatic_purchase.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 if stock.vendor_id.tax_id else False,
+ 'price': price,
+ 'subtotal': subtotal,
+ })
+
+ self.env['automatic.purchase.line'].create(lines_to_create)
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'automatic.purchase',
+ 'view_mode': 'form',
+ 'res_id': automatic_purchase.id,
+ 'target': 'current',
+ }
+
+
diff --git a/fixco_custom/models/purchase_pricelist.py b/fixco_custom/models/purchase_pricelist.py
index 2d0a77c..6f5bd21 100644
--- a/fixco_custom/models/purchase_pricelist.py
+++ b/fixco_custom/models/purchase_pricelist.py
@@ -13,6 +13,10 @@ class PurchasePricelist(models.Model):
vendor_id = fields.Many2one('res.partner', string="Vendor", required=True)
price = fields.Float(string='Price', required=True)
+ _sql_constraints = [
+ ('product_unique', 'unique (product_id)', 'This product already has a purchase pricelist!'),
+ ]
+
@api.depends('product_id', 'vendor_id')
def _compute_name(self):
self.name = self.vendor_id.name + ', ' + self.product_id.name \ No newline at end of file
diff --git a/fixco_custom/models/purchasing_job.py b/fixco_custom/models/purchasing_job.py
new file mode 100644
index 0000000..4f301f9
--- /dev/null
+++ b/fixco_custom/models/purchasing_job.py
@@ -0,0 +1,113 @@
+from odoo import models, fields, tools
+from odoo.exceptions import AccessError, UserError, ValidationError
+from datetime import timedelta, date
+
+class PurchasingJob(models.Model):
+ _name = 'purchasing.job'
+ _description = 'Procurement Monitoring by Product'
+ _auto = False
+ _rec_name = 'product'
+
+ id = fields.Integer(string='ID', readonly=True)
+ item_code = fields.Char(string='Item Code')
+ product = fields.Char(string='Product')
+ onhand = fields.Float(string='On Hand')
+ incoming = fields.Float(string='Incoming')
+ outgoing = fields.Float(string='Outgoing')
+ action = fields.Char(string='Action')
+ product_id = fields.Many2one('product.product', string='Product')
+ vendor_id = fields.Many2one('res.partner', string='Vendor')
+
+ def create_automatic_purchase(self):
+ if not self:
+ raise UserError("No Purchasing Job selected.")
+
+ automatic_purchase = self.env['automatic.purchase'].create({
+ 'apo_type': 'purchasing_job',
+ })
+
+ lines_to_create = []
+
+ for stock in self:
+ manage_stock = self.env['manage.stock'].search([
+ ('product_id', '=', stock.product_id.id)
+ ], limit=1)
+
+ qty_purchase = stock.outgoing - (stock.onhand + stock.incoming)
+
+ qty_purchase = max(qty_purchase, 0.0)
+
+ 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
+
+ lines_to_create.append({
+ 'automatic_purchase_id': automatic_purchase.id,
+ 'product_id': stock.product_id.id,
+ 'qty_purchase': qty_purchase,
+ 'qty_min': manage_stock.min_stock,
+ 'qty_buffer': manage_stock.buffer_stock,
+ 'partner_id': stock.vendor_id.id,
+ 'taxes_id': stock.vendor_id.tax_id.id if stock.vendor_id.tax_id else False,
+ 'price': price,
+ 'subtotal': subtotal,
+ })
+
+ self.env['automatic.purchase.line'].create(lines_to_create)
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'automatic.purchase',
+ 'view_mode': 'form',
+ 'res_id': automatic_purchase.id,
+ 'target': 'current',
+ }
+
+ def init(self):
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self.env.cr.execute("""
+ CREATE OR REPLACE VIEW %s AS
+ SELECT
+ row_number() OVER () AS id,
+ a.item_code,
+ a.product,
+ a.onhand,
+ a.incoming,
+ a.outgoing,
+ CASE
+ WHEN (a.incoming + a.onhand) < a.outgoing THEN 'kurang'
+ ELSE 'cukup'
+ END AS action,
+ a.product_id,
+ pp2.vendor_id
+ FROM (
+ SELECT
+ COALESCE(pp.default_code, pt.default_code) AS item_code,
+ pt.name AS product,
+ get_qty_onhand(pp.id::numeric) AS onhand,
+ get_qty_incoming(pp.id::numeric) AS incoming,
+ get_qty_outgoing(pp.id::numeric) AS outgoing,
+ pp.id AS product_id
+ FROM stock_move sm
+ JOIN stock_picking sp ON sp.id = sm.picking_id
+ JOIN product_product pp ON pp.id = sm.product_id
+ JOIN product_template pt ON pt.id = pp.product_tmpl_id
+ WHERE sp.state IN ('draft', 'waiting', 'confirmed', 'assigned')
+ AND sp.name LIKE '%%OUT%%'
+ AND sm.location_id = 55
+ GROUP BY pp.id, pp.default_code, pt.default_code, pt.name
+ ) a
+ LEFT JOIN LATERAL (
+ SELECT vendor_id
+ FROM purchase_pricelist
+ WHERE product_id = a.product_id
+ ORDER BY id ASC
+ LIMIT 1
+ ) pp2 ON true
+ """ % self._table)
+
+
diff --git a/fixco_custom/security/ir.model.access.csv b/fixco_custom/security/ir.model.access.csv
index cdfaa49..3c4541b 100755
--- a/fixco_custom/security/ir.model.access.csv
+++ b/fixco_custom/security/ir.model.access.csv
@@ -25,4 +25,5 @@ access_upload_ginee_line,access.upload.ginee.line,model_upload_ginee_line,,1,1,1
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
+access_automatic_purchase,access.automatic.purchase,model_automatic_purchase,,1,1,1,1
+access_purchasing_job,access.purchasing.job,model_purchasing_job,,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
index 704a5e3..d303b24 100644
--- a/fixco_custom/views/automatic_purchase.xml
+++ b/fixco_custom/views/automatic_purchase.xml
@@ -25,7 +25,10 @@
<field name="qty_purchase"/>
<field name="qty_min"/>
<field name="qty_buffer"/>
- <field name="qty_available"/>
+ <field name="qty_available" optional="hide"/>
+ <field name="qty_onhand" optional="hide"/>
+ <field name="qty_incoming" optional="hide"/>
+ <field name="qty_outgoing" optional="hide"/>
<field name="price"/>
<field name="subtotal"/>
<field name="last_order_id" readonly="1" optional="hide"/>
@@ -44,12 +47,18 @@
string="Generate Lines"
type="object"
class="mr-2 oe_highlight"
+ attrs="{'invisible': [('apo_type', '!=', 'reordering')]}"
/>
<button name="create_purchase_orders"
string="Create PO"
type="object"
class="mr-2 oe_highlight"
/>
+ <button name="action_generate_lines_from_so"
+ string="Generate Lines from Sales Order"
+ type="object"
+ class="btn-primary"
+ attrs="{'invisible': [('sale_order_id', '=', False)]}" />
</header>
<sheet string="Purchase">
<div class="oe_button_box" name="button_box">
@@ -63,6 +72,7 @@
<group>
<field name="number"/>
<field name="apo_type" required="1"/>
+ <field name="sale_order_id" attrs="{'invisible': [('apo_type', '!=', 'requisition')]}"/>
</group>
<group>
<field name="date_doc"/>
diff --git a/fixco_custom/views/manage_stock.xml b/fixco_custom/views/manage_stock.xml
index ee24706..c617e11 100644
--- a/fixco_custom/views/manage_stock.xml
+++ b/fixco_custom/views/manage_stock.xml
@@ -8,6 +8,8 @@
<field name="product_id"/>
<field name="min_stock"/>
<field name="buffer_stock"/>
+ <field name="qty_available"/>
+ <field name="qty_onhand"/>
<field name="vendor_id"/>
</tree>
</field>
@@ -24,6 +26,8 @@
<field name="product_id"/>
<field name="min_stock"/>
<field name="buffer_stock"/>
+ <field name="qty_available"/>
+ <field name="qty_onhand"/>
<field name="vendor_id"/>
</group>
</group>
@@ -36,6 +40,16 @@
</field>
</record>
+ <record id="action_create_automatic_purchase_manage_stock" model="ir.actions.server">
+ <field name="name">Create Automatic Purchase</field>
+ <field name="model_id" ref="model_manage_stock"/>
+ <field name="binding_model_id" ref="model_manage_stock"/>
+ <field name="state">code</field>
+ <field name="code">
+ action = records.create_automatic_purchase()
+ </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>
diff --git a/fixco_custom/views/purchasing_job.xml b/fixco_custom/views/purchasing_job.xml
new file mode 100644
index 0000000..7d16682
--- /dev/null
+++ b/fixco_custom/views/purchasing_job.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+ <!-- Tree View -->
+ <record id="view_purchasing_job_tree" model="ir.ui.view">
+ <field name="name">purchasing.job.tree</field>
+ <field name="model">purchasing.job</field>
+ <field name="arch" type="xml">
+ <tree string="Procurement Monitoring by Product" create="false" delete="false">
+ <field name="item_code"/>
+ <field name="product"/>
+ <field name="vendor_id"/>
+ <field name="onhand"/>
+ <field name="incoming"/>
+ <field name="outgoing"/>
+ <field name="action"/>
+ </tree>
+ </field>
+ </record>
+
+ <record id="view_purchasing_job_filter" model="ir.ui.view">
+ <field name="name">purchasing.job.list.select</field>
+ <field name="model">purchasing.job</field>
+ <field name="priority" eval="15"/>
+ <field name="arch" type="xml">
+ <search string="Search">
+ <field name="product_id"/>
+ </search>
+ </field>
+ </record>
+
+ <record id="action_create_automatic_purchase_purchasing_job" model="ir.actions.server">
+ <field name="name">Create Automatic Purchase</field>
+ <field name="model_id" ref="model_purchasing_job"/>
+ <field name="binding_model_id" ref="model_purchasing_job"/>
+ <field name="state">code</field>
+ <field name="code">
+ action = records.create_automatic_purchase()
+ </field>
+ </record>
+
+ <record id="purchasing_job_action" model="ir.actions.act_window">
+ <field name="name">Purchasing Job</field>
+ <field name="type">ir.actions.act_window</field>
+ <field name="res_model">purchasing.job</field>
+ <field name="search_view_id" ref="view_purchasing_job_filter"/>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <!-- Menu Item -->
+ <menuitem
+ id="menu_purchasing_job"
+ name="Purchasing Job"
+ parent="purchase.menu_procurement_management"
+ sequence="201"
+ action="purchasing_job_action"
+ />
+</odoo>