summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/refund_sale_order.py
diff options
context:
space:
mode:
authorMiqdad <ahmadmiqdad27@gmail.com>2025-09-15 13:40:46 +0700
committerMiqdad <ahmadmiqdad27@gmail.com>2025-09-15 13:40:46 +0700
commit2f835b71aaad9d2d6fef1fafcb600bf50b034f2b (patch)
tree1e4463e3b4fd8f86231625253152bc2a8d7ea215 /indoteknik_custom/models/refund_sale_order.py
parenta47bdc61945b8ab153d80590f06975210f8d2a80 (diff)
parentcf64a8c5913308c3121a55b1b4cd1acf17c86d73 (diff)
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into cbd-apt
merge
Diffstat (limited to 'indoteknik_custom/models/refund_sale_order.py')
-rw-r--r--indoteknik_custom/models/refund_sale_order.py707
1 files changed, 591 insertions, 116 deletions
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 11bfd07f..f511ed5d 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -10,20 +10,24 @@ from lxml import etree
class RefundSaleOrder(models.Model):
_name = 'refund.sale.order'
_description = 'Refund Sales Order'
- _inherit = ['mail.thread']
+ _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 Invoice')
+ 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'),
@@ -45,6 +49,7 @@ class RefundSaleOrder(models.Model):
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')
invoice_names = fields.Html(string="Group Invoice Number", compute="_compute_invoice_names")
so_names = fields.Html(string="Group SO Number", compute="_compute_so_names")
@@ -55,9 +60,36 @@ class RefundSaleOrder(models.Model):
('uang', 'Refund Lebih Bayar'),
('retur_half', 'Refund Retur Sebagian'),
('retur', 'Refund Retur Full'),
- ('lainnya', 'Lainnya')
+ ('salah_transfer', 'Salah Transfer')
], 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 (journal_id=11) 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')
@@ -89,7 +121,7 @@ class RefundSaleOrder(models.Model):
bukti_refund_type = fields.Selection([
('pdf', 'PDF'),
('image', 'Image'),
- ], string="Attachment Type", default='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")
@@ -107,28 +139,80 @@ class RefundSaleOrder(models.Model):
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')
-
-
-
- @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, '')
+ 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 not in allowed_user_ids
+ self.env.user.id in allowed_user_ids
):
- raise UserError("❌ Hanya user Sales dan Finance yang boleh membuat refund.")
+ 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
@@ -138,6 +222,9 @@ class RefundSaleOrder(models.Model):
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
invoices = sale_orders.mapped('invoice_ids').filtered(
@@ -150,40 +237,79 @@ class RefundSaleOrder(models.Model):
refund_type = vals.get('refund_type')
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 []
-
if invoice_ids and refund_type and refund_type not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half']:
- raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice")
+ 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', 'barang_kosong', 'retur_half']:
- raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih 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'] 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 != 'lainnya':
- raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.")
-
- refund = refund_type in ['retur', 'retur_half']
- if refund and so_ids:
- so = self.env['sale.order'].browse(so_ids)
- pickings = self.env['stock.picking'].search([
- ('state', '=', 'done'),
- ('picking_type_id', '=', 73),
- ('sale_id', 'in', so_ids)
+
+ 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:
+ vals['uang_masuk'] = move.amount_total_signed
+ vals['remaining_refundable'] = 0
+ else:
+ # ==== perhitungan normal ====
+ moves = self.env['account.move'].search([
+ ('sale_id', 'in', so_ids),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
])
- if not pickings:
- raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.")
+ total_uang_muka = sum(moves.mapped('amount_total_signed')) if moves else 0.0
+ total_midtrans = sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) if so_ids else 0.0
+ total_pembayaran = total_uang_muka + total_midtrans
+
+ existing_refunds = self.env['refund.sale.order'].search([
+ ('sale_order_ids', 'in', so_ids)
+ ], order='id desc', limit=1)
- if refund_type == 'retur_half' and not invoice_ids:
- raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk Retur Sebagian.")
+ if existing_refunds:
+ sisa_uang_masuk = existing_refunds.remaining_refundable
+ else:
+ sisa_uang_masuk = total_pembayaran
- total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total')) if invoice_ids else 0.0
- uang_masuk = vals.get('uang_masuk', 0.0)
- ongkir = vals.get('ongkir', 0.0)
- pengurangan = total_invoice + ongkir
+ if sisa_uang_masuk < 0:
+ raise UserError("❌ Tidak ada sisa transaksi untuk di-refund.")
- if uang_masuk > pengurangan:
- vals['amount_refund'] = uang_masuk - pengurangan
- else:
- raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir untuk melakukan 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)
+ 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)
@@ -212,6 +338,9 @@ class RefundSaleOrder(models.Model):
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)
@@ -229,8 +358,13 @@ class RefundSaleOrder(models.Model):
refund_type = vals.get('refund_type', rec.refund_type)
- if not so_ids and refund_type != 'lainnya':
- raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.")
+ 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)
@@ -245,44 +379,53 @@ class RefundSaleOrder(models.Model):
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']:
- raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian jika ada invoice")
+ if invoice_ids and vals.get('refund_type', rec.refund_type) not in ['uang', 'barang_kosong_sebagian', 'barang_kosong', 'retur_half', 'retur']:
+ 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', 'barang_kosong', 'retur_half']:
+ 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 refund_type in ['retur', 'retur_half'] and so_ids:
- so = self.env['sale.order'].browse(so_ids)
- pickings = self.env['stock.picking'].search([
- ('state', '=', 'done'),
- ('picking_type_id', '=', 73),
- ('sale_id', 'in', so_ids)
- ])
+ 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 = rec.uang_masuk
- if not pickings:
- raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.")
+ amount_refund = vals.get('amount_refund', rec.amount_refund)
- if refund_type == 'retur_half' and not invoice_ids:
- raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk retur sebagian.")
+ if amount_refund <= 0:
+ raise ValidationError("Total Refund harus lebih dari 0.")
- if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_ids']):
- total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total'))
- uang_masuk = vals.get('uang_masuk', rec.uang_masuk)
- ongkir = vals.get('ongkir', rec.ongkir)
+ 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 uang_masuk <= (total_invoice + ongkir):
- raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir")
- vals['amount_refund'] = uang_masuk - (total_invoice + ongkir)
+ if remaining < 0:
+ raise ValidationError("Semua dana sudah dikembalikan, tidak bisa mengajukan refund")
- if vals.get('status') == 'refund' and not vals.get('refund_date'):
- vals['refund_date'] = fields.Date.context_today(self)
+ 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')
+ @api.depends('status_payment', 'status')
def _compute_is_locked(self):
for rec in self:
- rec.is_locked = rec.status_payment in ['done', 'reject']
+ 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):
@@ -319,13 +462,28 @@ class RefundSaleOrder(models.Model):
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 != 'cancel'
)
all_invoices |= valid_invoices
- total_invoice += sum(valid_invoices.mapped('amount_total'))
+ 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'),
+ ])
+ total_uang_muka = sum(moves.mapped('amount_total_signed')) if moves else 0.0
+ total_midtrans = sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount')) if so_ids else 0.0
+ self.uang_masuk = (total_uang_muka + total_midtrans) - amount_refund_before
self.invoice_ids = all_invoices
@@ -341,6 +499,37 @@ class RefundSaleOrder(models.Model):
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 = []
+ total_uang_masuk = 0.0
+
+ 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'),
+ ])
+
+ if not moves and so.payment_status != 'settlement':
+ invalid_orders.append(so.name)
+
+ if moves:
+ total_uang_muka = sum(moves.mapped('amount_total_signed')) or 0.0
+ total_uang_masuk += total_uang_muka
+ else:
+ # fallback Midtrans gross_amount
+ total_uang_masuk += so.gross_amount or 0.0
+
+ if invalid_orders:
+ raise ValidationError(
+ f"Tidak dapat membuat refund untuk SO {', '.join(invalid_orders)} "
+ "karena tidak memiliki Record Uang Masuk (Journal Uang Muka/Midtrans).\n"
+ "Pastikan semua SO yang dipilih sudah memiliki Record pembayaran yang valid."
+ )
@api.onchange('refund_type')
def _onchange_refund_type(self):
@@ -353,8 +542,15 @@ class RefundSaleOrder(models.Model):
line_vals.append((0, 0, {
'product_id': line.product_id.id,
'quantity': line.product_uom_qty,
+ '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
@@ -362,20 +558,78 @@ class RefundSaleOrder(models.Model):
line_vals = []
StockPicking = self.env['stock.picking']
for so in self.sale_order_ids:
- pickings = StockPicking.search([
+ # BU/SRT
+ pickings_srt = StockPicking.search([
('state', '=', 'done'),
('picking_type_id', '=', 73),
('sale_id', 'in', so.ids)
])
-
- for picking in pickings:
- for move in picking.move_lines:
- line_vals.append((0, 0, {
- 'product_id': move.product_id.id,
- 'quantity': move.product_uom_qty,
- 'reason': '',
- }))
- self.line_ids = line_vals
+ # 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')
@@ -400,10 +654,10 @@ class RefundSaleOrder(models.Model):
record.amount_refund_text = ''
def unlink(self):
- not_draft = self.filtered(lambda r: r.status != 'draft')
- if not_draft:
- names = ', '.join(not_draft.mapped('name'))
- raise UserError(f"Refund hanya bisa dihapus jika statusnya masih draft.\nTidak bisa hapus: {names}")
+ 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')
@@ -433,30 +687,6 @@ class RefundSaleOrder(models.Model):
pengurangan = rec.total_invoice + rec.ongkir
refund = rec.uang_masuk - pengurangan
rec.amount_refund = refund if refund > 0 else 0.0
-
- @api.model
- def default_get(self, fields_list):
- res = super().default_get(fields_list)
- sale_order_id = self.env.context.get('default_sale_order_id')
- if sale_order_id:
- so = self.env['sale.order'].browse(sale_order_id)
- res['sale_order_ids'] = [(6, 0, [so.id])]
- invoice_ids = so.invoice_ids.filtered(
- lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel'
- ).ids
- res['invoice_ids'] = [(6, 0, invoice_ids)]
- res['uang_masuk'] = 0.0
- res['ongkir'] = so.delivery_amt or 0.0
- line_vals = []
- for line in so.order_line:
- line_vals.append((0, 0, {
- 'product_id': line.product_id.id,
- 'quantity': line.product_uom_qty,
- 'reason': '',
- }))
- res['line_ids'] = line_vals
- res['refund_type'] = 'uang' if invoice_ids else False
- return res
@api.onchange('invoice_ids')
def _onchange_invoice_ids(self):
@@ -464,10 +694,26 @@ class RefundSaleOrder(models.Model):
if self.refund_type not in ['uang', 'barang_kosong']:
self.refund_type = False
- self.total_invoice = sum(self.invoice_ids.mapped('amount_total'))
+ 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'
@@ -481,6 +727,19 @@ class RefundSaleOrder(models.Model):
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':
@@ -512,7 +771,7 @@ class RefundSaleOrder(models.Model):
is_fat = self.env.user.has_group('indoteknik_custom.group_role_fat')
allowed_user_ids = [19, 688, 7]
for rec in self:
- if self.user.id not in allowed_user_ids and not is_fat:
+ 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 not in ['refund', 'reject']:
rec.status = 'reject'
@@ -528,7 +787,7 @@ class RefundSaleOrder(models.Model):
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 refund.")
+ raise UserError("Hanya Finance yang dapat mengkonfirmasi pembayaran refund.")
if rec.status_payment == 'pending':
rec.status_payment = 'done'
rec.refund_date = fields.Date.context_today(self)
@@ -565,15 +824,26 @@ class RefundSaleOrder(models.Model):
# Ambil label refund type
refund_type_label = dict(
self.fields_get(allfields=['refund_type'])['refund_type']['selection']
- ).get(refund.refund_type, '').replace("Refund ", "").upper()
-
+ ).get(refund.refund_type, '')
+
+ # Normalisasi
+ refund_type_label = refund_type_label.upper()
+
+ if refund.refund_type in ['barang_kosong', 'barang_kosong_sebagian']:
+ 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 {refund_type_label} {refund.name or ''} {partner.display_name}".upper()
+ ref_text = f"{refund_type_label} {refund.name or ''} {partner.display_name}".upper()
# Buat Account Move (Journal Entry)
account_move = self.env['account.move'].create({
@@ -586,8 +856,8 @@ class RefundSaleOrder(models.Model):
})
amount = refund.amount_refund
-
- second_account_id = 450 if has_invoice else 668
+ # 450 Penerimaan Belum Teridentifikasi, 668 Penerimaan Belum Alokasi
+ second_account_id = 450 if refund.refund_type not in ['barang_kosong', 'barang_kosong_sebagian'] else 668
debit_line = {
'move_id': account_move.id,
@@ -623,12 +893,20 @@ class RefundSaleOrder(models.Model):
def _compute_journal_refund_move_id(self):
for rec in self:
move = self.env['account.move'].search([
- ('refund_id', '=', rec.id)
+ ('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'),
@@ -640,14 +918,211 @@ class RefundSaleOrder(models.Model):
}
+ @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': 'revisi_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'
- _inherit = ['mail.thread']
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 \ No newline at end of file