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