summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-07-23 14:50:10 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-07-23 14:50:10 +0700
commitdeb60713ed39979b34083ee094de79fa3afac3b8 (patch)
treeb1648b3b7822034fb893b82e78f16769c5db54aa
parentc667a8699762057c9e6191466a182ebb69cb66c7 (diff)
<hafid> Refund System
-rwxr-xr-xindoteknik_custom/__manifest__.py1
-rwxr-xr-xindoteknik_custom/models/__init__.py1
-rw-r--r--indoteknik_custom/models/account_move.py35
-rw-r--r--indoteknik_custom/models/refund_sale_order.py649
-rwxr-xr-xindoteknik_custom/models/sale_order.py123
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv2
-rw-r--r--indoteknik_custom/views/account_move.xml4
-rw-r--r--indoteknik_custom/views/ir_sequence.xml10
-rw-r--r--indoteknik_custom/views/refund_sale_order.xml199
-rwxr-xr-xindoteknik_custom/views/sale_order.xml38
10 files changed, 1058 insertions, 4 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index 17cec7b6..13f0399b 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -169,6 +169,7 @@
'views/public_holiday.xml',
'views/stock_inventory.xml',
'views/sale_order_delay.xml',
+ 'views/refund_sale_order.xml',
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index b815b472..44f383b0 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -152,4 +152,5 @@ from . import stock_inventory
from . import sale_order_delay
from . import approval_invoice_date
from . import approval_payment_term
+from . import refund_sale_order
# from . import patch
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index b6627867..7bb71e03 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -1,5 +1,6 @@
from odoo import models, api, fields
from odoo.exceptions import AccessError, UserError, ValidationError
+from markupsafe import escape as html_escape
from datetime import timedelta, date, datetime
from pytz import timezone, utc
import logging
@@ -71,7 +72,24 @@ class AccountMove(models.Model):
# Di model account.move
bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini')
down_payment = fields.Boolean('Down Payments?')
-
+ refund_id = fields.Many2one('refund.sale.order', string='Refund Reference')
+ refund_so_ids = fields.Many2many(
+ 'sale.order',
+ 'account_move_sale_order_rel',
+ 'move_id',
+ 'sale_order_id',
+ string='Group SO Number'
+ )
+
+ refund_so_links = fields.Html(
+ string="Group SO Numbers",
+ compute="_compute_refund_so_links",
+ )
+
+ has_refund_so = fields.Boolean(
+ string='Has Refund SO',
+ compute='_compute_has_refund_so',
+ )
# def name_get(self):
# result = []
@@ -98,6 +116,21 @@ class AccountMove(models.Model):
if self.date:
self.invoice_date = self.date
+ @api.depends('refund_so_ids')
+ def _compute_refund_so_links(self):
+ for rec in self:
+ links = []
+ for so in rec.refund_so_ids:
+ url = f"/web#id={so.id}&model=sale.order&view_type=form"
+ name = html_escape(so.name or so.display_name)
+ links.append(f'<a href="{url}" target="_blank">{name}</a>')
+ rec.refund_so_links = ', '.join(links) if links else "-"
+
+ @api.depends('refund_so_ids')
+ def _compute_has_refund_so(self):
+ for rec in self:
+ rec.has_refund_so = bool(rec.refund_so_ids)
+
# def compute_length_of_payment(self):
# for rec in self:
diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py
new file mode 100644
index 00000000..5c9c4d83
--- /dev/null
+++ b/indoteknik_custom/models/refund_sale_order.py
@@ -0,0 +1,649 @@
+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']
+ _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', compute='_compute_total_invoice', readonly=True)
+ 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)
+ 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)
+ 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")
+
+ refund_type = fields.Selection([
+ ('barang_kosong_sebagian', 'Refund Barang Kosong Sebagian'),
+ ('barang_kosong', 'Refund Barang Kosong Full'),
+ ('uang', 'Refund Lebih Bayar'),
+ ('retur_half', 'Refund Retur Sebagian'),
+ ('retur', 'Refund Retur Full'),
+ ('lainnya', 'Lainnya')
+ ], string='Refund Type', required=True)
+
+ 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 SO", 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", default='image')
+ 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")
+
+
+ @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
+ ):
+ raise UserError("❌ Hanya user 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
+
+ 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)
+ vals['partner_id'] = sale_orders[0].partner_id.id
+
+ invoices = sale_orders.mapped('invoice_ids').filtered(
+ lambda inv: inv.move_type in ['out_invoice', 'out_refund'] and inv.state != 'cancel'
+ )
+ if invoices:
+ vals['invoice_ids'] = [(6, 0, invoices.ids)]
+
+
+ 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', 'retur_half']:
+ raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian 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, Barang Kosong Sebagian, atau Retur Sebagian Hanya Bisa dipilih Jika Ada Invoice")
+
+
+ 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 pickings:
+ raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.")
+
+ 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')) 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 uang_masuk > pengurangan:
+ vals['amount_refund'] = uang_masuk - pengurangan
+ else:
+ raise UserError("Uang masuk harus lebih besar dari total invoice + ongkir untuk melakukan refund")
+
+ 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)
+ 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 != 'cancel'
+ )
+ 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 not so_ids and refund_type != 'lainnya':
+ raise ValidationError("Jika tidak ada Sales Order yang dipilih, maka Tipe Refund hanya boleh 'Lainnya'.")
+
+
+ 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', 'retur_half']:
+ raise UserError("Refund type Hanya Bisa Lebih Bayar, Barang Kosong Sebagian, atau Retur Sebagian 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 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 not pickings:
+ raise ValidationError(f"SO {', '.join(so.mapped('name'))} tidak melakukan retur barang.")
+
+ 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']):
+ 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)
+
+ 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 vals.get('status') == 'refund' and not vals.get('refund_date'):
+ vals['refund_date'] = fields.Date.context_today(self)
+
+ return super().write(vals)
+
+ @api.depends('status_payment')
+ def _compute_is_locked(self):
+ for rec in self:
+ rec.is_locked = rec.status_payment in ['done', 'reject']
+
+ @api.depends('invoice_ids.amount_total')
+ def _compute_total_invoice(self):
+ for rec in self:
+ rec.total_invoice = sum(inv.amount_total for inv in rec.invoice_ids)
+
+ @api.depends('sale_order_ids')
+ def _compute_advance_move_names(self):
+ for rec in self:
+ move_links = []
+ moves = self.env['account.move'].search([
+ ('sale_id', 'in', rec.sale_order_ids.ids),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted')
+ ])
+ for move in moves:
+ url = f"/web#id={move.id}&model=account.move&view_type=form"
+ name = html_escape(move.name or 'Unnamed')
+ move_links.append(f'<a href="{url}" target="_blank">{name}</a>')
+ 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
+
+ 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'))
+
+
+ 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.onchange('refund_type')
+ def _onchange_refund_type(self):
+ self.line_ids = [(5, 0, 0)]
+ if self.refund_type in ['barang_kosong_sebagian', 'barang_kosong'] and self.sale_order_ids:
+ line_vals = []
+ for so in self.sale_order_ids:
+ for line in so.order_line:
+ if line.qty_delivered == 0:
+ line_vals.append((0, 0, {
+ 'product_id': line.product_id.id,
+ 'quantity': line.product_uom_qty,
+ 'reason': '',
+ }))
+
+ 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:
+ pickings = 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
+
+
+ @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):
+ 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}")
+ 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'<a href="{url}" target="_blank">{name}</a>')
+ 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'<a href="{url}" target="_blank">{name}</a>')
+ 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
+
+ if rec.uang_masuk and rec.uang_masuk <= pengurangan:
+ return {
+ 'warning': {
+ 'title': 'Uang Masuk Kurang',
+ 'message': 'Uang masuk harus lebih besar dari total invoice + ongkir untuk dapat melakukan refund.'
+ }
+ }
+
+ @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):
+ 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'))
+
+ def action_ask_approval(self):
+ for rec in self:
+ 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:
+ 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 == 19:
+ 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.user.id 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'
+ 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 refund.")
+ 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, '').replace("Refund ", "").upper()
+
+
+
+ if not partner:
+ raise UserError("❌ Partner tidak ditemukan.")
+
+ # Ref format
+ ref_text = f"REFUND {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,
+ })
+
+ amount = refund.amount_refund
+
+ second_account_id = 450 if has_invoice 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,
+ }
+
+ 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,
+ }
+
+ self.env['account.move.line'].create([debit_line, credit_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)
+ ], limit=1)
+ rec.journal_refund_move_id = move
+
+ def action_open_journal_refund(self):
+ self.ensure_one()
+ 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'
+ }
+
+
+
+
+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')
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 591951ca..8d40bfb5 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -356,6 +356,14 @@ class SaleOrder(models.Model):
compute="_compute_eta_date_reserved",
help="Tanggal pertama kali barang berhasil di-reservasi pada DO (BU/PICK/) yang berstatus Siap Dikirim."
)
+ refund_ids = fields.Many2many('refund.sale.order', compute='_compute_refund_ids', string='Refunds')
+ has_refund = fields.Boolean(string='Has Refund', compute='_compute_has_refund')
+ refund_count = fields.Integer(string='Refund Count', compute='_compute_refund_count')
+ advance_payment_move_id = fields.Many2one(
+ 'account.move',
+ compute='_compute_advance_payment_move',
+ string='Advance Payment Move',
+ )
@api.depends('order_line.product_id', 'date_order')
def _compute_et_products(self):
@@ -3077,4 +3085,117 @@ class SaleOrder(models.Model):
if any(field in vals for field in ["order_line", "client_order_ref"]):
self._calculate_etrts_date()
- return res \ No newline at end of file
+ return res
+
+ def button_refund(self):
+ self.ensure_one()
+
+ invoice_ids = self.invoice_ids.filtered(lambda inv: inv.state != 'cancel')
+
+ return {
+ 'name': 'Refund Sale Order',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'refund.sale.order',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': {
+ 'default_sale_order_ids': [(6, 0, [self.id])],
+ 'default_invoice_ids': [(6, 0, invoice_ids.ids)],
+ 'default_uang_masuk': sum(invoice_ids.mapped('amount_total')) + (self.delivery_amt or 0.0) + 1000,
+ 'default_ongkir': self.delivery_amt or 0.0,
+ 'default_bank': '', # bisa isi default bank kalau mau
+ 'default_account_name': '',
+ 'default_account_no': '',
+ 'default_refund_type': '',
+ },
+ }
+
+ def open_form_multi_create_refund(self):
+ if not self:
+ raise UserError("Tidak ada Sale Order yang dipilih.")
+
+ 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.")
+
+ invoice_status_set = set(self.mapped('invoice_status'))
+ if len(invoice_status_set) > 1:
+ raise UserError("Tidak dapat membuat refund untuk SO dengan status invoice berbeda. Harus memiliki status invoice yang sama.")
+
+ already_refunded = self.filtered(lambda so: so.has_refund)
+ if already_refunded:
+ so_names = ', '.join(already_refunded.mapped('name'))
+ raise UserError(f"❌ Tidak bisa refund ulang. {so_names} sudah melakukan refund.")
+
+ invoice_ids = self.mapped('invoice_ids').filtered(lambda inv: inv.state != 'cancel')
+ delivery_total = sum(self.mapped('delivery_amt'))
+ total_invoice = sum(invoice_ids.mapped('amount_total'))
+
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Create Refund',
+ 'res_model': 'refund.sale.order',
+ 'view_mode': 'form',
+ 'target': 'current',
+ 'context': {
+ 'default_sale_order_ids': [(6, 0, self.ids)],
+ 'default_invoice_ids': [(6, 0, invoice_ids.ids)],
+ 'default_uang_masuk': total_invoice + delivery_total + 1000,
+ 'default_ongkir': delivery_total,
+ 'default_bank': '',
+ 'default_account_name': '',
+ 'default_account_no': '',
+ 'default_refund_type': '',
+ }
+ }
+
+ @api.depends('refund_ids')
+ def _compute_has_refund(self):
+ for so in self:
+ so.has_refund = bool(so.refund_ids)
+
+ def action_view_related_refunds(self):
+ self.ensure_one()
+ return {
+ 'type': 'ir.actions.act_window',
+ 'name': 'Refunds',
+ 'res_model': 'refund.sale.order',
+ 'view_mode': 'tree,form',
+ 'domain': [('sale_order_ids', 'in', [self.id])],
+ 'context': {'default_sale_order_ids': [self.id]},
+ }
+
+ def _compute_refund_ids(self):
+ for order in self:
+ refunds = self.env['refund.sale.order'].search([
+ ('sale_order_ids', 'in', [order.id])
+ ])
+ order.refund_ids = refunds
+
+ def _compute_refund_count(self):
+ for order in self:
+ order.refund_count = self.env['refund.sale.order'].search_count([
+ ('sale_order_ids', 'in', order.id)
+ ])
+
+ @api.depends('invoice_ids')
+ def _compute_advance_payment_move(self):
+ for order in self:
+ move = self.env['account.move'].search([
+ ('sale_id', '=', order.id),
+ ('journal_id', '=', 11),
+ ('state', '=', 'posted'),
+ ], limit=1, order="id desc")
+ order.advance_payment_move_id = move
+
+ def action_open_advance_payment_move(self):
+ self.ensure_one()
+ if not self.advance_payment_move_id:
+ return
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'account.move',
+ 'res_id': self.advance_payment_move_id.id,
+ 'view_mode': 'form',
+ 'target': 'current',
+ } \ No newline at end of file
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index 2b970cfd..f3764177 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -183,3 +183,5 @@ access_production_purchase_match,access.production.purchase.match,model_producti
access_image_carousel,access.image.carousel,model_image_carousel,,1,1,1,1
access_v_sale_notin_matchpo,access.v.sale.notin.matchpo,model_v_sale_notin_matchpo,,1,1,1,1
access_approval_payment_term,access.approval.payment.term,model_approval_payment_term,,1,1,1,1
+access_refund_sale_order,access.refund.sale.order,model_refund_sale_order,base.group_user,1,1,1,1
+access_refund_sale_order_line,access.refund.sale.order.line,model_refund_sale_order_line,base.group_user,1,1,1,1 \ No newline at end of file
diff --git a/indoteknik_custom/views/account_move.xml b/indoteknik_custom/views/account_move.xml
index 2f52b3d9..ae944a4a 100644
--- a/indoteknik_custom/views/account_move.xml
+++ b/indoteknik_custom/views/account_move.xml
@@ -36,7 +36,9 @@
<!-- <field name="purchase_order_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'in_invoice')]}"/> -->
</field>
<field name="ref" position="after">
- <field name="sale_id" readonly="1" attrs="{'invisible': [('move_type', '!=', 'entry')]}"/>
+ <field name="sale_id" readonly="1" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', True)]}"/>
+ <field name="refund_so_links" readonly="1" widget="html" attrs="{'invisible': ['|', ('move_type', '!=', 'entry'), ('has_refund_so', '=', False)]}"/>
+ <field name="has_refund_so" invisible="1"/>
</field>
<field name="reklas_misc_id" position="after">
<field name="purchase_order_id" context="{'form_view_ref': 'purchase.purchase_order_form'}" options="{'no_create': True}"/>
diff --git a/indoteknik_custom/views/ir_sequence.xml b/indoteknik_custom/views/ir_sequence.xml
index a0f5fc6b..a4c98e3f 100644
--- a/indoteknik_custom/views/ir_sequence.xml
+++ b/indoteknik_custom/views/ir_sequence.xml
@@ -200,5 +200,15 @@
<field name="number_next">1</field>
<field name="number_increment">1</field>
</record>
+
+ <record id="seq_refund_sale_order" model="ir.sequence">
+ <field name="name">Refund Sale Order</field>
+ <field name="code">refund.sale.order</field>
+ <field name="prefix">RC/%(year)s/%(month)s/</field>
+ <field name="padding">4</field>
+ <field name="number_next">1</field>
+ <field name="number_increment">1</field>
+ <field name="active">True</field>
+ </record>
</data>
</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/refund_sale_order.xml b/indoteknik_custom/views/refund_sale_order.xml
new file mode 100644
index 00000000..3b348730
--- /dev/null
+++ b/indoteknik_custom/views/refund_sale_order.xml
@@ -0,0 +1,199 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <!-- Tree View -->
+ <record id="view_refund_sale_order_tree" model="ir.ui.view">
+ <field name="name">refund.sale.order.tree</field>
+ <field name="model">refund.sale.order</field>
+ <field name="arch" type="xml">
+ <tree string="Refund Sales Orders">
+ <field name="name" readonly="1"/>
+ <field name="created_date" readonly="1"/>
+ <field name="partner_id" readonly="1"/>
+ <field name="sale_order_ids" widget="many2many_tags" readonly="1"/>
+ <field name="uang_masuk" readonly="1"/>
+ <field name="ongkir" readonly="1"/>
+ <field name="total_invoice" readonly="1"/>
+ <field name="amount_refund" readonly="1"/>
+ <field name="status"
+ decoration-info="status == 'draft'"
+ decoration-danger="status == 'reject'"
+ decoration-success="status == 'refund'"
+ decoration-warning="status == 'pengajuan1' or status == 'pengajuan2' or status == 'pengajuan3'"
+ widget="badge"
+ readonly="1"/>
+ <field name="status_payment"
+ decoration-info="status_payment == 'pending'"
+ decoration-danger="status_payment == 'reject'"
+ decoration-success="status_payment == 'done'"
+ widget="badge"
+ readonly="1"/>
+ <field name="refund_date" readonly="1"/>
+ <field name="amount_refund_text" readonly="1" optional="hide"/>
+ <field name="invoice_ids" readonly="1" optional="hide"/>
+ <field name="refund_type" readonly="1" optional="hide"/>
+ <field name="user_ids" readonly="1" optional="hide"/>
+ </tree>
+ </field>
+ </record>
+
+ <!-- Form View -->
+ <record id="view_refund_sale_order_form" model="ir.ui.view">
+ <field name="name">refund.sale.order.form</field>
+ <field name="model">refund.sale.order</field>
+ <field name="arch" type="xml">
+ <form string="Refund Sales Order">
+ <header>
+ <button name="action_ask_approval"
+ type="object"
+ string="Ask Approval"
+ attrs="{'invisible': [('status', '!=', 'draft')]}"/>
+
+ <button name="action_approve_flow"
+ type="object"
+ string="Approve"
+ class="oe_highlight"
+ attrs="{'invisible': [('status', 'in', ['refund', 'reject', 'draft'])]}"/>
+ <button name="action_trigger_cancel"
+ type="object"
+ string="Cancel"
+ attrs="{'invisible': ['|', ('status_payment', '!=', 'pending'), ('status', '=', 'reject')]}" />
+ <button name="action_confirm_refund"
+ type="object"
+ string="Confirm Refund"
+ class="btn-primary"
+ attrs="{'invisible': ['|', ('status', 'not in', ['pengajuan3','refund']), ('status_payment', '!=', 'pending')]}"/>
+ <button name="action_create_journal_refund"
+ string="Journal Refund"
+ type="object"
+ class="oe_highlight"
+ attrs="{'invisible': ['|', ('status', 'not in', ['pengajuan3','refund']), ('journal_refund_state', '=', 'posted')]}"/>
+
+ <field name="status"
+ widget="statusbar"
+ statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,reject"
+ attrs="{'invisible': [('status', '!=', 'reject')]}" />
+
+ <field name="status"
+ widget="statusbar"
+ statusbar_visible="draft,pengajuan1,pengajuan2,pengajuan3,refund"
+ attrs="{'invisible': [('status', '=', 'reject')]}" />
+ </header>
+ <sheet>
+ <div class="oe_button_box" name="button_box">
+ <button name="action_open_journal_refund"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-book"
+ width="250px"
+ attrs="{'invisible': ['|', ('journal_refund_move_id', '=', False), ('journal_refund_state', '!=', 'posted')]}">
+ <field name="journal_refund_move_id" string="Journal Refund" widget="statinfo"/>
+ </button>
+ </div>
+ <widget name="web_ribbon"
+ title="PAID"
+ bg_color="bg-success"
+ attrs="{'invisible': [('status_payment', '!=', 'done')]}"/>
+
+ <widget name="web_ribbon"
+ title="CANCEL"
+ bg_color="bg-danger"
+ attrs="{'invisible': [('status_payment', '!=', 'reject')]}"/>
+ <h1>
+ <field name="name" readonly="1"/>
+ </h1>
+ <group col="2">
+ <group>
+ <field name="is_locked" invisible="1"/>
+ <field name="status_payment" invisible="1"/>
+ <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="refund_type" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ <field name="note_refund" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ </group>
+ <group>
+ <field name="uang_masuk" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ <field name="total_invoice" readonly="1"/>
+ <field name="ongkir" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ <field name="amount_refund" readonly="1"/>
+ <field name="amount_refund_text" readonly="1"/>
+ <field name="uang_masuk_type" required="1" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ <field name="bukti_uang_masuk_image" widget="image"
+ attrs="{'invisible': [('uang_masuk_type', '=', 'pdf')], 'readonly': [('is_locked', '=', True)]}"/>
+ <field name="bukti_uang_masuk_pdf" widget="pdf_viewer"
+ attrs="{'invisible': [('uang_masuk_type', '=', 'image')], 'readonly': [('is_locked', '=', True)]}"/>
+ </group>
+ </group>
+
+ <notebook>
+ <page string="Produk Line">
+ <field name="line_ids" attrs="{'readonly': [('is_locked', '=', True)]}">
+ <tree editable="bottom">
+ <field name="product_id"/>
+ <field name="quantity"/>
+ <field name="reason"/>
+ </tree>
+ </field>
+ </page>
+
+ <page string="Other Info">
+ <group col="2">
+ <group>
+ <field name="user_ids" widget="many2many_tags" readonly="1"/>
+ <field name="created_date" readonly="1"/>
+ <field name="refund_date" attrs="{'readonly': [('status', 'not in', ['pengajuan3','refund'])]}"/>
+ </group>
+ <group>
+ <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)]}"/>
+ </group>
+ </group>
+ </page>
+
+ <page string="Finance Note">
+ <group col="2">
+ <group>
+ <field name="finance_note" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ </group>
+ <group>
+ <field name="bukti_refund_type" reqiured="1" attrs="{'readonly': [('is_locked', '=', True)]}"/>
+ <field name="bukti_transfer_refund_pdf" widget="pdf_viewer" attrs="{'invisible': [('bukti_refund_type', '=', 'image')]}"/>
+ <field name="bukti_transfer_refund_image" widget="image" attrs="{'invisible': [('bukti_refund_type', '=', 'pdf')]}"/>
+ </group>
+ </group>
+ </page>
+
+ <page string="Cancel Reason" attrs="{'invisible': [('status', '=', 'refund')]}">
+ <group>
+ <field name="reason_reject"/>
+ </group>
+ </page>
+ </notebook>
+ </sheet>
+ <div class="oe_chatter">
+ <field name="message_follower_ids" widget="mail_followers"/>
+ <field name="message_ids" widget="mail_thread"/>
+ </div>
+ </form>
+ </field>
+ </record>
+ <!-- Action -->
+ <record id="action_refund_sale_order" model="ir.actions.act_window">
+ <field name="name">Refund Sales Order</field>
+ <field name="res_model">refund.sale.order</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <!-- Menu -->
+ <menuitem id="menu_refund_sale_order"
+ name="Refund"
+ parent="sale.sale_order_menu"
+ sequence="10"
+ action="action_refund_sale_order"/>
+</odoo>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 2a159307..bb8bdc08 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -35,7 +35,32 @@
string="UangMuka"
type="action" attrs="{'invisible': [('approval_status', '!=', 'approved')]}"/>
</button>
- <field name="payment_term_id" position="after">
+ <xpath expr="//header" position="inside">
+ <button name="button_refund"
+ type="object"
+ string="Refund"
+ class="btn-primary"
+ attrs="{'invisible': ['|', ('state', 'not in', ['sale', 'done']), ('has_refund', '=', True)]}" />
+ </xpath>
+ <div class="oe_button_box" name="button_box">
+ <button name="action_open_advance_payment_move"
+ type="object"
+ class="oe_stat_button"
+ icon="fa-book"
+ width="250px"
+ attrs="{'invisible': [('advance_payment_move_id','=',False)]}">
+ <field name="advance_payment_move_id" string="Journal Uang Muka" widget="statinfo"/>
+ </button>
+
+ <button type="object"
+ name="action_view_related_refunds"
+ class="oe_stat_button"
+ icon="fa-refresh"
+ attrs="{'invisible': [('refund_count', '=', 0)]}">
+ <field name="refund_count" widget="statinfo" string="Refund"/>
+ </button>
+ </div>
+ <field name="payment_term_id" position="after">
<field name="create_uid" invisible="1"/>
<field name="create_date" invisible="1"/>
<field name="shipping_cost_covered"
@@ -151,6 +176,7 @@
<field name="expected_ready_to_ship"/>
<field name="eta_date_start"/>
<field name="eta_date" readonly="1"/>
+ <field name="has_refund" readonly="1"/>
</group>
</xpath>
<xpath expr="//form/sheet/notebook/page/field[@name='order_line']"
@@ -633,6 +659,16 @@
</data>
<data>
+ <record id="sale_order_multi_create_refund_ir_actions_server" model="ir.actions.server">
+ <field name="name">Refund</field>
+ <field name="model_id" ref="sale.model_sale_order"/>
+ <field name="binding_model_id" ref="sale.model_sale_order"/>
+ <field name="state">code</field>
+ <field name="code">action = records.open_form_multi_create_refund()</field>
+ </record>
+ </data>
+
+ <data>
<record id="mail_template_sale_order_notification_to_salesperson" model="mail.template">
<field name="name">Sale Order: Notification to Salesperson</field>
<field name="model_id" ref="sale.model_sale_order"/>