diff options
| author | unknown <it@fixcomart.co.id> | 2025-09-16 15:01:02 +0700 |
|---|---|---|
| committer | unknown <it@fixcomart.co.id> | 2025-09-16 15:01:02 +0700 |
| commit | 540136f8096f090b481d139c58c3f36c28aa69bb (patch) | |
| tree | 564e27b879251c961afe6550058e9f47a1744bd0 | |
| parent | 462fb823c79d8652a6a08e267b00b13c7bbe5df0 (diff) | |
(andri) add payment diff + fix onchange ketika pilih partner
| -rw-r--r-- | indoteknik_custom/models/down_payment.py | 1088 | ||||
| -rw-r--r-- | indoteknik_custom/models/letter_receivable.py | 12 | ||||
| -rw-r--r-- | indoteknik_custom/models/unpaid_invoice_view.py | 7 | ||||
| -rw-r--r-- | indoteknik_custom/views/letter_receivable.xml | 5 | ||||
| -rw-r--r-- | indoteknik_custom/views/unpaid_invoice_view.xml | 4 |
5 files changed, 21 insertions, 1095 deletions
diff --git a/indoteknik_custom/models/down_payment.py b/indoteknik_custom/models/down_payment.py deleted file mode 100644 index 5adbafd9..00000000 --- a/indoteknik_custom/models/down_payment.py +++ /dev/null @@ -1,1088 +0,0 @@ -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 DownPayment(models.Model): - _name = 'down.payment' - _description = 'Down Payment Management' - _rec_name = 'number' - _inherit = ['mail.thread', 'mail.activity.mixin'] - - user_id = fields.Many2one('res.users', string='Diajukan Oleh', default=lambda self: self.env.user, tracking=3) - partner_id = fields.Many2one('res.partner', string='Partner', related='user_id.partner_id', readonly=True) - - number = fields.Char(string='No. Dokumen', default='New Draft', tracking=3) - - applicant_name = fields.Char(string='Nama Pemohon', tracking=3, required=True) - nominal = fields.Float(string='Nominal', tracking=3, required=True) - - bank_name = fields.Char(string='Bank', tracking=3, required=True) - account_name = fields.Char(string='Nama Account', tracking=3, required=True) - bank_account = fields.Char(string='No. Rekening', tracking=3, required=True) - detail_note = fields.Text(string='Keterangan Penggunaan Rinci', tracking=3) - - date_back_to_office = fields.Date( - string='Tanggal Kembali ke Kantor', - tracking=3, - required=True - ) - - estimated_return_date = fields.Date( - string='Batas Pengajuan', - help='Tanggal batas maksimal pengajuan realisasi setelah kembali ke kantor. ' - '7 hari setelah tanggal kembali.' - ) - - days_remaining = fields.Integer( - string='Sisa Hari Pengajuan', - compute='_compute_days_remaining', - help='Sisa hari batas maksimal pengajuan realisasi setelah kembali ke kantor. ' - '7 hari setelah tanggal kembali.' - ) - - status = fields.Selection([ - ('draft', 'Draft'), - ('pengajuan1', 'Menunggu Approval Departement'), - ('pengajuan2', 'Menunggu Pengecekan AP'), - ('pengajuan3', 'Menunggu Approval Pimpinan'), - ('approved', 'Approved'), - ('reject', 'Rejected') - ], string='Status', default='draft', tracking=3, index=True, track_visibility='onchange') - - last_status = fields.Selection([ - ('draft', 'Draft'), - ('pengajuan1', 'Menunggu Approval Departement'), - ('pengajuan2', 'Menunggu Pengecekan AP'), - ('pengajuan3', 'Menunggu Approval Pimpinan'), - ('approved', 'Approved'), - ('reject', 'Rejected') - ], string='Status') - - status_pay_down_payment = fields.Selection([ - ('pending', 'Pending'), - ('payment', 'Payment'), - ], string='Status Pembayaran', default='pending', tracking=3) - - name_approval_departement = fields.Char(string='Approval Departement', tracking=True) - name_approval_ap = fields.Char(string='Approval AP', tracking=True) - email_ap = fields.Char(string = 'Email AP') - name_approval_pimpinan = fields.Char(string='Approval Pimpinan', tracking=True) - - date_approved_department = fields.Datetime(string="Date Approved Department") - date_approved_ap = fields.Datetime(string="Date Approved AP") - date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan") - - position_department = fields.Char(string='Position Departement', tracking=True) - position_ap = fields.Char(string='Position AP', tracking=True) - position_pimpinan = fields.Char(string='Position Pimpinan', tracking=True) - - approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always') - - departement_type = fields.Selection([ - ('sales', 'Sales'), - ('merchandiser', 'Merchandiser'), - ('marketing', 'Marketing'), - ('logistic', 'Logistic'), - ('procurement', 'Procurement'), - ('fat', 'FAT'), - ('hr_ga', 'HR & GA'), - ], string='Departement Type', tracking=3, required=True) - - attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') - attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') - attachment_filename_image = fields.Char(string='Filename Image') - attachment_filename_pdf = fields.Char(string='Filename PDF') - - attachment_type = fields.Selection([ - ('pdf', 'PDF'), - ('image', 'Image'), - ], string="Attachment Type", default='pdf') - - move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')]) - is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible') - - reason_reject = fields.Text(string='Alasan Penolakan') - - currency_id = fields.Many2one( - 'res.currency', string='Currency', - default=lambda self: self.env.company.currency_id - ) - - @api.onchange('nominal') - def _onchange_nominal_no_minus(self): - if self.nominal and self.nominal < 0: - self.nominal = 0 - return { - 'warning': { - 'title': _('Nominal Tidak Valid'), - 'message': _( - "Nominal tidak boleh diisi minus.\n" - "Nilai di set menjadi nol." - ) - } - } - - def _get_jasper_attachment(self): - self.ensure_one() - report = self.env['ir.actions.report'].browse(1134) # ID Downpayment Report - if not report: - raise UserError("Report Jasper tidak ditemukan.") - - data = report.render_jasper(self.ids, data={})[0] - filename = f"{self.number}.pdf" - return { - 'name': filename, - 'datas': base64.b64encode(data), - 'type': 'binary', - 'mimetype': 'application/pdf', - 'filename': filename, - } - - def action_send_pum_reminder(self): - """ - Kirim email reminder PUM otomatis. - - Hari ini = kirim dengan template 'mail_template_pum_reminder_today' - - H-2 dari due date = kirim dengan template 'mail_template_pum_reminder_h_2' - """ - today = date.today() - pum_ids = self.search([ - ('date_back_to_office', '!=', False), - ('status', 'not in', ['draft', 'reject']), - ]) - - template_today = self.env.ref('indoteknik_custom.mail_template_pum_reminder_today', raise_if_not_found=False) - template_h2 = self.env.ref('indoteknik_custom.mail_template_pum_reminder_h_2', raise_if_not_found=False) - - if not template_today or not template_h2: - _logger.warning("Salah satu template email tidak ditemukan.") - return - - for pum in pum_ids: - _logger.info(f"[REMINDER] Memproses PUM {pum.number}") - - if not pum.email_ap or not pum.user_id.partner_id.email: - _logger.warning(f"[REMINDER] Lewati PUM {pum.number} karena email_ap atau email user kosong.") - continue - - due_date = pum.date_back_to_office + timedelta(days=7) - days_remaining = (due_date - today).days - - realization = self.env['realization.down.payment'].search([('pum_id', '=', pum.id)], limit=1) - if not realization or realization.done_status != 'remaining': - _logger.info(f"[REMINDER] Lewati PUM {pum.number}, status realisasi bukan 'remaining'.") - continue - - # Tentukan template - if pum.date_back_to_office == today: - template = template_today - elif days_remaining == 2: - template = template_h2 - else: - _logger.info(f"[REMINDER] Lewati PUM {pum.number}, hari ini bukan tanggal pengingat.") - continue - - # Generate attachment - try: - attachment_vals = pum._get_jasper_attachment() - attachment = self.env['ir.attachment'].create({ - 'name': attachment_vals['name'], - 'type': 'binary', - 'datas': attachment_vals['datas'], - 'res_model': 'down.payment', - 'res_id': pum.id, - 'mimetype': 'application/pdf', - }) - except Exception as e: - _logger.error(f"[REMINDER] Gagal membuat attachment untuk PUM {pum.number}: {str(e)}") - continue - - email_values = { - # 'email_to': pum.user_id.partner_id.email, - 'email_to': 'andrifebriyadiputra@gmail.com', - 'email_from': pum.email_ap, - 'attachment_ids': [(6, 0, [attachment.id])], - } - - _logger.info(f"[REMINDER] Mengirim email PUM {pum.number} ke {email_values['email_to']} dari {email_values['email_from']}") - - try: - body_html = template._render_field('body_html', [pum.id])[pum.id] - - template.send_mail(pum.id, force_send=True, email_values=email_values) - _logger.info(f"[REMINDER] Email berhasil dikirim untuk PUM {pum.number}") - - # Post info sederhana - pum.message_post( - body="Email Reminder Berhasil dikirimkan", - message_type="comment", - subtype_xmlid="mail.mt_note", - ) - - user_system = self.env['res.users'].browse(25) - system_id = user_system.partner_id.id if user_system else False - - # Post isi email ke chatter - pum.message_post( - body=body_html, - message_type="comment", - subtype_xmlid="mail.mt_note", - author_id=system_id, - ) - except Exception as e: - _logger.error(f"[REMINDER] Gagal mengirim email untuk PUM {pum.number}: {str(e)}") - - return True - - - @api.depends('move_id.state') - def _compute_is_cab_visible(self): - for rec in self: - move = rec.move_id - rec.is_cab_visible = bool(move and move.state == 'posted') - - def action_view_journal_uangmuka(self): - self.ensure_one() - - ap_user_ids = [23, 9468] - # if self.env.user.id not in ap_user_ids: - # raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') - - if not self.move_id: - raise UserError("Journal Uang Muka belum tersedia.") - - return { - 'name': 'Journal Entry', - 'view_mode': 'form', - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'res_id': self.move_id.id, - 'target': 'current', - } - - @api.onchange('attachment_type') - def _onchange_attachment_type(self): - self.attachment_file_image = False - self.attachment_filename_image = False - self.attachment_file_pdf = False - self.attachment_filename_pdf = False - - # Sales & MD : Darren ID 19 - # Marketing : Iwan ID 216 - # Logistic & Procurement : Rafly H ID 21 - # FAT : Stephan ID 28 - # HR & GA : Akbar ID 7 / Pimpinan - # --------------------------------------- - # AP : Manzila (Finance) ID 23 - - def _get_departement_approver(self): - mapping = { - 'sales': 19, - 'merchandiser': 19, - 'marketing': 216, - 'logistic': 21, - 'procurement': 21, - 'fat': 28, - 'hr_ga': 7, - } - return mapping.get(self.departement_type) - - def action_realisasi_pum(self): - self.ensure_one() - - realization = self.env['realization.down.payment'].search([('pum_id', '=', self.id)], limit=1) - - if realization: - return { - 'type': 'ir.actions.act_window', - 'name': 'Realisasi PUM', - 'res_model': 'realization.down.payment', - 'view_mode': 'form', - 'target': 'current', - 'res_id': realization.id, - } - else: - return { - 'type': 'ir.actions.act_window', - 'name': 'Realisasi PUM', - 'res_model': 'realization.down.payment', - 'view_mode': 'form', - 'target': 'current', - 'context': { - 'default_pum_id': self.id, - 'default_value_down_payment': self.nominal, - 'default_name': f'Realisasi - {self.number or ""}', - 'default_pemberian_line_ids': [ - (0, 0, { - 'date': self.create_date.date() if self.create_date else fields.Date.today(), - 'description': 'Uang Muka', - 'value': self.nominal - }) - ] - } - } - - - def action_confirm_payment(self): - ap_user_ids = [23, 9468] - if self.env.user.id not in ap_user_ids: - raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') - - for rec in self: - if not rec.attachment_file_image and not rec.attachment_file_pdf: - raise UserError( - f'Tidak bisa konfirmasi pembayaran PUM {rec.name or ""} ' - f'karena belum ada bukti attachment (PDF/Image).' - ) - - rec.status_pay_down_payment = 'payment' - - rec.message_post( - body="Status pembayaran telah <b>dikonfirmasi</b> oleh <b>AP</b>.", - message_type="comment", - subtype_xmlid="mail.mt_note", - ) - - - - # def action_approval_check(self): - # for record in self: - # # user = record.user_id - # user = self.env['res.users'].browse(3401) - # roles = sorted(set( - # f"{group - # .name} (Category: {group.category_id.name})" - # for group in user.groups_id - # if group.category_id.name == 'Roles' - # )) - # _logger.info(f"[ROLE CHECK] User: {user.name} (Login: {user.login}) Roles: {roles}") - # return - - def action_approval_check(self): - jakarta_tz = pytz.timezone('Asia/Jakarta') - now = datetime.now(jakarta_tz).replace(tzinfo=None) - formatted_date = now.strftime('%d %B %Y %H:%M') - - for rec in self: - if not rec.departement_type: - raise UserError("Field 'departement_type' wajib diisi sebelum approval.") - - approver_id = rec._get_departement_approver() - - if rec.status == 'pengajuan1': - if self.env.user.id != approver_id: - raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") - rec.name_approval_departement = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement - rec.date_approved_department = now - - # Mapping posisi berdasarkan departement_type - department_titles = { - 'sales': 'Sales Manager', - 'merchandiser': 'Merchandiser Manager', - 'marketing': 'Marketing Manager', - 'logistic': 'Logistic Manager', - 'procurement': 'Procurement Manager', - 'fat': 'Finance & Accounting Manager', - 'hr_ga': 'HR & GA Manager', - } - rec.position_department = department_titles.get(rec.departement_type, 'Departement Manager') - - rec.status = 'pengajuan2' - - rec.message_post( - body=f"Approval <b>Departement</b> oleh <b>{self.env.user.name}</b> " - f"pada <i>{formatted_date}</i>." - ) - - elif rec.status == 'pengajuan2': - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - if self.env.user.id not in ap_user_ids: - raise UserError("Hanya AP yang berhak menyetujui tahap ini.") - rec.name_approval_ap = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap - rec.email_ap = self.env.user.email - rec.date_approved_ap = now - rec.position_ap = 'Finance AP' - rec.status = 'pengajuan3' - - rec.message_post( - body=f"Approval <b>AP</b> oleh <b>{self.env.user.name}</b> " - f"pada <i>{formatted_date}</i>." - ) - - 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 <b>Pimpinan</b> oleh <b>{self.env.user.name}</b> " - f"pada <i>{formatted_date}</i>." - ) - - else: - raise UserError("Status saat ini tidak bisa di-approve lagi.") - - # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap <b>{rec.status}</b>.") - - - def action_reject(self): - return { - 'type': 'ir.actions.act_window', - 'name': 'Alasan Penolakan', - 'res_model': 'reject.reason.downpayment', - 'view_mode': 'form', - 'target': 'new', - 'context': {'default_request_id': self.id}, - } - - def action_draft(self): - for record in self: - # Pastikan hanya yang statusnya 'reject' yang bisa di-reset - if record.status != 'reject': - raise UserError("Hanya data dengan status 'Reject' yang bisa dikembalikan ke Draft atau status sebelumnya.") - - # Jika ada last_status, gunakan itu; jika tidak, fallback ke 'draft' - new_status = 'pengajuan1' - - # Reset field-field approval & alasan reject - record.write({ - 'status': new_status, - 'reason_reject': False, - 'last_status': False, - 'name_approval_departement': False, - 'name_approval_ap': False, - 'name_approval_pimpinan': False, - 'date_approved_department': False, - 'date_approved_ap': False, - 'date_approved_pimpinan': False, - 'position_department': False, - 'position_ap': False, - 'position_pimpinan': False, - }) - - record.message_post(body=f"Status dikembalikan ke <b>{new_status.capitalize()}</b> oleh {self.env.user.name}.") - - - def action_ap_only(self): - self.ensure_one() - - ap_user_ids = [23, 9468] # Ganti sesuai kebutuhan - # if self.env.user.id not in ap_user_ids: - # raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') - - if self.move_id: - raise UserError('CAB / Jurnal sudah pernah dibuat untuk PUM ini.') - - return { - 'name': 'Create CAB AP Only', - 'type': 'ir.actions.act_window', - 'res_model': 'down.payment.ap.only', - 'view_mode': 'form', - 'target': 'new', - 'context': { - 'default_nominal': self.nominal, - 'default_down_payment_id': self.id, - } - } - - - @api.depends('date_back_to_office', 'status') - def _compute_days_remaining(self): - today = date.today() - for rec in self: - if rec.status in ['approved', 'reject'] and rec.days_remaining: - continue - - if rec.date_back_to_office: - due_date = rec.date_back_to_office + timedelta(days=7) - rec.estimated_return_date = due_date - - # Jika hari ini sebelum tanggal kembali, maka anggap belum mulai dihitung - effective_today = max(today, rec.date_back_to_office) - rec.days_remaining = (due_date - effective_today).days - else: - rec.estimated_return_date = False - rec.days_remaining = 0 - - @api.onchange('date_back_to_office') - def _onchange_date_back_to_office(self): - if self.date_back_to_office and self.date_back_to_office < date.today(): - return { - 'warning': { - 'title': _('Tanggal Tidak Valid'), - 'message': _('Tanggal kembali ke kantor tidak boleh lebih awal dari hari ini.') - } - } - - @api.onchange('applicant_name') - def _onchange_applicant_name(self): - if self.applicant_name: - self.account_name = self.applicant_name - - @api.onchange('account_name') - def _onchange_account_name(self): - if self.account_name: - self.applicant_name = self.account_name - - @api.onchange('user_id') - def _onchange_user_id_limit_check(self): - if not self.user_id: - return - - pum_ids = self.search([ - ('user_id', '=', self.user_id.id), - ('status', '!=', 'reject') - ]) - - active_pum_count = 0 - for pum in pum_ids: - realization = self.env['realization.down.payment'].search([('pum_id', '=', pum.id)], limit=1) - if not realization or realization.done_status != 'done_not_realized': - active_pum_count += 1 - - if active_pum_count >= 2: - return { - 'warning': { - 'title': 'Batas Pengajuan Tercapai', - 'message': 'User ini sudah memiliki 2 PUM aktif. Tidak dapat mengajukan lagi sampai salah satu direalisasi.', - } - } - - @api.model - def create(self, vals): - user = self.env.user - - pum_ids = self.search([ - ('user_id', '=', user.id), - ('status', '!=', 'reject') - ]) - - active_pum_count = 0 - for pum in pum_ids: - realization = self.env['realization.down.payment'].search([('pum_id', '=', pum.id)], limit=1) - if not realization or realization.done_status != 'done_not_realized': - active_pum_count += 1 - - if active_pum_count >= 2: - raise UserError("Anda hanya dapat mengajukan maksimal 2 PUM aktif. Silakan realisasikan salah satunya terlebih dahulu.") - - if not vals.get('number') or vals['number'] == 'New Draft': - vals['number'] = self.env['ir.sequence'].next_by_code('down.payment') or 'New Draft' - - vals['status'] = 'pengajuan1' - return super(DownPayment, self).create(vals) - - -class RealizationDownPaymentLine(models.Model): - _name = 'realization.down.payment.line' - _description = 'Rincian Pemberian PUM' - - realization_id = fields.Many2one('realization.down.payment', string='Realization') - date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) - description = fields.Char(string='Description', required=True) - value = fields.Float(string='Nilai', required=True) - - -class RealizationDownPaymentUseLine(models.Model): - _name = 'realization.down.payment.use.line' - _description = 'Rincian Penggunaan PUM' - - realization_id = fields.Many2one('realization.down.payment', string='Realization') - date = fields.Date(string='Tanggal', required=True, default=fields.Date.today) - description = fields.Char(string='Description', required=True) - nominal = fields.Float(string='Nominal', required=True) - done_attachment = fields.Boolean(string='Checked', default=False) - - lot_of_attachment = fields.Selection( - related='realization_id.lot_of_attachment', - string='Lot of Attachment (Related)', - store=False - ) - - attachment_type = fields.Selection([ - ('pdf', 'PDF'), - ('image', 'Image'), - ], string="Attachment Type", default='pdf') - - attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') - attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') - attachment_filename_image = fields.Char(string='Filename Image') - attachment_filename_pdf = fields.Char(string='Filename PDF') - - account_id = fields.Many2one( - 'account.account', string='Jenis Biaya', required=True, - domain="[('id', 'in', [484, 486, 488, 506, 507, 625, 471, 519, 527, 528, 529, 530, 565])]" # ID Jenis Biaya yang dibutuhkan - ) - - @api.onchange('account_id') - def _onchange_account_id(self): - for rec in self: - if rec.account_id: - rec.description = rec.account_id.name + " - " - - @api.onchange('attachment_type') - def _onchange_attachment_type(self): - self.attachment_file_image = False - self.attachment_filename_image = False - self.attachment_file_pdf = False - self.attachment_filename_pdf = False - - @api.onchange('done_attachment') - def _onchange_done_attachment(self): - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - - if self.done_attachment and self.env.user.id not in ap_user_ids: - self.done_attachment = False - return { - 'warning': { - 'title': _('Tidak Diizinkan'), - 'message': _('Hanya user AP yang bisa mencentang Done Attachment.') - } - } - - @api.onchange('nominal') - def _onchange_nominal_no_minus(self): - if self.nominal and self.nominal < 0: - self.nominal = 0 - return { - 'warning': { - 'title': _('Nominal Tidak Valid'), - 'message': _( - "Nominal penggunaan PUM tidak boleh diisi minus.\n" - "Nilai di Set menjadi nol." - ) - } - } - -class RealizationDownPayment(models.Model): - _name = 'realization.down.payment' - _description = 'Realization Down Payment Management' - _inherit = ['mail.thread'] - - pum_id = fields.Many2one('down.payment', string='No PUM') - name = fields.Char(string='Nama', readonly=True, tracking=3) - title = fields.Char(string='Judul', tracking=3) - goals = fields.Text(string='Tujuan', tracking=3) - related = fields.Char(string='Terkait', tracking=3) - - pemberian_line_ids = fields.One2many( - 'realization.down.payment.line', 'realization_id', string='Rincian Pemberian' - ) - penggunaan_line_ids = fields.One2many( - 'realization.down.payment.use.line', 'realization_id', string='Rincian Penggunaan' - ) - - grand_total = fields.Float(string='Grand Total Pemberian', tracking=3, compute='_compute_grand_total') - grand_total_use = fields.Float(string='Grand Total Penggunaan', tracking=3, compute='_compute_grand_total_use') - value_down_payment = fields.Float(string='PUM', tracking=3) - remaining_value = fields.Float(string='Sisa Uang PUM', tracking=3, compute='_compute_remaining_value') - - note_approval = fields.Text(string='Note Persetujuan', tracking=3) - - name_approval_departement = fields.Char(string='Approval Departement', tracking=True) - name_approval_ap = fields.Char(string='Approval AP', tracking=True) - name_approval_pimpinan = fields.Char(string='Approval Pimpinan', tracking=True) - - date_approved_department = fields.Datetime(string="Date Approved Department") - date_approved_ap = fields.Datetime(string="Date Approved AP") - date_approved_pimpinan = fields.Datetime(string="Date Approved Pimpinan") - - position_department = fields.Char(string='Position Departement', tracking=True) - position_ap = fields.Char(string='Position AP', tracking=True) - position_pimpinan = fields.Char(string='Position Pimpinan', tracking=True) - - approved_by = fields.Char(string='Approved By', tracking=True, track_visibility='always') - - status = fields.Selection([ - ('pengajuan1', 'Menunggu Approval Departement'), - ('pengajuan2', 'Menunggu Pengecekan AP'), - ('pengajuan3', 'Menunggu Approval Pimpinan'), - ('approved', 'Approved'), - ], string='Status', default='pengajuan1', tracking=3, index=True, track_visibility='onchange') - - done_status = fields.Selection([ - ('remaining', 'Remaining'), - ('done_not_realized', 'Done Not Realized'), - ('done_realized', 'Done Realized') - ], string='Status Realisasi', tracking=3, default='remaining') - - date_done_not_realized = fields.Date(string='Tanggal Done Not Realized', tracking=3) - - currency_id = fields.Many2one( - 'res.currency', string='Currency', - default=lambda self: self.env.company.currency_id - ) - - attachment_file_image = fields.Binary(string='Attachment Image', attachment_filename='attachment_filename_image') - attachment_file_pdf = fields.Binary(string='Attachment PDF', attachment_filename='attachment_filename_pdf') - attachment_filename_image = fields.Char(string='Filename Image') - attachment_filename_pdf = fields.Char(string='Filename PDF') - - attachment_type = fields.Selection([ - ('pdf', 'PDF'), - ('image', 'Image'), - ], string="Attachment Type", default='pdf') - - lot_of_attachment = fields.Selection([ - ('one_for_all_line', '1 Attachment Untuk Semua Line Penggunaan PUM'), - ('one_for_one_line', '1 Attachment per 1 Line Penggunaan PUM'), - ], string = "Banyaknya Attachment", default='one_for_one_line') - - move_id = fields.Many2one('account.move', string='Journal Entries', domain=[('move_type', '=', 'entry')]) - is_cab_visible = fields.Boolean(string='Is Journal Uang Muka Visible', compute='_compute_is_cab_visible') - - def action_toggle_check_attachment(self): - ap_user_ids = [23, 9468] - if self.env.user.id not in ap_user_ids: - raise UserError('Hanya User AP yang dapat menggunakan tombol ini.') - - for rec in self: - if not rec.penggunaan_line_ids: - continue - - if all(line.done_attachment for line in rec.penggunaan_line_ids): - for line in rec.penggunaan_line_ids: - line.done_attachment = False - else: - for line in rec.penggunaan_line_ids: - line.done_attachment = True - - @api.onchange('lot_of_attachment') - def _onchange_lot_of_attachment(self): - if self.lot_of_attachment == 'one_for_all_line': - for line in self.penggunaan_line_ids: - line.attachment_file_pdf = False - line.attachment_file_image = False - line.attachment_filename_pdf = False - line.attachment_filename_image = False - - - @api.depends('move_id.state') - def _compute_is_cab_visible(self): - for rec in self: - move = rec.move_id - rec.is_cab_visible = bool(move and move.state == 'posted') - - def action_view_journal_uangmuka(self): - self.ensure_one() - - ap_user_ids = [23, 9468] - if self.env.user.id not in ap_user_ids: - raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') - - if not self.move_id: - raise UserError("Journal Uang Muka belum tersedia.") - - return { - 'name': 'Journal Entry', - 'view_mode': 'form', - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'res_id': self.move_id.id, - 'target': 'current', - } - - - @api.onchange('attachment_type') - def _onchange_attachment_type(self): - self.attachment_file_image = False - self.attachment_filename_image = False - self.attachment_file_pdf = False - self.attachment_filename_pdf = False - - @api.depends('pemberian_line_ids.value') - def _compute_grand_total(self): - for rec in self: - rec.grand_total = sum(line.value for line in rec.pemberian_line_ids) - - @api.depends('penggunaan_line_ids.nominal') - def _compute_grand_total_use(self): - for rec in self: - rec.grand_total_use = sum(line.nominal for line in rec.penggunaan_line_ids) - - @api.depends('grand_total', 'grand_total_use') - def _compute_remaining_value(self): - for rec in self: - rec.remaining_value = rec.value_down_payment - rec.grand_total_use - - def action_validation(self): - self.ensure_one() - - # Validasi hanya AP yang bisa validasi - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - if self.env.user.id not in ap_user_ids: - raise UserError('Hanya AP yang dapat melakukan validasi realisasi.') - - if self.done_status == 'remaining': - self.done_status = 'done_not_realized' - self.date_done_not_realized = fields.Date.today() - elif self.done_status == 'done_not_realized': - self.done_status = 'done_realized' - else: - raise UserError('Realisasi sudah berstatus Done Realized.') - - # Opsional: Tambah log di chatter - self.message_post(body=f"Status realisasi diperbarui menjadi <b>{dict(self._fields['done_status'].selection).get(self.done_status)}</b> oleh {self.env.user.name}.") - - def action_cab(self): - self.ensure_one() - - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - if self.env.user.id not in ap_user_ids: - raise UserError('Hanya User AP yang dapat menggunakan ini.') - if self.move_id: - raise UserError("CAB / Jurnal sudah pernah dibuat untuk Realisasi ini.") - - if not self.pum_id or not self.pum_id.move_id: - raise UserError("PUM terkait atau CAB belum tersedia.") - - partner_id = self.pum_id.user_id.partner_id.id - cab_move = self.pum_id.move_id - - # Account Bank Intransit dari CAB: - bank_intransit_line = cab_move.line_ids.filtered(lambda l: l.account_id.id in [573, 389, 392]) - if not bank_intransit_line: - raise UserError("Account Bank Intransit dengan tidak ditemukan di CAB terkait.") - account_sisa_pum = bank_intransit_line[0].account_id.id - - # Account Uang Muka Operasional - account_uang_muka = 403 - - # Tanggal pakai create_date atau hari ini - account_date = self.date_done_not_realized or fields.Date.today() - - ref_label = f"Realisasi {self.pum_id.number} Biaya {self.pum_id.detail_note} ({cab_move.name})" - - label_sisa_pum = f"Sisa PUM {self.pum_id.detail_note} {self.pum_id.number} ({cab_move.name})" - - lines = [] - - # Sisa PUM (Debit) - if self.remaining_value > 0: - lines.append((0, 0, { - 'account_id': account_sisa_pum, - 'partner_id': partner_id, - 'name': label_sisa_pum, - 'debit': self.remaining_value, - 'credit': 0, - })) - - # Biaya Penggunaan (Debit) - total_biaya = 0 - for line in self.penggunaan_line_ids: - lines.append((0, 0, { - 'account_id': line.account_id.id, - 'partner_id': partner_id, - 'name': f"{line.description} ({line.date})", - 'debit': line.nominal, - 'credit': 0, - })) - total_biaya += line.nominal - - # Uang Muka Operasional (Credit) - total_credit = self.remaining_value + total_biaya - if total_credit > 0: - lines.append((0, 0, { - 'account_id': account_uang_muka, - 'partner_id': partner_id, - 'name': ref_label, - 'debit': 0, - 'credit': total_credit, - })) - - move = self.env['account.move'].create({ - 'ref': ref_label, - 'date': account_date, - 'journal_id': 11, # MISC - 'line_ids': lines, - }) - - # self.message_post(body=f"Jurnal CAB telah dibuat dengan nomor: <b>{move.name}</b>.") - - self.move_id = move.id - - return { - 'name': _('Journal Entry'), - 'view_mode': 'form', - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'res_id': move.id, - 'target': 'current', - } - - def action_approval_check(self): - jakarta_tz = pytz.timezone('Asia/Jakarta') - now = datetime.now(jakarta_tz).replace(tzinfo=None) - formatted_date = now.strftime('%d %B %Y %H:%M') - - for rec in self: - if not rec.pum_id.departement_type: - raise UserError("Field 'departement_type' wajib diisi sebelum approval.") - - approver_id = rec.pum_id._get_departement_approver() - - if rec.status == 'pengajuan1': - if self.env.user.id != approver_id: - raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") - rec.name_approval_departement = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement - rec.date_approved_department = now - - # Mapping posisi berdasarkan departement_type - department_titles = { - 'sales': 'Sales Manager', - 'merchandiser': 'Merchandiser Manager', - 'marketing': 'Marketing Manager', - 'logistic': 'Logistic Manager', - 'procurement': 'Procurement Manager', - 'fat': 'Finance & Accounting Manager', - 'hr_ga': 'HR & GA Manager', - } - rec.position_department = department_titles.get(rec.pum_id.departement_type, 'Departement Manager') - - rec.status = 'pengajuan2' - - rec.message_post( - body=f"Approval <b>Departement</b> oleh <b>{self.env.user.name}</b> " - f"pada <i>{formatted_date}</i>." - ) - - elif rec.status == 'pengajuan2': - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - if self.env.user.id not in ap_user_ids: - raise UserError("Hanya AP yang berhak menyetujui tahap ini.") - rec.name_approval_ap = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap - rec.date_approved_ap = now - rec.position_ap = 'Finance AP' - rec.status = 'pengajuan3' - - rec.message_post( - body=f"Approval <b>AP</b> oleh <b>{self.env.user.name}</b> " - f"pada <i>{formatted_date}</i>." - ) - - elif rec.status == 'pengajuan3': - if self.env.user.id != 7: # ID user Pimpinan - raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") - rec.name_approval_pimpinan = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan - rec.date_approved_pimpinan = now - rec.position_pimpinan = 'Pimpinan' - rec.status = 'approved' - rec.done_status = 'done_not_realized' # Set status done untuk realisasi - - rec.message_post( - body=f"Approval <b>Pimpinan</b> oleh <b>{self.env.user.name}</b> " - f"pada <i>{formatted_date}</i>." - ) - - else: - raise UserError("Status saat ini tidak bisa di-approve lagi.") - - # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap <b>{rec.status}</b>.") - - def _check_remaining_value(self): - for rec in self: - # Cek sisa PUM - if rec.remaining_value < 0: - raise ValidationError( - "Sisa uang PUM tidak boleh kurang dari 0.\n" - "Jika ada penggunaan uang pribadi, maka ajukan dengan sistem reimburse." - ) - - @api.model - def create(self, vals): - rec = super().create(vals) - rec._check_remaining_value() - return rec - - def write(self, vals): - res = super().write(vals) - self._check_remaining_value() - return res - -class RejectReasonDownPayment(models.TransientModel): - _name = 'reject.reason.downpayment' - _description = 'Wizard for Reject Reason Down Payment' - - request_id = fields.Many2one('down.payment', string='Pengajuan PUM') - reason_reject = fields.Text(string='Alasan Penolakan', required=True) - - def confirm_reject(self): - if self.request_id: - self.request_id.write({ - 'status': 'reject', - 'last_status': self.request_id.status, - 'reason_reject': self.reason_reject, - }) - return {'type': 'ir.actions.act_window_close'} - -class DownPaymentApOnly(models.TransientModel): - _name = 'down.payment.ap.only' - _description = 'Create CAB from Down Payment for AP Only' - - down_payment_id = fields.Many2one('down.payment', string='Down Payment', required=True) - account_id = fields.Many2one( - 'account.account', string='Bank Intransit', required=True, - domain="[('id', 'in', [573, 389, 392])]" # ID Bank Intransit - ) - nominal = fields.Float(string='Nominal', related='down_payment_id.nominal') - - def action_create_cab(self): - self.ensure_one() - - # if self.env.user.id != 23: - # raise UserError('Hanya AP yang dapat menggunakan ini.') - - dp = self.down_payment_id - partner_id = dp.user_id.partner_id.id - - ref_label = f'{dp.number} - Biaya {dp.detail_note or "-"}' - - move = self.env['account.move'].create({ - 'ref': ref_label, - 'date': fields.Date.context_today(self), - 'journal_id': 11, # Cash & Bank - 'line_ids': [ - (0, 0, { - 'account_id': 403, # Uang Muka Operasional - 'partner_id': partner_id, - 'name': ref_label, - 'debit': dp.nominal, - 'credit': 0, - }), - (0, 0, { - 'account_id': self.account_id.id, # Bank Intransit yang dipilih - 'partner_id': partner_id, - 'name': ref_label, - 'debit': 0, - 'credit': dp.nominal, - }) - ] - }) - - dp.move_id = move.id # jika ada field untuk menampung move_id - - return { - 'name': _('Journal Entry'), - 'view_mode': 'form', - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'res_id': move.id, - 'target': 'current', - } - diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 79a4a3e0..63d7e726 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -31,7 +31,8 @@ class SuratPiutang(models.Model): line_ids = fields.One2many("surat.piutang.line", "surat_id", string="Invoice Lines") state = fields.Selection([ ("draft", "Draft"), - ("waiting_approval", "Menunggu Approval"), + ("waiting_approval_sales", "Menunggu Approval Sales Manager"), + ("waiting_approval_pimpinan", "Menunggu Approval Pimpinan"), ("sent", "Approved & Sent") ], default="draft", tracking=True) send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) @@ -55,8 +56,9 @@ class SuratPiutang(models.Model): compute="_compute_grand_total_text", ) - perihal_label = fields.Char( - compute="_compute_perihal_label", string="Perihal Label") + perihal_label = fields.Char(compute="_compute_perihal_label", string="Perihal Label") + + payment_difficulty = fields.Selection(string="Payment Difficulty", related='partner_id.payment_difficulty', readonly=True) def _compute_perihal_label(self): for rec in self: @@ -248,8 +250,8 @@ class SuratPiutang(models.Model): order='new_invoice_day_to_due asc' ) selected_invoice_id = self.env.context.get('default_selected_invoice_id') - lines = [(0, 0, { - # 'invoice_view_id': inv.id, + lines = [(5, 0, 0)] # hapus semua line lama + lines += [(0, 0, { 'invoice_id': inv.invoice_id.id, 'invoice_number': inv.invoice_number, 'invoice_date': inv.invoice_date, diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py index 25e04968..3eb6efc7 100644 --- a/indoteknik_custom/models/unpaid_invoice_view.py +++ b/indoteknik_custom/models/unpaid_invoice_view.py @@ -34,6 +34,13 @@ class UnpaidInvoiceView(models.Model): date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur') sale_id = fields.Many2one('sale.order', string='Sale Order') + payment_difficulty = fields.Selection([ + ('bermasalah', 'Bermasalah'), + ('sulit', 'Sulit'), + ('agak_sulit', 'Agak Sulit'), + ('normal', 'Normal'), + ], string="Payment Difficulty") + def action_create_surat_piutang(self): self.ensure_one() return { diff --git a/indoteknik_custom/views/letter_receivable.xml b/indoteknik_custom/views/letter_receivable.xml index 3a7c2c65..f55d84e8 100644 --- a/indoteknik_custom/views/letter_receivable.xml +++ b/indoteknik_custom/views/letter_receivable.xml @@ -10,7 +10,7 @@ <field name="perihal"/> <field name="state" widget="badge" decoration-danger="state == 'draft'" - decoration-warning="state == 'waiting_approval'" + decoration-warning="state in ('waiting_approval_sales', 'waiting_approval_pimpinan')" decoration-success="state == 'sent'"/> <field name="send_date"/> <!-- <field name="line_ids" widget="one2many_list"/> --> @@ -25,7 +25,7 @@ <field name="arch" type="xml"> <form string="Surat Piutang"> <header> - <field name="state" widget="statusbar" statusbar_visible="draft,waiting_approval,sent"/> + <field name="state" widget="statusbar" statusbar_visible="draft,waiting_approval_sales,waiting_approval_pimpinan,sent"/> <button name="action_approve" type="object" string="Approve & Send" class="btn-primary"/> <!-- <button name="action_send_letter" type="object" string="Email Send" class="btn-primary"/> --> </header> @@ -58,6 +58,7 @@ <field name="tujuan_email"/> <field name="perihal"/> <field name="partner_id" options="{'no_create': True}"/> + <field name="payment_difficulty"/> <field name="send_date" readonly="1"/> </group> <group> diff --git a/indoteknik_custom/views/unpaid_invoice_view.xml b/indoteknik_custom/views/unpaid_invoice_view.xml index 20145a46..e56bbee7 100644 --- a/indoteknik_custom/views/unpaid_invoice_view.xml +++ b/indoteknik_custom/views/unpaid_invoice_view.xml @@ -21,6 +21,10 @@ decoration-danger="payment_state == 'not_paid'" decoration-warning="payment_state == 'partial'"/> <field name="invoice_user_id"/> + <field name="payment_difficulty" widget="badge" + decoration-info="payment_difficulty == 'normal'" + decoration-warning="payment_difficulty in ('agak_sulit', 'sulit')" + decoration-danger="payment_difficulty == 'bermasalah'"/> </tree> </field> </record> |
