from odoo import models, api, fields, _ from odoo.exceptions import UserError, ValidationError from datetime import date, datetime, timedelta # import datetime import logging _logger = logging.getLogger(__name__) from terbilang import Terbilang import pytz from pytz import timezone import base64 class AdvancePaymentRequest(models.Model): _name = 'advance.payment.request' _description = 'Advance Payment Request & Reimburse' _rec_name = 'number' _inherit = ['mail.thread', 'mail.activity.mixin'] user_id = fields.Many2one('res.users', string='Diajukan Oleh', default=lambda self: self.env.user, tracking=3) partner_id = fields.Many2one('res.partner', string='Partner', related='user_id.partner_id', readonly=True) number = fields.Char(string='No. Dokumen', default='New Draft', tracking=3) applicant_name = fields.Many2one('res.users', string='Nama Pemohon', required=True, tracking=3, domain="[('groups_id', 'in', [1])]") # applicant_name = fields.One2many(string='Nama Pemohon', related='res.users') nominal = fields.Float(string='Nominal', tracking=3, required=True) bank_name = fields.Char(string='Bank', tracking=3, required=True) account_name = fields.Many2one('res.users', string='Nama Account', required=True, tracking=3, domain="[('groups_id', 'in', [1])]") # account_name = fields.Char(string='Nama Account', tracking=3, required=True) bank_account = fields.Char(string='No. Rekening', tracking=3, required=True) detail_note = fields.Text(string='Keterangan Penggunaan Rinci', tracking=3) date_back_to_office = fields.Date( string='Tanggal Kembali ke Kantor', tracking=3, required=True ) estimated_return_date = fields.Date( string='Batas Pengajuan', help='Tanggal batas maksimal pengajuan realisasi setelah kembali ke kantor. ' '7 hari setelah tanggal kembali.' ) days_remaining = fields.Integer( string='Sisa Hari Pengajuan', compute='_compute_days_remaining', help='Sisa hari batas maksimal pengajuan realisasi setelah kembali ke kantor. ' '7 hari setelah tanggal kembali.' ) status = fields.Selection([ ('draft', 'Draft'), ('pengajuan1', 'Menunggu Approval Departement'), ('pengajuan2', 'Menunggu Pengecekan AP'), ('pengajuan3', 'Menunggu Approval Pimpinan'), ('approved', 'Approved'), ('reject', 'Rejected') ], string='Status', default='draft', tracking=3, index=True, track_visibility='onchange') last_status = fields.Selection([ ('draft', 'Draft'), ('pengajuan1', 'Menunggu Approval Departement'), ('pengajuan2', 'Menunggu Pengecekan AP'), ('pengajuan3', 'Menunggu Approval Pimpinan'), ('approved', 'Approved'), ('reject', 'Rejected') ], string='Status') status_pay_down_payment = fields.Selection([ ('pending', 'Pending'), ('payment', 'Payment'), ], string='Status Pembayaran', default='pending', tracking=3) name_approval_departement = fields.Char(string='Approval Departement', tracking=True) name_approval_ap = fields.Char(string='Approval AP', tracking=True) email_ap = fields.Char(string = 'Email AP') name_approval_pimpinan = fields.Char(string='Approval Pimpinan', tracking=True) date_approved_department = fields.Datetime(string="Date Approved Department") date_approved_ap = fields.Datetime(string="Date Approved AP") date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan") position_department = fields.Char(string='Position Departement', tracking=True) position_ap = fields.Char(string='Position AP', tracking=True) position_pimpinan = fields.Char(string='Position Pimpinan', tracking=True) approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always') departement_type = fields.Selection([ ('sales', 'Sales'), ('merchandiser', 'Merchandiser'), ('marketing', 'Marketing'), ('logistic', 'Logistic'), ('procurement', 'Procurement'), ('fat_it', 'FAT & IT'), ('hr_ga', 'HR & GA'), ], string='Departement Type', tracking=3, required=True) attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') attachment_filename_image = fields.Char(string='Filename Image') attachment_filename_pdf = fields.Char(string='Filename PDF') attachment_type = fields.Selection([ ('pdf', 'PDF'), ('image', 'Image'), ], string="Attachment Type") move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')]) is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible') reason_reject = fields.Text(string='Alasan Penolakan') currency_id = fields.Many2one( 'res.currency', string='Currency', default=lambda self: self.env.company.currency_id ) type_request = fields.Selection([ ('pum', 'PUM'), ('reimburse', 'Reimburse')], string='Tipe Pengajuan', default='pum', tracking=3) position_type = fields.Selection([ ('staff', 'Staff'), ('manager', 'Manager'), ('pimpinan', 'Pimpinan')], string='Jabatan') settlement_type = fields.Selection([ ('no_settlement', 'Belum Realisasi'), ('settlement', 'Realisasi') ]) apr_perjalanan = fields.Boolean(string = "PUM Perjalanan?", default = False) @api.onchange('nominal') def _onchange_nominal_no_minus(self): if self.nominal and self.nominal < 0: self.nominal = 0 return { 'warning': { 'title': _('Nominal Tidak Valid'), 'message': _( "Nominal tidak boleh diisi minus.\n" "Nilai di set menjadi nol." ) } } def _get_jasper_attachment(self): self.ensure_one() report = self.env['ir.actions.report'].browse(1134) # ID Downpayment Report if not report: raise UserError("Report Jasper tidak ditemukan.") data = report.render_jasper(self.ids, data={})[0] filename = f"{self.number}.pdf" return { 'name': filename, 'datas': base64.b64encode(data), 'type': 'binary', 'mimetype': 'application/pdf', 'filename': filename, } def action_send_pum_reminder(self): """ Kirim email reminder PUM otomatis. - Hari ini = kirim dengan template 'mail_template_pum_reminder_today' - H-2 dari due date = kirim dengan template 'mail_template_pum_reminder_h_2' """ today = date.today() pum_ids = self.search([ ('date_back_to_office', '!=', False), ('status', 'not in', ['draft', 'reject']), ]) template_today = self.env.ref('indoteknik_custom.mail_template_pum_reminder_today', raise_if_not_found=False) template_h2 = self.env.ref('indoteknik_custom.mail_template_pum_reminder_h_2', raise_if_not_found=False) if not template_today or not template_h2: _logger.warning("Salah satu template email tidak ditemukan.") return for pum in pum_ids: _logger.info(f"[REMINDER] Memproses PUM {pum.number}") if not pum.email_ap or not pum.user_id.partner_id.email: _logger.warning(f"[REMINDER] Lewati PUM {pum.number} karena email_ap atau email user kosong.") continue due_date = pum.date_back_to_office + timedelta(days=7) days_remaining = (due_date - today).days realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1) if not realization or realization.done_status != 'remaining': _logger.info(f"[REMINDER] Lewati PUM {pum.number}, status realisasi bukan 'remaining'.") continue # Tentukan template if pum.date_back_to_office == today: template = template_today elif days_remaining == 2: template = template_h2 else: _logger.info(f"[REMINDER] Lewati PUM {pum.number}, hari ini bukan tanggal pengingat.") continue # Generate attachment try: attachment_vals = pum._get_jasper_attachment() attachment = self.env['ir.attachment'].create({ 'name': attachment_vals['name'], 'type': 'binary', 'datas': attachment_vals['datas'], 'res_model': 'advance.payment.request', 'res_id': pum.id, 'mimetype': 'application/pdf', }) except Exception as e: _logger.error(f"[REMINDER] Gagal membuat attachment untuk PUM {pum.number}: {str(e)}") continue email_values = { # 'email_to': pum.user_id.partner_id.email, 'email_to': 'andrifebriyadiputra@gmail.com', 'email_from': pum.email_ap, 'attachment_ids': [(6, 0, [attachment.id])], } _logger.info(f"[REMINDER] Mengirim email PUM {pum.number} ke {email_values['email_to']} dari {email_values['email_from']}") try: body_html = template._render_field('body_html', [pum.id])[pum.id] template.send_mail(pum.id, force_send=True, email_values=email_values) _logger.info(f"[REMINDER] Email berhasil dikirim untuk PUM {pum.number}") # Post info sederhana pum.message_post( body="Email Reminder Berhasil dikirimkan", message_type="comment", subtype_xmlid="mail.mt_note", ) user_system = self.env['res.users'].browse(25) system_id = user_system.partner_id.id if user_system else False # Post isi email ke chatter pum.message_post( body=body_html, message_type="comment", subtype_xmlid="mail.mt_note", author_id=system_id, ) except Exception as e: _logger.error(f"[REMINDER] Gagal mengirim email untuk PUM {pum.number}: {str(e)}") return True @api.depends('move_id.state') def _compute_is_cab_visible(self): for rec in self: move = rec.move_id rec.is_cab_visible = bool(move and move.state == 'posted') def action_view_journal_uangmuka(self): self.ensure_one() ap_user_ids = [23, 9468] if self.env.user.id not in ap_user_ids: raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') if not self.move_id: raise UserError("Journal Uang Muka belum tersedia.") return { 'name': 'Journal Entry', 'view_mode': 'form', 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'res_id': self.move_id.id, 'target': 'current', } @api.onchange('attachment_type') def _onchange_attachment_type(self): self.attachment_file_image = False self.attachment_filename_image = False self.attachment_file_pdf = False self.attachment_filename_pdf = False # Sales & MD : Darren ID 19 # Marketing : Iwan ID 216 # Logistic & Procurement : Rafly H ID 21 # FAT : Stephan ID 28 # HR & GA : Akbar ID 7 / Pimpinan # --------------------------------------- # AP : Manzila (Finance) ID 23 def _get_approver_mapping(self): return { 'sales': 19, 'merchandiser': 19, 'marketing': 216, 'logistic': 21, 'procurement': 21, 'fat': 28, 'hr_ga': 7, } def _get_departement_approver(self): mapping = self._get_approver_mapping() return mapping.get(self.departement_type) @api.model def default_get(self, fields_list): defaults = super(AdvancePaymentRequest, self).default_get(fields_list) user_id = defaults.get('user_id', self.env.uid) mapping = self._get_approver_mapping() manager_ids = set(mapping.values()) pimpinan_id = 7 position = 'staff' if user_id == pimpinan_id: position = 'pimpinan' elif user_id in manager_ids: position = 'manager' defaults['position_type'] = position return defaults def action_realisasi_pum(self): self.ensure_one() realization = self.env['advance.payment.settlement'].search([('pum_id', '=', self.id)], limit=1) if realization: return { 'type': 'ir.actions.act_window', 'name': 'Realisasi PUM', 'res_model': 'advance.payment.settlement', 'view_mode': 'form', 'target': 'current', 'res_id': realization.id, } else: return { 'type': 'ir.actions.act_window', 'name': 'Realisasi PUM', 'res_model': 'advance.payment.settlement', 'view_mode': 'form', 'target': 'current', 'context': { 'default_pum_id': self.id, 'default_value_down_payment': self.nominal, 'default_name': f'Realisasi - {self.number or ""}', 'default_pemberian_line_ids': [ (0, 0, { 'date': self.create_date.date() if self.create_date else fields.Date.today(), 'description': 'Uang Muka', 'value': self.nominal }) ] } } def action_confirm_payment(self): ap_user_ids = [23, 9468] if self.env.user.id not in ap_user_ids: raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') for rec in self: if not rec.attachment_file_image and not rec.attachment_file_pdf: raise UserError( f'Tidak bisa konfirmasi pembayaran PUM {rec.name or ""} ' f'karena belum ada bukti attachment (PDF/Image).' ) rec.status_pay_down_payment = 'payment' rec.message_post( body="Status pembayaran telah dikonfirmasi oleh AP.", message_type="comment", subtype_xmlid="mail.mt_note", ) # def action_approval_check(self): # for record in self: # # user = record.user_id # user = self.env['res.users'].browse(3401) # roles = sorted(set( # f"{group # .name} (Category: {group.category_id.name})" # for group in user.groups_id # if group.category_id.name == 'Roles' # )) # _logger.info(f"[ROLE CHECK] User: {user.name} (Login: {user.login}) Roles: {roles}") # return def action_approval_check(self): jakarta_tz = pytz.timezone('Asia/Jakarta') now = datetime.now(jakarta_tz).replace(tzinfo=None) formatted_date = now.strftime('%d %B %Y %H:%M') for rec in self: if not rec.departement_type: raise UserError("Field 'departement_type' wajib diisi sebelum approval.") approver_id = rec._get_departement_approver() if rec.status == 'pengajuan1': if self.env.user.id != approver_id: raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") rec.name_approval_departement = self.env.user.name rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement rec.date_approved_department = now # Mapping posisi berdasarkan departement_type department_titles = { 'sales': 'Sales Manager', 'merchandiser': 'Merchandiser Manager', 'marketing': 'Marketing Manager', 'logistic': 'Logistic Manager', 'procurement': 'Procurement Manager', 'fat': 'Finance & Accounting Manager', 'hr_ga': 'HR & GA Manager', } rec.position_department = department_titles.get(rec.departement_type, 'Departement Manager') rec.status = 'pengajuan2' rec.message_post( body=f"Approval Departement oleh {self.env.user.name} " f"pada {formatted_date}." ) elif rec.status == 'pengajuan2': ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP if self.env.user.id not in ap_user_ids: raise UserError("Hanya AP yang berhak menyetujui tahap ini.") rec.name_approval_ap = self.env.user.name rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap rec.email_ap = self.env.user.email rec.date_approved_ap = now rec.position_ap = 'Finance AP' rec.status = 'pengajuan3' rec.message_post( body=f"Approval AP oleh {self.env.user.name} " f"pada {formatted_date}." ) elif rec.status == 'pengajuan3': if self.env.user.id != 7: # ID user Pimpinan raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") rec.name_approval_pimpinan = self.env.user.name rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan rec.date_approved_pimpinan = now rec.position_pimpinan = 'Pimpinan' rec.status = 'approved' rec.message_post( body=f"Approval Pimpinan oleh {self.env.user.name} " f"pada {formatted_date}." ) else: raise UserError("Status saat ini tidak bisa di-approve lagi.") # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap {rec.status}.") def action_reject(self): return { 'type': 'ir.actions.act_window', 'name': 'Alasan Penolakan', 'res_model': 'advance.payment.request.reject', 'view_mode': 'form', 'target': 'new', 'context': {'default_request_id': self.id}, } def action_draft(self): for record in self: # Pastikan hanya yang statusnya 'reject' yang bisa di-reset if record.status != 'reject': raise UserError("Hanya data dengan status 'Reject' yang bisa dikembalikan ke Draft atau status sebelumnya.") # Jika ada last_status, gunakan itu; jika tidak, fallback ke 'draft' new_status = 'pengajuan1' # Reset field-field approval & alasan reject record.write({ 'status': new_status, 'reason_reject': False, 'last_status': False, 'name_approval_departement': False, 'name_approval_ap': False, 'name_approval_pimpinan': False, 'date_approved_department': False, 'date_approved_ap': False, 'date_approved_pimpinan': False, 'position_department': False, 'position_ap': False, 'position_pimpinan': False, }) record.message_post(body=f"Status dikembalikan ke {new_status.capitalize()} oleh {self.env.user.name}.") def action_ap_only(self): self.ensure_one() ap_user_ids = [23, 9468] # Ganti sesuai kebutuhan if self.env.user.id not in ap_user_ids: raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') if self.move_id: raise UserError('CAB / Jurnal sudah pernah dibuat untuk PUM ini.') return { 'name': 'Create CAB AP Only', 'type': 'ir.actions.act_window', 'res_model': 'advance.payment.create.bill', 'view_mode': 'form', 'target': 'new', 'context': { 'default_nominal': self.nominal, 'default_down_payment_id': self.id, } } @api.depends('date_back_to_office', 'status') def _compute_days_remaining(self): today = date.today() for rec in self: if rec.status in ['approved', 'reject'] and rec.days_remaining: continue if rec.date_back_to_office: due_date = rec.date_back_to_office + timedelta(days=7) rec.estimated_return_date = due_date # Jika hari ini sebelum tanggal kembali, maka anggap belum mulai dihitung effective_today = max(today, rec.date_back_to_office) rec.days_remaining = (due_date - effective_today).days else: rec.estimated_return_date = False rec.days_remaining = 0 @api.onchange('date_back_to_office') def _onchange_date_back_to_office(self): if self.date_back_to_office and self.date_back_to_office < date.today(): return { 'warning': { 'title': _('Tanggal Tidak Valid'), 'message': _('Tanggal kembali ke kantor tidak boleh lebih awal dari hari ini.') } } @api.onchange('applicant_name') def _onchange_applicant_name(self): if self.applicant_name: self.account_name = self.applicant_name.id @api.onchange('account_name') def _onchange_account_name(self): if self.account_name: self.applicant_name = self.account_name.id @api.onchange('user_id') def _onchange_user_id_limit_check(self): if not self.user_id: return pum_ids = self.search([ ('user_id', '=', self.user_id.id), ('status', '!=', 'reject') ]) active_pum_count = 0 for pum in pum_ids: realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1) if not realization or realization.done_status != 'done_not_realized': active_pum_count += 1 if active_pum_count >= 2: return { 'warning': { 'title': 'Batas Pengajuan Tercapai', 'message': 'User ini sudah memiliki 2 PUM aktif. Tidak dapat mengajukan lagi sampai salah satu direalisasi.', } } @api.model def create(self, vals): user = self.env.user pum_ids = self.search([ ('user_id', '=', user.id), ('status', '!=', 'reject') ]) active_pum_count = 0 for pum in pum_ids: realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1) if not realization or realization.done_status != 'done_not_realized': active_pum_count += 1 if active_pum_count >= 2: raise UserError("Anda hanya dapat mengajukan maksimal 2 PUM aktif. Silakan realisasikan salah satunya terlebih dahulu.") if not vals.get('number') or vals['number'] == 'New Draft': vals['number'] = self.env['ir.sequence'].next_by_code('advance.payment.request') or 'New Draft' vals['status'] = 'pengajuan1' return super(AdvancePaymentRequest, self).create(vals) class AdvancePaymentSettlementLine(models.Model): _name = 'advance.payment.settlement.line' _description = 'Advance Payment Settlement Line' realization_id = fields.Many2one('advance.payment.settlement', string='Realization') date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) description = fields.Char(string='Description', required=True) value = fields.Float(string='Nilai', required=True) class AdvancePaymentUsageLine(models.Model): _name = 'advance.payment.usage.line' _description = 'Advance Payment Usage Line' realization_id = fields.Many2one('advance.payment.settlement', string='Realization') date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) description = fields.Char(string='Description', required=True) nominal = fields.Float(string='Nominal', required=True) done_attachment = fields.Boolean(string='Checked', default=False) lot_of_attachment = fields.Selection( related='realization_id.lot_of_attachment', string='Lot of Attachment (Related)', store=False ) attachment_type = fields.Selection([ ('pdf', 'PDF'), ('image', 'Image'), ], string="Attachment Type", default='pdf') attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') attachment_filename_image = fields.Char(string='Filename Image') attachment_filename_pdf = fields.Char(string='Filename PDF') account_id = fields.Many2one( 'account.account', string='Jenis Biaya', required=True, domain="[('id', 'in', [484, 486, 488, 506, 507, 625, 471, 519, 527, 528, 529, 530, 565])]" # ID Jenis Biaya yang dibutuhkan ) @api.onchange('account_id') def _onchange_account_id(self): for rec in self: if rec.account_id: rec.description = rec.account_id.name + " - " @api.onchange('attachment_type') def _onchange_attachment_type(self): self.attachment_file_image = False self.attachment_filename_image = False self.attachment_file_pdf = False self.attachment_filename_pdf = False @api.onchange('done_attachment') def _onchange_done_attachment(self): ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP if self.done_attachment and self.env.user.id not in ap_user_ids: self.done_attachment = False return { 'warning': { 'title': _('Tidak Diizinkan'), 'message': _('Hanya user AP yang bisa mencentang Done Attachment.') } } @api.onchange('nominal') def _onchange_nominal_no_minus(self): if self.nominal and self.nominal < 0: self.nominal = 0 return { 'warning': { 'title': _('Nominal Tidak Valid'), 'message': _( "Nominal penggunaan PUM tidak boleh diisi minus.\n" "Nilai di Set menjadi nol." ) } } class AdvancePaymentSettlement(models.Model): _name = 'advance.payment.settlement' _description = 'Advance Payment Settlement' _inherit = ['mail.thread'] pum_id = fields.Many2one('advance.payment.request', string='No PUM') name = fields.Char(string='Nama', readonly=True, tracking=3) title = fields.Char(string='Judul', tracking=3) goals = fields.Text(string='Tujuan', tracking=3) related = fields.Char(string='Terkait', tracking=3) pemberian_line_ids = fields.One2many( 'advance.payment.settlement.line', 'realization_id', string='Rincian Pemberian' ) penggunaan_line_ids = fields.One2many( 'advance.payment.usage.line', 'realization_id', string='Rincian Penggunaan' ) grand_total = fields.Float(string='Grand Total Pemberian', tracking=3, compute='_compute_grand_total') grand_total_use = fields.Float(string='Grand Total Penggunaan', tracking=3, compute='_compute_grand_total_use') value_down_payment = fields.Float(string='PUM', tracking=3) remaining_value = fields.Float(string='Sisa Uang PUM', tracking=3, compute='_compute_remaining_value') note_approval = fields.Text(string='Note Persetujuan', tracking=3) name_approval_departement = fields.Char(string='Approval Departement', tracking=True) name_approval_ap = fields.Char(string='Approval AP', tracking=True) name_approval_pimpinan = fields.Char(string='Approval Pimpinan', tracking=True) date_approved_department = fields.Datetime(string="Date Approved Department") date_approved_ap = fields.Datetime(string="Date Approved AP") date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan") position_department = fields.Char(string='Position Departement', tracking=True) position_ap = fields.Char(string='Position AP', tracking=True) position_pimpinan = fields.Char(string='Position Pimpinan', tracking=True) approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always') status = fields.Selection([ ('pengajuan1', 'Menunggu Approval Departement'), ('pengajuan2', 'Menunggu Pengecekan AP'), ('pengajuan3', 'Menunggu Approval Pimpinan'), ('approved', 'Approved'), ], string='Status', default='pengajuan1', tracking=3, index=True, track_visibility='onchange') done_status = fields.Selection([ ('remaining', 'Remaining'), ('done_not_realized', 'Done Not Realized'), ('done_realized', 'Done Realized') ], string='Status Realisasi', tracking=3, default='remaining') date_done_not_realized = fields.Date(string='Tanggal Done Not Realized', tracking=3) currency_id = fields.Many2one( 'res.currency', string='Currency', default=lambda self: self.env.company.currency_id ) attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') attachment_filename_image = fields.Char(string='Filename Image') attachment_filename_pdf = fields.Char(string='Filename PDF') attachment_type = fields.Selection([ ('pdf', 'PDF'), ('image', 'Image'), ], string="Attachment Type", default='pdf') lot_of_attachment = fields.Selection([ ('one_for_all_line', '1 Attachment Untuk Semua Line Penggunaan PUM'), ('one_for_one_line', '1 Attachment per 1 Line Penggunaan PUM'), ], string = "Banyaknya Attachment", default='one_for_one_line') move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')]) is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible') def action_toggle_check_attachment(self): ap_user_ids = [23, 9468] if self.env.user.id not in ap_user_ids: raise UserError('Hanya User AP yang dapat menggunakan tombol ini.') for rec in self: if not rec.penggunaan_line_ids: continue if all(line.done_attachment for line in rec.penggunaan_line_ids): for line in rec.penggunaan_line_ids: line.done_attachment = False else: for line in rec.penggunaan_line_ids: line.done_attachment = True @api.onchange('lot_of_attachment') def _onchange_lot_of_attachment(self): if self.lot_of_attachment == 'one_for_all_line': for line in self.penggunaan_line_ids: line.attachment_file_pdf = False line.attachment_file_image = False line.attachment_filename_pdf = False line.attachment_filename_image = False @api.depends('move_id.state') def _compute_is_cab_visible(self): for rec in self: move = rec.move_id rec.is_cab_visible = bool(move and move.state == 'posted') def action_view_journal_uangmuka(self): self.ensure_one() ap_user_ids = [23, 9468] if self.env.user.id not in ap_user_ids: raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') if not self.move_id: raise UserError("Journal Uang Muka belum tersedia.") return { 'name': 'Journal Entry', 'view_mode': 'form', 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'res_id': self.move_id.id, 'target': 'current', } @api.onchange('attachment_type') def _onchange_attachment_type(self): self.attachment_file_image = False self.attachment_filename_image = False self.attachment_file_pdf = False self.attachment_filename_pdf = False @api.depends('pemberian_line_ids.value') def _compute_grand_total(self): for rec in self: rec.grand_total = sum(line.value for line in rec.pemberian_line_ids) @api.depends('penggunaan_line_ids.nominal') def _compute_grand_total_use(self): for rec in self: rec.grand_total_use = sum(line.nominal for line in rec.penggunaan_line_ids) @api.depends('grand_total', 'grand_total_use') def _compute_remaining_value(self): for rec in self: rec.remaining_value = rec.value_down_payment - rec.grand_total_use def action_validation(self): self.ensure_one() # Validasi hanya AP yang bisa validasi ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP if self.env.user.id not in ap_user_ids: raise UserError('Hanya AP yang dapat melakukan validasi realisasi.') if self.done_status == 'remaining': self.done_status = 'done_not_realized' self.date_done_not_realized = fields.Date.today() elif self.done_status == 'done_not_realized': self.done_status = 'done_realized' else: raise UserError('Realisasi sudah berstatus Done Realized.') # Opsional: Tambah log di chatter self.message_post(body=f"Status realisasi diperbarui menjadi {dict(self._fields['done_status'].selection).get(self.done_status)} oleh {self.env.user.name}.") def action_cab(self): self.ensure_one() ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP if self.env.user.id not in ap_user_ids: raise UserError('Hanya User AP yang dapat menggunakan ini.') if self.move_id: raise UserError("CAB / Jurnal sudah pernah dibuat untuk Realisasi ini.") if not self.pum_id or not self.pum_id.move_id: raise UserError("PUM terkait atau CAB belum tersedia.") partner_id = self.pum_id.user_id.partner_id.id cab_move = self.pum_id.move_id # Account Bank Intransit dari CAB: bank_intransit_line = cab_move.line_ids.filtered(lambda l: l.account_id.id in [573, 389, 392]) if not bank_intransit_line: raise UserError("Account Bank Intransit dengan tidak ditemukan di CAB terkait.") account_sisa_pum = bank_intransit_line[0].account_id.id # Account Uang Muka Operasional account_uang_muka = 403 # Tanggal pakai create_date atau hari ini account_date = self.date_done_not_realized or fields.Date.today() ref_label = f"Realisasi {self.pum_id.number} Biaya {self.pum_id.detail_note} ({cab_move.name})" label_sisa_pum = f"Sisa PUM {self.pum_id.detail_note} {self.pum_id.number} ({cab_move.name})" lines = [] # Sisa PUM (Debit) if self.remaining_value > 0: lines.append((0, 0, { 'account_id': account_sisa_pum, 'partner_id': partner_id, 'name': label_sisa_pum, 'debit': self.remaining_value, 'credit': 0, })) # Biaya Penggunaan (Debit) total_biaya = 0 for line in self.penggunaan_line_ids: lines.append((0, 0, { 'account_id': line.account_id.id, 'partner_id': partner_id, 'name': f"{line.description} ({line.date})", 'debit': line.nominal, 'credit': 0, })) total_biaya += line.nominal # Uang Muka Operasional (Credit) total_credit = self.remaining_value + total_biaya if total_credit > 0: lines.append((0, 0, { 'account_id': account_uang_muka, 'partner_id': partner_id, 'name': ref_label, 'debit': 0, 'credit': total_credit, })) move = self.env['account.move'].create({ 'ref': ref_label, 'date': account_date, 'journal_id': 11, # MISC 'line_ids': lines, }) # self.message_post(body=f"Jurnal CAB telah dibuat dengan nomor: {move.name}.") self.move_id = move.id return { 'name': _('Journal Entry'), 'view_mode': 'form', 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'res_id': move.id, 'target': 'current', } def action_approval_check(self): jakarta_tz = pytz.timezone('Asia/Jakarta') now = datetime.now(jakarta_tz).replace(tzinfo=None) formatted_date = now.strftime('%d %B %Y %H:%M') for rec in self: if not rec.pum_id.departement_type: raise UserError("Field 'departement_type' wajib diisi sebelum approval.") approver_id = rec.pum_id._get_departement_approver() if rec.status == 'pengajuan1': if self.env.user.id != approver_id: raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") rec.name_approval_departement = self.env.user.name rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement rec.date_approved_department = now # Mapping posisi berdasarkan departement_type department_titles = { 'sales': 'Sales Manager', 'merchandiser': 'Merchandiser Manager', 'marketing': 'Marketing Manager', 'logistic': 'Logistic Manager', 'procurement': 'Procurement Manager', 'fat': 'Finance & Accounting Manager', 'hr_ga': 'HR & GA Manager', } rec.position_department = department_titles.get(rec.pum_id.departement_type, 'Departement Manager') rec.status = 'pengajuan2' rec.message_post( body=f"Approval Departement oleh {self.env.user.name} " f"pada {formatted_date}." ) elif rec.status == 'pengajuan2': ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP if self.env.user.id not in ap_user_ids: raise UserError("Hanya AP yang berhak menyetujui tahap ini.") rec.name_approval_ap = self.env.user.name rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap rec.date_approved_ap = now rec.position_ap = 'Finance AP' rec.status = 'pengajuan3' rec.message_post( body=f"Approval AP oleh {self.env.user.name} " f"pada {formatted_date}." ) elif rec.status == 'pengajuan3': if self.env.user.id != 7: # ID user Pimpinan raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") rec.name_approval_pimpinan = self.env.user.name rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan rec.date_approved_pimpinan = now rec.position_pimpinan = 'Pimpinan' rec.status = 'approved' rec.done_status = 'done_not_realized' # Set status done untuk realisasi rec.message_post( body=f"Approval Pimpinan oleh {self.env.user.name} " f"pada {formatted_date}." ) else: raise UserError("Status saat ini tidak bisa di-approve lagi.") # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap {rec.status}.") def _check_remaining_value(self): for rec in self: # Cek sisa PUM if rec.remaining_value < 0: raise ValidationError( "Sisa uang PUM tidak boleh kurang dari 0.\n" "Jika ada penggunaan uang pribadi, maka ajukan dengan sistem reimburse." ) @api.model def create(self, vals): rec = super().create(vals) rec._check_remaining_value() return rec def write(self, vals): res = super().write(vals) self._check_remaining_value() return res class AdvancePaymentRequestReject(models.TransientModel): _name = 'advance.payment.request.reject' _description = 'Wizard for Reject Reason APR' request_id = fields.Many2one('advance.payment.request', string='Pengajuan PUM') reason_reject = fields.Text(string='Alasan Penolakan', required=True) def confirm_reject(self): if self.request_id: self.request_id.write({ 'status': 'reject', 'last_status': self.request_id.status, 'reason_reject': self.reason_reject, }) return {'type': 'ir.actions.act_window_close'} class AdvancePaymentCreateBill(models.TransientModel): _name = 'advance.payment.create.bill' _description = 'Create Bill from Advance Payment' down_payment_id = fields.Many2one('advance.payment.request', string='Down Payment', required=True) account_id = fields.Many2one( 'account.account', string='Bank Intransit', required=True, domain="[('id', 'in', [573, 389, 392])]" # ID Bank Intransit ) nominal = fields.Float(string='Nominal', related='down_payment_id.nominal') def action_create_cab(self): self.ensure_one() # if self.env.user.id != 23: # raise UserError('Hanya AP yang dapat menggunakan ini.') dp = self.down_payment_id partner_id = dp.user_id.partner_id.id ref_label = f'{dp.number} - Biaya {dp.detail_note or "-"}' move = self.env['account.move'].create({ 'ref': ref_label, 'date': fields.Date.context_today(self), 'journal_id': 11, # Cash & Bank 'line_ids': [ (0, 0, { 'account_id': 403, # Uang Muka Operasional 'partner_id': partner_id, 'name': ref_label, 'debit': dp.nominal, 'credit': 0, }), (0, 0, { 'account_id': self.account_id.id, # Bank Intransit yang dipilih 'partner_id': partner_id, 'name': ref_label, 'debit': 0, 'credit': dp.nominal, }) ] }) dp.move_id = move.id # jika ada field untuk menampung move_id return { 'name': _('Journal Entry'), 'view_mode': 'form', 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'res_id': move.id, 'target': 'current', }