From 4a7dd26f06d2d59768724dd13350f8aa1f0719f5 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 22 Oct 2025 13:47:21 +0700 Subject: push locator --- indoteknik_custom/models/stock_move.py | 47 ++++++++++++++++++++- indoteknik_custom/models/stock_picking.py | 70 +++++++++++++++++++------------ indoteknik_custom/views/stock_picking.xml | 2 + 3 files changed, 92 insertions(+), 27 deletions(-) diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py index a0c3ed95..e7ea8fd5 100644 --- a/indoteknik_custom/models/stock_move.py +++ b/indoteknik_custom/models/stock_move.py @@ -22,7 +22,6 @@ 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): @@ -247,6 +246,52 @@ class StockMoveLine(models.Model): line_no = fields.Integer('No', default=0) note = fields.Char('Note') manufacture = fields.Many2one('x_manufactures', string="Brands", related="product_id.x_manufacture", store=True) + outstanding_qty = fields.Float( + string='Outstanding Qty', + compute='_compute_delivery_line_status', + store=True + ) + delivery_status = fields.Selection([ + ('none', 'No Movement'), + ('partial', 'Partial'), + ('partial_final', 'Partial Final'), + ('full', 'Full'), + ], string='Delivery Status', compute='_compute_delivery_line_status', store=True) + + @api.depends('qty_done', 'product_uom_qty', 'picking_id.state') + def _compute_delivery_line_status(self): + for line in self: + line.outstanding_qty = 0.0 + line.delivery_status = 'none' + + if not line.picking_id or line.picking_id.picking_type_id.code != 'outgoing': + continue + + total_qty = line.move_id.product_uom_qty or 0 + done_qty = line.qty_done or 0 + + # Hitung sisa qty + line.outstanding_qty = max(total_qty - done_qty, 0) + + if total_qty == 0: + continue + + # 🟢 full + if done_qty >= total_qty: + line.delivery_status = 'full' + + # 🟡 partial + elif 0 < done_qty < total_qty: + # cek apakah masih ada backorder untuk picking ini + backorder_exists = self.env['stock.picking'].search_count([ + ('backorder_id', '=', line.picking_id.id), + ('state', 'not in', ('done', 'cancel')) + ]) + if backorder_exists: + line.delivery_status = 'partial' + else: + # 🔴 partial_final + line.delivery_status = 'partial_final' # Ambil uom dari stock move @api.model diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 3491ec26..c5b112a3 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -2191,33 +2191,44 @@ class CheckProduct(models.Model): self.quantity = 1 def unlink(self): - # Get all affected pickings before deletion + """ + Override unlink untuk: + 1. Simpan picking dan product_id yang terdampak sebelum delete. + 2. Hapus record check.product. + 3. Reset quantity_done untuk product yang udah gak ada di check_product_lines. + 4. Sync ulang product yang masih ada. + """ + # Step 1: ambil data sebelum hapus pickings = self.mapped('picking_id') - - # Store product_ids that will be deleted deleted_product_ids = self.mapped('product_id') - # Perform the deletion + # Step 2: hapus record result = super(CheckProduct, self).unlink() - # After deletion, update moves for affected pickings + # Step 3: update masing-masing picking for picking in pickings: - # For products that were completely removed (no remaining check.product lines) + # pastikan picking masih valid (kadang record udah kehapus bareng cascade) + if not picking.exists(): + continue + remaining_product_ids = picking.check_product_lines.mapped('product_id') removed_product_ids = deleted_product_ids - remaining_product_ids - # Set quantity_done to 0 for moves of completely removed products - moves_to_reset = picking.move_ids_without_package.filtered( - lambda move: move.product_id in removed_product_ids - ) - for move in moves_to_reset: - move.quantity_done = 0.0 + # Reset quantity_done untuk produk yang udah gak ada + if removed_product_ids: + moves_to_reset = picking.move_line_ids_without_package.filtered( + lambda m: m.product_id in removed_product_ids + ) + for move_line in moves_to_reset: + move_line.qty_done = 0.0 - # Also sync remaining products in case their totals changed - self._sync_check_product_to_moves(picking) + # Step 4: sync ulang product yang masih ada + if remaining_product_ids: + self._sync_check_product_to_moves(picking) return result + @api.depends('quantity') def _compute_status(self): for record in self: @@ -2249,33 +2260,40 @@ class CheckProduct(models.Model): def _sync_check_product_to_moves(self, picking): """ - Sinkronisasi quantity_done di move_line_ids_without_package + Sinkronisasi qty_done di move_line_ids_without_package berdasarkan total quantity dari check_product_lines per product_id, - dan distribusikan ke masing-masing move_line. + dan distribusikan ke move_line berdasarkan urutan rack_level ascending. """ for product_id in picking.check_product_lines.mapped('product_id'): - total_quantity = sum( + # Hitung total quantity dari check_product_lines untuk produk ini + remaining_qty = sum( line.quantity for line in picking.check_product_lines.filtered(lambda l: l.product_id == product_id) ) - move_lines = picking.move_line_ids_without_package.filtered(lambda m: m.product_id == product_id) + # Ambil move_line untuk product_id ini dan urutkan berdasarkan rack_level ASC + move_lines = picking.move_line_ids_without_package.filtered( + lambda m: m.product_id == product_id + ) + move_lines = sorted( + move_lines, + key=lambda m: m.location_id.rack_level or 0 # kalau rack_level kosong, dianggap 0 + ) - 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: + if remaining_qty <= 0: + move_line.qty_done = 0.0 continue - # kalau sisa qty cukup, isi penuh; kalau enggak, isi sebagian + needed = move_line.product_uom_qty 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 + # (Opsional) Log biar bisa dilacak + # _logger.info(f"[SYNC] {picking.name} - {product_id.display_name}: Sisa {remaining_qty}") + def _consolidate_duplicate_lines(self): diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml index 44ab6355..7668946c 100644 --- a/indoteknik_custom/views/stock_picking.xml +++ b/indoteknik_custom/views/stock_picking.xml @@ -394,6 +394,8 @@ decoration-danger="qty_done>product_uom_qty and state!='done' and parent.picking_type_code != 'incoming'" decoration-success="qty_done==product_uom_qty and state!='done' and not result_package_id"> + + -- cgit v1.2.3