summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-08-29 14:23:18 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-08-29 14:23:18 +0700
commit3139c592dd1f74b7c6a6c4917419628895296406 (patch)
treec77d8f30e9e6404be3237733b6f3e3e656f6729d
parent8fb3427f625867b7d47b7ed0d40f994fa52c00e6 (diff)
<hafid> refund abis testing aman
-rw-r--r--indoteknik_custom/models/refund_sale_order.py306
-rwxr-xr-xindoteknik_custom/models/sale_order.py36
-rw-r--r--indoteknik_custom/models/tukar_guling.py61
-rw-r--r--indoteknik_custom/views/refund_sale_order.xml76
4 files changed, 374 insertions, 105 deletions
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
index 0b4d2893..086c7a81 100644
--- a/indoteknik_custom/models/refund_sale_order.py
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -10,7 +10,7 @@ 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)
@@ -49,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")
@@ -63,8 +64,19 @@ class RefundSaleOrder(models.Model):
], string='Refund Type', required=True)
tukar_guling_ids = fields.One2many(
- 'tukar.guling', 'refund_id',
- string="Tukar Guling"
+ '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",
+ help="Pilih transaksi salah transfer dari jurnal Uang Muka (journal_id=11) yang tidak terkait SO."
)
tukar_guling_count = fields.Integer(
@@ -72,6 +84,11 @@ class RefundSaleOrder(models.Model):
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')
@@ -152,6 +169,34 @@ class RefundSaleOrder(models.Model):
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):
@@ -176,6 +221,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(
@@ -188,10 +236,10 @@ 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', 'retur']:
+ 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 jika ada invoice")
- if not invoice_ids and refund_type and refund_type in ['uang', 'barang_kosong_sebagian']:
+ 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:
@@ -206,7 +254,7 @@ class RefundSaleOrder(models.Model):
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
+ 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.")
@@ -215,21 +263,37 @@ class RefundSaleOrder(models.Model):
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 == 'barang_kosong_sebagian' and so_ids:
- sale_orders = self.env['sale.order'].browse(so_ids)
- vals['uang_masuk'] = sum(sale_orders.mapped('amount_total'))
-
- moves = self.env['account.move'].search([
- ('sale_id', 'in', so_ids),
- ('journal_id', '=', 11),
- ('state', '=', 'posted'),
+ 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'),
])
- total_uang_muka = sum(moves.mapped('amount_total_signed'))
+ 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
- uang_masuk = total_uang_muka if moves else sum(self.env['sale.order'].browse(so_ids).mapped('gross_amount'))
- vals['uang_masuk'] = uang_masuk
- total_invoice = sum(self.env['account.move'].browse(invoice_ids).mapped('amount_total_signed')) if invoice_ids else 0.0
+ 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
+ else:
+ sisa_uang_masuk = total_pembayaran
+
+ 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)
if amount_refund <= 0.00:
@@ -240,16 +304,13 @@ class RefundSaleOrder(models.Model):
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
+ 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)
+ return super().create(vals)
def write(self, vals):
@@ -276,6 +337,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)
@@ -294,15 +358,12 @@ class RefundSaleOrder(models.Model):
refund_type = vals.get('refund_type', rec.refund_type)
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)
+ 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 != 'lainnya':
- raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.")
-
- if refund_type == 'barang_kosong_sebagian' and sale_orders:
- vals['uang_masuk'] = sum(sale_orders.mapped('amount_total'))
+ 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)
@@ -320,8 +381,12 @@ class RefundSaleOrder(models.Model):
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']:
- raise UserError("Refund type Lebih Bayar, Barang Kosong Sebagian, atau Retur Hanya Bisa dipilih 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'))
@@ -349,15 +414,23 @@ class RefundSaleOrder(models.Model):
@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
+ # ambil refund terakhir untuk SO
+ existing_refund = self.env['refund.sale.order'].search([
+ ('sale_order_ids', 'in', rec.sale_order_ids.ids)
+ ], order='id desc', limit=1)
+
+ if existing_refund:
+ sisa_uang_masuk = existing_refund.remaining_refundable
+ else:
+ sisa_uang_masuk = rec.uang_masuk or 0.0
+
+ # update field uang_masuk supaya form menampilkan sisa aktual
+ rec.uang_masuk = sisa_uang_masuk
+
+ # hitung remaining setelah input refund
+ rec.remaining_refundable = sisa_uang_masuk - refund_input
@api.depends('status_payment', 'status')
def _compute_is_locked(self):
@@ -399,6 +472,8 @@ class RefundSaleOrder(models.Model):
all_invoices = self.env['account.move']
total_invoice = 0.0
+ so_ids = self.sale_order_ids.ids
+
for so in self.sale_order_ids:
self.ongkir += so.delivery_amt or 0.0
valid_invoices = so.invoice_ids.filtered(
@@ -407,6 +482,15 @@ class RefundSaleOrder(models.Model):
all_invoices |= valid_invoices
total_invoice += sum(valid_invoices.mapped('amount_total_signed'))
+ 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
+
self.invoice_ids = all_invoices
self.total_invoice = total_invoice
@@ -464,9 +548,15 @@ class RefundSaleOrder(models.Model):
line_vals.append((0, 0, {
'product_id': line.product_id.id,
'quantity': line.product_uom_qty,
- 'ref_id': line.order_id.id,
+ '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
@@ -495,33 +585,57 @@ class RefundSaleOrder(models.Model):
])
for picking in product_out:
for move in picking.move_lines:
- line_vals.append((0, 0, {
- 'product_id': move.product_id.id,
- 'ref_id': picking.id,
- 'quantity': move.product_uom_qty,
- 'reason': '',
- }))
+ 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:
- line_vals.append((0, 0, {
- 'product_id': move.product_id.id,
- 'ref_id': picking.ref_id,
- 'quantity': move.product_uom_qty,
- 'reason': '',
- }))
+ 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:
- line_vals.append((0, 0, {
- 'product_id': move.product_id.id,
- 'ref_id': picking.ref_id,
- 'quantity': move.product_uom_qty,
- 'reason': '',
- }))
- self.line_ids = line_vals
+ 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')
@@ -602,6 +716,10 @@ class RefundSaleOrder(models.Model):
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'
@@ -856,6 +974,41 @@ class RefundSaleOrder(models.Model):
])
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']:
@@ -897,8 +1050,7 @@ class RefundSaleOrder(models.Model):
'origin_so': refund.sale_order_ids.id,
'operations': picking.id,
'return_type': 'revisi_so',
- 'is_has_invoice': bool(refund.invoice_ids.ids),
- 'invoice_id': refund.invoice_ids.ids if refund.invoice_ids.ids else None,
+ 'invoice_id': [(6, 0, refund.invoice_ids.ids)],
'refund_id': refund.id,
'line_ids': line_vals,
'mapping_koli_ids': koli_lines
@@ -933,10 +1085,38 @@ class RefundSaleOrder(models.Model):
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='Product Reference')
+ 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
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index a357eb70..998363ef 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -3196,6 +3196,16 @@ class SaleOrder(models.Model):
def button_refund(self):
self.ensure_one()
+
+ if self.state not in ['cancel', 'sale']:
+ raise UserError(f"❌ SO {self.name} tidak bisa direfund. Status harus Cancel atau Sale.")
+ if self.state == 'sale':
+ not_done_pickings = self.picking_ids.filtered(lambda p: p.state not in ['done', 'cancel'])
+ if not_done_pickings:
+ raise UserError(
+ f"❌ SO {self.name} Belum melakukan kirim barang "
+ f"({', '.join(not_done_pickings.mapped('name'))}). Selesaikan Pengiriman untuk melakukan refund."
+ )
moves = self.env['account.move'].search([
('sale_id', '=', self.id),
('journal_id', '=', 11),
@@ -3220,7 +3230,7 @@ class SaleOrder(models.Model):
"(Journal Uang Muka/Midtrans Payment)."
)
invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
- total_refunded = sum(self.refund_ids.mapped('amount_refund')) or 0.0
+ total_refunded = sum(self.refund_ids.mapped('amount_refund'))
sisa_uang_muka = total_uang_muka - total_refunded
if sisa_uang_muka <= 0:
@@ -3249,6 +3259,30 @@ class SaleOrder(models.Model):
if not self:
raise UserError("Tidak ada Sale Order yang dipilih.")
+ if len(self) > 1:
+ not_cancel_orders = self.filtered(lambda so: so.state != 'cancel')
+ if not_cancel_orders:
+ raise ValidationError(
+ f"❌ Refund Multi SO hanya bisa dibuat untuk SO dengan status Cancel. "
+ f"SO berikut tidak Cancel: {', '.join(not_cancel_orders.mapped('name'))}"
+ )
+
+
+ invalid_status_orders = []
+ for order in self:
+ if order.state not in ['cancel', 'sale']:
+ invalid_status_orders.append(order.name)
+ elif order.state == 'sale':
+ not_done_pickings = order.picking_ids.filtered(lambda p: p.state != 'done')
+ if not_done_pickings:
+ invalid_status_orders.append(order.name)
+
+ if invalid_status_orders:
+ raise ValidationError(
+ f"❌ Refund tidak bisa dibuat untuk SO {', '.join(invalid_status_orders)}. "
+ f"SO harus Cancel atau Sale dengan semua Pengiriman sudah selesai."
+ )
+
partner_set = set(self.mapped('partner_id.id'))
if len(partner_set) > 1:
raise UserError("Tidak dapat membuat refund untuk Multi SO dengan Customer berbeda. Harus memiliki Customer yang sama.")
diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py
index 1309fcfe..ff4edc84 100644
--- a/indoteknik_custom/models/tukar_guling.py
+++ b/indoteknik_custom/models/tukar_guling.py
@@ -96,38 +96,39 @@ class TukarGuling(models.Model):
so = self.env['sale.order'].search([('name', '=', origin_str)], limit=1)
rec.origin_so = so.id if so else False
- @api.depends('origin', 'origin_so', 'partner_id', 'line_ids.product_id')
+ @api.depends('origin', 'origin_so', 'partner_id', 'line_ids.product_id', 'invoice_id', 'operations')
def _compute_is_has_invoice(self):
Move = self.env['account.move']
for rec in self:
- rec.is_has_invoice = False
- rec.invoice_id = [(5, 0, 0)]
-
- product_ids = rec.line_ids.mapped('product_id').ids
- if not product_ids:
- continue
-
- domain = [
- ('move_type', 'in', ['out_invoice', 'in_invoice']),
- ('state', 'not in', ['draft', 'cancel']),
- ('invoice_line_ids.product_id', 'in', product_ids),
- ]
-
- if rec.partner_id:
- domain.append(('partner_id', '=', rec.partner_id.id))
-
- extra = []
- if rec.origin:
- extra.append(('invoice_origin', 'ilike', rec.origin))
- if rec.origin_so:
- extra.append(('invoice_line_ids.sale_line_ids.order_id', '=', rec.origin_so.id))
- if extra:
- domain = domain + ['|'] * (len(extra) - 1) + extra
-
- invoices = Move.search(domain).with_context(active_test=False)
- if invoices:
- rec.invoice_id = [(6, 0, invoices.ids)]
- rec.is_has_invoice = True
+ invoices = rec.invoice_id
+
+ if not invoices:
+ product_ids = rec.line_ids.mapped('product_id').ids
+ if product_ids:
+ domain = [
+ ('move_type', 'in', ['out_invoice', 'out_refund', 'in_invoice']),
+ ('state', 'not in', ['draft', 'cancel']),
+ ('invoice_line_ids.product_id', 'in', product_ids),
+ ]
+
+ # if rec.partner_id:
+ # domain.append(
+ # ('partner_id.commercial_partner_id', '=', rec.partner_id.commercial_partner_id.id)
+ # )
+
+ extra = []
+ if rec.origin:
+ extra.append(('invoice_origin', 'ilike', rec.origin))
+ if rec.origin_so:
+ extra.append(('invoice_line_ids.sale_line_ids.order_id', '=', rec.origin_so.id))
+ if extra:
+ domain += ['|'] * (len(extra) - 1) + extra
+
+ invoices = Move.search(domain).with_context(active_test=False)
+ if invoices:
+ rec.invoice_id = [(6, 0, invoices.ids)]
+
+ rec.is_has_invoice = bool(invoices)
def set_opt(self):
if not self.val_inv_opt and self.is_has_invoice == True:
@@ -1009,4 +1010,4 @@ class TukarGulingMappingKoli(models.Model):
for rec in self:
if rec.tukar_guling_id and rec.tukar_guling_id.state not in ['draft', 'cancel']:
raise UserError("Tidak bisa menghapus Mapping Koli karena status Tukar Guling bukan Draft atau Cancel.")
- return super(TukarGulingMappingKoli, self).unlink()
+ return super(TukarGulingMappingKoli, self).unlink() \ No newline at end of file
diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml
index a2538f42..ae0861e2 100644
--- a/indoteknik_custom/views/refund_sale_order.xml
+++ b/indoteknik_custom/views/refund_sale_order.xml
@@ -90,6 +90,11 @@
attrs="{'invisible': [('show_return_alert', '=', False)]}">
⚠️ SO belum melakukan retur barang. Silakan buat pengajuan retur.
</div>
+ <field name="show_approval_alert" invisible="1"/>
+ <div class="alert alert-info" role="alert"
+ attrs="{'invisible': ['|', ('show_approval_alert', '=', False), ('status', 'in', ['reject', 'refund'])]}">
+ ⚠️ SO sudah melakukan retur barang. Silakan lanjutkan refund.
+ </div>
</xpath>
<sheet>
<div class="oe_button_box" name="button_box">
@@ -101,12 +106,24 @@
attrs="{'invisible': [('journal_refund_move_id', '=', False)]}">
<field name="journal_refund_move_id" string="Journal Refund" widget="statinfo"/>
</button>
- <button name="action_open_tukar_guling"
+
+ <button name="action_open_tukar_guling"
type="object"
class="oe_stat_button"
icon="fa-refresh"
- attrs="{'invisible': ['|', ('refund_type', 'not in', ['retur', 'retur_half']), ('tukar_guling_count', '=', 0)]}">
- <field name="tukar_guling_count" string="Pengajuan Return SO" widget="statinfo"/>
+ attrs="{'invisible': ['|', ('tukar_guling_count','=', 0), ('has_picking','=',True)]}">
+ <div class="o_stat_info">
+ <field name="tukar_guling_count" widget="statinfo"/>
+ <span class="o_stat_text">Pengajuan Return SO</span>
+ </div>
+ </button>
+
+ <button name="action_view_picking"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-truck"
+ attrs="{'invisible': [('has_picking','=',False)]}">
+ <field name="picking_ids" widget="statinfo" string="Delivery"/>
</button>
</div>
<widget name="web_ribbon"
@@ -128,21 +145,26 @@
<field name="journal_refund_state" invisible="1"/>
<field name="partner_id" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="sale_order_ids" widget="many2many_tags" attrs="{'readonly': [('is_locked', '=', True)]}"/>
- <field name="invoice_ids" widget="many2many_tags" readonly="1"/>
- <field name="invoice_names" widget="html" readonly="1"/>
- <field name="so_names" widget="html" readonly="1"/>
- <field name="advance_move_names" widget="html" readonly="1"/>
+ <field name="sale_order_ids" widget="many2many_tags" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('refund_type', '=', 'salah_transfer')]}"/>
+ <field name="invoice_ids" widget="many2many_tags" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/>
+ <field name="tukar_guling_count" invisible="1"/>
+ <field name="invoice_names" widget="html" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/>
+ <field name="so_names" widget="html" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/>
+ <field name="advance_move_names" widget="html" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/>
+ <field name="transfer_move_id"
+ attrs="{'invisible': [('refund_type', '!=', 'salah_transfer')],
+ 'required': [('refund_type', '=', 'salah_transfer')]}"/>
<field name="refund_type" attrs="{'readonly': [('is_locked', '=', True)]}"/>
<field name="note_refund" attrs="{'readonly': [('is_locked', '=', True)]}"/>
</group>
<group>
<field name="uang_masuk" attrs="{'readonly': [('refund_type', '!=', 'salah_transfer')]}"/>
- <field name="total_invoice" readonly="1"/>
- <field name="ongkir" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ <field name="total_invoice" readonly="1" attrs="{'invisible': [('refund_type', '=', 'salah_transfer')]}"/>
+ <field name="ongkir" attrs="{'readonly': [('is_locked', '=', True)], 'invisible': [('refund_type', '=', 'salah_transfer')]}"/>
<field name="amount_refund" attrs="{'readonly': [('is_locked', '=', True)]}"/>
<field name="amount_refund_text" readonly="1"/>
<field name="sale_order_count" invisible="1"/>
+ <field name="has_picking" invisible="1"/>
<field name="tukar_guling_ids" invisible="1"/>
<field name="remaining_refundable" readonly="1" attrs="{'invisible': [('sale_order_count', '>', 1)]}"/>
<field name="uang_masuk_type" required="1" attrs="{'readonly': [('is_locked', '=', True)]}"/>
@@ -157,9 +179,17 @@
<page string="Produk Line">
<field name="line_ids" attrs="{'readonly': [('is_locked', '=', True)]}">
<tree editable="bottom" create="0" delete="1">
- <field name="ref_id"/>
+ <field name="from_name"/>
+ <field name="prod_id" invisible="1"/>
+ <field name="ref_id" invisible="1"/>
<field name="product_id"/>
<field name="quantity"/>
+ <field name="price_unit"/>
+ <field name="discount"/>
+ <field name="subtotal"/>
+ <field name="tax" widget="many2many_tags"/>
+ <field name="tax_amt" widget="monetary" options="{'currency_field': 'currency_id'}"/>
+ <field name="total" widget="monetary" options="{'currency_field': 'currency_id'}" sum="Grand Total"/>
<field name="reason"/>
</tree>
</field>
@@ -176,6 +206,7 @@
<field name="bank" attrs="{'readonly': [('is_locked', '=', True)]}"/>
<field name="account_name" attrs="{'readonly': [('is_locked', '=', True)]}"/>
<field name="account_no" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ <field name="kcp" attrs="{'readonly': [('is_locked', '=', True)]}"/>
</group>
</group>
</page>
@@ -230,11 +261,34 @@
<field name="reason_reject"/>
</group>
</page>
+
+ <page string="Return Line" attrs="{'invisible': ['|', ('tukar_guling_count','=', 0), ('has_picking', '=', False)]}">
+ <group>
+ <field name="tukar_guling_ids" readonly="1" nolabel="1">
+ <tree>
+ <field name="name"/>
+ <field name="partner_id" string="Customer"/>
+ <field name="origin" string="SO Number"/>
+ <field name="operations" string="Operations"/>
+ <field name="return_type" string="Return Type"/>
+ <field name="state" widget="badge"
+ decoration-info="state in ('draft', 'approval_sales', 'approval_finance','approval_logistic')"
+ decoration-warning="state == 'approved'"
+ decoration-success="state == 'done'"
+ decoration-muted="state == 'cancel'"
+ />
+ <field name="ba_num" string="Nomor BA"/>
+ <field name="date"/>
+ </tree>
+ </field>
+ </group>
+ </page>
</notebook>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
<field name="message_ids" widget="mail_thread"/>
+ <field name="activity_ids" widget="mail_activity"/>
</div>
</form>
</field>