From 9f9081714356e87500ab05bc5a294e9ca9e526fe Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 6 Aug 2025 08:49:56 +0700 Subject: inv and bil return option --- indoteknik_custom/models/tukar_guling.py | 41 ++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 3f81393a..d511779e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -74,6 +74,38 @@ class TukarGuling(models.Model): date_sales = fields.Datetime('Approved Date Sales', tracking=3, readonly=True) date_logistic = fields.Datetime('Approved Date Logistic', tracking=3, readonly=True) + val_inv_opt = fields.Selection([ + ('tanpa_cancel', 'Tanpa Cancel Invoice'), + ('cancel_invoice', 'Cancel Invoice'), + ], tracking=3, string='Invoice Option') + + is_has_invoice = fields.Boolean('Has Invoice?', compute='_compute_is_has_invoice', readonly=True, default=False) + + invoice_id = fields.Many2one('account.move', string='Invoice Ref', readonly=True) + + @api.depends('origin') + def _compute_is_has_invoice(self): + for rec in self: + invoices = self.env['account.move'].search([ + ('invoice_origin', 'ilike', rec.origin), + ('move_type', '=', 'out_invoice'), # hanya invoice + ('state', 'not in', ['draft', 'cancel']) + ]) + if invoices: + rec.is_has_invoice = True + rec.invoice_id = invoices + else: + rec.is_has_invoice = False + + def set_opt(self): + if not self.val_inv_opt and self.is_has_invoice == True: + raise UserError("Kalau sudah ada invoice Return Invoice Option harus diisi!") + for rec in self: + if rec.val_inv_opt == 'cancel_invoice' and self.is_has_invoice == True: + raise UserError("Tidak bisa mengubah Return karena sudah ada invoice dan belum di cancel.") + elif rec.val_inv_opt == 'tanpa_cancel' and self.is_has_invoice == True: + continue + # @api.onchange('operations') # def get_partner_id(self): # if self.operations and self.operations.partner_id and self.operations.partner_id.name: @@ -288,7 +320,6 @@ class TukarGuling(models.Model): # ('state', '!=', 'cancel') # ]) > 0 - @api.constrains('return_type', 'operations') def _check_invoice_on_revisi_so(self): for record in self: if record.return_type == 'revisi_so' and record.origin: @@ -348,7 +379,7 @@ class TukarGuling(models.Model): self.ensure_one() if self.operations.picking_type_id.id not in [29, 30]: raise UserError("❌ Picking type harus BU/OUT atau BU/PICK") - self._check_invoice_on_revisi_so() + # self._check_invoice_on_revisi_so() operasi = self.operations.picking_type_id.id tipe = self.return_type pp = vals.get('return_type', tipe) @@ -455,7 +486,7 @@ class TukarGuling(models.Model): raise UserError( _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) - self._check_invoice_on_revisi_so() + # self._check_invoice_on_revisi_so() self._validate_product_lines() if self.state != 'draft': @@ -506,7 +537,7 @@ class TukarGuling(models.Model): def action_approve(self): self.ensure_one() self._validate_product_lines() - self._check_invoice_on_revisi_so() + # self._check_invoice_on_revisi_so() self._check_not_allow_tukar_guling_on_bu_pick() operasi = self.operations.picking_type_id.id @@ -546,6 +577,8 @@ class TukarGuling(models.Model): elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") + rec._check_invoice_on_revisi_so() + rec.set_opt() rec.state = 'approval_logistic' rec.date_finance = now -- cgit v1.2.3 From 9fb81ccccb8f2735ac5811d81b644b0ae70c4d8e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 6 Aug 2025 10:33:25 +0700 Subject: push --- indoteknik_custom/models/tukar_guling.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index d511779e..3091acce 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -320,17 +320,17 @@ class TukarGuling(models.Model): # ('state', '!=', 'cancel') # ]) > 0 - def _check_invoice_on_revisi_so(self): - for record in self: - if record.return_type == 'revisi_so' and record.origin: - invoices = self.env['account.move'].search([ - ('invoice_origin', 'ilike', record.origin), - ('state', 'not in', ['draft', 'cancel']) - ]) - if invoices: - raise ValidationError( - _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin - ) + # def _check_invoice_on_revisi_so(self): + # for record in self: + # if record.return_type == 'revisi_so' and record.origin: + # invoices = self.env['account.move'].search([ + # ('invoice_origin', 'ilike', record.origin), + # ('state', 'not in', ['draft', 'cancel']) + # ]) + # if invoices: + # raise ValidationError( + # _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin + # ) @api.model def create(self, vals): @@ -577,7 +577,7 @@ class TukarGuling(models.Model): elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") - rec._check_invoice_on_revisi_so() + # rec._check_invoice_on_revisi_so() rec.set_opt() rec.state = 'approval_logistic' rec.date_finance = now -- cgit v1.2.3 From ab11c9455c6e125195ec5004adfd855058d46f5f Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 6 Aug 2025 12:56:35 +0700 Subject: fix error singleton --- indoteknik_custom/models/tukar_guling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 3091acce..8de5b671 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -81,7 +81,7 @@ class TukarGuling(models.Model): is_has_invoice = fields.Boolean('Has Invoice?', compute='_compute_is_has_invoice', readonly=True, default=False) - invoice_id = fields.Many2one('account.move', string='Invoice Ref', readonly=True) + invoice_id = fields.Many2many('account.move', string='Invoice Ref', readonly=True) @api.depends('origin') def _compute_is_has_invoice(self): -- cgit v1.2.3 From cad9851dc642c877dcb1913e5bb12dc6405d9652 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Wed, 6 Aug 2025 14:19:32 +0700 Subject: fix when no bu pick --- indoteknik_custom/models/tukar_guling.py | 45 +++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 8de5b671..881021ab 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -479,13 +479,22 @@ class TukarGuling(models.Model): # raise UserError("BU ini sudah pernah diretur oleh dokumen lain.") if self.operations.picking_type_id.id == 29: - for line in self.line_ids: - mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) - total_qty = sum(l.qty_return for l in mapping_lines) - if total_qty != line.product_uom_qty: - raise UserError( - _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) - + # Cek apakah ada BU/PICK di origin + origin = self.operations.origin + has_bu_pick = self.env['stock.picking'].search_count([ + ('origin', '=', origin), + ('picking_type_id', '=', 30), + ('state', '!=', 'cancel') + ]) > 0 + + if has_bu_pick: + for line in self.line_ids: + mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) + total_qty = sum(l.qty_return for l in mapping_lines) + if total_qty != line.product_uom_qty: + raise UserError( + _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name + ) # self._check_invoice_on_revisi_so() self._validate_product_lines() @@ -544,12 +553,22 @@ class TukarGuling(models.Model): tipe = self.return_type if self.operations.picking_type_id.id == 29: - for line in self.line_ids: - mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) - total_qty = sum(l.qty_return for l in mapping_lines) - if total_qty != line.product_uom_qty: - raise UserError( - _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name) + # Cek apakah ada BU/PICK di origin + origin = self.operations.origin + has_bu_pick = self.env['stock.picking'].search_count([ + ('origin', '=', origin), + ('picking_type_id', '=', 30), + ('state', '!=', 'cancel') + ]) > 0 + + if has_bu_pick: + for line in self.line_ids: + mapping_lines = self.mapping_koli_ids.filtered(lambda x: x.product_id == line.product_id) + total_qty = sum(l.qty_return for l in mapping_lines) + if total_qty != line.product_uom_qty: + raise UserError( + _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name + ) if operasi == 30 and self.operations.linked_manual_bu_out.state == 'done': raise UserError("❌ Tidak bisa retur BU/PICK karena BU/OUT sudah done") -- cgit v1.2.3 From 663b5280d5f9d940c99ccf34f0d88c520eaebeb7 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 7 Aug 2025 18:00:22 +0700 Subject: fix move line location not sync with stock picking --- indoteknik_custom/models/tukar_guling.py | 49 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 19 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 881021ab..394672d0 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -636,6 +636,23 @@ class TukarGuling(models.Model): def _create_pickings(self): _logger.info("🛠 Starting _create_pickings()") + + def _force_locations(picking, from_loc, to_loc): + picking.write({ + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + for move in picking.move_lines: + move.write({ + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + for move_line in move.move_line_ids: + move_line.write({ + 'location_id': from_loc, + 'location_dest_id': to_loc, + }) + for record in self: if not record.operations: raise UserError("BU/OUT dari field operations tidak ditemukan.") @@ -680,14 +697,13 @@ class TukarGuling(models.Model): }).create({ 'picking_id': bu_out.id, 'location_id': PARTNER_LOCATION_ID, - 'original_location_id': BU_OUTPUT_LOCATION_ID, 'product_return_moves': srt_return_lines }) srt_vals = srt_wizard.create_returns() srt_picking = self.env['stock.picking'].browse(srt_vals['res_id']) + _force_locations(srt_picking, PARTNER_LOCATION_ID, BU_OUTPUT_LOCATION_ID) + srt_picking.write({ - 'location_id': PARTNER_LOCATION_ID, - 'location_dest_id': BU_OUTPUT_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, 'sale_order': record.origin @@ -700,13 +716,11 @@ class TukarGuling(models.Model): ### ======== ORT dari BU/PICK ========= ort_pickings = [] is_retur_from_bu_pick = record.operations.picking_type_id.id == 30 - picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped( - 'pick_id') or line.product_uom_qty + picks_to_return = [record.operations] if is_retur_from_bu_pick else mapping_koli.mapped('pick_id') for pick in picks_to_return: ort_return_lines = [] if is_retur_from_bu_pick: - # Ambil dari tukar.guling.line for line in record.line_ids: move = pick.move_lines.filtered(lambda m: m.product_id == line.product_id) if not move: @@ -720,7 +734,6 @@ class TukarGuling(models.Model): _logger.info( f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}") else: - # Ambil dari mapping koli for mk in mapping_koli.filtered(lambda m: m.pick_id == pick): move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id) if not move: @@ -743,27 +756,27 @@ class TukarGuling(models.Model): }).create({ 'picking_id': pick.id, 'location_id': BU_OUTPUT_LOCATION_ID, - 'original_location_id': BU_STOCK_LOCATION_ID, 'product_return_moves': ort_return_lines }) + ort_vals = ort_wizard.create_returns() ort_picking = self.env['stock.picking'].browse(ort_vals['res_id']) + _force_locations(ort_picking, BU_OUTPUT_LOCATION_ID, BU_STOCK_LOCATION_ID) + ort_picking.write({ - 'location_id': BU_OUTPUT_LOCATION_ID, - 'location_dest_id': BU_STOCK_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, 'sale_order': record.origin }) + created_returns.append(ort_picking) ort_pickings.append(ort_picking) _logger.info(f"✅ ORT created: {ort_picking.name}") record.message_post( body=f"📦 {ort_picking.name} created by {self.env.user.name} (state: {ort_picking.state})") - ### ======== Tukar Guling: BU/OUT dan BU/PICK baru ======== + ### ======== BU/PICK & BU/OUT Baru dari SRT/ORT ======== if record.return_type == 'tukar_guling': - # BU/PICK Baru dari ORT for ort_p in ort_pickings: return_lines = [] @@ -789,19 +802,18 @@ class TukarGuling(models.Model): }).create({ 'picking_id': ort_p.id, 'location_id': BU_STOCK_LOCATION_ID, - 'original_location_id': BU_OUTPUT_LOCATION_ID, 'product_return_moves': return_lines }) bu_pick_vals = bu_pick_wizard.create_returns() new_pick = self.env['stock.picking'].browse(bu_pick_vals['res_id']) + _force_locations(new_pick, BU_STOCK_LOCATION_ID, BU_OUTPUT_LOCATION_ID) + new_pick.write({ - 'location_id': BU_STOCK_LOCATION_ID, - 'location_dest_id': BU_OUTPUT_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, 'sale_order': record.origin }) - new_pick.action_assign() # Penting agar bisa trigger check koli + new_pick.action_assign() new_pick.action_confirm() created_returns.append(new_pick) _logger.info(f"✅ BU/PICK Baru dari ORT created: {new_pick.name}") @@ -829,14 +841,13 @@ class TukarGuling(models.Model): }).create({ 'picking_id': srt_picking.id, 'location_id': BU_OUTPUT_LOCATION_ID, - 'original_location_id': PARTNER_LOCATION_ID, 'product_return_moves': return_lines }) bu_out_vals = bu_out_wizard.create_returns() new_out = self.env['stock.picking'].browse(bu_out_vals['res_id']) + _force_locations(new_out, BU_OUTPUT_LOCATION_ID, PARTNER_LOCATION_ID) + new_out.write({ - 'location_id': BU_OUTPUT_LOCATION_ID, - 'location_dest_id': PARTNER_LOCATION_ID, 'group_id': bu_out.group_id.id, 'tukar_guling_id': record.id, 'sale_order': record.origin -- cgit v1.2.3 From 26b1df8d150a46297d84f24283687c56b81e4e65 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 7 Aug 2025 20:12:49 +0700 Subject: fix for SO lama, where it doesnt have bu pick. srt set to partner loc to stcok --- indoteknik_custom/models/tukar_guling.py | 44 ++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 394672d0..4c7722d5 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -675,33 +675,51 @@ class TukarGuling(models.Model): ### ======== SRT dari BU/OUT ========= srt_return_lines = [] - for prod in mapping_koli.mapped('product_id'): - qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) - move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) - if not move: - raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}") - srt_return_lines.append((0, 0, { - 'product_id': prod.id, - 'quantity': qty_total, - 'move_id': move.id, - })) - _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") + if mapping_koli: + for prod in mapping_koli.mapped('product_id'): + qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) + move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) + if not move: + raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}") + srt_return_lines.append((0, 0, { + 'product_id': prod.id, + 'quantity': qty_total, + 'move_id': move.id, + })) + _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") + + elif not mapping_koli: + for line in record.line_ids: + move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id) + if not move: + raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}") + srt_return_lines.append((0, 0, { + 'product_id': line.product_id.id, + 'quantity': line.product_uom_qty, + 'move_id': move.id, + })) + _logger.info( + f"📟 SRT line (fallback line_ids): {line.product_id.display_name} | qty={line.product_uom_qty}") srt_picking = None if srt_return_lines: + # Tentukan tujuan lokasi berdasarkan ada/tidaknya mapping_koli + dest_location_id = BU_OUTPUT_LOCATION_ID if mapping_koli else BU_STOCK_LOCATION_ID + srt_wizard = self.env['stock.return.picking'].with_context({ 'active_id': bu_out.id, 'default_location_id': PARTNER_LOCATION_ID, - 'default_location_dest_id': BU_OUTPUT_LOCATION_ID, + 'default_location_dest_id': dest_location_id, 'from_ui': False, }).create({ 'picking_id': bu_out.id, 'location_id': PARTNER_LOCATION_ID, 'product_return_moves': srt_return_lines }) + srt_vals = srt_wizard.create_returns() srt_picking = self.env['stock.picking'].browse(srt_vals['res_id']) - _force_locations(srt_picking, PARTNER_LOCATION_ID, BU_OUTPUT_LOCATION_ID) + _force_locations(srt_picking, PARTNER_LOCATION_ID, dest_location_id) srt_picking.write({ 'group_id': bu_out.group_id.id, -- cgit v1.2.3 From 176dd85c3d809c035128847378bf78d96aa0896a Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 8 Aug 2025 09:16:15 +0700 Subject: handle duplicate prod in stock move line --- indoteknik_custom/models/tukar_guling.py | 87 ++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 20 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 4c7722d5..eadb164c 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -2,6 +2,7 @@ from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError import logging from datetime import datetime +from collections import defaultdict _logger = logging.getLogger(__name__) @@ -540,7 +541,7 @@ class TukarGuling(models.Model): ]) if self.state == 'approved' and done_ort: self.state = 'done' - else: + elif self.operations.picking_type_id.id == 30 and self.return_type == 'revisi_so' and not has_bu_pick: raise UserError("Tidak bisa menentukan jenis retur.") def action_approve(self): @@ -676,30 +677,47 @@ class TukarGuling(models.Model): ### ======== SRT dari BU/OUT ========= srt_return_lines = [] if mapping_koli: - for prod in mapping_koli.mapped('product_id'): - qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) - move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) - if not move: - raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}") + move_per_product = defaultdict(list) + for move in bu_out.move_lines: + move_per_product[move.product_id.id].append(move) + + mapped_product_ids = set(mapping_koli.mapped('product_id').ids) + + for product_id in mapped_product_ids: + qty_ret = sum( + line.qty_return for line in mapping_koli.filtered(lambda l: l.product_id.id == product_id)) + if qty_ret <= 0: + continue + + product_moves = move_per_product[product_id] + if not product_moves: + raise UserError(f"❌ Move BU/OUT tidak ditemukan untuk product ID {product_id}") + + if len(product_moves) > 1: + _logger.warning(f"🟠 Detected duplicate moves for product {product_id}, picking {bu_out.name}") + chosen_move = product_moves[0] + else: + chosen_move = product_moves[0] + srt_return_lines.append((0, 0, { - 'product_id': prod.id, - 'quantity': qty_total, - 'move_id': move.id, + 'product_id': product_id, + 'quantity': qty_ret, + 'move_id': chosen_move.id, })) - _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") + _logger.info(f"📟 SRT line: {chosen_move.product_id.display_name} | qty={qty_ret}") - elif not mapping_koli: + else: + # --- Fallback ke line_ids jika tidak ada mapping_koli --- for line in record.line_ids: move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id) if not move: - raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}") + raise UserError(f"❌ Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}") srt_return_lines.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, 'move_id': move.id, })) - _logger.info( - f"📟 SRT line (fallback line_ids): {line.product_id.display_name} | qty={line.product_uom_qty}") + _logger.info(f"📟 SRT line (fallback): {line.product_id.display_name} | qty={line.product_uom_qty}") srt_picking = None if srt_return_lines: @@ -738,33 +756,62 @@ class TukarGuling(models.Model): for pick in picks_to_return: ort_return_lines = [] + if is_retur_from_bu_pick: + # Build map produk -> move list + move_map = defaultdict(list) + for move in pick.move_lines: + move_map[move.product_id.id].append(move) + for line in record.line_ids: - move = pick.move_lines.filtered(lambda m: m.product_id == line.product_id) - if not move: + moves = move_map.get(line.product_id.id) + if not moves: raise UserError( f"Move tidak ditemukan di BU/PICK {pick.name} untuk {line.product_id.display_name}") + + chosen_move = moves[0] + if len(moves) > 1: + _logger.warning( + f"🟠 Duplicate move detected for {line.product_id.display_name} in {pick.name}. Using the first move only.") + pick.message_post( + body=f"🟠 Duplicate move ditemukan untuk produk {line.product_id.display_name}. Hanya 1 move yang dipakai.") + ort_return_lines.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, - 'move_id': move.id, + 'move_id': chosen_move.id, })) _logger.info( f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}") + else: + # Mapping koli case + move_map = defaultdict(list) + for move in pick.move_lines: + move_map[move.product_id.id].append(move) + for mk in mapping_koli.filtered(lambda m: m.pick_id == pick): - move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id) - if not move: + moves = move_map.get(mk.product_id.id) + if not moves: raise UserError( f"Move tidak ditemukan di BU/PICK {pick.name} untuk {mk.product_id.display_name}") + + chosen_move = moves[0] + if len(moves) > 1: + _logger.warning( + f"🟠 Duplicate move detected for {mk.product_id.display_name} in {pick.name}. Using the first move only.") + pick.message_post( + body=f"🟠 Duplicate move ditemukan untuk produk {mk.product_id.display_name}. Hanya 1 move yang dipakai.") + ort_return_lines.append((0, 0, { 'product_id': mk.product_id.id, 'quantity': mk.qty_return, - 'move_id': move.id, + 'move_id': chosen_move.id, })) _logger.info( f"📟 ORT (mapping koli) | {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}") + # Buat retur jika ada return line if ort_return_lines: ort_wizard = self.env['stock.return.picking'].with_context({ 'active_id': pick.id, -- cgit v1.2.3 From 181646555a2d7467acf0595544f5a533342563e5 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 8 Aug 2025 15:53:46 +0700 Subject: revert to last good commit --- indoteknik_custom/models/tukar_guling.py | 87 ++++++++------------------------ 1 file changed, 20 insertions(+), 67 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index eadb164c..4c7722d5 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -2,7 +2,6 @@ from odoo import models, fields, api, _ from odoo.exceptions import UserError, ValidationError import logging from datetime import datetime -from collections import defaultdict _logger = logging.getLogger(__name__) @@ -541,7 +540,7 @@ class TukarGuling(models.Model): ]) if self.state == 'approved' and done_ort: self.state = 'done' - elif self.operations.picking_type_id.id == 30 and self.return_type == 'revisi_so' and not has_bu_pick: + else: raise UserError("Tidak bisa menentukan jenis retur.") def action_approve(self): @@ -677,47 +676,30 @@ class TukarGuling(models.Model): ### ======== SRT dari BU/OUT ========= srt_return_lines = [] if mapping_koli: - move_per_product = defaultdict(list) - for move in bu_out.move_lines: - move_per_product[move.product_id.id].append(move) - - mapped_product_ids = set(mapping_koli.mapped('product_id').ids) - - for product_id in mapped_product_ids: - qty_ret = sum( - line.qty_return for line in mapping_koli.filtered(lambda l: l.product_id.id == product_id)) - if qty_ret <= 0: - continue - - product_moves = move_per_product[product_id] - if not product_moves: - raise UserError(f"❌ Move BU/OUT tidak ditemukan untuk product ID {product_id}") - - if len(product_moves) > 1: - _logger.warning(f"🟠 Detected duplicate moves for product {product_id}, picking {bu_out.name}") - chosen_move = product_moves[0] - else: - chosen_move = product_moves[0] - + for prod in mapping_koli.mapped('product_id'): + qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) + move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) + if not move: + raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {prod.display_name}") srt_return_lines.append((0, 0, { - 'product_id': product_id, - 'quantity': qty_ret, - 'move_id': chosen_move.id, + 'product_id': prod.id, + 'quantity': qty_total, + 'move_id': move.id, })) - _logger.info(f"📟 SRT line: {chosen_move.product_id.display_name} | qty={qty_ret}") + _logger.info(f"📟 SRT line: {prod.display_name} | qty={qty_total}") - else: - # --- Fallback ke line_ids jika tidak ada mapping_koli --- + elif not mapping_koli: for line in record.line_ids: move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id) if not move: - raise UserError(f"❌ Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}") + raise UserError(f"Move BU/OUT tidak ditemukan untuk produk {line.product_id.display_name}") srt_return_lines.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, 'move_id': move.id, })) - _logger.info(f"📟 SRT line (fallback): {line.product_id.display_name} | qty={line.product_uom_qty}") + _logger.info( + f"📟 SRT line (fallback line_ids): {line.product_id.display_name} | qty={line.product_uom_qty}") srt_picking = None if srt_return_lines: @@ -756,62 +738,33 @@ class TukarGuling(models.Model): for pick in picks_to_return: ort_return_lines = [] - if is_retur_from_bu_pick: - # Build map produk -> move list - move_map = defaultdict(list) - for move in pick.move_lines: - move_map[move.product_id.id].append(move) - for line in record.line_ids: - moves = move_map.get(line.product_id.id) - if not moves: + move = pick.move_lines.filtered(lambda m: m.product_id == line.product_id) + if not move: raise UserError( f"Move tidak ditemukan di BU/PICK {pick.name} untuk {line.product_id.display_name}") - - chosen_move = moves[0] - if len(moves) > 1: - _logger.warning( - f"🟠 Duplicate move detected for {line.product_id.display_name} in {pick.name}. Using the first move only.") - pick.message_post( - body=f"🟠 Duplicate move ditemukan untuk produk {line.product_id.display_name}. Hanya 1 move yang dipakai.") - ort_return_lines.append((0, 0, { 'product_id': line.product_id.id, 'quantity': line.product_uom_qty, - 'move_id': chosen_move.id, + 'move_id': move.id, })) _logger.info( f"📟 ORT (BU/PICK langsung) | {pick.name} | {line.product_id.display_name} | qty={line.product_uom_qty}") - else: - # Mapping koli case - move_map = defaultdict(list) - for move in pick.move_lines: - move_map[move.product_id.id].append(move) - for mk in mapping_koli.filtered(lambda m: m.pick_id == pick): - moves = move_map.get(mk.product_id.id) - if not moves: + move = pick.move_lines.filtered(lambda m: m.product_id == mk.product_id) + if not move: raise UserError( f"Move tidak ditemukan di BU/PICK {pick.name} untuk {mk.product_id.display_name}") - - chosen_move = moves[0] - if len(moves) > 1: - _logger.warning( - f"🟠 Duplicate move detected for {mk.product_id.display_name} in {pick.name}. Using the first move only.") - pick.message_post( - body=f"🟠 Duplicate move ditemukan untuk produk {mk.product_id.display_name}. Hanya 1 move yang dipakai.") - ort_return_lines.append((0, 0, { 'product_id': mk.product_id.id, 'quantity': mk.qty_return, - 'move_id': chosen_move.id, + 'move_id': move.id, })) _logger.info( f"📟 ORT (mapping koli) | {pick.name} | {mk.product_id.display_name} | qty={mk.qty_return}") - # Buat retur jika ada return line if ort_return_lines: ort_wizard = self.env['stock.return.picking'].with_context({ 'active_id': pick.id, -- cgit v1.2.3 From b6ae7b2c9f1c564f3bf2a471f4871fda745d215d Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sat, 9 Aug 2025 13:59:11 +0700 Subject: origin so can be clicked --- indoteknik_custom/models/tukar_guling.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 4c7722d5..6aedb70e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -32,7 +32,7 @@ class TukarGuling(models.Model): 'tukar_guling_id', string='Transfers' ) - # origin_so = fields.Many2one('sale.order', string='Origin SO') + origin_so = fields.Many2one('sale.order', string='Origin SO', compute='_compute_origin_so') name = fields.Char('Number', required=True, copy=False, readonly=True, default='New') date = fields.Datetime('Date', default=fields.Datetime.now, required=True) operations = fields.Many2one( @@ -83,6 +83,15 @@ class TukarGuling(models.Model): invoice_id = fields.Many2many('account.move', string='Invoice Ref', readonly=True) + @api.depends('origin', 'operations') + def _compute_origin_so(self): + for rec in self: + rec.origin_so = False + origin_str = rec.origin or rec.operations.origin + if origin_str: + so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1) + rec.origin_so = so.id if so else False + @api.depends('origin') def _compute_is_has_invoice(self): for rec in self: @@ -144,8 +153,6 @@ class TukarGuling(models.Model): if self.line_ids and from_return_picking: # Hanya update origin, jangan ubah lines - if self.operations.origin: - self.origin = self.operations.origin _logger.info("📌 Menggunakan product lines dari return wizard, tidak populate ulang.") # 🚀 Tapi tetap populate mapping koli jika BU/OUT @@ -177,6 +184,7 @@ class TukarGuling(models.Model): # Set origin dari operations if self.operations.origin: self.origin = self.operations.origin + self.origin_so = self.operations.group_id.id # Auto-populate lines dari move_ids operations lines_data = [] @@ -332,17 +340,20 @@ class TukarGuling(models.Model): # _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin # ) + @api.model def create(self, vals): - # Generate sequence number if not vals.get('name') or vals['name'] == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('tukar.guling') - # Auto-fill origin from operations - if not vals.get('origin') and vals.get('operations'): + if vals.get('operations'): picking = self.env['stock.picking'].browse(vals['operations']) if picking.origin: vals['origin'] = picking.origin + # Find matching SO + so = self.env['sale.order'].search([('name', '=', picking.origin)], limit=1) + if so: + vals['origin_so'] = so.id if picking.partner_id: vals['partner_id'] = picking.partner_id.id @@ -350,6 +361,10 @@ class TukarGuling(models.Model): res.message_post(body=_("CCM Created By %s") % self.env.user.name) return res + res = super(TukarGuling, self).create(vals) + res.message_post(body=_("CCM Created By %s") % self.env.user.name) + return res + def copy(self, default=None): if default is None: default = {} -- cgit v1.2.3 From b3cd6aeb021259b4004a58270a2a7b6b0d82ba1d Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sun, 10 Aug 2025 19:42:25 +0700 Subject: show invoice rev --- indoteknik_custom/models/tukar_guling.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 6aedb70e..624de7a9 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -92,19 +92,35 @@ class TukarGuling(models.Model): so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1) rec.origin_so = so.id if so else False - @api.depends('origin') + @api.depends('origin_so', 'mapping_koli_ids.product_id', 'line_ids.product_id') def _compute_is_has_invoice(self): for rec in self: - invoices = self.env['account.move'].search([ - ('invoice_origin', 'ilike', rec.origin), - ('move_type', '=', 'out_invoice'), # hanya invoice - ('state', 'not in', ['draft', 'cancel']) - ]) + rec.is_has_invoice = False + rec.invoice_id = False + + if not rec.origin_so: + continue + + # Ambil produk dari mapping_koli kalau ada, kalau kosong pakai line_ids + if rec.mapping_koli_ids: + retur_products = rec.mapping_koli_ids.mapped('product_id') + else: + retur_products = rec.line_ids.mapped('product_id') + + # Semua invoice dari SO asal + invoices = rec.origin_so.invoice_ids.filtered( + lambda inv: inv.state not in ('draft', 'cancel') + and inv.move_type in ('out_invoice', 'out_refund') + ) + + # Filter invoice yang punya produk yang diretur + invoices = invoices.filtered( + lambda inv: any(line.product_id in retur_products for line in inv.invoice_line_ids) + ) + if invoices: rec.is_has_invoice = True rec.invoice_id = invoices - else: - rec.is_has_invoice = False def set_opt(self): if not self.val_inv_opt and self.is_has_invoice == True: -- cgit v1.2.3 From 356e53b85511c98cf4c942c32f2f370f58c9d849 Mon Sep 17 00:00:00 2001 From: Miqdad Date: Sun, 10 Aug 2025 20:16:09 +0700 Subject: pengajuan return so revert --- indoteknik_custom/models/tukar_guling.py | 32 ++++++++------------------------ 1 file changed, 8 insertions(+), 24 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 624de7a9..6aedb70e 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -92,35 +92,19 @@ class TukarGuling(models.Model): so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1) rec.origin_so = so.id if so else False - @api.depends('origin_so', 'mapping_koli_ids.product_id', 'line_ids.product_id') + @api.depends('origin') def _compute_is_has_invoice(self): for rec in self: - rec.is_has_invoice = False - rec.invoice_id = False - - if not rec.origin_so: - continue - - # Ambil produk dari mapping_koli kalau ada, kalau kosong pakai line_ids - if rec.mapping_koli_ids: - retur_products = rec.mapping_koli_ids.mapped('product_id') - else: - retur_products = rec.line_ids.mapped('product_id') - - # Semua invoice dari SO asal - invoices = rec.origin_so.invoice_ids.filtered( - lambda inv: inv.state not in ('draft', 'cancel') - and inv.move_type in ('out_invoice', 'out_refund') - ) - - # Filter invoice yang punya produk yang diretur - invoices = invoices.filtered( - lambda inv: any(line.product_id in retur_products for line in inv.invoice_line_ids) - ) - + invoices = self.env['account.move'].search([ + ('invoice_origin', 'ilike', rec.origin), + ('move_type', '=', 'out_invoice'), # hanya invoice + ('state', 'not in', ['draft', 'cancel']) + ]) if invoices: rec.is_has_invoice = True rec.invoice_id = invoices + else: + rec.is_has_invoice = False def set_opt(self): if not self.val_inv_opt and self.is_has_invoice == True: -- cgit v1.2.3 From d896d5cb51437d366c10e854616faee46736688c Mon Sep 17 00:00:00 2001 From: Miqdad Date: Tue, 12 Aug 2025 15:10:16 +0700 Subject: get bill based on product --- indoteknik_custom/models/tukar_guling.py | 37 ++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models/tukar_guling.py') diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 6aedb70e..ff641f34 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -92,19 +92,38 @@ class TukarGuling(models.Model): so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1) rec.origin_so = so.id if so else False - @api.depends('origin') + @api.depends('origin', 'origin_so', 'partner_id', 'line_ids.product_id') def _compute_is_has_invoice(self): + Move = self.env['account.move'] for rec in self: - invoices = self.env['account.move'].search([ - ('invoice_origin', 'ilike', rec.origin), - ('move_type', '=', 'out_invoice'), # hanya invoice - ('state', 'not in', ['draft', 'cancel']) - ]) + rec.is_has_invoice = False + rec.invoice_id = [(5, 0, 0)] + + product_ids = rec.line_ids.mapped('product_id').ids + if not product_ids: + continue + + domain = [ + ('move_type', 'in', ['out_invoice', 'in_invoice']), + ('state', 'not in', ['draft', 'cancel']), + ('invoice_line_ids.product_id', 'in', product_ids), + ] + + if rec.partner_id: + domain.append(('partner_id', '=', rec.partner_id.id)) + + extra = [] + if rec.origin: + extra.append(('invoice_origin', 'ilike', rec.origin)) + if rec.origin_so: + extra.append(('invoice_line_ids.sale_line_ids.order_id', '=', rec.origin_so.id)) + if extra: + domain = domain + ['|'] * (len(extra) - 1) + extra + + invoices = Move.search(domain).with_context(active_test=False) if invoices: + rec.invoice_id = [(6, 0, invoices.ids)] rec.is_has_invoice = True - rec.invoice_id = invoices - else: - rec.is_has_invoice = False def set_opt(self): if not self.val_inv_opt and self.is_has_invoice == True: -- cgit v1.2.3