diff options
| author | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-12 10:40:06 +0700 |
|---|---|---|
| committer | Azka Nathan <darizkyfaz@gmail.com> | 2025-07-12 10:40:06 +0700 |
| commit | ad1f21b27dff6eca4eb90d2a4496cd9ff80701c4 (patch) | |
| tree | d3335e6a6d1be85a903ef5699b07cc303619c954 | |
| parent | f12beff2f1e4da1244e7a8e014e73e5e5023aa9d (diff) | |
purchasing job, requisition, reordering v2
| -rwxr-xr-x | fixco_custom/__manifest__.py | 1 | ||||
| -rwxr-xr-x | fixco_custom/models/__init__.py | 1 | ||||
| -rw-r--r-- | fixco_custom/models/automatic_purchase.py | 106 | ||||
| -rw-r--r-- | fixco_custom/models/manage_stock.py | 75 | ||||
| -rw-r--r-- | fixco_custom/models/purchase_pricelist.py | 4 | ||||
| -rw-r--r-- | fixco_custom/models/purchasing_job.py | 113 | ||||
| -rwxr-xr-x | fixco_custom/security/ir.model.access.csv | 3 | ||||
| -rw-r--r-- | fixco_custom/views/automatic_purchase.xml | 12 | ||||
| -rw-r--r-- | fixco_custom/views/manage_stock.xml | 14 | ||||
| -rw-r--r-- | fixco_custom/views/purchasing_job.xml | 57 |
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> |
