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') 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 3ea10365d477bba3e5565dc636824a2b42bc8fca Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Wed, 6 Aug 2025 14:49:35 +0700 Subject: refactor due extension --- indoteknik_custom/models/account_move_due_extension.py | 9 +++++++++ 1 file changed, 9 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move_due_extension.py b/indoteknik_custom/models/account_move_due_extension.py index 4a3f40e2..d354e3e3 100644 --- a/indoteknik_custom/models/account_move_due_extension.py +++ b/indoteknik_custom/models/account_move_due_extension.py @@ -33,6 +33,7 @@ class DueExtension(models.Model): counter = fields.Integer(string="Counter", compute='_compute_counter') approve_by = fields.Many2one('res.users', string="Approve By", readonly=True) date_approve = fields.Datetime(string="Date Approve", readonly=True) + def _compute_counter(self): for due in self: due.counter = due.partner_id.counter @@ -102,6 +103,14 @@ class DueExtension(models.Model): self.date_approve = datetime.utcnow() template = self.env.ref('indoteknik_custom.mail_template_due_extension_approve') template.send_mail(self.id, force_send=True) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'sale.order', + 'view_mode': 'form', + 'res_id': self.order_id.id, + 'views': [(False, 'form')], + 'target': 'current', + } def generate_due_line(self): partners = self.partner_id.get_child_ids() -- cgit v1.2.3 From d1a6e4a2d63cf3dd82ede0f8906957675ca38934 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Thu, 7 Aug 2025 09:22:07 +0700 Subject: payment partial --- indoteknik_custom/models/account_move.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 595c5655..c5e01f0c 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -95,6 +95,14 @@ class AccountMove(models.Model): ) payment_date = fields.Date(string="Payment Date", compute='_compute_payment_date') + partial_payment = fields.Float(string="Partial Payment", compute='compute_partial_payment') + + def compute_partial_payment(self): + for move in self: + if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial': + move.partial_payment = move.amount_total_signed - move.amount_residual_signed + else: + move.partial_payment = 0 def _compute_payment_date(self): for move in self: -- cgit v1.2.3 From b223b752b43eb0aa8fd685d895d5649996473baf Mon Sep 17 00:00:00 2001 From: Miqdad Date: Thu, 7 Aug 2025 13:26:53 +0700 Subject: Fix role permission --- indoteknik_custom/models/tukar_guling_po.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index 5d444472..cbbfb348 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -430,8 +430,8 @@ class TukarGulingPO(models.Model): # Cek hak akses berdasarkan state for rec in self: if rec.state == 'approval_purchase': - if not rec.env.user.has_group('indoteknik_custom.group_role_sales'): - raise UserError("Hanya Sales yang boleh approve tahap ini.") + if not rec.env.user.has_group('indoteknik_custom.group_role_purchasing'): + raise UserError("Hanya Purchasing yang boleh approve tahap ini.") rec.state = 'approval_finance' rec.date_purchase = now -- cgit v1.2.3 From 191674fade65d63c54807e30b569fc308688df9a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 7 Aug 2025 16:22:02 +0700 Subject: (andri) rev approval --- indoteknik_custom/models/sale_order.py | 35 +++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 94cfdc39..6dc1b4a2 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2159,7 +2159,9 @@ class SaleOrder(models.Model): # if order.validate_partner_invoice_due(): # return self._create_notification_action('Notification', # 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') - + + if not order._is_request_to_own_team_leader(): + raise UserError("Anda hanya dapat mengonfirmasi SO dari anggota tim Anda sendiri.") if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') @@ -2490,10 +2492,37 @@ class SaleOrder(models.Model): def _requires_approval_margin_leader(self): return self.total_percent_margin <= 15 and not self.env.user.is_leader - def _requires_approval_margin_manager(self): - return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader + # def _requires_approval_margin_manager(self): + # return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader # return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager + def _requires_approval_margin_manager(self): + margin = self.total_percent_margin + user = self.env.user + user_id = user.id + + if margin <= 15 or margin > 24: # Di luar range margin manager approval + return False + if (user.is_leader or (margin >= 17 and user.is_sales_manager) or (margin >= 18 and user_id in [11, 9, 375])): + return False + + return True # Butuh approval manager + + def _is_request_to_own_team_leader(self): + approver_id = self.env.user.id + salesperson_id = self.user_id.id + team_map = { + 11: [10406], # Eko : Firman + 9: [11314, 6609], # Ade : Boy, Mario + 375: [9928, 10], # Putra : Aro, Putri + } + + for lead_id, members in team_map.items(): + if salesperson_id in members: + return approver_id == lead_id + + return True + def _create_approval_notification(self, approval_role): title = 'Warning' message = f'SO butuh approval {approval_role}' -- cgit v1.2.3 From cad72ba8d36cff00190143a655c18765e93851fe Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Thu, 7 Aug 2025 16:34:25 +0700 Subject: (andri) use salesteam --- indoteknik_custom/models/sale_order.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 6dc1b4a2..a2817ea6 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2509,17 +2509,15 @@ class SaleOrder(models.Model): return True # Butuh approval manager def _is_request_to_own_team_leader(self): - approver_id = self.env.user.id + if not self.team_id or not self.team_id.user_id: + return True + salesperson_id = self.user_id.id - team_map = { - 11: [10406], # Eko : Firman - 9: [11314, 6609], # Ade : Boy, Mario - 375: [9928, 10], # Putra : Aro, Putri - } + approver_id = self.env.user.id + team_leader_id = self.team_id.user_id.id - for lead_id, members in team_map.items(): - if salesperson_id in members: - return approver_id == lead_id + if salesperson_id != approver_id and approver_id != team_leader_id: + return False return True -- 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') 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') 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') 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 00cb2b4dcf1e6892a6b4943b08c3bd3f5810432b Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 8 Aug 2025 10:09:25 +0700 Subject: fix hold outgoing reserve --- indoteknik_custom/models/approval_payment_term.py | 2 +- indoteknik_custom/models/stock_picking.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 6c857b45..a2ecd27f 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -38,7 +38,7 @@ class ApprovalPaymentTerm(models.Model): ('approved', 'Approved'), ('rejected', 'Rejected')], default='waiting_approval_sales_manager', tracking=True) - reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True) + reason_reject = fields.Text(string='Reason Reject', tracking=True) sale_order_ids = fields.Many2many( 'sale.order', string='Sale Orders', diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index f2f5f52a..46bb6cee 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1059,17 +1059,21 @@ class StockPicking(models.Model): return self.sale_id.date_doc_kirim = self.date_doc_kirim + from odoo import fields + def action_assign(self): - res = super(StockPicking, self).action_assign() - for move in self: - # if not move.sale_id.hold_outgoing and move.location_id.id != 57 and move.location_dest_id.id != 60: - # TODO cant skip hold outgoing cause of not singleton method - current_time = datetime.datetime.utcnow() - move.real_shipping_id = move.sale_id.real_shipping_id - move.date_availability = current_time - # self.check_state_reserve() + pickings_to_assign = self.filtered(lambda p: not (p.sale_id and p.sale_id.hold_outgoing)) + + res = super(StockPicking, pickings_to_assign).action_assign() + + current_time = datetime.datetime.utcnow() + for picking in pickings_to_assign: + picking.real_shipping_id = picking.sale_id.real_shipping_id + picking.date_availability = current_time + return res + def ask_approval(self): if self.env.user.is_accounting: raise UserError("Bisa langsung Validate") -- cgit v1.2.3 From 84bc1b36415d2f8baecaf6d5acdd4bf77d4187e0 Mon Sep 17 00:00:00 2001 From: Azka Nathan Date: Fri, 8 Aug 2025 10:17:52 +0700 Subject: fix bug apt --- indoteknik_custom/models/approval_payment_term.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index a2ecd27f..4cf9a4c8 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -38,7 +38,8 @@ class ApprovalPaymentTerm(models.Model): ('approved', 'Approved'), ('rejected', 'Rejected')], default='waiting_approval_sales_manager', tracking=True) - reason_reject = fields.Text(string='Reason Reject', tracking=True) + reason_reject = fields.Selection([('reason1', 'Reason 1'), ('reason2', 'Reason 2'), ('reason3', 'Reason 3')], string='Reason Reject', tracking=True) + reject_reason = fields.Text('Reject Reason', tracking=True) sale_order_ids = fields.Many2many( 'sale.order', string='Sale Orders', -- cgit v1.2.3 From ccdebe1c144323569efa2a9eecbcb4ca7849960e Mon Sep 17 00:00:00 2001 From: Miqdad Date: Fri, 8 Aug 2025 13:13:31 +0700 Subject: cancel permission --- indoteknik_custom/models/tukar_guling_po.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index cbbfb348..0badc117 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -497,7 +497,7 @@ class TukarGulingPO(models.Model): user = self.env.user if not ( - user.has_group('indoteknik_custom.group_role_sales') or + user.has_group('indoteknik_custom.group_role_purchasing') or user.has_group('indoteknik_custom.group_role_fat') or user.has_group('indoteknik_custom.group_role_logistic') ): -- cgit v1.2.3 From 678f0a55a4e340ed8fb448a528cb738e10ed84bd Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 8 Aug 2025 14:37:10 +0700 Subject: (andri) fix --- indoteknik_custom/models/sale_order.py | 53 ++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index a2817ea6..5e3f8965 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2161,7 +2161,10 @@ class SaleOrder(models.Model): # 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') if not order._is_request_to_own_team_leader(): - raise UserError("Anda hanya dapat mengonfirmasi SO dari anggota tim Anda sendiri.") + return self._create_notification_action( + 'Peringatan', + 'Hanya bisa konfirmasi SO tim Anda.' + ) if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') @@ -2171,6 +2174,12 @@ class SaleOrder(models.Model): self.check_limit_so_to_invoice() order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') + elif order._requires_approval_team_sales(): + self.check_product_bom() + self.check_credit_limit() + self.check_limit_so_to_invoice() + order.approval_status = 'pengajuan1' + return self._create_approval_notification('Team Sales') raise UserError("Bisa langsung Confirm") @@ -2387,12 +2396,20 @@ class SaleOrder(models.Model): return self._create_notification_action('Notification', 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') + if not order._is_request_to_own_team_leader(): + return self._create_notification_action( + 'Warning', + 'Hanya bisa konfirmasi SO tim Anda.' + ) if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') elif order._requires_approval_margin_manager(): order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') + elif order._requires_approval_team_sales(): + order.approval_status = 'pengajuan1' + return self._create_approval_notification('Team Sales') order.approval_status = 'approved' order._set_sppkp_npwp_contact() @@ -2492,34 +2509,34 @@ class SaleOrder(models.Model): def _requires_approval_margin_leader(self): return self.total_percent_margin <= 15 and not self.env.user.is_leader - # def _requires_approval_margin_manager(self): - # return 15 < self.total_percent_margin <= 24 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader - # return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager - def _requires_approval_margin_manager(self): - margin = self.total_percent_margin - user = self.env.user - user_id = user.id + return 15 < self.total_percent_margin < 18 and not self.env.user.is_sales_manager and not self.env.user.id == 375 and not self.env.user.is_leader + + def _requires_approval_team_sales(self): + return ( + 18 <= self.total_percent_margin <= 24 + and self.env.user.id not in [11, 9, 375] # Eko, Ade, Putra + and not self.env.user.is_sales_manager + and not self.env.user.is_leader + ) - if margin <= 15 or margin > 24: # Di luar range margin manager approval - return False - if (user.is_leader or (margin >= 17 and user.is_sales_manager) or (margin >= 18 and user_id in [11, 9, 375])): - return False - - return True # Butuh approval manager def _is_request_to_own_team_leader(self): + user = self.env.user + + # Pengecualian Pak Akbar & Darren + if user.is_leader or user.is_sales_manager: + return True + if not self.team_id or not self.team_id.user_id: return True salesperson_id = self.user_id.id - approver_id = self.env.user.id + approver_id = user.id team_leader_id = self.team_id.user_id.id - if salesperson_id != approver_id and approver_id != team_leader_id: - return False + return salesperson_id == approver_id or approver_id == team_leader_id - return True def _create_approval_notification(self, approval_role): title = 'Warning' -- cgit v1.2.3 From 9951cd96e968f0e3b934dd97838f4825e6d27c6b Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Fri, 8 Aug 2025 14:44:07 +0700 Subject: (andri) fix --- indoteknik_custom/models/sale_order.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 5e3f8965..5eb90d83 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -2178,7 +2178,7 @@ class SaleOrder(models.Model): self.check_product_bom() self.check_credit_limit() self.check_limit_so_to_invoice() - order.approval_status = 'pengajuan1' + order.approval_status = 'approved' return self._create_approval_notification('Team Sales') raise UserError("Bisa langsung Confirm") @@ -2408,7 +2408,7 @@ class SaleOrder(models.Model): order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') elif order._requires_approval_team_sales(): - order.approval_status = 'pengajuan1' + order.approval_status = 'approved' return self._create_approval_notification('Team Sales') order.approval_status = 'approved' -- cgit v1.2.3