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