from odoo import fields, models, api, _ from datetime import date, datetime from terbilang import Terbilang from odoo.exceptions import UserError, ValidationError from markupsafe import escape as html_escape import pytz from lxml import etree class RefundSaleOrder(models.Model): _name = 'refund.sale.order' _description = 'Refund Sales Order' _inherit = ['mail.thread', 'mail.activity.mixin'] _rec_name = 'name' name = fields.Char(string='Refund Number', default='New', copy=False, readonly=True) note_refund = fields.Text(string='Note Refund') sale_order_ids = fields.Many2many('sale.order', string='Sales Order Numbers') uang_masuk = fields.Float(string='Uang Masuk', required=True) total_invoice = fields.Float(string='Total Order') ongkir = fields.Float(string='Ongkir', required=True, default=0.0) amount_refund = fields.Float(string='Total Refund', required=True) amount_refund_text = fields.Char(string='Total Refund Text', compute='_compute_refund_text') user_ids = fields.Many2many('res.users', string='Salespersons', compute='_compute_user_ids', domain=[('active', 'in', [True, False])]) create_uid = fields.Many2one('res.users', string='Created By', readonly=True) created_date = fields.Date(string='Tanggal Request Refund', readonly=True) sale_order_count = fields.Integer( string="Sale Order Count", compute="_compute_sale_order_count", ) status = fields.Selection([ ('draft', 'Draft'), ('pengajuan1', 'Approval Sales Manager'), ('pengajuan2', 'Approval AR'), ('pengajuan3', 'Approval Pimpinan'), ('reject', 'Cancel'), ('refund', 'Approved') ], string='Status Refund', default='draft', tracking=True) status_payment = fields.Selection([ ('pending', 'Pending'), ('reject', 'Cancel'), ('done', 'Payment') ], string='Status Payment', default='pending', tracking=True) reason_reject = fields.Text(string='Reason Cancel') refund_date = fields.Date(string='Tanggal Refund') invoice_ids = fields.Many2many('account.move', string='Invoices') bank = fields.Char(string='Bank', required=True) account_name = fields.Char(string='Account Name', required=True) account_no = fields.Char(string='Account No', required=True) kcp = fields.Char(string='Alamat KCP') finance_note = fields.Text(string='Finance Note') biaya_admin = fields.Float(string='Biaya Admin Transfer') invoice_names = fields.Html(string="Group Invoice Number", compute="_compute_invoice_names") so_names = fields.Html(string="Group SO Number", compute="_compute_so_names") refund_type = fields.Selection([ ('barang_kosong_sebagian', 'Refund Barang Kosong Sebagian'), ('barang_kosong', 'Refund Barang Kosong Full'), ('barang_kosong_indent', 'Refund Barang Kosong Sebagian(Indent)'), ('uang', 'Refund Lebih Bayar'), ('retur_half', 'Refund Retur Sebagian'), ('retur', 'Refund Retur Full'), ('salah_transfer', 'Salah Transfer'), ('berita_acara', 'Kebutuhan Berita Acara') ], string='Refund Type', required=True) tukar_guling_ids = fields.One2many( 'tukar.guling', 'refund_id', string="Pengajuan Return SO", ) picking_ids = fields.Many2many( 'stock.picking', string="Pickings", compute="_compute_picking_ids", ) transfer_move_id = fields.Many2one( 'account.move', string="Journal Payment", copy=False, help="Pilih transaksi salah transfer dari jurnal Uang Muka yang tidak terkait SO." ) tukar_guling_count = fields.Integer( string="Tukar Guling Count", compute="_compute_tukar_guling_count" ) has_picking = fields.Boolean( string="Has Picking", compute="_compute_has_picking", ) refund_type_display = fields.Char(string="Refund Type Label", compute="_compute_refund_type_display") line_ids = fields.One2many('refund.sale.order.line', 'refund_id', string='Refund Lines') invoice_line_ids = fields.One2many( comodel_name='account.move.line', inverse_name='move_id', string='Invoice Lines', compute='_compute_invoice_lines' ) approved_by = fields.Text(string='Approved By', readonly=True) date_approved_sales = fields.Datetime(string='Date Approved (Sales Manager)', readonly=True) date_approved_ar = fields.Datetime(string='Date Approved (AR)', readonly=True) date_approved_pimpinan = fields.Datetime(string='Date Approved (Pimpinan)', readonly=True) position_sales = fields.Char(string='Position Sales', readonly=True) position_ar = fields.Char(string='Position AR', readonly=True) position_pimpinan = fields.Char(string='Position Pimpinan', readonly=True) partner_id = fields.Many2one( 'res.partner', string='Customer', required=True ) advance_move_names = fields.Html(string="Group Journal Payment", compute="_compute_advance_move_names") uang_masuk_type = fields.Selection([ ('pdf', 'PDF'), ('image', 'Image'), ], string="Attachment Type", default='image') bukti_refund_type = fields.Selection([ ('pdf', 'PDF'), ('image', 'Image'), ], string="Attachment Type") bukti_uang_masuk_image = fields.Binary(string="Upload Bukti Uang Masuk") bukti_transfer_refund_image = fields.Binary(string="Upload Bukti Transfer Refund") bukti_uang_masuk_pdf = fields.Binary(string="Upload Bukti Uang Masuk") bukti_transfer_refund_pdf = fields.Binary(string="Upload Bukti Transfer Refund") journal_refund_move_id = fields.Many2one( 'account.move', string='Journal Refund', compute='_compute_journal_refund_move_id', ) journal_refund_state = fields.Selection( related='journal_refund_move_id.state', string='Journal Refund State', ) is_locked = fields.Boolean(string="Locked", compute="_compute_is_locked") sale_order_names_jasper = fields.Char(string='Sales Order List', compute='_compute_order_invoice_names') invoice_names_jasper = fields.Char(string='Invoice List', compute='_compute_order_invoice_names') so_order_line_ids = fields.Many2many( "sale.order.line", string="SO Order Lines", compute="_compute_so_order_lines", store=False ) currency_id = fields.Many2one( "res.currency", string="Currency", default=lambda self: self.env.company.currency_id, required=True ) amount_untaxed = fields.Monetary( string="Untaxed Amount", compute="_compute_amount_from_so", ) amount_tax = fields.Monetary( string="Taxes", compute="_compute_amount_from_so", ) amount_total = fields.Monetary( string="Total", compute="_compute_amount_from_so", ) total_margin = fields.Monetary( string="Total Margin", compute="_compute_amount_from_so", ) grand_total = fields.Monetary( string="Grand Total", compute="_compute_amount_from_so", ) delivery_amt = fields.Monetary( string="Delivery Amount", help="Ongkos kirim yang Dibayarkan Customer", default=0.0, compute="_compute_amount_from_so", ) remaining_refundable = fields.Float( string="Sisa Uang Masuk", help="Sisa uang masuk yang masih bisa direfund (hanya berlaku untuk 1 SO)", ) show_return_alert = fields.Boolean(compute="_compute_show_return_alert") show_approval_alert = fields.Boolean(compute="_compute_show_approval_alert") @api.onchange('refund_type', 'partner_id') def _onchange_refund_type_partner(self): if self.refund_type == 'salah_transfer' and self.partner_id: return { 'domain': { 'transfer_move_id': [ ('journal_id', '=', 11), ('line_ids.partner_id', '=', self.partner_id.id), ('state', '=', 'posted'), ('sale_id', '=', False), ] } } else: return { 'domain': {'transfer_move_id': [('id', '=', 0)]} } @api.onchange('transfer_move_id') def _onchange_transfer_move_id(self): """Set nilai uang_masuk dari move yang dipilih""" if self.transfer_move_id and self.refund_type == 'salah_transfer': self.uang_masuk = self.transfer_move_id.amount_total_signed elif self.refund_type != 'salah_transfer' and not self.sale_order_ids: self.uang_masuk = 0.0 @api.model def create(self, vals): allowed_user_ids = [23, 19, 688, 7] if not ( self.env.user.has_group('indoteknik_custom.group_role_sales') or self.env.user.has_group('indoteknik_custom.group_role_fat') or self.env.user.id in allowed_user_ids ): raise UserError("❌ Hanya Sales dan Finance yang boleh membuat refund.") if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].next_by_code('refund.sale.order') or 'New' vals['created_date'] = fields.Date.context_today(self) vals['create_uid'] = self.env.user.id refund_type = vals.get('refund_type') if 'sale_order_ids' in vals: so_cmd = vals['sale_order_ids'] so_ids = so_cmd[0][2] if so_cmd and so_cmd[0][0] == 6 else [] if so_ids: sale_orders = self.env['sale.order'].browse(so_ids) partner = sale_orders.mapped('partner_id.id') if len(partner) > 1: raise UserError("❌ Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.") vals['partner_id'] = sale_orders[0].partner_id.id if refund_type not in ['barang_kosong_indent', 'salah_transfer']: for so in sale_orders: if so.state not in ['cancel', 'sale']: raise UserError(f"❌ SO {so.name} tidak bisa direfund. Status harus Cancel atau Sale.") if so.state == 'sale': not_done_pickings = so.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel']) if not_done_pickings: raise UserError( f"❌ SO {so.name} Belum melakukan kirim barang " f"({', '.join(not_done_pickings.mapped('name'))}). " "Selesaikan Pengiriman untuk melakukan refund." ) invoices = sale_orders.mapped('invoice_ids').filtered( lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted' ) if invoices: vals['invoice_ids'] = [(6, 0, invoices.ids)] invoice_ids_data = vals.get('invoice_ids', []) invoice_ids = invoice_ids_data[0][2] if invoice_ids_data and invoice_ids_data[0][0] == 6 else [] invoices = self.env['account.move'].browse(invoice_ids) if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'berita_acara']: raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice") if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian', 'retur_half']: raise UserError("Refund type Lebih Bayar dan Barang Kosong Sebagian Hanya Bisa dipilih Jika Ada Invoice") if refund_type in ['barang_kosong', 'barang_kosong_sebagian', 'barang_kosong_indent'] and so_ids: sale_orders = self.env['sale.order'].browse(so_ids) if refund_type == 'barang_kosong': zero_delivery_lines = sale_orders.mapped('order_line').filtered( lambda l: l.qty_delivered == 0 and l.product_uom_qty > 0 ) if not zero_delivery_lines: raise UserError("❌ Tidak ada barang kosong di SO yang terpilih.") elif refund_type == 'barang_kosong_sebagian': partial_delivery_lines = sale_orders.mapped('order_line').filtered( lambda l: l.qty_delivered >= 0 and l.product_uom_qty > l.qty_delivered ) if not partial_delivery_lines: raise UserError("❌ Tidak ada barang yang tidak Terkirim/Kosong di SO yang dipilih.") if not so_ids and refund_type != 'salah_transfer': raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Salah Transfer'.") if refund_type == 'salah_transfer' and vals.get('transfer_move_id'): move = self.env['account.move'].browse(vals['transfer_move_id']) if move: sisa_uang_masuk = move.amount_total_signed # ← set dengan nilai move vals['uang_masuk'] = move.amount_total_signed vals['remaining_refundable'] = 0 else: sisa_uang_masuk = 0.0 else: # ==== perhitungan normal ==== moves = self.env['account.move'].search([ ('sale_id', 'in', so_ids), ('journal_id', '=', 11), ('state', '=', 'posted'), ('ref', 'not ilike', 'dp'), ]) piutangbca = self.env['account.move'] piutangmdr = self.env['account.move'] cabinvoice = self.env['account.move'] for inv_name in invoices.mapped('name'): piutangbca |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 4), ('state', '=', 'posted'), ]) piutangmdr |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 7), ('state', '=', 'posted'), ]) cabinvoice |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 11), ('state', '=', 'posted'), ]) misc = self.env['account.move'] if invoices: misc = self.env['account.move'].search([ ('ref', 'ilike', invoices.mapped('name')[0]), ('ref', 'not ilike', 'reklas'), ('journal_id', '=', 13), ('state', '=', 'posted'), ]) moves2 = self.env['account.move'] if so_ids: so_names = self.env['sale.order'].browse(so_ids).mapped('name') domain = [ ('journal_id', '=', 11), ('state', '=', 'posted'), ('ref', 'ilike', 'dp') ] if so_names: domain += ['|'] * (len(so_names) - 1) for n in so_names: domain.append(('ref', 'ilike', n)) moves2 = self.env['account.move'].search(domain) moves3 = self.env['account.move'] if so_ids: so_names = self.env['sale.order'].browse(so_ids).mapped('name') domain = [ ('journal_id', '=', 11), ('sale_id', '=', False), ('state', '=', 'posted'), ('ref', 'ilike', 'uang muka penjualan'), ('ref', 'not ilike', 'reklas'), ('ref', 'not ilike', 'dp'), ] if so_names: domain += ['|'] * (len(so_names) - 1) for n in so_names: domain.append(('ref', 'ilike', n)) moves3 = self.env['account.move'].search(domain) moves_ongkir = self.env['account.move'] if so_ids: so_records = self.env['sale.order'].browse(so_ids) so_names = so_records.mapped('name') domain = [ ('journal_id', '=', 11), ('state', '=', 'posted'), ('sale_id', '=', False), '|', ('ref', 'ilike', 'pendapatan ongkos kirim'), ('ref', 'ilike', 'ongkir'), '|', ('line_ids.account_id', '=', 450), ('line_ids.account_id', '=', 668), ] if so_names: domain += ['|'] * (len(so_names) - 1) for name in so_names: domain.append(('ref', 'ilike', name)) moves_ongkir = self.env['account.move'].search(domain) has_moves = bool(moves) has_moves2 = bool(moves2) has_moves3 = bool(moves3) has_piutangmdr = bool(piutangmdr) has_piutangbca = bool(piutangbca) has_cabinvoice = bool(cabinvoice) has_misc = bool(misc) has_ongkir = bool(moves_ongkir) ssos = self.env['sale.order'].browse(so_ids) has_settlement = any(so.payment_status == 'settlement' for so in ssos) sisa_uang_masuk = 0.0 has_journal = has_moves or has_moves2 or has_moves3 or has_piutangbca or has_piutangmdr or has_misc or has_cabinvoice if has_moves: sisa_uang_masuk += sum(moves.mapped('amount_total_signed')) if has_ongkir: sisa_uang_masuk += sum(moves_ongkir.mapped('amount_total_signed')) if has_moves2: sisa_uang_masuk += sum(moves2.mapped('amount_total_signed')) if has_moves3: sisa_uang_masuk += sum(moves3.mapped('amount_total_signed')) if has_piutangbca: sisa_uang_masuk += sum(piutangbca.mapped('amount_total_signed')) if has_cabinvoice: sisa_uang_masuk += sum(cabinvoice.mapped('amount_total_signed')) if has_piutangmdr: sisa_uang_masuk += sum(piutangmdr.mapped('amount_total_signed')) if has_misc: sisa_uang_masuk += sum(misc.mapped('amount_total_signed')) if has_settlement and not has_journal: sisa_uang_masuk += sum(ssos.mapped('gross_amount')) if not sisa_uang_masuk: raise UserError( "❌ Tidak bisa melakukan refund karena SO tidak memiliki Record Uang Masuk " "(Journal Uang Muka / Payment Invoices / Midtrans Payment)." ) existing_refunds = self.env['refund.sale.order'].search([ ('sale_order_ids', 'in', so_ids) ], order='id desc', limit=1) if existing_refunds: sisa_uang_masuk = existing_refunds.remaining_refundable if sisa_uang_masuk < 0: raise UserError("❌ Tidak ada sisa transaksi untuk di-refund.") vals['uang_masuk'] = sisa_uang_masuk total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0 vals['total_invoice'] = total_invoice amount_refund = vals.get('amount_refund', 0.0) can_refund = 0.0 if refund_type == 'berita_acara': can_refund = sisa_uang_masuk else: can_refund = sisa_uang_masuk - total_invoice if refund_type != 'berita_acara': if amount_refund > can_refund or can_refund == 0.0: raise ValidationError( _("Maksimal refund yang bisa dilakukan adalah sebesar %s. " "Silakan sesuaikan jumlah refund.") % (can_refund) ) if amount_refund <= 0.00: raise ValidationError('Total Refund harus lebih dari 0 jika ingin mengajukan refund') if so_ids and len(so_ids) > 1: existing_refund = self.search([('sale_order_ids', 'in', so_ids)], limit=1) if existing_refund: raise UserError("❌ Refund multi SO hanya bisa 1 kali.") vals['remaining_refundable'] = 0.0 elif so_ids and len(so_ids) == 1 and refund_type != 'salah_transfer': remaining = vals['uang_masuk'] - amount_refund if remaining < 0: raise ValidationError("❌ Tidak ada sisa transaksi untuk di-refund di SO ini. Semua dana sudah dikembalikan.") vals['remaining_refundable'] = remaining return super().create(vals) def write(self, vals): allowed_user_ids = [23, 19, 688, 7] if not ( self.env.user.has_group('indoteknik_custom.group_role_sales') or self.env.user.has_group('indoteknik_custom.group_role_fat') or self.env.user.id in allowed_user_ids ): raise UserError("❌ Hanya user Sales dan Finance yang boleh mengedit refund.") for rec in self: if 'sale_order_ids' in vals: so_commands = vals['sale_order_ids'] so_ids = [] for cmd in so_commands: if cmd[0] == 6: so_ids = cmd[2] elif cmd[0] == 4: so_ids.append(cmd[1]) elif cmd[0] == 3: if cmd[1] in so_ids: so_ids.remove(cmd[1]) if so_ids: sale_orders = self.env['sale.order'].browse(so_ids) partner = sale_orders.mapped('partner_id.id') if len(partner) > 1: raise UserError("❌ Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.") vals['partner_id'] = sale_orders[0].partner_id.id sale_orders = self.env['sale.order'].browse(so_ids) valid_invoices = sale_orders.mapped('invoice_ids').filtered( lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted' ) vals['invoice_ids'] = [(6, 0, valid_invoices.ids)] vals['ongkir'] = sum(so.delivery_amt or 0.0 for so in sale_orders) else: so_ids = rec.sale_order_ids.ids sale_orders = self.env['sale.order'].browse(so_ids) refund_type = vals.get('refund_type', rec.refund_type) if refund_type not in ['barang_kosong_indent', 'salah_transfer']: for so in sale_orders: if so.state not in ['cancel', 'sale']: raise UserError(f"❌ SO {so.name} tidak bisa direfund. Status harus Cancel atau Sale.") if so.state == 'sale': not_done_pickings = so.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel']) if not_done_pickings: raise UserError( f"❌ SO {so.name} Belum melakukan kirim barang " f"({', '.join(not_done_pickings.mapped('name'))}). " "Selesaikan Pengiriman untuk melakukan refund." ) if refund_type in ['barang_kosong', 'barang_kosong_sebagian'] and sale_orders: zero_delivery_lines = sale_orders.mapped('order_line').filtered(lambda l: l.qty_delivered >= 0 or l.product_uom_qty > l.qty_delivered) if not zero_delivery_lines: raise UserError("❌ Tidak ada barang yang Tidak Terikirim di Sales Order yang dipilih.") if not so_ids and refund_type != 'salah_transfer': raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Salah Transfer'.") invoice_ids = vals.get('invoice_ids', False) if invoice_ids: final_invoice_ids = [] for cmd in invoice_ids: if cmd[0] == 6: final_invoice_ids = cmd[2] elif cmd[0] == 4: final_invoice_ids.append(cmd[1]) invoice_ids = final_invoice_ids else: invoice_ids = rec.invoice_ids.ids if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'retur', 'berita_acara']: raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur jika ada invoice") if not invoice_ids and vals.get('refund_type', rec.refund_type) in ['uang', 'barang_kosong_sebagian', 'retur_half']: raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice") if refund_type == 'salah_transfer' and vals.get('transfer_move_id'): move = self.env['account.move'].browse(vals['transfer_move_id']) if move: vals['uang_masuk'] = move.amount_total_signed if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_ids', 'amount_refund']): total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) vals['total_invoice'] = total_invoice uang_masuk = vals.get('uang_masuk', rec.uang_masuk) amount_refund = vals.get('amount_refund', rec.amount_refund) can_refund = 0.0 total_refunded = 0.0 if refund_type == 'berita_acara': can_refund = uang_masuk remaining = uang_masuk - amount_refund else: can_refund = uang_masuk - total_invoice existing_refunds = self.search([ ('sale_order_ids', 'in', so_ids), ('id', '!=', rec.id) ]) total_refunded = sum(existing_refunds.mapped('amount_refund')) if existing_refunds: remaining = uang_masuk - total_refunded else: remaining = uang_masuk - amount_refund if amount_refund > can_refund: raise ValidationError( _("Maksimal refund yang bisa dilakukan adalah sebesar %s. " "Silakan sesuaikan jumlah refund.") % (can_refund) ) if amount_refund <= 0: raise ValidationError("Total Refund harus lebih dari 0.") existing_refunds = self.search([ ('sale_order_ids', 'in', so_ids), ('id', '!=', rec.id) ]) total_refunded = sum(existing_refunds.mapped('amount_refund')) if existing_refunds: remaining = uang_masuk - total_refunded else: remaining = uang_masuk - amount_refund if remaining < 0: raise ValidationError("Semua dana sudah dikembalikan, tidak bisa mengajukan refund") vals['remaining_refundable'] = remaining return super().write(vals) @api.onchange('amount_refund') def _onchange_refund_fields(self): for rec in self: refund_input = rec.amount_refund or 0.0 rec.remaining_refundable = (rec.uang_masuk or 0.0) - refund_input @api.depends('status_payment', 'status') def _compute_is_locked(self): for rec in self: rec.is_locked = rec.status_payment in ['done', 'reject'] or rec.status in ['pengajuan3', 'refund', 'reject'] @api.depends('sale_order_ids.name', 'invoice_ids.name') def _compute_order_invoice_names(self): for rec in self: rec.sale_order_names_jasper = ', '.join(rec.sale_order_ids.mapped('name')) or '' rec.invoice_names_jasper = ', '.join(rec.invoice_ids.mapped('name')) or '' @api.depends('sale_order_ids') def _compute_advance_move_names(self): for rec in self: move_links = [] invoice_ids = rec.sale_order_ids.mapped('invoice_ids').filtered(lambda m: m.state == 'posted') moves = self.env['account.move'].search([ ('sale_id', 'in', rec.sale_order_ids.ids), ('journal_id', '=', 11), ('state', '=', 'posted'), ]) piutangbca = self.env['account.move'] piutangmdr = self.env['account.move'] cabinvoice = self.env['account.move'] for inv_name in invoice_ids.mapped('name'): piutangbca |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 4), ('state', '=', 'posted'), ]) piutangmdr |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 7), ('state', '=', 'posted'), ]) cabinvoice |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 11), ('state', '=', 'posted'), ]) moves2 = self.env['account.move'] if rec.sale_order_ids: so_names = rec.sale_order_ids.mapped('name') domain = [ ('journal_id', '=', 11), ('state', '=', 'posted'), '|', '|', ('ref', 'ilike', 'dp'), ('ref', 'ilike', 'payment'), ('ref', 'ilike', 'uang muka penjualan'), ] domain += ['|'] * (len(so_names) - 1) for n in so_names: domain.append(('ref', 'ilike', n)) moves2 = self.env['account.move'].search(domain) misc = self.env['account.move'] if invoice_ids: invoice_name = invoice_ids.mapped('name')[0] misc = self.env['account.move'].search([ ('ref', 'ilike', invoice_name), ('ref', 'not ilike', 'reklas'), ('journal_id', '=', 13), ('state', '=', 'posted'), ]) if rec.sale_order_ids: so_records = rec.sale_order_ids so_names = so_records.mapped('name') domain = [ ('journal_id', '=', 13), ('state', '=', 'posted'), ('sale_id', '=', False), ('ref', 'ilike', 'selisih'), ] domain += ['|'] * (len(so_names) - 1) for name in so_names: domain.append(('ref', 'ilike', name)) misc = self.env['account.move'].search(domain) moves_ongkir = self.env['account.move'] if rec.sale_order_ids: so_records = rec.sale_order_ids so_names = so_records.mapped('name') domain = [ ('journal_id', '=', 11), ('state', '=', 'posted'), ('sale_id', '=', False), '|', ('ref', 'ilike', 'pendapatan ongkos kirim'), ('ref', 'ilike', 'ongkir'), '|', ('line_ids.account_id', '=', 450), ('line_ids.account_id', '=', 668), ] domain += ['|'] * (len(so_names) - 1) for name in so_names: domain.append(('ref', 'ilike', name)) moves_ongkir = self.env['account.move'].search(domain) all_moves = moves | piutangbca | piutangmdr | misc | moves2 | moves_ongkir | cabinvoice for move in all_moves: url = f"/web#id={move.id}&model=account.move&view_type=form" name = html_escape(move.name or 'Unnamed') move_links.append(f'{name}') rec.advance_move_names = ', '.join(move_links) if move_links else "-" @api.depends('sale_order_ids.user_id') def _compute_user_ids(self): for rec in self: user_ids = list({so.user_id.id for so in rec.sale_order_ids if so.user_id}) rec.user_ids = [(6, 0, user_ids)] @api.onchange('sale_order_ids') def _onchange_sale_order_ids(self): self.invoice_ids = [(5, 0, 0)] self.line_ids = [(5, 0, 0)] self.ongkir = 0.0 all_invoices = self.env['account.move'] total_invoice = 0.0 so_ids = self.sale_order_ids.ids amount_refund_before = 0.0 for so in self.sale_order_ids: self.ongkir += so.delivery_amt or 0.0 valid_invoices = so.invoice_ids.filtered( lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state == 'posted' ) all_invoices |= valid_invoices total_invoice += sum(valid_invoices.mapped('amount_total_signed')) refunds = self.env['refund.sale.order'].search([ ('sale_order_ids', 'in', so_ids) ]) amount_refund_before += sum(refunds.mapped('amount_refund')) if refunds else 0.0 moves = self.env['account.move'].search([ ('sale_id', 'in', so_ids), ('journal_id', '=', 11), ('state', '=', 'posted'), ('ref', 'not ilike', 'dp'), ]) piutangbca = self.env['account.move'] piutangmdr = self.env['account.move'] cabinvoice = self.env['account.move'] for inv_name in all_invoices.mapped('name'): piutangbca |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 4), ('state', '=', 'posted'), ]) piutangmdr |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 7), ('state', '=', 'posted'), ]) cabinvoice |= self.env['account.move'].search([ ('ref', 'ilike', inv_name), ('journal_id', '=', 11), ('state', '=', 'posted'), ]) misc = self.env['account.move'] if all_invoices: misc = self.env['account.move'].search([ ('ref', 'ilike', all_invoices.mapped('name')[0]), ('ref', 'not ilike', 'reklas'), ('journal_id', '=', 13), ('state', '=', 'posted'), ]) moves_ongkir = self.env['account.move'] if so_ids: so_records = self.env['sale.order'].browse(so_ids) so_names = so_records.mapped('name') domain = [ ('journal_id', '=', 11), ('state', '=', 'posted'), ('sale_id', '=', False), '|', ('ref', 'ilike', 'pendapatan ongkos kirim'), ('ref', 'ilike', 'ongkir'), '|', ('line_ids.account_id', '=', 450), ('line_ids.account_id', '=', 668), ] if so_names: domain += ['|'] * (len(so_names) - 1) for name in so_names: domain.append(('ref', 'ilike', name)) moves_ongkir = self.env['account.move'].search(domain) moves2 = self.env['account.move'] if so_ids: so_records = self.env['sale.order'].browse(so_ids) so_names = so_records.mapped('name') domain = [ ('journal_id', '=', 11), ('state', '=', 'posted'), ('ref', 'ilike', 'dp') ] domain += ['|'] * (len(so_names) - 1) for n in so_names: domain.append(('ref', 'ilike', n)) moves2 = self.env['account.move'].search(domain) moves3 = self.env['account.move'] if so_ids: so_records = self.env['sale.order'].browse(so_ids) so_names = so_records.mapped('name') domain = [ ('journal_id', '=', 11), ('sale_id', '=', False), ('state', '=', 'posted'), ('ref', 'ilike', 'uang muka penjualan'), ('ref', 'not ilike', 'reklas'), ('ref', 'not ilike', 'dp'), ] domain += ['|'] * (len(so_names) - 1) for n in so_names: domain.append(('ref', 'ilike', n)) moves3 = self.env['account.move'].search(domain) has_moves = bool(moves) has_moves2 = bool(moves2) has_moves3 = bool(moves3) has_piutangmdr = bool(piutangmdr) has_piutangbca = bool(piutangbca) has_cabinvoice = bool(cabinvoice) has_misc = bool(misc) has_ongkir = bool(moves_ongkir) ssos = self.env['sale.order'].browse(so_ids) has_settlement = any(so.payment_status == 'settlement' for so in ssos) sisa_uang_masuk = 0.0 has_journal = has_moves or has_moves2 or has_moves3 or has_piutangbca or has_piutangmdr or has_misc if has_moves: sisa_uang_masuk += sum(moves.mapped('amount_total_signed')) if has_moves2: sisa_uang_masuk += sum(moves2.mapped('amount_total_signed')) if has_cabinvoice: sisa_uang_masuk += sum(cabinvoice.mapped('amount_total_signed')) if has_moves3: sisa_uang_masuk += sum(moves3.mapped('amount_total_signed')) if has_piutangbca: sisa_uang_masuk += sum(piutangbca.mapped('amount_total_signed')) if has_piutangmdr: sisa_uang_masuk += sum(piutangmdr.mapped('amount_total_signed')) if has_misc: sisa_uang_masuk += sum(misc.mapped('amount_total_signed')) if has_ongkir: sisa_uang_masuk += sum(moves_ongkir.mapped('amount_total_signed')) if has_settlement and not has_journal: sisa_uang_masuk += sum(ssos.mapped('gross_amount')) self.uang_masuk = sisa_uang_masuk - amount_refund_before self.invoice_ids = all_invoices self.total_invoice = total_invoice self.refund_type = 'uang' if all_invoices else False pengurangan = total_invoice + self.ongkir if self.uang_masuk > pengurangan: self.amount_refund = self.uang_masuk - pengurangan else: self.amount_refund = 0.0 if self.sale_order_ids: self.partner_id = self.sale_order_ids[0].partner_id # @api.constrains('sale_order_ids') # def _check_sale_orders_payment(self): # """ Validasi SO harus punya uang masuk (Journal Uang Muka / Midtrans) """ # for rec in self: # invalid_orders = [] # for so in rec.sale_order_ids: # # cari journal uang muka # moves = self.env['account.move'].search([ # ('sale_id', '=', so.id), # ('journal_id', '=', 11), # Journal Uang Muka # ('state', '=', 'posted'), # ]) # piutangbca = self.env['account.move'].search([ # ('ref', 'in', rec.invoice_ids.mapped('name')), # ('journal_id', '=', 4), # ('state', '=', 'posted'), # ]) # piutangmdr = self.env['account.move'].search([ # ('ref', 'in', rec.invoice_ids.mapped('name')), # ('journal_id', '=', 7), # ('state', '=', 'posted'), # ]) # if not moves and so.payment_status != 'settlement' and not piutangbca and not piutangmdr: # invalid_orders.append(so.name) # if invalid_orders: # raise ValidationError( # f"Tidak dapat membuat refund untuk SO {', '.join(invalid_orders)} " # "karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Payment Invoice/Midtrans).\n" # "Pastikan semua SO yang dipilih sudah memiliki Record pembayaran yang valid." # ) @api.onchange('refund_type') def _onchange_refund_type(self): self.line_ids = [(5, 0, 0)] if self.refund_type in ['barang_kosong_sebagian', 'barang_kosong', 'barang_kosong_indent'] and self.sale_order_ids: line_vals = [] for so in self.sale_order_ids: for line in so.order_line: barang_kurang = line.product_uom_qty - line.qty_delivered if line.qty_delivered == 0 or barang_kurang > 0: line_vals.append((0, 0, { 'product_id': line.product_id.id, 'quantity': barang_kurang, 'from_name': so.name, 'prod_id': so.id, 'reason': '', 'price_unit': line.price_unit, 'discount': line.discount, 'tax_amt': line.price_tax, 'tax': [(6, 0, line.tax_id.ids)], })) self.line_ids = line_vals elif self.refund_type in ['retur', 'retur_half'] and self.sale_order_ids: line_vals = [] StockPicking = self.env['stock.picking'] for so in self.sale_order_ids: # BU/SRT pickings_srt = StockPicking.search([ ('state', '=', 'done'), ('picking_type_id', '=', 73), ('sale_id', 'in', so.ids) ]) # BU/ORT pickings_ort = StockPicking.search([ ('state', '=', 'done'), ('picking_type_id', '=', 74), ('sale_id', 'in', so.ids) ]) if not pickings_ort and not pickings_srt: # BU/OUT product_out = StockPicking.search([ ('state', '=', 'done'), ('picking_type_id', '=', 29), ('sale_id', 'in', so.ids) ]) for picking in product_out: for move in picking.move_lines: so_lines = so.order_line.filtered( lambda l: l.product_id == move.product_id ) for so_line in so_lines: line_vals.append((0, 0, { 'product_id': move.product_id.id, 'ref_id': picking.id, 'from_name': picking.name, 'quantity': move.product_uom_qty, 'reason': '', 'price_unit': so_line.price_unit, 'discount': so_line.discount, 'tax': [(6, 0, so_line.tax_id.ids)], })) has_bu_pick = any(p.picking_type_id.id == 30 for p in so.picking_ids) if not has_bu_pick: for picking in pickings_srt: for move in picking.move_lines: so_lines = so.order_line.filtered( lambda l: l.product_id == move.product_id ) for so_line in so_lines: line_vals.append((0, 0, { 'product_id': move.product_id.id, 'ref_id': picking.id, 'from_name': picking.name, 'quantity': move.product_uom_qty, 'reason': '', 'price_unit': so_line.price_unit, 'discount': so_line.discount, 'tax': [(6, 0, so_line.tax_id.ids)], })) else: for picking in pickings_ort: for move in picking.move_lines: so_lines = so.order_line.filtered( lambda l: l.product_id == move.product_id ) for so_line in so_lines: line_vals.append((0, 0, { 'product_id': move.product_id.id, 'ref_id': picking.id, 'from_name': picking.name, 'quantity': move.product_uom_qty, 'reason': '', 'price_unit': so_line.price_unit, 'discount': so_line.discount, 'tax': [(6, 0, so_line.tax_id.ids)], })) self.line_ids = line_vals @api.depends('invoice_ids') def _compute_invoice_lines(self): for rec in self: lines = self.env['account.move.line'] for inv in rec.invoice_ids: lines |= inv.invoice_line_ids rec.invoice_line_ids = lines @api.depends('amount_refund') def _compute_refund_text(self): tb = Terbilang() for record in self: res = '' try: if record.amount_refund > 0: tb.parse(int(record.amount_refund)) res = tb.getresult().title() record.amount_refund_text = res + ' Rupiah' except: record.amount_refund_text = '' def unlink(self): incantdelete = self.filtered(lambda r: r.status in ['refund', 'reject']) if incantdelete: names = ', '.join(incantdelete.mapped('name')) raise UserError(f"Refund tidak dapat di hapus jika sudah Confirm/Cancel.\nTidak bisa hapus: {names}") return super().unlink() @api.depends('invoice_ids') def _compute_invoice_names(self): for rec in self: names = [] for inv in rec.invoice_ids: url = f"/web#id={inv.id}&model=account.move&view_type=form" name = html_escape(inv.name) names.append(f'{name}') rec.invoice_names = ', '.join(names) @api.depends('sale_order_ids') def _compute_so_names(self): for rec in self: so_links = [] for so in rec.sale_order_ids: url = f"/web#id={so.id}&model=sale.order&view_type=form" name = html_escape(so.name) so_links.append(f'{name}') rec.so_names = ', '.join(so_links) if so_links else "-" @api.onchange('uang_masuk', 'total_invoice', 'ongkir') def _onchange_amount_refund(self): for rec in self: pengurangan = rec.total_invoice + rec.ongkir refund = rec.uang_masuk - pengurangan rec.amount_refund = refund if refund > 0 else 0.0 @api.onchange('invoice_ids') def _onchange_invoice_ids(self): if self.invoice_ids: if self.refund_type not in ['uang', 'barang_kosong']: self.refund_type = False self.total_invoice = sum(self.invoice_ids.mapped('amount_total_signed')) def action_ask_approval(self): for rec in self: if rec.refund_type in ['retur', 'retur_half']: so = rec.sale_order_ids if so: retur_done = self.env['stock.picking'].search_count([ ('sale_id', '=', so.id), ('picking_type_id', 'in', [73, 74]), ('state', '=', 'done') ]) if retur_done == 0: raise ValidationError( f"⚠️ SO {so.name} memiliki refund tipe Retur. Selesaikan pengajuan retur untuk melanjutkan refund" ) allowed_sales_ids = rec.sale_order_ids.mapped("user_id.id") if self.env.user.id not in allowed_sales_ids and rec.refund_type != 'salah_transfer': raise ValidationError("❌ Hanya Sales pemilik Sales Order terkait yang boleh meminta approval refund ini.") if rec.status == 'draft': rec.status = 'pengajuan1' def _get_status_label(self, code): status_dict = dict(self.fields_get(allfields=['status'])['status']['selection']) return status_dict.get(code, code) def action_approve_flow(self): jakarta_tz = pytz.timezone('Asia/Jakarta') now = datetime.now(jakarta_tz).replace(tzinfo=None) for rec in self: if rec.refund_type in ['retur', 'retur_half']: so = rec.sale_order_ids if so: retur_done = self.env['stock.picking'].search_count([ ('sale_id', '=', so.id), ('picking_type_id', 'in', [73, 74]), ('state', '=', 'done') ]) if retur_done == 0: raise ValidationError( f"⚠️ SO {so.name} memiliki refund tipe Retur. Selesaikan retur untuk melanjutkan refund" ) user_name = self.env.user.name if not rec.status or rec.status == 'draft': rec.status = 'pengajuan1' elif rec.status == 'pengajuan1' and self.env.user.id in [19, 28]: rec.status = 'pengajuan2' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_sales = now rec.position_sales = 'Sales Manager' elif rec.status == 'pengajuan2' and self.env.user.id == 688: rec.status = 'pengajuan3' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_ar = now rec.position_ar = 'AR' elif rec.status == 'pengajuan3' and self.env.user.id == 7: rec.status = 'refund' rec.approved_by = f"{rec.approved_by}, {user_name}" if rec.approved_by else user_name rec.date_approved_pimpinan = now rec.position_pimpinan = 'Pimpinan' rec.refund_date = fields.Date.context_today(self) else: raise UserError("❌ Hanya bisa diapproved oleh yang bersangkutan.") def action_trigger_cancel(self): is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') allowed_user_ids = [19, 688, 7] for rec in self: if self.env.uid not in allowed_user_ids and not is_fat: raise UserError("❌ Hanya user yang bersangkutan atau Finance (FAT) yang bisa melakukan penolakan.") if rec.status != 'reject': rec.status = 'reject' rec.status_payment = 'reject' @api.constrains('status', 'reason_reject') def _check_reason_if_rejected(self): for rec in self: if rec.status == 'reject' and not rec.reason_reject: raise ValidationError("Alasan pembatalan harus diisi ketika status Reject.") def action_confirm_refund(self): is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') for rec in self: if not is_fat: raise UserError("Hanya Finance yang dapat mengkonfirmasi pembayaran refund.") is_journal = self.env['account.move'].search([ ('refund_id', '=', rec.id), ('state', '=', 'posted') ]) amount = rec.amount_refund + rec.biaya_admin if not is_journal: raise UserError("Journal Payment Refund belum dibuat, buat Journal Payment Refund sebelum confirm refund.") if is_journal and amount != sum(is_journal.mapped('amount_total_signed')): raise UserError("Total Refund dengan Total Journal Harus Sama.") if rec.status_payment == 'pending': rec.status_payment = 'done' rec.refund_date = fields.Date.context_today(self) else: raise UserError("Refund hanya bisa dikonfirmasi setelah Approval Pimpinan.") def _compute_approval_label(self): for rec in self: label = 'Approval Done' if rec.status == 'draft': label = 'Approval Sales Manager' elif rec.status == 'pengajuan1': label = 'Approval AR' elif rec.status == 'pengajuan2': label = 'Approval Pimpinan' elif rec.status == 'pengajuan3': label = 'Confirm Refund' rec.approval_button_label = label def action_create_journal_refund(self): is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') if not is_fat: raise UserError("❌ Akses ditolak. Hanya Finance yang dapat membuat journal refund.") for refund in self: current_time = fields.Datetime.now() has_invoice = any(refund.sale_order_ids.mapped('invoice_ids')) # Penentuan partner (dari SO atau partner_id langsung) partner = ( refund.sale_order_ids[0].partner_id.parent_id or refund.sale_order_ids[0].partner_id ) if refund.sale_order_ids else refund.partner_id # Ambil label refund type refund_type_label = dict( self.fields_get(allfields=['refund_type'])['refund_type']['selection'] ).get(refund.refund_type, '') # Normalisasi refund_type_label = refund_type_label.upper() if refund.refund_type in ['barang_kosong', 'barang_kosong_sebagian', 'barang_kosong_indent']: refund_type_label = "REFUND BARANG KOSONG" elif refund.refund_type in ['retur_half', 'retur']: refund_type_label = "REFUND RETUR BARANG" elif refund.refund_type == 'uang': refund_type_label = "REFUND LEBIH BAYAR" elif refund.refund_type == 'salah_transfer': refund_type_label = "REFUND SALAH TRANSFER" if not partner: raise UserError("❌ Partner tidak ditemukan.") # Ref format ref_text = f"{refund_type_label} {refund.name or ''} {partner.display_name}".upper() admintex = f"BIAYA ADMIN BANK {refund_type_label} {refund.name or ''} {partner.display_name}".upper() # Buat Account Move (Journal Entry) account_move = self.env['account.move'].create({ 'ref': ref_text, 'date': current_time, 'journal_id': 11, 'refund_id': refund.id, 'refund_so_ids': [(6, 0, refund.sale_order_ids.ids)], 'partner_id': partner.id, }) admintf = refund.biaya_admin amount = refund.amount_refund # 450 Penerimaan Belum Teridentifikasi, 668 Penerimaan Belum Alokasi second_account_id = 450 if refund.refund_type not in ['barang_kosong', 'barang_kosong_sebagian', 'barang_kosong_indent'] else 668 debit_line = { 'move_id': account_move.id, 'account_id': second_account_id, 'partner_id': partner.id, 'currency_id': 12, 'debit': amount, 'credit': 0.0, 'name': ref_text, } adminline = { 'move_id': account_move.id, 'account_id': 555, 'partner_id': partner.id, 'currency_id': 12, 'debit': admintf, 'credit': 0.0, 'name': admintex, } credit_line = { 'move_id': account_move.id, 'account_id': 389, # Intransit BCA 'partner_id': partner.id, 'currency_id': 12, 'debit': 0.0, 'credit': amount, 'name': ref_text, } credit_admin_line = { 'move_id': account_move.id, 'account_id': 389, # Intransit BCA 'partner_id': partner.id, 'currency_id': 12, 'debit': 0.0, 'credit': admintf, 'name': admintex, } journal_line = [debit_line, credit_line, adminline, credit_admin_line] if admintf > 0 else [debit_line, credit_line] self.env['account.move.line'].create(journal_line) return { 'name': _('Journal Entries'), 'view_mode': 'form', 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'res_id': account_move.id, 'target': 'current' } def _compute_journal_refund_move_id(self): for rec in self: move = self.env['account.move'].search([ ('refund_id', '=', rec.id), ('state', '!=', 'cancel') ], limit=1) rec.journal_refund_move_id = move def action_open_journal_refund(self): self.ensure_one() is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat') allowed_user_ids = [19, 688, 7] if not is_fat and self.env.user.id not in allowed_user_ids: raise UserError(_('Anda tidak memiliki akses untuk membuka Journal Refund.')) if self.journal_refund_move_id: return { 'name': _('Journal Refund'), 'view_mode': 'form', 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'res_id': self.journal_refund_move_id.id, 'target': 'current' } @api.depends( "sale_order_ids", "sale_order_ids.order_line.price_subtotal", "sale_order_ids.order_line.price_tax", "sale_order_ids.order_line.price_total", "sale_order_ids.order_line.purchase_price", "sale_order_ids.order_line.product_uom_qty", "sale_order_ids.delivery_amt", "sale_order_ids.shipping_cost_covered", ) def _compute_amount_from_so(self): for rec in self: untaxed = tax = total_margin = delivery = 0.0 for so in rec.sale_order_ids: if so.shipping_cost_covered == 'customer': delivery += so.delivery_amt or 0.0 for line in so.order_line: untaxed += line.price_subtotal tax += line.price_tax cost = line.purchase_price * line.product_uom_qty margin = line.price_subtotal - cost total_margin += margin rec.amount_untaxed = untaxed rec.amount_tax = tax rec.amount_total = untaxed + tax rec.total_margin = total_margin rec.delivery_amt = delivery rec.grand_total = rec.amount_total + rec.delivery_amt @api.depends("sale_order_ids", "sale_order_ids.order_line") def _compute_so_order_lines(self): for rec in self: rec.so_order_line_ids = rec.sale_order_ids.mapped("order_line") @api.depends('refund_type') def _compute_refund_type_display(self): for rec in self: rec.refund_type_display = dict(self.fields_get(allfields=['refund_type'])['refund_type']['selection']).get(rec.refund_type, '') def _compute_sale_order_count(self): for rec in self: rec.sale_order_count = len(rec.sale_order_ids) def _compute_show_return_alert(self): for rec in self: retur_ort = self.env['stock.picking'].search([ ('state', '=', 'done'), ('picking_type_id', '=', 74), ('sale_id', 'in', rec.sale_order_ids.ids) ]) retur_srt = self.env['stock.picking'].search([ ('state', '=', 'done'), ('picking_type_id', '=', 73), ('sale_id', 'in', rec.sale_order_ids.ids) ]) rec.show_return_alert = not retur_ort and not retur_srt and rec.refund_type in ['retur', 'retur_half'] def _compute_show_approval_alert(self): for rec in self: retur_ort = self.env['stock.picking'].search([ ('state', '=', 'done'), ('picking_type_id', '=', 74), ('sale_id', 'in', rec.sale_order_ids.ids) ]) retur_srt = self.env['stock.picking'].search([ ('state', '=', 'done'), ('picking_type_id', '=', 73), ('sale_id', 'in', rec.sale_order_ids.ids) ]) rec.show_approval_alert = retur_ort or retur_srt and rec.refund_type in ['retur', 'retur_half'] @api.depends('tukar_guling_ids', 'tukar_guling_ids.picking_ids') def _compute_picking_ids(self): for rec in self: rec.picking_ids = rec.tukar_guling_ids.mapped('picking_ids') def action_view_picking(self): self.ensure_one() action = self.env.ref('stock.action_picking_tree_all').read()[0] if len(self.picking_ids) == 1: action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] action['res_id'] = self.picking_ids.id else: action['domain'] = [('id', 'in', self.picking_ids.ids)] return action @api.depends('picking_ids') def _compute_has_picking(self): for rec in self: rec.has_picking = bool(rec.picking_ids) def action_create_tukar_guling(self): for refund in self: if refund.refund_type not in ['retur', 'retur_half']: raise UserError("Refund Type harus Retur Full atau Retur Sebagian untuk membuat Tukar Guling.") tg_records = [] for picking in refund.line_ids.mapped('ref_id'): if not picking: continue lines = refund.line_ids.filtered(lambda l: l.ref_id.id == picking.id) line_vals = [] koli_lines = [] for r_line in lines: qty_done = 0.0 move_line = r_line.ref_id.move_line_ids_without_package.filtered( lambda ml: ml.product_id.id == r_line.product_id.id ) if move_line: qty_done = sum(move_line.mapped('qty_done')) line_vals.append((0, 0, { 'product_id': r_line.product_id.id, 'product_uom_qty': r_line.quantity, 'name':r_line.product_id.name, 'product_uom':r_line.product_id.uom_id.id })) if r_line.ref_id.konfirm_koli_lines.pick_id: koli_lines.append((0, 0,{ 'pick_id': r_line.ref_id.konfirm_koli_lines.pick_id.id, 'product_id': r_line.product_id.id, 'qty_done': qty_done, 'qty_return': r_line.quantity, })) tg = self.env['tukar.guling'].create({ 'partner_id': refund.partner_id.id, 'origin': ','.join(refund.sale_order_ids.mapped('name')), 'origin_so': refund.sale_order_ids.id, 'operations': picking.id, 'return_type': 'retur_so', 'invoice_id': [(6, 0, refund.invoice_ids.ids)], 'refund_id': refund.id, 'line_ids': line_vals, 'mapping_koli_ids': koli_lines }) tg_records.append(tg.id) return { 'type': 'ir.actions.act_window', 'name': 'Pengajuan Retur SO', 'res_model': 'tukar.guling', 'view_mode': 'tree,form', 'domain': [('id', 'in', tg_records)], } def _compute_tukar_guling_count(self): for rec in self: rec.tukar_guling_count = len(rec.tukar_guling_ids) def action_open_tukar_guling(self): self.ensure_one() return { 'name': 'Pengajuan Return SO', 'type': 'ir.actions.act_window', 'view_mode': 'tree,form', 'res_model': 'tukar.guling', 'domain': [('id', 'in', self.tukar_guling_ids.ids)], 'context': dict(self.env.context, default_refund_id=self.id), } class RefundSaleOrderLine(models.Model): _name = 'refund.sale.order.line' _description = 'Refund Sales Order Line' refund_id = fields.Many2one('refund.sale.order', string='Refund Ref') product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Qty') reason = fields.Char(string='Reason') ref_id = fields.Many2one('stock.picking', string='Picking Reference') prod_id = fields.Many2one('sale.order', string='Sales Order Reference') from_name = fields.Char(string="Product Reference") price_unit = fields.Float(string="Unit Price") tax_amt = fields.Float(string="Amount Tax", compute='_compute_amounts') discount = fields.Float(string="Discount %") tax = fields.Many2many('account.tax',string="Taxes") subtotal = fields.Float(string="Subtotal", compute='_compute_amounts') total = fields.Float(string="Grand Total", compute='_compute_amounts') @api.depends('quantity', 'price_unit', 'discount', 'tax') def _compute_amounts(self): for line in self: price_unit = line.price_unit * (1 - (line.discount or 0.0) / 100.0) subtotal = price_unit * line.quantity tax_amount = 0.0 if line.tax: taxes = line.tax.compute_all( price_unit=price_unit, # Gunakan harga setelah diskon quantity=line.quantity, product=line.product_id, partner=line.refund_id.partner_id ) tax_amount = taxes['total_included'] - taxes['total_excluded'] subtotal = taxes['total_excluded'] line.subtotal = subtotal line.tax_amt = tax_amount line.total = subtotal + tax_amount