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 or Reimburse' _rec_name = 'number' _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'create_date desc' 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', default=lambda self: self.env.user, 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', default=lambda self: self.env.user, 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 ) estimated_return_date = fields.Date( string='Batas Pengajuan', help='Tanggal batas maksimal durasi pengajuan realisasi' ) days_remaining = fields.Integer( string='Sisa Hari Realisasi', 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 Approval AP'), ('pengajuan3', 'Menunggu Approval Pimpinan'), ('approved', 'Approved'), ('cancel','Cancel') ], string='Status', default='draft', tracking=3, index=True, track_visibility='onchange') status_pay_down_payment = fields.Selection([ ('pending', 'Pending'), ('payment', 'Payment'), ('cancel','Cancel') ], string='Status Pembayaran', default='pending', tracking=3) name_approval_departement = fields.Char(string='Approval Departement') name_approval_ap = fields.Char(string='Approval AP') email_ap = fields.Char(string = 'Email AP') name_approval_pimpinan = fields.Char(string='Approval Pimpinan') 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') position_ap = fields.Char(string='Position AP') position_pimpinan = fields.Char(string='Position Pimpinan') 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', 'FAT'), ('it', 'IT'), ('hr_ga', 'HR & GA'), ('pimpinan', 'Pimpinan') ], 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') 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', tracking=3) position_type = fields.Selection([ ('staff', 'Staff'), ('manager', 'Manager'), ('pimpinan', 'Pimpinan')], string='Jabatan') settlement_type = fields.Selection([ ('no_settlement', 'Belum Realisasi'), ('settlement', 'Realisasi') ]) is_represented = fields.Boolean(string='Nama Pemohon Berbeda?', default=False) apr_perjalanan = fields.Boolean(string = "PUM Perjalanan?", default = False) reimburse_line_ids = fields.One2many('reimburse.line', 'request_id', string='Rincian Reimburse') upload_attachment_date = fields.Datetime(string='Upload Attachment Date', tracking=3) settlement_ids = fields.One2many( 'advance.payment.settlement', 'pum_id', string='Realisasi' ) has_settlement = fields.Boolean( string='Has Settlement', compute='_compute_has_settlement' ) settlement_name = fields.Char( string="Nama Realisasi", compute='_compute_settlement_name' ) grand_total_reimburse = fields.Monetary( string='Total Reimburse', compute='_compute_grand_total_reimburse', currency_field='currency_id' ) is_current_user_ap = fields.Boolean( string="Is Current User AP", compute='_compute_is_current_user_ap' ) estimate_line_ids = fields.One2many('advance.payment.request.estimate.line', 'request_id', string='Rincian Estimasi') @api.constrains('nominal', 'estimate_line_ids') def _check_nominal_vs_estimate_total(self): for rec in self: if rec.type_request == 'pum': if not rec.estimate_line_ids: raise UserError("Rincian estimasi wajib diisi untuk PUM. Silakan tambahkan rincian estimasi.") total_estimate = sum(line.nominal for line in rec.estimate_line_ids) if round(total_estimate, 2) != round(rec.nominal, 2): raise UserError("Total estimasi harus sama dengan nominal PUM. Silakan sesuaikan rincian estimasi atau nominal PUM.") @api.onchange('grand_total_reimburse', 'type_request') def _onchange_reimburse_line_update_nominal(self): if self.type_request == 'reimburse': self.nominal = self.grand_total_reimburse def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] is_ap = self.env.user.id in ap_user_ids for line in self: line.is_current_user_ap = is_ap @api.depends('reimburse_line_ids.total') def _compute_grand_total_reimburse(self): for request in self: request.grand_total_reimburse = sum(request.reimburse_line_ids.mapped('total')) def action_open_create_reimburse_cab(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'name': 'Buat Jurnal Reimburse', 'res_model': 'create.reimburse.cab.wizard', 'view_mode': 'form', 'target': 'new', 'context': { 'default_request_id': self.id, 'default_total_reimburse': self.grand_total_reimburse, } } @api.depends('settlement_ids') def _compute_has_settlement(self): for rec in self: rec.has_settlement = bool(rec.settlement_ids) @api.depends('settlement_ids', 'settlement_ids.name') def _compute_settlement_name(self): for request in self: if request.settlement_ids: request.settlement_name = request.settlement_ids[0].name else: request.settlement_name = False @api.onchange('is_represented') def _onchange_is_represented(self): if self.is_represented: self.account_name = False self.applicant_name = False else: self.account_name = self.env.user.id self.applicant_name = self.env.user.id @api.constrains('is_represented', 'applicant_name', 'account_name', 'user_id') def _check_applicant_consistency(self): for rec in self: if not rec.is_represented: if rec.applicant_name != rec.user_id or rec.account_name != rec.user_id: raise ValidationError("Nama Pemohon harus sesuai dengan User yang sedang Login, centang 'Nama Pemohon Berbeda?' jika ingin mewakilkan pemohon lain.") @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. # - PUM Perjalanan: # - Hari H kembali ke kantor = template 'mail_template_pum_reminder_today' # - H-2 dari due date = template 'mail_template_pum_reminder_h_2' # - PUM Non-Perjalanan: # - H-2 dari due date = template 'mail_template_pum_reminder_h_2' # """ # today = date.today() # # Penyesuaian 1: Cari semua PUM yang sudah disetujui (bukan draft/reject) # # Kita tidak filter 'date_back_to_office' di sini lagi. # pum_ids = self.search([ # ('status', 'not in', ['draft', 'reject']), # ('type_request', '=', 'pum') # ]) # 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 PUM (today/h2) tidak ditemukan.") # return # for pum in pum_ids: # _logger.info(f"[REMINDER] Memproses PUM {pum.number}") # # Penyesuaian 2: Logika ini sudah benar (sesuai update kita sebelumnya) # # Jika realisasi sudah dibuat, PUM tidak aktif lagi, lewati. # realization = self.env['advance.payment.settlement'].search([('pum_id', '=', pum.id)], limit=1) # if realization: # _logger.info(f"[REMINDER] Lewati PUM {pum.number}, realisasi sudah dibuat.") # continue # 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 # # Penyesuaian 3: Logika penentuan Due Date (Wajib) # due_date = False # base_date_for_today_check = False # Khusus PUM Perjalanan # if pum.apr_perjalanan: # if pum.date_back_to_office: # due_date = pum.date_back_to_office + timedelta(days=7) # base_date_for_today_check = pum.date_back_to_office # else: # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} (perjalanan) karena tgl kembali kosong.") # continue # else: # # Ini adalah PUM Non-Perjalanan # if not pum.create_date: # _logger.warning(f"[REMINDER] Lewati PUM {pum.number} (non-perjalanan) karena create_date kosong.") # continue # base_date = pum.create_date.date() # due_date = base_date + timedelta(days=7) # # Hitung sisa hari # days_remaining = (due_date - today).days # # Penyesuaian 4: Tentukan template berdasarkan sisa hari # template = False # if pum.apr_perjalanan and base_date_for_today_check == today: # # Hari H kembali ke kantor (HANYA PUM Perjalanan) # template = template_today # elif days_remaining == 2: # # H-2 due date (Untuk SEMUA jenis PUM) # template = template_h2 # else: # _logger.info(f"[REMINDER] Lewati PUM {pum.number}, hari ini bukan tgl pengingat (Sisa hari: {days_remaining}).") # continue # # --- Sisanya (Generate attachment & kirim email) sudah aman --- # # 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', # Masih hardcode # 'email_from': pum.email_ap, # 'email_from': 'finance@indoteknik.co.id', # '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, 16729] is_it = self.env.user.has_group('indoteknik_custom.group_role_it') if self.env.user.id not in ap_user_ids and not is_it: 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 & IT : 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, 'it': 28, 'hr_ga': 7, } def _get_departement_approver(self): mapping = self._get_approver_mapping() return mapping.get(self.departement_type) @api.constrains('apr_perjalanan', 'date_back_to_office') def _check_date_back_to_office(self): if self.apr_perjalanan and not self.date_back_to_office: raise ValidationError("Tanggal Kembali ke Kantor wajib diisi jika PUM Perjalanan dicentang.") @api.onchange('applicant_name') def _onchange_applicant_name_set_position(self): if self.applicant_name: user_id = self.applicant_name.id mapping = self._get_approver_mapping() manager_ids = set(mapping.values()) pimpinan_id = 7 if user_id == pimpinan_id: self.position_type = 'pimpinan' elif user_id in manager_ids: self.position_type = 'manager' else: self.position_type = 'staff' else: self.position_type = False # @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_name': f'Realisasi - {self.number or ""}', } } def action_confirm_payment(self): # jakarta_tz = pytz.timezone('Asia/Jakarta') # now = datetime.now(jakarta_tz).replace(tzinfo=None) ap_user_ids = [23, 9468, 16729] 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 {rec.number or ""} ' f'karena belum ada bukti attachment (PDF/Image).' ) rec.status_pay_down_payment = 'payment' rec.upload_attachment_date = datetime.utcnow() rec.message_post( body="Bukti transfer telah di upload oleh Finance 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', 'it': 'IT Manager', 'hr_ga': 'HR & GA Manager', 'pimpinan': 'Pimpinan', } 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, 16729] # 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' if rec.position_type == 'pimpinan': rec.status = 'approved' else: 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_ap_only(self): self.ensure_one() ap_user_ids = [23, 9468, 16729] # 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_apr_id': self.id, } } @api.depends('date_back_to_office', 'status', 'apr_perjalanan', 'create_date', 'settlement_ids.status', 'type_request') def _compute_days_remaining(self): today = date.today() for rec in self: # current_days = rec.days_remaining or 0 # current_due_date = rec.estimated_return_date or False current_days = 0 current_due_date = False is_settlement_approved = any(s.status == 'approved' for s in rec.settlement_ids) is_pum_canceled = (rec.status == 'cancel') if rec.type_request == 'pum' and not is_pum_canceled and not is_settlement_approved: if not is_settlement_approved: due_date = False if rec.apr_perjalanan: # Alur PUM Perjalanan if rec.date_back_to_office: due_date = rec.date_back_to_office + timedelta(days=7) effective_today = max(today, rec.date_back_to_office) current_due_date = due_date current_days = (due_date - effective_today).days else: current_due_date = False current_days = 0 else: # Alur PUM Non-Perjalanan if rec.create_date: base_date = rec.create_date.date() due_date = base_date + timedelta(days=7) current_due_date = due_date current_days = (due_date - today).days else: current_due_date = False current_days = 0 else: current_due_date = False current_days = 0 rec.days_remaining = current_days rec.estimated_return_date = current_due_date @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 self.type_request != 'pum': return if not self.user_id: return pum_ids = self.search([ ('user_id', '=', self.user_id.id), ('status', '!=', 'reject'), ('type_request', '=', 'pum') ]) 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: 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.', } } def _get_department_titles_mapping(self): return { 'sales': 'Sales Manager', 'merchandiser': 'Merchandiser Manager', 'marketing': 'Marketing Manager', 'logistic': 'Logistic Manager', 'procurement': 'Procurement Manager', 'fat': 'Finance & Accounting Manager', 'it': 'IT Manager', 'hr_ga': 'HR & GA Manager', } @api.model def create(self, vals): jakarta_tz = pytz.timezone('Asia/Jakarta') now = datetime.now(jakarta_tz).replace(tzinfo=None) user = self.env.user pum_ids = self.search([ ('user_id', '=', user.id), ('status', '!=', 'cancel'), ('type_request', '=', 'pum') ]) 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: active_pum_count += 1 if active_pum_count >= 2 and vals.get('type_request') == 'pum': raise UserError("Anda hanya dapat mengajukan maksimal 2 PUM aktif. Silakan realisasikan salah satunya terlebih dahulu.") if not vals.get('apr_perjalanan'): if 'estimated_return_date' not in vals: today = date.today() due_date = today + timedelta(days=7) vals['estimated_return_date'] = due_date initial_status = '' position = vals.get('position_type') department = vals.get('departement_type') if department == 'hr_ga' or position in ('manager', 'pimpinan'): initial_status = 'pengajuan2' else: initial_status = 'pengajuan1' vals['status'] = initial_status if initial_status == 'pengajuan2' and department != 'hr_ga': applicant_name = vals.get('applicant_name') vals['name_approval_departement'] = self.env['res.users'].browse(applicant_name).name or '' vals['date_approved_department'] = now department_type = vals.get('departement_type') department_titles = self._get_department_titles_mapping() vals['position_department'] = department_titles.get(department_type, 'Departement Manager') if position == 'pimpinan' and department != 'hr_ga': vals['name_approval_pimpinan'] = self.env['res.users'].browse(vals.get('applicant_name')).name or '' vals['position_pimpinan'] = 'Pimpinan' vals['date_approved_pimpinan'] = now # if position == 'staff': # initial_status = 'pengajuan1' # elif position == 'manager': # initial_status = 'pengajuan2' # applicant_name = vals.get('applicant_name') # vals['name_approval_departement'] = self.env['res.users'].browse(applicant_name).name or '' # vals['date_approved_department'] = now # department_type = vals.get('departement_type') # department_titles = self._get_department_titles_mapping() # vals['position_department'] = department_titles.get(department_type, 'Departement Manager') # elif position == 'pimpinan': # initial_status = 'pengajuan2' # applicant_name = vals.get('applicant_name') # vals['name_approval_pimpinan'] = self.env['res.users'].browse(applicant_name).name or '' # vals['position_pimpinan'] = 'Pimpinan' # vals['date_approved_pimpinan'] = now # vals['status'] = initial_status if not vals.get('number') or vals['number'] == 'New Draft': if vals.get('type_request') == 'reimburse': vals['number'] = self.env['ir.sequence'].next_by_code('reimburse.request') or 'New Draft' else: vals['number'] = self.env['ir.sequence'].next_by_code('advance.payment.request') or 'New Draft' # vals['status'] = 'pengajuan1' # return super(AdvancePaymentRequest, self).create(vals) rec = super(AdvancePaymentRequest, self).create(vals) if rec.type_request == 'reimburse': rec._compute_grand_total_reimburse() rec.nominal = rec.grand_total_reimburse return rec def action_open_cancel_wizard(self): """Membuka Wizard Pop-up untuk Cancel PUM/Reimburse""" self.ensure_one() if self.move_id: raise UserError(_("Pengajuan tidak dapat dibatalkan karena Journal sudah terbentuk.")) if self.settlement_ids and any(s.status != 'draft' for s in self.settlement_ids): raise UserError(_("Pengajuan tidak dapat dibatalkan karena sudah ada proses realisasi.")) return { 'name': _('Alasan Pembatalan'), 'type': 'ir.actions.act_window', 'res_model': 'advance.payment.cancel.wizard', 'view_mode': 'form', 'target': 'new', 'context': { 'default_request_id': self.id, } } class AdvancePaymentCancelWizard(models.TransientModel): _name = 'advance.payment.cancel.wizard' _description = 'Wizard untuk Membatalkan PUM/Reimburse' request_id = fields.Many2one('advance.payment.request', string='Dokumen', readonly=True) reason = fields.Text(string='Alasan Pembatalan', required=True) def action_confirm_cancel(self): self.ensure_one() request = self.request_id if request.move_id: raise UserError("Tidak bisa melakukan cancel karena Jurnal (Move ID) sudah terbentuk.") request.write({'status': 'cancel'}) request.write({'status_pay_down_payment': 'cancel'}) request.message_post( body=f"Pengajuan telah DIBATALKAN oleh {self.env.user.name}.
" f"Alasan: {self.reason}", message_type="comment", subtype_xmlid="mail.mt_note", ) return {'type': 'ir.actions.act_window_close'} class AdvancePaymentUsageLine(models.Model): _name = 'advance.payment.usage.line' _description = 'Advance Payment Usage Line' _order = 'sequence, id' sequence = fields.Integer(string='Sequence', default=10) realization_id = fields.Many2one('advance.payment.settlement', string='Realization') date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) description = fields.Text(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', tracking=3 # ID Jenis Biaya yang dibutuhkan ) # domain="[('id', 'in', [484, 486, 488, 506, 507, 625, 471, 519, 527, 528, 529, 530, 565])]" # ID Jenis Biaya yang dibutuhkan is_current_user_ap = fields.Boolean( string="Is Current User AP", compute='_compute_is_current_user_ap' ) category_usage = fields.Selection([ ('parkir', 'Parkir'), ('tol', 'Tol'), ('bbm', 'BBM'), ('kuli', 'Kuli'), ('konsumsi', 'Konsumsi'), ('lain_lain', 'Lain-lain'), ], string='Kategori System', compute='_compute_category_usage') @api.depends('account_id') def _compute_category_usage(self): for rec in self: if not rec.account_id: rec.category_usage = False continue name = rec.account_id.name.lower() if 'bbm' in name or 'bahan bakar' in name: rec.category_usage = 'bbm' elif 'tol' in name: rec.category_usage = 'tol' elif 'parkir' in name: rec.category_usage = 'parkir' elif 'kuli' in name or 'bongkar' in name: rec.category_usage = 'kuli' elif 'konsumsi' in name or 'makan' in name or 'minum' in name: rec.category_usage = 'konsumsi' else: rec.category_usage = 'lain_lain' def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] is_ap = self.env.user.id in ap_user_ids for line in self: line.is_current_user_ap = is_ap # @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, 16729] # 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 ReimburseLine(models.Model): _name = 'reimburse.line' _description = 'Reimburse Line' _order = 'sequence, id' sequence = fields.Integer(string='Sequence', default=10) request_id = fields.Many2one('advance.payment.request', string='Request') date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) account_id = fields.Many2one( 'account.account', string='Jenis Biaya', tracking=3 ) description = fields.Text(string='Description', required=True, tracking=3) distance_departure = fields.Float(string='Pergi (Km)', tracking=3) distance_return = fields.Float(string='Pulang (Km)', tracking=3) quantity = fields.Float(string='Quantity', tracking=3, default=1) price_unit = fields.Float(string='Price', tracking=3) total = fields.Float(string='Total', tracking=3, compute='_compute_total') # total = fields.Float(string='Total', tracking=3) currency_id = fields.Many2one(related='request_id.currency_id') is_vehicle = fields.Boolean(string='Berkendara?') vehicle_type = fields.Selection([ ('motor', 'Motor'), ('car', 'Mobil'), ], string='Tipe Kendaraan', tracking=3) attachment_image = fields.Binary(string='Image', attachment_filename='attachment_name_image') attachment_pdf = fields.Binary(string='PDF', attachment_filename='attachment_name_pdf') attachment_name_image = fields.Char(string='Filename Image') attachment_name_pdf = fields.Char(string='Filename PDF') attachment_type = fields.Selection([ ('pdf', 'PDF'), ('image', 'Image'), ], string="Attachment Type") is_checked = fields.Boolean(string='Checked', default=False) is_current_user_ap = fields.Boolean( string="Is Current User AP", compute='_compute_is_current_user_ap' ) def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] is_ap = self.env.user.id in ap_user_ids for line in self: line.is_current_user_ap = is_ap @api.depends('quantity', 'price_unit', 'is_vehicle') def _compute_total(self): for line in self: line.total = line.quantity * line.price_unit @api.onchange('is_vehicle', 'vehicle_type', 'distance_departure', 'distance_return') def _onchange_vehicle_data(self): if not self.is_vehicle: self.vehicle_type = False self.distance_departure = 0 self.distance_return = 0 self.price_unit = 0 return total_distance = self.distance_departure + self.distance_return if self.vehicle_type and total_distance > 0: biaya_per_km = 0 if self.vehicle_type == 'car': biaya_per_km = 1000 # Rp 10.000 / 10 km self.price_unit = biaya_per_km elif self.vehicle_type == 'motor': biaya_per_km = 500 # Rp 10.000 / 20 km self.price_unit = biaya_per_km self.total = total_distance * biaya_per_km self.quantity = total_distance else: self.total = 0 class AdvancePaymentSettlement(models.Model): _name = 'advance.payment.settlement' _description = 'Advance Payment Settlement' _inherit = ['mail.thread'] _rec_name = 'name' _order = 'create_date desc' pum_id = fields.Many2one('advance.payment.request', string='No PUM', ondelete='cascade') 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='Dok. 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' ) nominal_pum = fields.Float( string='Nominal Pemberian PUM', related='pum_id.nominal', readonly=True ) # 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') def _get_default_note_approval(self): template = ( "Demikian dokumen Realisasi Uang Muka ini saya buat, dengan ini saya meminta persetujuan dibawah atas hasil penggunaan uang muka yang saya gunakan untuk kebutuhan realisasi " ) return template note_approval = fields.Text(string='Note Persetujuan', tracking=3, default=_get_default_note_approval) name_approval_departement = fields.Char(string='Approval Departement') name_approval_ap = fields.Char(string='Approval AP') name_approval_pimpinan = fields.Char(string='Approval Pimpinan') 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') position_ap = fields.Char(string='Position AP') position_pimpinan = fields.Char(string='Position Pimpinan') approved_by = fields.Char(string='Approved By', track_visibility='always') status = fields.Selection([ ('pengajuan1', 'Menunggu Approval Departement'), ('pengajuan2', 'Menunggu Approval AP'), ('pengajuan3', 'Menunggu Approval Pimpinan'), ('approved', 'Approved'), ], string='Status', default='pengajuan1', tracking=3, index=True, track_visibility='onchange') # --- DIHAPUS --- # 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) # --- BATAS DIHAPUS --- 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='Status Jurnal', compute='_compute_is_cab_visible') user_id = fields.Many2one( 'res.users', string='Diajukan Oleh', related='pum_id.user_id', readonly=True ) applicant_name = fields.Many2one( 'res.users', string='Nama Pemohon', related='pum_id.applicant_name', readonly=True ) is_current_user_ap = fields.Boolean( string="Is Current User AP", compute='_compute_is_current_user_ap' ) pum_estimate_line_ids = fields.One2many( related='pum_id.estimate_line_ids', string='Rincian Estimasi PUM', readonly=True ) def _compute_is_current_user_ap(self): ap_user_ids = [23, 9468, 16729] is_ap = self.env.user.id in ap_user_ids for line in self: line.is_current_user_ap = is_ap def action_toggle_check_attachment(self): ap_user_ids = [23, 9468, 16729] 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, 16729] is_it = self.env.user.has_group('indoteknik_custom.group_role_it') if self.env.user.id not in ap_user_ids and not is_it: 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('nominal_pum', 'grand_total_use') def _compute_remaining_value(self): for rec in self: rec.remaining_value = rec.nominal_pum - rec.grand_total_use return # --- DIHAPUS --- # 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}.") # --- BATAS DIHAPUS --- def action_cab(self): self.ensure_one() ap_user_ids = [23, 9468, 16729] # 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 partner_id = self.pum_id.applicant_name.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, 683]) 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 # --- PENYESUAIAN LOGIKA --- # Tanggal pakai create_date atau hari ini account_date = fields.Date.context_today(self) # --- BATAS PENYESUAIAN --- ref_label = f"Realisasi {self.pum_id.number} {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', 'it': 'IT Manager', 'hr_ga': 'HR & GA Manager', 'pimpinan': 'Pimpinan', } 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, 16729] # 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' if rec.pum_id.position_type == 'pimpinan': rec.status = 'approved' else: 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' # --- DIHAPUS --- # rec.done_status = 'done_not_realized' # Set status done untuk realisasi # --- BATAS DIHAPUS --- 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 Rp 0.\n" "Jika ada penggunaan uang pribadi, maka ajukan dengan sistem reimburse." ) @api.model def create(self, vals): jakarta_tz = pytz.timezone('Asia/Jakarta') # --- PENYESUAIAN LOGIKA WAKTU --- now = datetime.now(jakarta_tz).replace(tzinfo=None) # Gunakan fields.Datetime.now() agar konsisten # now = fields.Datetime.now() # --- BATAS PENYESUAIAN --- pum_id = vals.get('pum_id') or self._context.get('default_pum_id') initial_status = '' if pum_id: pum_request = self.env['advance.payment.request'].browse(pum_id) if pum_request: position_dari_pum = pum_request.position_type department_dari_pum = pum_request.departement_type if position_dari_pum == 'staff': if department_dari_pum == 'hr_ga': initial_status = 'pengajuan2' else: initial_status = 'pengajuan1' elif position_dari_pum == 'manager': initial_status = 'pengajuan2' applicant_name_str = pum_request.applicant_name.name or '' department_type = pum_request.departement_type department_titles = pum_request._get_department_titles_mapping() dept_position = department_titles.get(department_type, 'Departement Manager') vals['date_approved_department'] = now vals['name_approval_departement'] = applicant_name_str vals['position_department'] = dept_position elif position_dari_pum == 'pimpinan': initial_status = 'pengajuan2' applicant_name_str = pum_request.applicant_name.name or '' vals['date_approved_pimpinan'] = now vals['name_approval_pimpinan'] = applicant_name_str vals['position_pimpinan'] = 'Pimpinan' # --- PENYESUAIAN LOGIKA: SET DEFAULT JIKA KOSONG --- vals['status'] = initial_status or 'pengajuan1' # --- BATAS PENYESUAIAN --- 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 AdvancePaymentCreateBill(models.TransientModel): _name = 'advance.payment.create.bill' _description = 'Create Bill from Advance Payment' apr_id = fields.Many2one('advance.payment.request', string='Advance Payment Request', required=True) account_id = fields.Many2one( 'account.account', string='Bank Intransit', required=True, domain="[('id', 'in', [573, 389, 392, 683, 380])]" # ID Bank Intransit ) nominal = fields.Float(string='Nominal', related='apr_id.nominal') def action_create_cab(self): self.ensure_one() # if self.env.user.id != 23: # raise UserError('Hanya AP yang dapat menggunakan ini.') apr = self.apr_id # partner_id = apr.user_id.partner_id.id partner_id = apr.applicant_name.partner_id.id ref_label = f'{apr.number} - {apr.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': apr.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': apr.nominal, }) ] }) apr.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', } class CreateReimburseCabWizard(models.TransientModel): _name = 'create.reimburse.cab.wizard' _description = 'Wizard untuk Membuat Jurnal Reimburse' # Field untuk menampung ID request yang sedang diproses request_id = fields.Many2one('advance.payment.request', string='Pengajuan', readonly=True) # Field untuk memilih salah satu dari dua bank Anda account_id = fields.Many2one( 'account.account', string='Bank Intransit (Credit)', required=True, # Domain untuk membatasi pilihan hanya pada ID 573 dan 389 domain="[('id', 'in', [573, 389])]" ) # Field untuk menampilkan total agar pengguna bisa konfirmasi total_reimburse = fields.Monetary( string='Total Reimburse', related='request_id.grand_total_reimburse', ) currency_id = fields.Many2one(related='request_id.currency_id', readonly=True) def action_create_reimburse_cab(self): """Metode ini yang akan membuat Journal Entry (CAB).""" self.ensure_one() request = self.request_id # --- Validasi --- if request.move_id: raise UserError("Jurnal sudah pernah dibuat untuk pengajuan ini.") if not request.reimburse_line_ids: raise UserError("Tidak ada rincian reimburse yang bisa dijurnalkan.") lines = [] # partner_id = request.user_id.partner_id.id partner_id = request.applicant_name.partner_id.id ref_label = f'{request.number} - {request.detail_note or "-"}' # 1. Buat Jurnal DEBIT dari setiap baris reimburse for line in request.reimburse_line_ids: if not line.account_id: raise UserError(f"Jenis Biaya pada baris '{line.description}' belum diisi oleh AP.") lines.append((0, 0, { 'account_id': line.account_id.id, 'partner_id': partner_id, 'name': f"{line.description} ({line.date})", 'debit': line.total, 'credit': 0, })) # 2. Buat satu Jurnal CREDIT ke bank yang dipilih di wizard lines.append((0, 0, { 'account_id': self.account_id.id, 'partner_id': partner_id, 'name': ref_label, 'debit': 0, 'credit': request.grand_total_reimburse, })) # 3. Buat Journal Entry move = self.env['account.move'].create({ 'ref': ref_label, 'date': fields.Date.context_today(self), 'journal_id': 11, # PENTING: Ganti 11 dengan ID Journal "Miscellaneous" Anda 'line_ids': lines, }) # 4. Tautkan journal yang baru dibuat ke request request.move_id = move.id # 5. Buka tampilan form journal yang baru dibuat return { 'name': _('Journal Entry'), 'view_mode': 'form', 'res_model': 'account.move', 'type': 'ir.actions.act_window', 'res_id': move.id, 'target': 'current', } class AdvancePaymentRequestEstimateLine(models.Model): _name = 'advance.payment.request.estimate.line' _description = 'Advance Payment Request Estimate Line' request_id = fields.Many2one('advance.payment.request', string='Request') category_estimate = fields.Selection([ ('parkir', 'Parkir'), ('tol', 'Tol'), ('bbm', 'BBM'), ('kuli', 'Kuli'), ('konsumsi', 'Konsumsi'), ('lain_lain', 'Lain-lain'), ], string='Kategori Estimasi', required=True) description = fields.Text(string='Description', help='Deskripsi tambahan untuk estimasi biaya yang diperlukan.') nominal = fields.Float(string='Nominal Estimasi', required=True, help='Masukkan nominal estimasi untuk kategori ini (tidak mesti akurat, hanya untuk gambaran umum).') currency_id = fields.Many2one(related='request_id.currency_id') total_actual = fields.Float(string='Nominal Realisasi', compute='_compute_actual_data') frequency = fields.Integer(string='Qty Realisasi', compute='_compute_actual_data') @api.depends('request_id.settlement_ids.penggunaan_line_ids.nominal', 'request_id.settlement_ids.penggunaan_line_ids.account_id') def _compute_actual_data(self): for rec in self: total_act = 0 freq = 0 if rec.request_id and rec.request_id.settlement_ids: all_usage_lines = rec.request_id.settlement_ids.mapped('penggunaan_line_ids') valid_lines = all_usage_lines.filtered(lambda l: l.account_id and l.category_usage) planned_category = rec.request_id.estimate_line_ids.mapped('category_estimate') if rec.category_estimate == 'lain_lain': matched_lines = valid_lines.filtered( lambda l: l.category_usage == 'lain_lain' or \ l.category_usage not in planned_category ) else: matched_lines = valid_lines.filtered( lambda l: l.category_usage == rec.category_estimate ) total_act = sum(matched_lines.mapped('nominal')) freq = len(matched_lines) rec.total_actual = total_act rec.frequency = freq