diff options
| author | Indoteknik . <it@fixcomart.co.id> | 2025-07-23 17:21:46 +0700 |
|---|---|---|
| committer | Indoteknik . <it@fixcomart.co.id> | 2025-07-23 17:21:46 +0700 |
| commit | 66d86b6f5b35274b66fe57bcee5864c64f564b97 (patch) | |
| tree | 8cb90e26ba8efc82553efebd2098319f57dd2283 | |
| parent | d02c3d5d0522e6ec5a43d1380c078f0dd5fd1275 (diff) | |
| parent | a5da6a49dda2d756f907f072a00fb50672893682 (diff) | |
(andri) fix conflict
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 1 | ||||
| -rwxr-xr-x | indoteknik_custom/models/__init__.py | 1 | ||||
| -rw-r--r-- | indoteknik_custom/models/account_move.py | 241 | ||||
| -rwxr-xr-x | indoteknik_custom/models/purchase_order.py | 2 | ||||
| -rw-r--r-- | indoteknik_custom/models/purchasing_job.py | 55 | ||||
| -rw-r--r-- | indoteknik_custom/models/refund_sale_order.py | 656 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 123 | ||||
| -rw-r--r-- | indoteknik_custom/models/sale_order_line.py | 4 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 5 | ||||
| -rw-r--r-- | indoteknik_custom/views/account_move.xml | 4 | ||||
| -rw-r--r-- | indoteknik_custom/views/ir_sequence.xml | 9 | ||||
| -rw-r--r-- | indoteknik_custom/views/purchasing_job.xml | 10 | ||||
| -rw-r--r-- | indoteknik_custom/views/refund_sale_order.xml | 199 | ||||
| -rwxr-xr-x | indoteknik_custom/views/sale_order.xml | 38 |
14 files changed, 1243 insertions, 105 deletions
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py index 29039f8b..f609acbf 100755 --- a/indoteknik_custom/__manifest__.py +++ b/indoteknik_custom/__manifest__.py @@ -172,6 +172,7 @@ 'views/sale_order_delay.xml', 'views/down_payment.xml', 'views/down_payment_realization.xml', + 'views/refund_sale_order.xml', ], 'demo': [], 'css': [], diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 1c54efc6..d855b64d 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -152,5 +152,6 @@ 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 from . import down_payment diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 72ac5452..1a6fad1c 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 @@ -74,103 +75,136 @@ 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 = [] + # for move in self: + # if move.move_type == 'entry': + # # Jika masih draft, tampilkan 'Draft CAB' + # if move.state == 'draft': + # label = 'Draft CAB' + # else: + # label = move.name + # result.append((move.id, label)) + # else: + # # Untuk invoice dan lainnya, pakai default + # result.append((move.id, move.display_name)) + # return result - def send_due_invoice_reminder(self): - today = fields.Date.today() - target_dates = [ - today - timedelta(days=7), - today - timedelta(days=3), - today, - today + timedelta(days=3), - today + timedelta(days=7), - ] - - partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1) - if not partner: - _logger.info("Partner tidak ditemukan.") - return - - invoices = self.env['account.move'].search([ - ('move_type', '=', 'out_invoice'), - ('state', '=', 'posted'), - ('payment_state', 'not in', ['paid','in_payment', 'reversed']), - ('invoice_date_due', 'in', target_dates), - ('partner_id', '=', partner.id), - ]) - - _logger.info(f"Invoices tahap 1: {invoices}") - - invoices = invoices.filtered( - lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower() - ) - _logger.info(f"Invoices tahap 2: {invoices}") - - if not invoices: - _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}") - return - - grouped = {} - for inv in invoices: - grouped.setdefault(inv.partner_id, []).append(inv) - - template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') - - for partner, invs in grouped.items(): - if not partner.email: - _logger.info(f"Partner {partner.name} tidak memiliki email") - continue - - invoice_table_rows = "" - for inv in invs: - days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0 - invoice_table_rows += f""" - <tr> - <td>{inv.name}</td> - <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td> - <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td> - <td>{days_to_due}</td> - <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td> - <td>{inv.ref or '-'}</td> - </tr> - """ - - subject = f"Reminder Invoice Due - {partner.name}" - body_html = re.sub( - r"<tbody[^>]*>.*?</tbody>", - f"<tbody>{invoice_table_rows}</tbody>", - template.body_html, - flags=re.DOTALL - ).replace('${object.name}', partner.name) \ - .replace('${object.partner_id.name}', partner.name) - # .replace('${object.email}', partner.email or '') - - values = { - 'subject': subject, - 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi - 'email_from': 'finance@indoteknik.co.id', - 'body_html': body_html, - 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id', - } - - _logger.info(f"VALUES: {values}") - - template.send_mail(invs[0].id, force_send=True, email_values=values) - - # Default System User - user_system = self.env['res.users'].browse(25) - system_id = user_system.partner_id.id if user_system else False - _logger.info(f"System User: {user_system.name} ({user_system.id})") - _logger.info(f"System User ID: {system_id}") - - for inv in invs: - inv.message_post( - subject=subject, - body=body_html, - subtype_id=self.env.ref('mail.mt_note').id, - author_id=system_id, - ) - - _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") + # def send_due_invoice_reminder(self): + # today = fields.Date.today() + # target_dates = [ + # today - timedelta(days=7), + # today - timedelta(days=3), + # today, + # today + timedelta(days=3), + # today + timedelta(days=7), + # ] + + # partner = self.env['res.partner'].search([('name', 'ilike', 'BANGUNAN TEKNIK GRUP')], limit=1) + # if not partner: + # _logger.info("Partner tidak ditemukan.") + # return + + # invoices = self.env['account.move'].search([ + # ('move_type', '=', 'out_invoice'), + # ('state', '=', 'posted'), + # ('payment_state', 'not in', ['paid','in_payment', 'reversed']), + # ('invoice_date_due', 'in', target_dates), + # ('partner_id', '=', partner.id), + # ]) + + # _logger.info(f"Invoices tahap 1: {invoices}") + + # invoices = invoices.filtered( + # lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower() + # ) + # _logger.info(f"Invoices tahap 2: {invoices}") + + # if not invoices: + # _logger.info(f"Tidak ada invoice yang due untuk partner: {partner.name}") + # return + + # grouped = {} + # for inv in invoices: + # grouped.setdefault(inv.partner_id, []).append(inv) + + # template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder') + + # for partner, invs in grouped.items(): + # if not partner.email: + # _logger.info(f"Partner {partner.name} tidak memiliki email") + # continue + + # invoice_table_rows = "" + # for inv in invs: + # days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0 + # invoice_table_rows += f""" + # <tr> + # <td>{inv.name}</td> + # <td>{fields.Date.to_string(inv.invoice_date) or '-'}</td> + # <td>{fields.Date.to_string(inv.invoice_date_due) or '-'}</td> + # <td>{days_to_due}</td> + # <td>{formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)}</td> + # <td>{inv.ref or '-'}</td> + # </tr> + # """ + + # subject = f"Reminder Invoice Due - {partner.name}" + # body_html = re.sub( + # r"<tbody[^>]*>.*?</tbody>", + # f"<tbody>{invoice_table_rows}</tbody>", + # template.body_html, + # flags=re.DOTALL + # ).replace('${object.name}', partner.name) \ + # .replace('${object.partner_id.name}', partner.name) + # # .replace('${object.email}', partner.email or '') + + # values = { + # 'subject': subject, + # 'email_to': 'andrifebriyadiputra@gmail.com', # Ubah ke partner.email untuk produksi + # 'email_from': 'finance@indoteknik.co.id', + # 'body_html': body_html, + # 'reply_to': f'invoice+account.move_{invs[0].id}@indoteknik.co.id', + # } + + # _logger.info(f"VALUES: {values}") + + # template.send_mail(invs[0].id, force_send=True, email_values=values) + + # # Default System User + # user_system = self.env['res.users'].browse(25) + # system_id = user_system.partner_id.id if user_system else False + # _logger.info(f"System User: {user_system.name} ({user_system.id})") + # _logger.info(f"System User ID: {system_id}") + + # for inv in invs: + # inv.message_post( + # subject=subject, + # body=body_html, + # subtype_id=self.env.ref('mail.mt_note').id, + # author_id=system_id, + # ) + + # _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice") @api.onchange('invoice_date') @@ -183,6 +217,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/purchase_order.py b/indoteknik_custom/models/purchase_order.py index f98a37be..5b9e1acb 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -970,7 +970,7 @@ class PurchaseOrder(models.Model): # ) if not self.from_apo: - if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader: + if (not self.matches_so or not self.sale_order_id) and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not self.manufacturing_id: raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager") send_email = False diff --git a/indoteknik_custom/models/purchasing_job.py b/indoteknik_custom/models/purchasing_job.py index 58f1c067..db733b5a 100644 --- a/indoteknik_custom/models/purchasing_job.py +++ b/indoteknik_custom/models/purchasing_job.py @@ -26,7 +26,46 @@ class PurchasingJob(models.Model): purchase_representative_id = fields.Many2one('res.users', string="Purchase Representative", readonly=True) note = fields.Char(string="Note Detail") date_po = fields.Datetime(string='Date PO', copy=False) - so_number = fields.Char(string='SO Number', copy=False) + so_number = fields.Text(string='SO Number', copy=False) + check_pj = fields.Boolean(compute='_get_check_pj', string='Linked') + + def action_open_job_detail(self): + self.ensure_one() + Seen = self.env['purchasing.job.seen'] + seen = Seen.search([ + ('user_id', '=', self.env.uid), + ('product_id', '=', self.product_id.id) + ], limit=1) + + if seen: + seen.so_snapshot = self.so_number + seen.seen_date = fields.Datetime.now() + else: + Seen.create({ + 'user_id': self.env.uid, + 'product_id': self.product_id.id, + 'so_snapshot': self.so_number, + }) + + return { + 'name': 'Purchasing Job Detail', + 'type': 'ir.actions.act_window', + 'res_model': 'v.purchasing.job', + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'current', + } + + + @api.depends('so_number') + def _get_check_pj(self): + for rec in self: + seen = self.env['purchasing.job.seen'].search([ + ('user_id', '=', self.env.uid), + ('product_id', '=', rec.product_id.id) + ], limit=1) + rec.check_pj = bool(seen and seen.so_snapshot == rec.so_number) + def unlink(self): # Example: Delete related records from the underlying model @@ -199,3 +238,17 @@ class OutstandingSales(models.Model): and sp.name like '%OUT%' ) """) + +class PurchasingJobSeen(models.Model): + _name = 'purchasing.job.seen' + _description = 'User Seen SO Snapshot' + _rec_name = 'product_id' + + user_id = fields.Many2one('res.users', required=True, ondelete='cascade') + product_id = fields.Many2one('product.product', required=True, ondelete='cascade') + so_snapshot = fields.Text("Last Seen SO") + seen_date = fields.Datetime(default=fields.Datetime.now) + + _sql_constraints = [ + ('user_product_unique', 'unique(user_id, product_id)', 'User already tracked this product.') + ] diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py new file mode 100644 index 00000000..559ca07a --- /dev/null +++ b/indoteknik_custom/models/refund_sale_order.py @@ -0,0 +1,656 @@ +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) + + refund_type_display = fields.Char(string="Refund Type Label", compute="_compute_refund_type_display") + + line_ids = fields.One2many('refund.sale.order.line', 'refund_id', string='Refund Lines') + invoice_line_ids = fields.One2many( + comodel_name='account.move.line', + inverse_name='move_id', + string='Invoice Lines', + compute='_compute_invoice_lines' + ) + + approved_by = fields.Text(string='Approved By', readonly=True) + date_approved_sales = fields.Datetime(string='Date Approved (Sales Manager)', readonly=True) + date_approved_ar = fields.Datetime(string='Date Approved (AR)', readonly=True) + date_approved_pimpinan = fields.Datetime(string='Date Approved (Pimpinan)', readonly=True) + position_sales = fields.Char(string='Position Sales', readonly=True) + position_ar = fields.Char(string='Position AR', readonly=True) + position_pimpinan = fields.Char(string='Position Pimpinan', readonly=True) + + partner_id = fields.Many2one( + 'res.partner', + string='Customer', + required=True + ) + advance_move_names = fields.Html(string="Group Journal 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.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, '') + + + @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 e197a6af..995cafba 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): @@ -3081,4 +3089,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/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 2a0160e8..5e9fc362 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -173,8 +173,8 @@ class SaleOrderLine(models.Model): # minus with delivery if covered by indoteknik if line.order_id.shipping_cost_covered == 'indoteknik': sales_price -= line.delivery_amt_line - if line.order_id.fee_third_party > 0: - sales_price -= line.fee_third_party_line + # if line.order_id.fee_third_party > 0: + # sales_price -= line.fee_third_party_line purchase_price = line.purchase_price if line.purchase_tax_id.price_include: diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv index 2fd497b9..3c958bda 100755 --- a/indoteknik_custom/security/ir.model.access.csv +++ b/indoteknik_custom/security/ir.model.access.csv @@ -188,4 +188,7 @@ access_realization_down_payment,access.realization.down.payment,model_realizatio access_realization_down_payment_line,access.realization.down.payment.line,model_realization_down_payment_line,,1,1,1,1 access_realization_down_payment_use_line,access.realization.down.payment.use.line,model_realization_down_payment_use_line,,1,1,1,1 access_down_payment_ap_only,access.down.payment.ap.only,model_down_payment_ap_only,,1,1,1,1 -access_reject_reason_downpayment,access.reject.reason.downpayment,model_reject_reason_downpayment,,1,1,1,1
\ No newline at end of file +access_reject_reason_downpayment,access.reject.reason.downpayment,model_reject_reason_downpayment,,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 +access_purchasing_job_seen,purchasing.job.seen,model_purchasing_job_seen,,1,1,1,1 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 e959f562..453a73e8 100644 --- a/indoteknik_custom/views/ir_sequence.xml +++ b/indoteknik_custom/views/ir_sequence.xml @@ -210,5 +210,14 @@ <field name="number_increment">1</field> <field name="active">True</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> </data> </odoo>
\ No newline at end of file diff --git a/indoteknik_custom/views/purchasing_job.xml b/indoteknik_custom/views/purchasing_job.xml index 976f1485..e3866d84 100644 --- a/indoteknik_custom/views/purchasing_job.xml +++ b/indoteknik_custom/views/purchasing_job.xml @@ -4,7 +4,7 @@ <field name="name">v.purchasing.job.tree</field> <field name="model">v.purchasing.job</field> <field name="arch" type="xml"> - <tree create="false" multi_edit="1"> + <tree decoration-info="(check_pj == False)" create="false" multi_edit="1"> <field name="product_id"/> <field name="vendor_id"/> <field name="purchase_representative_id"/> @@ -19,6 +19,14 @@ <field name="note"/> <field name="date_po"/> <field name="so_number"/> + <field name="check_pj" invisible="1"/> + <button name="action_open_job_detail" + string="📄" + type="object" + icon="fa-file" + attrs="{'invisible': [('check_pj','=',True)]}" + context="{}"/> + </tree> </field> </record> 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"/> |
