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 | |
| parent | b858358ffbdd14c9b56ac96f035bddccae4d872d (diff) | |
reordering rules and manage stock
| -rwxr-xr-x | fixco_custom/__manifest__.py | 2 | ||||
| -rwxr-xr-x | fixco_custom/models/__init__.py | 2 | ||||
| -rw-r--r-- | fixco_custom/models/automatic_purchase.py | 247 | ||||
| -rw-r--r-- | fixco_custom/models/manage_stock.py | 21 | ||||
| -rwxr-xr-x | fixco_custom/models/partner.py | 1 | ||||
| -rw-r--r-- | fixco_custom/models/purchase_order.py | 6 | ||||
| -rw-r--r-- | fixco_custom/models/purchase_order_line.py | 10 | ||||
| -rwxr-xr-x | fixco_custom/security/ir.model.access.csv | 5 | ||||
| -rw-r--r-- | fixco_custom/views/automatic_purchase.xml | 98 | ||||
| -rw-r--r-- | fixco_custom/views/ir_sequence.xml | 11 | ||||
| -rw-r--r-- | fixco_custom/views/manage_stock.xml | 53 | ||||
| -rwxr-xr-x | fixco_custom/views/res_partner.xml | 3 |
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> |
