summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/refund_sale_order.py
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-08-20 09:06:33 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-08-20 09:06:33 +0700
commitbbc454fd6e13d12e9674769a555264f2c2343b5b (patch)
treed4973e8aab48e52cc0006600306ff7b19c11d639 /indoteknik_custom/models/refund_sale_order.py
parent9ea33822721c3c548675633effdf679cc1a99b0d (diff)
<hafid> refund system sisa uang muka
Diffstat (limited to 'indoteknik_custom/models/refund_sale_order.py')
-rw-r--r--indoteknik_custom/models/refund_sale_order.py226
1 files changed, 189 insertions, 37 deletions
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 077809e9..2dc72f0f 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -17,13 +17,17 @@ class RefundSaleOrder(models.Model):
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'),
@@ -55,7 +59,7 @@ 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)
refund_type_display = fields.Char(string="Refund Type Label", compute="_compute_refund_type_display")
@@ -89,7 +93,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,22 +111,44 @@ 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)",
+ )
-
@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 Sales dan Finance yang boleh membuat refund.")
@@ -158,9 +184,20 @@ class RefundSaleOrder(models.Model):
if refund_type in ['barang_kosong', 'barang_kosong_sebagian'] and so_ids:
sale_orders = self.env['sale.order'].browse(so_ids)
- zero_delivery_lines = sale_orders.mapped('order_line').filtered(lambda l: l.qty_delivered == 0)
- if not zero_delivery_lines:
- raise UserError("❌ Tidak ada barang yang Tidak Terikirim di Sales Order yang dipilih.")
+
+ 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':
@@ -180,22 +217,40 @@ class RefundSaleOrder(models.Model):
if refund_type == 'retur_half' and not invoice_ids:
raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk Retur Sebagian.")
- total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0
- ongkir = vals.get('ongkir', 0.0)
- vals['total_invoice'] = total_invoice
- pengurangan = total_invoice + ongkir
-
if refund_type == 'barang_kosong_sebagian' and so_ids:
sale_orders = self.env['sale.order'].browse(so_ids)
vals['uang_masuk'] = sum(sale_orders.mapped('amount_total'))
-
- uang_masuk = vals.get('uang_masuk', 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 uang_masuk > pengurangan:
- vals['amount_refund'] = uang_masuk - pengurangan
- else:
- raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir untuk melakukan refund")
+ uang_masuk = total_uang_muka if moves else sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount'))
+ vals['uang_masuk'] = uang_masuk
+ ongkir = vals.get('ongkir', 0.0)
+ 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:
+ so = self.env['sale.order'].browse(so_ids[0])
+ existing_refunds = self.search([('sale_order_ids', 'in', so_ids)])
+ total_refunded = sum(existing_refunds.mapped('amount_refund')) + amount_refund
+ remaining = uang_masuk - total_refunded
+ 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)
@@ -285,20 +340,41 @@ class RefundSaleOrder(models.Model):
if refund_type == 'retur_half' and not invoice_ids:
raise ValidationError(f"SO {', '.join(so.mapped('name'))} belum memiliki invoice untuk retur sebagian.")
- if any(field in vals for field in ['uang_masuk', 'invoice_ids', 'ongkir', 'sale_order_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 = vals.get('uang_masuk', rec.uang_masuk)
- ongkir = vals.get('ongkir', rec.ongkir)
+ uang_masuk = rec.uang_masuk
+ amount_refund = vals.get('amount_refund', rec.amount_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')) + amount_refund
+ remaining = uang_masuk - total_refunded
- 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("❌ Dana uang masuk telah sepenuhnya di refund tidak bisa Mengubah Nominal 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('ongkir', 'amount_refund')
+ def _onchange_refund_fields(self):
+ for rec in self:
+ uang_masuk = rec.uang_masuk or 0.0
+ ongkir = rec.ongkir or 0.0
+ refund_input = rec.amount_refund or 0.0
+
+ total_refund_with_ongkir = refund_input + ongkir
+ if total_refund_with_ongkir > uang_masuk:
+ raise UserError("❌ Refund + Ongkir tidak boleh melebihi Uang Masuk.")
+ remaining = uang_masuk - refund_input
+ rec.remaining_refundable = remaining
@api.depends('status_payment', 'status')
def _compute_is_locked(self):
@@ -362,6 +438,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):
@@ -374,14 +481,12 @@ class RefundSaleOrder(models.Model):
line_vals.append((0, 0, {
'product_id': line.product_id.id,
'quantity': line.product_uom_qty,
+ 'product_from': line.order_id.name,
'reason': '',
}))
self.line_ids = line_vals
- if self.refund_type == 'barang_kosong_sebagian' and self.sale_order_ids:
- self.uang_masuk = sum(self.sale_order_ids.mapped('amount_total')) + sum(self.sale_order_ids.mapped('delivery_amt'))
-
elif self.refund_type in ['retur', 'retur_half'] and self.sale_order_ids:
line_vals = []
StockPicking = self.env['stock.picking']
@@ -476,6 +581,7 @@ class RefundSaleOrder(models.Model):
line_vals.append((0, 0, {
'product_id': line.product_id.id,
'quantity': line.product_uom_qty,
+ 'product_from': line.order_id.name,
'reason': '',
}))
res['line_ids'] = line_vals
@@ -671,7 +777,52 @@ 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)
class RefundSaleOrderLine(models.Model):
_name = 'refund.sale.order.line'
@@ -682,3 +833,4 @@ class RefundSaleOrderLine(models.Model):
product_id = fields.Many2one('product.product', string='Product')
quantity = fields.Float(string='Qty')
reason = fields.Char(string='Reason')
+ product_from = fields.Char(string='Product Reference')