From 826f19a3f5de7747192fd8ed4ba0c247084ed03c Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Mon, 20 Oct 2025 16:13:29 +0700 Subject: push locator --- indoteknik_custom/models/__init__.py | 2 + indoteknik_custom/models/stock_location.py | 40 ++++++++++++++ indoteknik_custom/models/stock_move.py | 72 +++++++++++++++++++++++++- indoteknik_custom/models/stock_picking.py | 33 ++++++++---- indoteknik_custom/models/stock_quant.py | 6 +++ indoteknik_custom/security/ir.model.access.csv | 3 ++ indoteknik_custom/views/stock_location.xml | 25 +++++++++ indoteknik_custom/views/stock_quant.xml | 14 +++++ 8 files changed, 184 insertions(+), 11 deletions(-) create mode 100644 indoteknik_custom/models/stock_location.py create mode 100644 indoteknik_custom/models/stock_quant.py diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 5ac4d6ca..c6a85b75 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -162,3 +162,5 @@ from . import letter_receivable from . import sj_tele from . import partial_delivery from . import domain_apo +from . import stock_location +from . import stock_quant diff --git a/indoteknik_custom/models/stock_location.py b/indoteknik_custom/models/stock_location.py new file mode 100644 index 00000000..d075ff2a --- /dev/null +++ b/indoteknik_custom/models/stock_location.py @@ -0,0 +1,40 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +class StockLocation(models.Model): + _inherit = 'stock.location' + + rack_level = fields.Integer( + string='Sequence', + default=1, + help='Indicates the vertical rack level (1 = lowest, 4 = highest).' + ) + + # level = fields.Integer( + # string='Rack Level', + # default=1, + # help='Indicates the vertical rack level (1 = lowest, 4 = highest).' + # ) + + is_locked = fields.Boolean( + string="Locked", + default=False, + help="Jika dicentang, lokasi ini tidak dapat digunakan untuk reservasi atau penerimaan barang." + ) + + @api.constrains('rack_level') + def _check_rack_level(self): + for rec in self: + if rec.rack_level < 1 or rec.rack_level > 4: + raise ValidationError(_("Rack level harus antara 1 sampai 4.")) + + @api.constrains('is_locked') + def _sync_locked_quant(self): + Quant = self.env['stock.quant'] + for rec in self: + quants = Quant.search([('location_id', '=', rec.id)]) + if quants: + if rec.is_locked: + quants.write({'note': 'Locked'}) + else: + quants.write({'note': False}) diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index 1da2befe..a0c3ed95 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -2,6 +2,8 @@ from odoo import fields, models, api from odoo.tools.misc import format_date, OrderedSet from odoo.exceptions import UserError import logging +from odoo.tools.float_utils import float_compare, float_is_zero, float_round + _logger = logging.getLogger(__name__) @@ -20,7 +22,7 @@ class StockMove(models.Model): hold_outgoingg = fields.Boolean('Hold Outgoing', default=False) product_image = fields.Binary(related="product_id.image_128", string="Product Image", readonly=True) partial = fields.Boolean('Partial?', default=False) - + # Ambil product uom dari SO line @api.model def create(self, vals): @@ -28,7 +30,66 @@ class StockMove(models.Model): sale_line = self.env['sale.order.line'].browse(vals['sale_line_id']) vals['product_uom'] = sale_line.product_uom.id return super().create(vals) + + def _update_reserved_quantity( + self, need, available_quantity, location_id, + lot_id=None, package_id=None, owner_id=None, strict=True + ): + self.ensure_one() + + picking = self.picking_id + if picking and 'BU/PICK' in (picking.name or ''): + _logger.info(f"[LocatorLogic] Running custom locator logic for {picking.name}") + + # Ambil semua lokasi anak dari source location (ex: BU/Stock) + locations = self.env['stock.location'].search([ + ('id', 'child_of', self.location_id.id), + ('usage', '=', 'internal'), + ('is_locked', '=', False), + # ('id', '!=', 57), + ], order='rack_level asc') + + total_reserved = 0.0 + remaining_need = need + + for loc in locations: + if remaining_need <= 0: + break + + quants = self.env['stock.quant']._gather(self.product_id, loc) + for quant in quants: + if quant.available_quantity <= 0: + continue + qty_to_take = min(quant.available_quantity, remaining_need) + _logger.info( + f"[LocatorLogic] Reserving {qty_to_take}/{remaining_need} " + f"from {loc.display_name} (avail={quant.available_quantity})" + ) + + reserved_now = super(StockMove, self)._update_reserved_quantity( + qty_to_take, quant.available_quantity, quant.location_id, + lot_id, package_id, owner_id, strict + ) + + total_reserved += reserved_now + remaining_need -= reserved_now + + if remaining_need <= 0: + break + + if total_reserved > 0: + _logger.info(f"[LocatorLogic] Total reserved: {total_reserved} / {need}") + return total_reserved + else: + _logger.info("[LocatorLogic] No available stock found in unlocked locations by level order.") + return 0 + + return super(StockMove, self)._update_reserved_quantity( + need, available_quantity, location_id, lot_id, package_id, owner_id, strict + ) + + # @api.model_create_multi # def create(self, vals_list): # moves = super(StockMove, self).create(vals_list) @@ -194,4 +255,11 @@ class StockMoveLine(models.Model): move = self.env['stock.move'].browse(vals['move_id']) if move.product_uom: vals['product_uom_id'] = move.product_uom.id - return super().create(vals) \ No newline at end of file + return super().create(vals) + def _action_done(self): + for line in self: + if line.location_dest_id and line.location_dest_id.is_locked: + raise UserError(f"Lokasi '{line.location_dest_id.display_name}' sedang dikunci dan tidak bisa menerima barang.") + if line.location_id and line.location_id.is_locked: + raise UserError(f"Lokasi '{line.location_id.display_name}' sedang dikunci dan tidak bisa reserve barang.") + return super(StockMoveLine, self)._action_done() diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 4772c433..3491ec26 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -2249,19 +2249,34 @@ class CheckProduct(models.Model): def _sync_check_product_to_moves(self, picking): """ - Sinkronisasi quantity_done di move_ids_without_package - dengan total quantity dari check.product berdasarkan product_id. + Sinkronisasi quantity_done di move_line_ids_without_package + berdasarkan total quantity dari check_product_lines per product_id, + dan distribusikan ke masing-masing move_line. """ for product_id in picking.check_product_lines.mapped('product_id'): - # Totalkan quantity dari semua baris check.product untuk product_id ini total_quantity = sum( - line.quantity for line in - picking.check_product_lines.filtered(lambda line: line.product_id == product_id) + line.quantity + for line in picking.check_product_lines.filtered(lambda l: l.product_id == product_id) ) - # Update quantity_done di move yang relevan - moves = picking.move_ids_without_package.filtered(lambda move: move.product_id == product_id) - for move in moves: - move.quantity_done = total_quantity + + move_lines = picking.move_line_ids_without_package.filtered(lambda m: m.product_id == product_id) + + remaining_qty = total_quantity + for move_line in move_lines: + # ambil qty yang idealnya diisi (biasanya product_uom_qty - quantity_done) + needed = move_line.product_uom_qty - move_line.qty_done + if needed <= 0: + continue + + # kalau sisa qty cukup, isi penuh; kalau enggak, isi sebagian + assigned_qty = min(needed, remaining_qty) + move_line.qty_done = assigned_qty + remaining_qty -= assigned_qty + + # kalau sisa udah 0, berhenti aja + if remaining_qty <= 0: + break + def _consolidate_duplicate_lines(self): """ diff --git a/indoteknik_custom/models/stock_quant.py b/indoteknik_custom/models/stock_quant.py new file mode 100644 index 00000000..05335115 --- /dev/null +++ b/indoteknik_custom/models/stock_quant.py @@ -0,0 +1,6 @@ +from odoo import _, api, fields, models + +class StockQuant(models.Model): + _inherit = 'stock.quant' + + note = fields.Char(string="Note") \ No newline at end of file diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index a6175b21..53560f44 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -205,3 +205,6 @@ access_unpaid_invoice_view,access.unpaid.invoice.view,model_unpaid_invoice_view, access_surat_piutang_user,surat.piutang user,model_surat_piutang,,1,1,1,1 access_surat_piutang_line_user,surat.piutang.line user,model_surat_piutang_line,,1,1,1,1 access_sj_tele,access.sj.tele,model_sj_tele,base.group_system,1,1,1,1 + +access_stock_location,access.stock.location,model_stock_location,,1,1,1,1 +access_stock_quant,access.stock.quant,model_stock_quant,,1,1,1,1 \ No newline at end of file diff --git a/indoteknik_custom/views/stock_location.xml b/indoteknik_custom/views/stock_location.xml index 82ab2bc5..af6706e9 100644 --- a/indoteknik_custom/views/stock_location.xml +++ b/indoteknik_custom/views/stock_location.xml @@ -5,4 +5,29 @@ 3 inventory + + + stock.location.tree.rack_level + stock.location + + + + + + + + + + + + stock.location.form.rack_level + stock.location + + + + + + + + \ No newline at end of file diff --git a/indoteknik_custom/views/stock_quant.xml b/indoteknik_custom/views/stock_quant.xml index 107f75f3..a665529e 100644 --- a/indoteknik_custom/views/stock_quant.xml +++ b/indoteknik_custom/views/stock_quant.xml @@ -9,6 +9,20 @@ + + + + + + + + stock.view_stock_quant_tree_inherited + stock.quant + + + + + -- cgit v1.2.3