From 8bb599fc4b3e3de94fada7c277518446a88630fa Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 1 Sep 2025 15:50:14 +0700 Subject: (andri) add view unpaid invoices --- indoteknik_custom/models/__init__.py | 3 ++- indoteknik_custom/models/unpaid_invoice_view.py | 28 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 indoteknik_custom/models/unpaid_invoice_view.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 3a9f9312..ba3dbad9 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -156,4 +156,5 @@ from . import refund_sale_order # from . import patch from . import tukar_guling from . import tukar_guling_po -from . import update_date_planned_po_wizard \ No newline at end of file +from . import update_date_planned_po_wizard +from . import unpaid_invoice_view \ No newline at end of file diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py new file mode 100644 index 00000000..77007102 --- /dev/null +++ b/indoteknik_custom/models/unpaid_invoice_view.py @@ -0,0 +1,28 @@ +from odoo import models, fields + +class UnpaidInvoiceView(models.Model): + _name = 'unpaid.invoice.view' + _description = 'Unpaid Invoices Monitoring' + _auto = False + _rec_name = 'partner_name' + + partner_id = fields.Many2one('res.partner', string='Partner') + partner_name = fields.Char(string='Partner Name') + email = fields.Char() + phone = fields.Char() + invoice_id = fields.Many2one('account.move', string='Invoice') + invoice_number = fields.Char(string='Invoice Number') + invoice_date = fields.Date() + invoice_date_due = fields.Date() + currency_id = fields.Many2one('res.currency', string='Currency') + amount_total = fields.Monetary(string='Total', currency_field='currency_id') + amount_residual = fields.Monetary(string='Residual', currency_field='currency_id') + payment_state = fields.Selection([ + ('not_paid','Not Paid'), + ('in_payment','In Payment'), + ('paid','Paid'), + ('reversed','Reversed') + ], string='Payment State') + payment_term = fields.Char() + invoice_day_to_due = fields.Integer(string="Day to Due") + new_invoice_day_to_due = fields.Integer(string="New Day Due") -- cgit v1.2.3 From 1330742f90b32c6dd214925ad893696cfae5ef38 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Mon, 1 Sep 2025 16:05:52 +0700 Subject: (andri) add string due date --- indoteknik_custom/models/unpaid_invoice_view.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py index 77007102..517d078b 100644 --- a/indoteknik_custom/models/unpaid_invoice_view.py +++ b/indoteknik_custom/models/unpaid_invoice_view.py @@ -5,6 +5,7 @@ class UnpaidInvoiceView(models.Model): _description = 'Unpaid Invoices Monitoring' _auto = False _rec_name = 'partner_name' + _order = 'partner_name, new_invoice_day_to_due DESC' partner_id = fields.Many2one('res.partner', string='Partner') partner_name = fields.Char(string='Partner Name') @@ -13,7 +14,7 @@ class UnpaidInvoiceView(models.Model): invoice_id = fields.Many2one('account.move', string='Invoice') invoice_number = fields.Char(string='Invoice Number') invoice_date = fields.Date() - invoice_date_due = fields.Date() + invoice_date_due = fields.Date(string='Due Date') currency_id = fields.Many2one('res.currency', string='Currency') amount_total = fields.Monetary(string='Total', currency_field='currency_id') amount_residual = fields.Monetary(string='Residual', currency_field='currency_id') -- cgit v1.2.3 From 0d0e2b9a448f6f96a7ab06ecb970dbbd2018f5fe Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 2 Sep 2025 14:19:33 +0700 Subject: (andri) fix layout dan revisi field yang dibutuhkan --- indoteknik_custom/models/unpaid_invoice_view.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py index 517d078b..f6ce79b8 100644 --- a/indoteknik_custom/models/unpaid_invoice_view.py +++ b/indoteknik_custom/models/unpaid_invoice_view.py @@ -9,21 +9,22 @@ class UnpaidInvoiceView(models.Model): partner_id = fields.Many2one('res.partner', string='Partner') partner_name = fields.Char(string='Partner Name') - email = fields.Char() - phone = fields.Char() + # email = fields.Char() + # phone = fields.Char() invoice_id = fields.Many2one('account.move', string='Invoice') invoice_number = fields.Char(string='Invoice Number') invoice_date = fields.Date() invoice_date_due = fields.Date(string='Due Date') + date_terima_tukar_faktur = fields.Date(string='Terima Faktur') currency_id = fields.Many2one('res.currency', string='Currency') - amount_total = fields.Monetary(string='Total', currency_field='currency_id') - amount_residual = fields.Monetary(string='Residual', currency_field='currency_id') + amount_total = fields.Monetary(string='Total Amount', currency_field='currency_id') + amount_residual = fields.Monetary(string='Sisa Amount', currency_field='currency_id') payment_state = fields.Selection([ ('not_paid','Not Paid'), ('in_payment','In Payment'), ('paid','Paid'), ('reversed','Reversed') ], string='Payment State') - payment_term = fields.Char() + payment_term_id = fields.Many2one('account.payment.term', string='Payment Term') invoice_day_to_due = fields.Integer(string="Day to Due") new_invoice_day_to_due = fields.Integer(string="New Day Due") -- cgit v1.2.3 From a35dcc5907500e5189516459160c972bb72e1686 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 2 Sep 2025 15:04:33 +0700 Subject: (andri) menambahkan field yang dibutuhkan --- indoteknik_custom/models/unpaid_invoice_view.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py index f6ce79b8..f35261eb 100644 --- a/indoteknik_custom/models/unpaid_invoice_view.py +++ b/indoteknik_custom/models/unpaid_invoice_view.py @@ -20,11 +20,16 @@ class UnpaidInvoiceView(models.Model): amount_total = fields.Monetary(string='Total Amount', currency_field='currency_id') amount_residual = fields.Monetary(string='Sisa Amount', currency_field='currency_id') payment_state = fields.Selection([ - ('not_paid','Not Paid'), - ('in_payment','In Payment'), - ('paid','Paid'), - ('reversed','Reversed') - ], string='Payment State') + ('not_paid', 'Not Paid'), + ('in_payment', 'In Payment'), + ('paid', 'Paid'), + ('partial', 'Partially Paid'), + ('reversed', 'Reversed')], string='Payment State') payment_term_id = fields.Many2one('account.payment.term', string='Payment Term') invoice_day_to_due = fields.Integer(string="Day to Due") new_invoice_day_to_due = fields.Integer(string="New Day Due") + + ref = fields.Char(string='Reference') + invoice_user_id = fields.Many2one('res.users', string='Salesperson') + date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur') + sale_id = fields.Many2one('sale.order', string='Sale Order') -- cgit v1.2.3 From d43eb7ff8ffd4b11120a7354295e86736135344a Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Tue, 2 Sep 2025 17:35:47 +0700 Subject: (andri) add menu surat piutang --- indoteknik_custom/models/__init__.py | 3 +- indoteknik_custom/models/letter_receivable.py | 90 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 indoteknik_custom/models/letter_receivable.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index ba3dbad9..c8910669 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -157,4 +157,5 @@ from . import refund_sale_order from . import tukar_guling from . import tukar_guling_po from . import update_date_planned_po_wizard -from . import unpaid_invoice_view \ No newline at end of file +from . import unpaid_invoice_view +from . import letter_receivable \ No newline at end of file diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py new file mode 100644 index 00000000..541f64ff --- /dev/null +++ b/indoteknik_custom/models/letter_receivable.py @@ -0,0 +1,90 @@ +from odoo import models, fields, api + +class SuratPiutang(models.Model): + _name = "surat.piutang" + _description = "Surat Piutang" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + name = fields.Char(string="Nomor Surat", readonly=True, copy=False) + partner_id = fields.Many2one("res.partner", string="Customer", required=True) + tujuan_nama = fields.Char(string="Nama Tujuan") + tujuan_email = fields.Char(string="Email Tujuan") + perihal = fields.Selection([ + ('sp1', 'Surat Peringatan Piutang'), + ('sp2', 'Surat Peringatan Piutang ke-2'), + ('sp3', 'Surat Peringatan Piutang ke-3') + ], string="Perihal", required=True, tracking=True) + line_ids = fields.One2many("surat.piutang.line", "surat_id", string="Invoice Lines") + state = fields.Selection([ + ("draft", "Draft"), + ("sent", "Sent") + ], default="draft", tracking=True) + send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) + + currency_id = fields.Many2one('res.currency') + + # Grand total (total sisa semua line yang dicentang) + grand_total = fields.Monetary( + string='Total Sisa', + currency_field='currency_id', + compute='_compute_grand_total', + ) + + @api.depends('line_ids.amount_residual', 'line_ids.selected') + def _compute_grand_total(self): + for rec in self: + rec.grand_total = sum( + line.amount_residual or 0.0 for line in rec.line_ids if line.selected + ) + + @api.onchange('partner_id') + def _onchange_partner_id(self): + if self.partner_id: + invoice_lines = self.env['unpaid.invoice.view'].search( + [('partner_id', '=', self.partner_id.id)], + order='new_invoice_day_to_due asc' + ) + lines = [(0, 0, { + 'invoice_view_id': inv.id, + 'invoice_id': inv.invoice_id.id, + 'invoice_number': inv.invoice_number, + 'invoice_date': inv.invoice_date, + 'invoice_date_due': inv.invoice_date_due, + 'invoice_day_to_due': inv.invoice_day_to_due, + 'ref': inv.ref, + 'amount_residual': inv.amount_residual, + 'currency_id': inv.currency_id.id, + 'payment_term_id': inv.payment_term_id.id, + 'selected': False + }) for inv in invoice_lines] + self.line_ids = lines + + @api.model + def create(self, vals): + # Generate nomor surat otomatis + if not vals.get("name"): + seq = self.env["ir.sequence"].next_by_code("surat.piutang") or "000" + today = fields.Date.today() + bulan_romawi = ["I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII"][today.month-1] + tahun = today.strftime("%y") + vals["name"] = f"{seq}/LO/FAT/IDG/{bulan_romawi}/{tahun}" + + return super().create(vals) + +class SuratPiutangLine(models.Model): + _name = 'surat.piutang.line' + _description = 'Surat Piutang Line' + + surat_id = fields.Many2one('surat.piutang', string='Surat Piutang', ondelete='cascade') + invoice_view_id = fields.Many2one('unpaid.invoice.view', string='Unpaid Invoice') + invoice_id = fields.Many2one('account.move', string='Invoice') + selected = fields.Boolean(string="Pilih", default=False) + + invoice_number = fields.Char(string='Invoice Number') + invoice_date = fields.Date(string='Invoice Date') + invoice_date_due = fields.Date(string='Due Date') + invoice_day_to_due = fields.Integer(string='Day to Due') + ref = fields.Char(string='Reference') + amount_residual = fields.Monetary(string='Amount Due Signed') + currency_id = fields.Many2one('res.currency') + payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms') -- cgit v1.2.3 From 1c71710dbf42106a82c0a8e30ec9cee7f452a387 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 3 Sep 2025 11:07:40 +0700 Subject: (andri) add refresh invoices agar data selalu update & fix layout form --- indoteknik_custom/models/letter_receivable.py | 61 ++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 541f64ff..10198fbf 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -4,19 +4,22 @@ class SuratPiutang(models.Model): _name = "surat.piutang" _description = "Surat Piutang" _inherit = ['mail.thread', 'mail.activity.mixin'] + _order = 'name desc' name = fields.Char(string="Nomor Surat", readonly=True, copy=False) partner_id = fields.Many2one("res.partner", string="Customer", required=True) tujuan_nama = fields.Char(string="Nama Tujuan") tujuan_email = fields.Char(string="Email Tujuan") perihal = fields.Selection([ - ('sp1', 'Surat Peringatan Piutang'), + ('penagihan', 'Surat Resmi Penagihan'), + ('sp1', 'Surat Peringatan Piutang ke-1'), ('sp2', 'Surat Peringatan Piutang ke-2'), ('sp3', 'Surat Peringatan Piutang ke-3') ], string="Perihal", required=True, tracking=True) line_ids = fields.One2many("surat.piutang.line", "surat_id", string="Invoice Lines") state = fields.Selection([ ("draft", "Draft"), + ("approval_pimpinan", "Menunggu Approval Pimpinan"), ("sent", "Sent") ], default="draft", tracking=True) send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) @@ -59,6 +62,62 @@ class SuratPiutang(models.Model): }) for inv in invoice_lines] self.line_ids = lines + def action_refresh_lines(self): + for rec in self: + if not rec.partner_id: + continue + + # Ambil semua unpaid terbaru + invoice_views = self.env['unpaid.invoice.view'].search( + [('partner_id', '=', rec.partner_id.id)], + order='new_invoice_day_to_due asc' + ) + + existing_lines = {line.invoice_id.id: line for line in rec.line_ids} + + # Cache selected status per invoice id + selected_map = {line.invoice_id.id: line.selected for line in rec.line_ids} + + # Invoice id yang masih ada di unpaid + new_invoice_ids = invoice_views.mapped('invoice_id.id') + + for inv in invoice_views: + if inv.invoice_id.id in existing_lines: + # update line lama + line = existing_lines[inv.invoice_id.id] + line.write({ + 'invoice_view_id': inv.id, + 'invoice_number': inv.invoice_number, + 'invoice_date': inv.invoice_date, + 'invoice_date_due': inv.invoice_date_due, + 'invoice_day_to_due': inv.invoice_day_to_due, + 'ref': inv.ref, + 'amount_residual': inv.amount_residual, + 'currency_id': inv.currency_id.id, + 'payment_term_id': inv.payment_term_id.id, + 'selected': selected_map.get(inv.invoice_id.id, line.selected), + }) + else: + # preserve selected kalau pernah ada di cache + self.env['surat.piutang.line'].create({ + 'surat_id': rec.id, + 'invoice_view_id': inv.id, + 'invoice_id': inv.invoice_id.id, + 'invoice_number': inv.invoice_number, + 'invoice_date': inv.invoice_date, + 'invoice_date_due': inv.invoice_date_due, + 'invoice_day_to_due': inv.invoice_day_to_due, + 'ref': inv.ref, + 'amount_residual': inv.amount_residual, + 'currency_id': inv.currency_id.id, + 'payment_term_id': inv.payment_term_id.id, + 'selected': selected_map.get(inv.invoice_id.id, False), + }) + + # Hapus line yang tidak ada lagi di unpaid view + rec.line_ids.filtered(lambda l: l.invoice_id.id not in new_invoice_ids).unlink() + + @api.model def create(self, vals): # Generate nomor surat otomatis -- cgit v1.2.3 From 4aae32a042a5f77feebfa7e4f504f32a5375eaae Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 3 Sep 2025 13:52:09 +0700 Subject: (andri) add validasi email, terbilang, & approval pimpinan --- indoteknik_custom/models/letter_receivable.py | 39 ++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 10198fbf..550aa9e3 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -1,4 +1,8 @@ -from odoo import models, fields, api +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from odoo.exceptions import ValidationError +from odoo.tools import mail +from terbilang import Terbilang class SuratPiutang(models.Model): _name = "surat.piutang" @@ -33,6 +37,23 @@ class SuratPiutang(models.Model): compute='_compute_grand_total', ) + grand_total_text = fields.Char( + string="Total Terbilang", + compute="_compute_grand_total_text", + ) + + def _compute_grand_total_text(self): + tb = Terbilang() + for record in self: + res = "" + if record.grand_total and record.grand_total > 0: + try: + tb.parse(int(record.grand_total)) + res = tb.getresult().title() + " Rupiah" + except Exception: + res = "" + record.grand_total_text = res + @api.depends('line_ids.amount_residual', 'line_ids.selected') def _compute_grand_total(self): for rec in self: @@ -40,6 +61,21 @@ class SuratPiutang(models.Model): line.amount_residual or 0.0 for line in rec.line_ids if line.selected ) + @api.constrains("tujuan_email") + def _check_email_format(self): + for rec in self: + if rec.tujuan_email and not mail.single_email_re.match(rec.tujuan_email): + raise ValidationError(_("Format email tidak valid: %s") % rec.tujuan_email) + + def action_approve(self): + pimpinan_user_ids = [7] # Pak Akbar + if self.env.user.id not in pimpinan_user_ids: + raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") + for rec in self: + if rec.state == "approval_pimpinan": + rec.state = "sent" + rec.send_date = fields.Datetime.now() + @api.onchange('partner_id') def _onchange_partner_id(self): if self.partner_id: @@ -128,6 +164,7 @@ class SuratPiutang(models.Model): tahun = today.strftime("%y") vals["name"] = f"{seq}/LO/FAT/IDG/{bulan_romawi}/{tahun}" + vals["state"] = "approval_pimpinan" return super().create(vals) class SuratPiutangLine(models.Model): -- cgit v1.2.3 From 1b389be56daecec87008f791e856205d2234e053 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Wed, 3 Sep 2025 17:53:25 +0700 Subject: (andri) add template mail & try testing --- indoteknik_custom/models/letter_receivable.py | 102 +++++++++++++++++++++++++- 1 file changed, 101 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 550aa9e3..4159ecc2 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -1,8 +1,13 @@ from odoo import models, fields, api, _ from odoo.exceptions import UserError from odoo.exceptions import ValidationError -from odoo.tools import mail +from odoo.tools import mail, formatLang from terbilang import Terbilang +import re +import logging +from datetime import timedelta + +_logger = logging.getLogger(__name__) class SuratPiutang(models.Model): _name = "surat.piutang" @@ -27,6 +32,7 @@ class SuratPiutang(models.Model): ("sent", "Sent") ], default="draft", tracking=True) send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) + seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim", tracking=True) currency_id = fields.Many2one('res.currency') @@ -75,6 +81,100 @@ class SuratPiutang(models.Model): if rec.state == "approval_pimpinan": rec.state = "sent" rec.send_date = fields.Datetime.now() + # Format tanggal + bulan (tanpa tahun) + month_map = { + 1: "Januari", 2: "Februari", 3: "Maret", 4: "April", + 5: "Mei", 6: "Juni", 7: "Juli", 8: "Agustus", + 9: "September", 10: "Oktober", 11: "November", 12: "Desember", + } + if rec.send_date: + target_date = rec.send_date.date() + timedelta(days=7) + rec.seven_days_after_sent_date = f"{target_date.day} {month_map[target_date.month]}" + + def action_send_letter(self): + self.ensure_one() + + selected_lines = self.line_ids.filtered('selected') + if not selected_lines: + raise UserError(_("Tidak ada invoice yang dicentang untuk dikirim.")) + + if not self.tujuan_email: + raise UserError(_("Email tujuan harus diisi.")) + + template = self.env.ref('indoteknik_custom.letter_receivable_mail_template') + today = fields.Date.today() + + month_map = { + 1: "Januari", 2: "Februari", 3: "Maret", 4: "April", + 5: "Mei", 6: "Juni", 7: "Juli", 8: "Agustus", + 9: "September", 10: "Oktober", 11: "November", 12: "Desember", + } + target_date = (self.send_date or fields.Datetime.now()).date() + timedelta(days=7) + self.seven_days_after_sent_date = f"{target_date.day} {month_map[target_date.month]}" + + perihal_map = { + 'penagihan': 'Surat Resmi Penagihan', + 'sp1': 'Surat Peringatan Pertama (I)', + 'sp2': 'Surat Peringatan Kedua (II)', + 'sp3': 'Surat Peringatan Ketiga (III)', + } + perihal_text = perihal_map.get(self.perihal, self.perihal or '') + + invoice_table_rows = "" + grand_total = 0 + for line in selected_lines: + inv = line.invoice_id + if not inv: + continue + days_to_due = (line.invoice_date_due - today).days if line.invoice_date_due else 0 + grand_total += line.amount_residual + invoice_table_rows += f""" + + {line.invoice_number or '-'} + {self.partner_id.name or '-'} + {fields.Date.to_string(line.invoice_date) or '-'} + {fields.Date.to_string(line.invoice_date_due) or '-'} + {days_to_due} + {line.ref or '-'} + {formatLang(self.env, line.amount_residual, currency_obj=line.currency_id)} + {line.payment_term_id.name or '-'} + + """ + + invoice_table_footer = f""" + + + Grand Total + {formatLang(self.env, grand_total, currency_obj=self.currency_id)} + + + + """ + # inject table rows ke template + body_html = re.sub( + r"]*>.*?", + f"{invoice_table_rows}{invoice_table_footer}", + template.body_html, + flags=re.DOTALL + ).replace('${object.name}', self.name or '') \ + .replace('${object.partner_id.name}', self.partner_id.name or '') \ + .replace('${object.seven_days_after_sent_date}', self.seven_days_after_sent_date or '') \ + .replace('${object.perihal}', perihal_text or '') + + values = { + 'subject': template.subject.replace('${object.name}', self.name or ''), + 'email_to': self.tujuan_email, + 'email_from': 'finance@indoteknik.co.id', + 'body_html': body_html, + 'reply_to': 'finance@indoteknik.co.id', + } + + template.send_mail(self.id, force_send=True, email_values=values) + + _logger.info( + f"Surat Piutang {self.name} terkirim ke {self.tujuan_email} " + f"({self.partner_id.name}), total {len(selected_lines)} invoice." + ) @api.onchange('partner_id') def _onchange_partner_id(self): -- cgit v1.2.3 From 943394bf2a90931134317b88615f23900664dcf0 Mon Sep 17 00:00:00 2001 From: AndriFP Date: Sun, 7 Sep 2025 20:18:18 +0700 Subject: (andri) fix --- indoteknik_custom/models/letter_receivable.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 4159ecc2..5a793873 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -16,9 +16,9 @@ class SuratPiutang(models.Model): _order = 'name desc' name = fields.Char(string="Nomor Surat", readonly=True, copy=False) - partner_id = fields.Many2one("res.partner", string="Customer", required=True) - tujuan_nama = fields.Char(string="Nama Tujuan") - tujuan_email = fields.Char(string="Email Tujuan") + partner_id = fields.Many2one("res.partner", string="Customer", required=True, tracking=True) + tujuan_nama = fields.Char(string="Nama Tujuan", tracking=True) + tujuan_email = fields.Char(string="Email Tujuan", tracking=True) perihal = fields.Selection([ ('penagihan', 'Surat Resmi Penagihan'), ('sp1', 'Surat Peringatan Piutang ke-1'), @@ -123,9 +123,6 @@ class SuratPiutang(models.Model): invoice_table_rows = "" grand_total = 0 for line in selected_lines: - inv = line.invoice_id - if not inv: - continue days_to_due = (line.invoice_date_due - today).days if line.invoice_date_due else 0 grand_total += line.amount_residual invoice_table_rows += f""" @@ -279,6 +276,7 @@ class SuratPiutangLine(models.Model): invoice_number = fields.Char(string='Invoice Number') invoice_date = fields.Date(string='Invoice Date') invoice_date_due = fields.Date(string='Due Date') + invoice_new_day_to_due = fields.Integer(string='New Day to Due') invoice_day_to_due = fields.Integer(string='Day to Due') ref = fields.Char(string='Reference') amount_residual = fields.Monetary(string='Amount Due Signed') -- cgit v1.2.3 From 15b5f549777065dac76eaaa23598f09ae6a22f8f Mon Sep 17 00:00:00 2001 From: AndriFP Date: Mon, 8 Sep 2025 08:54:28 +0700 Subject: (andri) fix --- indoteknik_custom/models/letter_receivable.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 5a793873..d4921bc5 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -187,6 +187,7 @@ class SuratPiutang(models.Model): 'invoice_date': inv.invoice_date, 'invoice_date_due': inv.invoice_date_due, 'invoice_day_to_due': inv.invoice_day_to_due, + 'new_invoice_day_to_due': inv.invoice_day_to_due, 'ref': inv.ref, 'amount_residual': inv.amount_residual, 'currency_id': inv.currency_id.id, @@ -224,6 +225,7 @@ class SuratPiutang(models.Model): 'invoice_date': inv.invoice_date, 'invoice_date_due': inv.invoice_date_due, 'invoice_day_to_due': inv.invoice_day_to_due, + 'new_invoice_day_to_due': inv.invoice_day_to_due, 'ref': inv.ref, 'amount_residual': inv.amount_residual, 'currency_id': inv.currency_id.id, @@ -240,6 +242,7 @@ class SuratPiutang(models.Model): 'invoice_date': inv.invoice_date, 'invoice_date_due': inv.invoice_date_due, 'invoice_day_to_due': inv.invoice_day_to_due, + 'new_invoice_day_to_due': inv.invoice_day_to_due, 'ref': inv.ref, 'amount_residual': inv.amount_residual, 'currency_id': inv.currency_id.id, @@ -276,8 +279,8 @@ class SuratPiutangLine(models.Model): invoice_number = fields.Char(string='Invoice Number') invoice_date = fields.Date(string='Invoice Date') invoice_date_due = fields.Date(string='Due Date') - invoice_new_day_to_due = fields.Integer(string='New Day to Due') invoice_day_to_due = fields.Integer(string='Day to Due') + new_invoice_day_to_due = fields.Integer(string='New Day to Due') ref = fields.Char(string='Reference') amount_residual = fields.Monetary(string='Amount Due Signed') currency_id = fields.Many2one('res.currency') -- cgit v1.2.3 From 2b14a2678e3e2782c362065032cfcaba5b091b88 Mon Sep 17 00:00:00 2001 From: AndriFP Date: Mon, 8 Sep 2025 11:13:21 +0700 Subject: (andri) add compute char periode invoices --- indoteknik_custom/models/letter_receivable.py | 64 ++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index d4921bc5..caf62a64 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -6,6 +6,7 @@ from terbilang import Terbilang import re import logging from datetime import timedelta +import babel _logger = logging.getLogger(__name__) @@ -33,6 +34,10 @@ class SuratPiutang(models.Model): ], default="draft", tracking=True) send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim", tracking=True) + periode_invoices_terpilih = fields.Char( + string="Periode Invoices Terpilih", + compute="_compute_periode_invoices", + ) currency_id = fields.Many2one('res.currency') @@ -48,6 +53,34 @@ class SuratPiutang(models.Model): compute="_compute_grand_total_text", ) + @api.depends("line_ids.selected", "line_ids.invoice_date") + def _compute_periode_invoices(self): + for rec in self: + selected_lines = rec.line_ids.filtered(lambda l: l.selected and l.invoice_date) + if not selected_lines: + rec.periode_invoices_terpilih = "-" + continue + + dates = selected_lines.mapped("invoice_date") + min_date, max_date = min(dates), max(dates) + + # Ambil bagian bulan & tahun + min_month = babel.dates.format_date(min_date, "MMMM", locale="id_ID") + min_year = min_date.year + max_month = babel.dates.format_date(max_date, "MMMM", locale="id_ID") + max_year = max_date.year + + if min_year == max_year: + if min_month == max_month: + # example: Januari 2025 + rec.periode_invoices_terpilih = f"{min_month} {min_year}" + else: + # example: Mei s/d Juni 2025 + rec.periode_invoices_terpilih = f"{min_month} s/d {max_month} {max_year}" + else: + # example: Desember 2024 s/d Januari 2025 + rec.periode_invoices_terpilih = f"{min_month} {min_year} s/d {max_month} {max_year}" + def _compute_grand_total_text(self): tb = Terbilang() for record in self: @@ -181,17 +214,21 @@ class SuratPiutang(models.Model): order='new_invoice_day_to_due asc' ) lines = [(0, 0, { - 'invoice_view_id': inv.id, + # 'invoice_view_id': inv.id, 'invoice_id': inv.invoice_id.id, 'invoice_number': inv.invoice_number, 'invoice_date': inv.invoice_date, 'invoice_date_due': inv.invoice_date_due, 'invoice_day_to_due': inv.invoice_day_to_due, - 'new_invoice_day_to_due': inv.invoice_day_to_due, + 'new_invoice_day_to_due': inv.new_invoice_day_to_due, 'ref': inv.ref, 'amount_residual': inv.amount_residual, 'currency_id': inv.currency_id.id, 'payment_term_id': inv.payment_term_id.id, + 'date_kirim_tukar_faktur': inv.date_kirim_tukar_faktur, + 'date_terima_tukar_faktur': inv.date_terima_tukar_faktur, + 'invoice_user_id': inv.invoice_user_id.id, + 'sale_id': inv.sale_id.id, 'selected': False }) for inv in invoice_lines] self.line_ids = lines @@ -220,33 +257,41 @@ class SuratPiutang(models.Model): # update line lama line = existing_lines[inv.invoice_id.id] line.write({ - 'invoice_view_id': inv.id, + # 'invoice_view_id': inv.id, 'invoice_number': inv.invoice_number, 'invoice_date': inv.invoice_date, 'invoice_date_due': inv.invoice_date_due, 'invoice_day_to_due': inv.invoice_day_to_due, - 'new_invoice_day_to_due': inv.invoice_day_to_due, + 'new_invoice_day_to_due': inv.new_invoice_day_to_due, 'ref': inv.ref, 'amount_residual': inv.amount_residual, 'currency_id': inv.currency_id.id, 'payment_term_id': inv.payment_term_id.id, + 'date_kirim_tukar_faktur': inv.date_kirim_tukar_faktur, + 'date_terima_tukar_faktur': inv.date_terima_tukar_faktur, + 'invoice_user_id': inv.invoice_user_id.id, + 'sale_id': inv.sale_id.id, 'selected': selected_map.get(inv.invoice_id.id, line.selected), }) else: # preserve selected kalau pernah ada di cache self.env['surat.piutang.line'].create({ 'surat_id': rec.id, - 'invoice_view_id': inv.id, + # 'invoice_view_id': inv.id, 'invoice_id': inv.invoice_id.id, 'invoice_number': inv.invoice_number, 'invoice_date': inv.invoice_date, 'invoice_date_due': inv.invoice_date_due, 'invoice_day_to_due': inv.invoice_day_to_due, - 'new_invoice_day_to_due': inv.invoice_day_to_due, + 'new_invoice_day_to_due': inv.new_invoice_day_to_due, 'ref': inv.ref, 'amount_residual': inv.amount_residual, 'currency_id': inv.currency_id.id, 'payment_term_id': inv.payment_term_id.id, + 'date_kirim_tukar_faktur': inv.date_kirim_tukar_faktur, + 'date_terima_tukar_faktur': inv.date_terima_tukar_faktur, + 'invoice_user_id': inv.invoice_user_id.id, + 'sale_id': inv.sale_id.id, 'selected': selected_map.get(inv.invoice_id.id, False), }) @@ -272,7 +317,7 @@ class SuratPiutangLine(models.Model): _description = 'Surat Piutang Line' surat_id = fields.Many2one('surat.piutang', string='Surat Piutang', ondelete='cascade') - invoice_view_id = fields.Many2one('unpaid.invoice.view', string='Unpaid Invoice') + # invoice_view_id = fields.Many2one('unpaid.invoice.view', string='Unpaid Invoice') invoice_id = fields.Many2one('account.move', string='Invoice') selected = fields.Boolean(string="Pilih", default=False) @@ -285,3 +330,8 @@ class SuratPiutangLine(models.Model): amount_residual = fields.Monetary(string='Amount Due Signed') currency_id = fields.Many2one('res.currency') payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms') + + date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur') + date_terima_tukar_faktur = fields.Date(string='Terima Faktur') + invoice_user_id = fields.Many2one('res.users', string='Salesperson') + sale_id = fields.Many2one('sale.order', string='Sale Order') -- cgit v1.2.3 From 6f7fd357434cf8372eeb1495612b74a9e16d22e0 Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Tue, 9 Sep 2025 09:40:01 +0700 Subject: (andri) fix --- indoteknik_custom/models/letter_receivable.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index caf62a64..8722ab8d 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -135,7 +135,7 @@ class SuratPiutang(models.Model): raise UserError(_("Email tujuan harus diisi.")) template = self.env.ref('indoteknik_custom.letter_receivable_mail_template') - today = fields.Date.today() + # today = fields.Date.today() month_map = { 1: "Januari", 2: "Februari", 3: "Maret", 4: "April", @@ -156,7 +156,7 @@ class SuratPiutang(models.Model): invoice_table_rows = "" grand_total = 0 for line in selected_lines: - days_to_due = (line.invoice_date_due - today).days if line.invoice_date_due else 0 + # days_to_due = (line.invoice_date_due - today).days if line.invoice_date_due else 0 grand_total += line.amount_residual invoice_table_rows += f""" @@ -164,7 +164,7 @@ class SuratPiutang(models.Model): {self.partner_id.name or '-'} {fields.Date.to_string(line.invoice_date) or '-'} {fields.Date.to_string(line.invoice_date_due) or '-'} - {days_to_due} + {line.new_invoice_day_to_due} {line.ref or '-'} {formatLang(self.env, line.amount_residual, currency_obj=line.currency_id)} {line.payment_term_id.name or '-'} -- cgit v1.2.3 From ac13214fcab7a580b7c9b80faec8cfef684c09aa Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:43:20 +0700 Subject: (andri) try qweb --- indoteknik_custom/models/letter_receivable.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 8722ab8d..9a6c664c 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -53,6 +53,11 @@ class SuratPiutang(models.Model): compute="_compute_grand_total_text", ) + def action_print_surat_piutang(self): + self.ensure_one() + return self.env.ref('indoteknik_custom.report_surat_piutang_formal').report_action(self) + + @api.depends("line_ids.selected", "line_ids.invoice_date") def _compute_periode_invoices(self): for rec in self: -- cgit v1.2.3 From d04150023a7145468858765209e8d6b3724b0fec Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Fri, 12 Sep 2025 16:05:57 +0700 Subject: (andri) fix template --- indoteknik_custom/models/account_move.py | 2 +- indoteknik_custom/models/down_payment.py | 1088 +++++++++++++++++++++++++ indoteknik_custom/models/letter_receivable.py | 13 +- 3 files changed, 1099 insertions(+), 4 deletions(-) create mode 100644 indoteknik_custom/models/down_payment.py (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index c93cfb76..764a8b20 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -394,7 +394,7 @@ class AccountMove(models.Model): 'reply_to': 'finance@indoteknik.co.id', } - template.send_mail(invs[0].id, force_send=True, email_values=values) + # template.send_mail(invs[0].id, force_send=True, email_values=values) _logger.info(f"Mengirim email ke: {values['email_to']} > email CC: {values['email_cc']}") _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})") # flag diff --git a/indoteknik_custom/models/down_payment.py b/indoteknik_custom/models/down_payment.py new file mode 100644 index 00000000..5adbafd9 --- /dev/null +++ b/indoteknik_custom/models/down_payment.py @@ -0,0 +1,1088 @@ +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 dikonfirmasi oleh AP.", + message_type="comment", + subtype_xmlid="mail.mt_note", + ) + + + + # def action_approval_check(self): + # for record in self: + # # user = record.user_id + # user = self.env['res.users'].browse(3401) + # roles = sorted(set( + # f"{group + # .name} (Category: {group.category_id.name})" + # for group in user.groups_id + # if group.category_id.name == 'Roles' + # )) + # _logger.info(f"[ROLE CHECK] User: {user.name} (Login: {user.login}) Roles: {roles}") + # return + + def action_approval_check(self): + jakarta_tz = pytz.timezone('Asia/Jakarta') + now = datetime.now(jakarta_tz).replace(tzinfo=None) + formatted_date = now.strftime('%d %B %Y %H:%M') + + for rec in self: + if not rec.departement_type: + raise UserError("Field 'departement_type' wajib diisi sebelum approval.") + + approver_id = rec._get_departement_approver() + + if rec.status == 'pengajuan1': + if self.env.user.id != approver_id: + raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") + rec.name_approval_departement = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement + rec.date_approved_department = now + + # Mapping posisi berdasarkan departement_type + department_titles = { + 'sales': 'Sales Manager', + 'merchandiser': 'Merchandiser Manager', + 'marketing': 'Marketing Manager', + 'logistic': 'Logistic Manager', + 'procurement': 'Procurement Manager', + 'fat': 'Finance & Accounting Manager', + 'hr_ga': 'HR & GA Manager', + } + rec.position_department = department_titles.get(rec.departement_type, 'Departement Manager') + + rec.status = 'pengajuan2' + + rec.message_post( + body=f"Approval Departement oleh {self.env.user.name} " + f"pada {formatted_date}." + ) + + elif rec.status == 'pengajuan2': + ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + if self.env.user.id not in ap_user_ids: + raise UserError("Hanya AP yang berhak menyetujui tahap ini.") + rec.name_approval_ap = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap + rec.email_ap = self.env.user.email + rec.date_approved_ap = now + rec.position_ap = 'Finance AP' + rec.status = 'pengajuan3' + + rec.message_post( + body=f"Approval AP oleh {self.env.user.name} " + f"pada {formatted_date}." + ) + + elif rec.status == 'pengajuan3': + if self.env.user.id != 7: # ID user Pimpinan + raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") + rec.name_approval_pimpinan = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan + rec.date_approved_pimpinan = now + rec.position_pimpinan = 'Pimpinan' + rec.status = 'approved' + + rec.message_post( + body=f"Approval Pimpinan oleh {self.env.user.name} " + f"pada {formatted_date}." + ) + + else: + raise UserError("Status saat ini tidak bisa di-approve lagi.") + + # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap {rec.status}.") + + + def action_reject(self): + return { + 'type': 'ir.actions.act_window', + 'name': 'Alasan Penolakan', + 'res_model': '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 {new_status.capitalize()} oleh {self.env.user.name}.") + + + def action_ap_only(self): + self.ensure_one() + + ap_user_ids = [23, 9468] # Ganti sesuai kebutuhan + # if self.env.user.id not in ap_user_ids: + # raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') + + if self.move_id: + raise UserError('CAB / Jurnal sudah pernah dibuat untuk PUM ini.') + + return { + 'name': 'Create CAB AP Only', + 'type': 'ir.actions.act_window', + 'res_model': '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 {dict(self._fields['done_status'].selection).get(self.done_status)} oleh {self.env.user.name}.") + + def action_cab(self): + self.ensure_one() + + ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + if self.env.user.id not in ap_user_ids: + raise UserError('Hanya User AP yang dapat menggunakan ini.') + if self.move_id: + raise UserError("CAB / Jurnal sudah pernah dibuat untuk Realisasi ini.") + + if not self.pum_id or not self.pum_id.move_id: + raise UserError("PUM terkait atau CAB belum tersedia.") + + partner_id = self.pum_id.user_id.partner_id.id + cab_move = self.pum_id.move_id + + # Account Bank Intransit dari CAB: + bank_intransit_line = cab_move.line_ids.filtered(lambda l: l.account_id.id in [573, 389, 392]) + if not bank_intransit_line: + raise UserError("Account Bank Intransit dengan tidak ditemukan di CAB terkait.") + account_sisa_pum = bank_intransit_line[0].account_id.id + + # Account Uang Muka Operasional + account_uang_muka = 403 + + # Tanggal pakai create_date atau hari ini + account_date = self.date_done_not_realized or fields.Date.today() + + ref_label = f"Realisasi {self.pum_id.number} Biaya {self.pum_id.detail_note} ({cab_move.name})" + + label_sisa_pum = f"Sisa PUM {self.pum_id.detail_note} {self.pum_id.number} ({cab_move.name})" + + lines = [] + + # Sisa PUM (Debit) + if self.remaining_value > 0: + lines.append((0, 0, { + 'account_id': account_sisa_pum, + 'partner_id': partner_id, + 'name': label_sisa_pum, + 'debit': self.remaining_value, + 'credit': 0, + })) + + # Biaya Penggunaan (Debit) + total_biaya = 0 + for line in self.penggunaan_line_ids: + lines.append((0, 0, { + 'account_id': line.account_id.id, + 'partner_id': partner_id, + 'name': f"{line.description} ({line.date})", + 'debit': line.nominal, + 'credit': 0, + })) + total_biaya += line.nominal + + # Uang Muka Operasional (Credit) + total_credit = self.remaining_value + total_biaya + if total_credit > 0: + lines.append((0, 0, { + 'account_id': account_uang_muka, + 'partner_id': partner_id, + 'name': ref_label, + 'debit': 0, + 'credit': total_credit, + })) + + move = self.env['account.move'].create({ + 'ref': ref_label, + 'date': account_date, + 'journal_id': 11, # MISC + 'line_ids': lines, + }) + + # self.message_post(body=f"Jurnal CAB telah dibuat dengan nomor: {move.name}.") + + self.move_id = move.id + + return { + 'name': _('Journal Entry'), + 'view_mode': 'form', + 'res_model': 'account.move', + 'type': 'ir.actions.act_window', + 'res_id': move.id, + 'target': 'current', + } + + def action_approval_check(self): + jakarta_tz = pytz.timezone('Asia/Jakarta') + now = datetime.now(jakarta_tz).replace(tzinfo=None) + formatted_date = now.strftime('%d %B %Y %H:%M') + + for rec in self: + if not rec.pum_id.departement_type: + raise UserError("Field 'departement_type' wajib diisi sebelum approval.") + + approver_id = rec.pum_id._get_departement_approver() + + if rec.status == 'pengajuan1': + if self.env.user.id != approver_id: + raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") + rec.name_approval_departement = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement + rec.date_approved_department = now + + # Mapping posisi berdasarkan departement_type + department_titles = { + 'sales': 'Sales Manager', + 'merchandiser': 'Merchandiser Manager', + 'marketing': 'Marketing Manager', + 'logistic': 'Logistic Manager', + 'procurement': 'Procurement Manager', + 'fat': 'Finance & Accounting Manager', + 'hr_ga': 'HR & GA Manager', + } + rec.position_department = department_titles.get(rec.pum_id.departement_type, 'Departement Manager') + + rec.status = 'pengajuan2' + + rec.message_post( + body=f"Approval Departement oleh {self.env.user.name} " + f"pada {formatted_date}." + ) + + elif rec.status == 'pengajuan2': + ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP + if self.env.user.id not in ap_user_ids: + raise UserError("Hanya AP yang berhak menyetujui tahap ini.") + rec.name_approval_ap = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap + rec.date_approved_ap = now + rec.position_ap = 'Finance AP' + rec.status = 'pengajuan3' + + rec.message_post( + body=f"Approval AP oleh {self.env.user.name} " + f"pada {formatted_date}." + ) + + elif rec.status == 'pengajuan3': + if self.env.user.id != 7: # ID user Pimpinan + raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") + rec.name_approval_pimpinan = self.env.user.name + rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan + rec.date_approved_pimpinan = now + rec.position_pimpinan = 'Pimpinan' + rec.status = 'approved' + rec.done_status = 'done_not_realized' # Set status done untuk realisasi + + rec.message_post( + body=f"Approval Pimpinan oleh {self.env.user.name} " + f"pada {formatted_date}." + ) + + else: + raise UserError("Status saat ini tidak bisa di-approve lagi.") + + # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap {rec.status}.") + + def _check_remaining_value(self): + for rec in self: + # Cek sisa PUM + if rec.remaining_value < 0: + raise ValidationError( + "Sisa uang PUM tidak boleh kurang dari 0.\n" + "Jika ada penggunaan uang pribadi, maka ajukan dengan sistem reimburse." + ) + + @api.model + def create(self, vals): + rec = super().create(vals) + rec._check_remaining_value() + return rec + + def write(self, vals): + res = super().write(vals) + self._check_remaining_value() + return res + +class 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 9a6c664c..a414eec3 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -53,9 +53,16 @@ class SuratPiutang(models.Model): compute="_compute_grand_total_text", ) - def action_print_surat_piutang(self): - self.ensure_one() - return self.env.ref('indoteknik_custom.report_surat_piutang_formal').report_action(self) + perihal_label = fields.Char( + compute="_compute_perihal_label", string="Perihal Label") + + def _compute_perihal_label(self): + for rec in self: + rec.perihal_label = dict(self._fields['perihal'].selection).get(rec.perihal, '') + + # def action_print_surat_piutang(self): + # self.ensure_one() + # return self.env.ref('indoteknik_custom.report_surat_piutang_formal').report_action(self) @api.depends("line_ids.selected", "line_ids.invoice_date") -- cgit v1.2.3 From f2b1b0ec605b552c2bf225de46094cd4707197ee Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Fri, 12 Sep 2025 17:34:14 +0700 Subject: (andri) add penomoran di tabel dan melengkapi teks surat --- indoteknik_custom/models/letter_receivable.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index a414eec3..3823a57a 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -347,3 +347,18 @@ class SuratPiutangLine(models.Model): date_terima_tukar_faktur = fields.Date(string='Terima Faktur') invoice_user_id = fields.Many2one('res.users', string='Salesperson') sale_id = fields.Many2one('sale.order', string='Sale Order') + + sort = fields.Integer(string='No Urut', compute='_compute_sort', store=False) + + @api.depends('surat_id.line_ids.selected') + def _compute_sort(self): + for line in self: + if line.surat_id: + # Ambil semua line yang selected + selected_lines = line.surat_id.line_ids.filtered(lambda l: l.selected) + try: + line.sort = selected_lines.ids.index(line.id) + 1 + except ValueError: + line.sort = 0 + else: + line.sort = 0 -- cgit v1.2.3 From fc83d226a0b49c1d0423f6e3ccadd353b1c45218 Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Sat, 13 Sep 2025 10:59:33 +0700 Subject: (andri) add attachment di email --- indoteknik_custom/models/letter_receivable.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 3823a57a..4f1395fe 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -7,6 +7,7 @@ import re import logging from datetime import timedelta import babel +import base64 _logger = logging.getLogger(__name__) @@ -203,11 +204,25 @@ class SuratPiutang(models.Model): .replace('${object.seven_days_after_sent_date}', self.seven_days_after_sent_date or '') \ .replace('${object.perihal}', perihal_text or '') + report = self.env.ref('indoteknik_custom.action_report_surat_piutang') + pdf_content, _ = report._render_qweb_pdf([self.id]) + attachment_base64 = base64.b64encode(pdf_content) + + attachment = self.env['ir.attachment'].create({ + 'name': f"Surat Piutang {self.name}.pdf", + 'type': 'binary', + 'datas': attachment_base64, + 'res_model': 'surat.piutang', + 'res_id': self.id, + 'mimetype': 'application/pdf', + }) + values = { 'subject': template.subject.replace('${object.name}', self.name or ''), 'email_to': self.tujuan_email, 'email_from': 'finance@indoteknik.co.id', 'body_html': body_html, + 'attachments': [(attachment.name, attachment.datas)], 'reply_to': 'finance@indoteknik.co.id', } -- cgit v1.2.3 From a627a71d911dbfb54cac7c8151331f218292a9fd Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Sat, 13 Sep 2025 13:12:54 +0700 Subject: (andri) add chatter + fix approve --- indoteknik_custom/models/letter_receivable.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 4f1395fe..2bac3754 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -5,9 +5,10 @@ from odoo.tools import mail, formatLang from terbilang import Terbilang import re import logging -from datetime import timedelta +from datetime import datetime, timedelta import babel import base64 +import pytz _logger = logging.getLogger(__name__) @@ -120,23 +121,25 @@ class SuratPiutang(models.Model): raise ValidationError(_("Format email tidak valid: %s") % rec.tujuan_email) def action_approve(self): + wib = pytz.timezone('Asia/Jakarta') + now_wib = datetime.now(wib) + pimpinan_user_ids = [7] # Pak Akbar if self.env.user.id not in pimpinan_user_ids: raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") for rec in self: if rec.state == "approval_pimpinan": rec.state = "sent" - rec.send_date = fields.Datetime.now() - # Format tanggal + bulan (tanpa tahun) - month_map = { - 1: "Januari", 2: "Februari", 3: "Maret", 4: "April", - 5: "Mei", 6: "Juni", 7: "Juli", 8: "Agustus", - 9: "September", 10: "Oktober", 11: "November", 12: "Desember", - } - if rec.send_date: - target_date = rec.send_date.date() + timedelta(days=7) - rec.seven_days_after_sent_date = f"{target_date.day} {month_map[target_date.month]}" - + now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None) + rec.send_date = now_utc + rec.action_send_letter() + + self.env.user.notify_info( + message=f"Surat piutang {rec.name} berhasil dikirim ke {rec.partner_id.name} ({rec.tujuan_email})", + title="Informasi", + sticky=False + ) + def action_send_letter(self): self.ensure_one() -- cgit v1.2.3 From e12d3f75b57abc6231566c486a114cd0bfc01b8e Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Sun, 14 Sep 2025 20:24:18 +0700 Subject: (andri) add button create surat piutang di view form unpaid inv monitoring --- indoteknik_custom/models/letter_receivable.py | 11 ++++++----- indoteknik_custom/models/unpaid_invoice_view.py | 13 +++++++++++++ 2 files changed, 19 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 2bac3754..17963232 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -125,11 +125,11 @@ class SuratPiutang(models.Model): now_wib = datetime.now(wib) pimpinan_user_ids = [7] # Pak Akbar - if self.env.user.id not in pimpinan_user_ids: - raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") + # if self.env.user.id not in pimpinan_user_ids: + # raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") for rec in self: if rec.state == "approval_pimpinan": - rec.state = "sent" + # rec.state = "sent" now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None) rec.send_date = now_utc rec.action_send_letter() @@ -191,7 +191,7 @@ class SuratPiutang(models.Model): Grand Total - {formatLang(self.env, grand_total, currency_obj=self.currency_id)} + {formatLang(self.env, grand_total, currency_obj=self.currency_id, monetary=True)} @@ -243,6 +243,7 @@ class SuratPiutang(models.Model): [('partner_id', '=', self.partner_id.id)], 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, 'invoice_id': inv.invoice_id.id, @@ -259,7 +260,7 @@ class SuratPiutang(models.Model): 'date_terima_tukar_faktur': inv.date_terima_tukar_faktur, 'invoice_user_id': inv.invoice_user_id.id, 'sale_id': inv.sale_id.id, - 'selected': False + 'selected': True if inv.invoice_id.id == selected_invoice_id else False, }) for inv in invoice_lines] self.line_ids = lines diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py index f35261eb..25e04968 100644 --- a/indoteknik_custom/models/unpaid_invoice_view.py +++ b/indoteknik_custom/models/unpaid_invoice_view.py @@ -33,3 +33,16 @@ class UnpaidInvoiceView(models.Model): invoice_user_id = fields.Many2one('res.users', string='Salesperson') date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur') sale_id = fields.Many2one('sale.order', string='Sale Order') + + def action_create_surat_piutang(self): + self.ensure_one() + return { + 'type': 'ir.actions.act_window', + 'res_model': 'surat.piutang', + 'view_mode': 'form', + 'target': 'current', + 'context': { + 'default_partner_id': self.partner_id.id, + 'default_selected_invoice_id': self.invoice_id.id, + } + } -- cgit v1.2.3 From 2b61b810f5b12f32bf837ab34c12d832d0be12eb Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Mon, 15 Sep 2025 11:48:33 +0700 Subject: (andri) edit status dan rev validasi --- indoteknik_custom/models/letter_receivable.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 17963232..fe598e50 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -31,8 +31,8 @@ class SuratPiutang(models.Model): line_ids = fields.One2many("surat.piutang.line", "surat_id", string="Invoice Lines") state = fields.Selection([ ("draft", "Draft"), - ("approval_pimpinan", "Menunggu Approval Pimpinan"), - ("sent", "Sent") + ("waiting_approval", "Menunggu Approval"), + ("sent", "Approved & Sent") ], default="draft", tracking=True) send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim", tracking=True) @@ -128,8 +128,11 @@ class SuratPiutang(models.Model): # if self.env.user.id not in pimpinan_user_ids: # raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") for rec in self: - if rec.state == "approval_pimpinan": - # rec.state = "sent" + if rec.state == "waiting_approval": + if rec.perihal in ("sp1", "sp2", "sp3"): + if self.env.user.id not in pimpinan_user_ids: + raise UserError("Hanya Pimpinan yang berhak menyetujui surat peringatan piutang (SP1, SP2, SP3).") + rec.state = "sent" now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None) rec.send_date = now_utc rec.action_send_letter() @@ -340,7 +343,7 @@ class SuratPiutang(models.Model): tahun = today.strftime("%y") vals["name"] = f"{seq}/LO/FAT/IDG/{bulan_romawi}/{tahun}" - vals["state"] = "approval_pimpinan" + vals["state"] = "waiting_approval" return super().create(vals) class SuratPiutangLine(models.Model): -- cgit v1.2.3 From 570087805ec46b8af7651187cfcf0ecef2733912 Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:45:43 +0700 Subject: (andri) rev penamaan surat dan doc --- indoteknik_custom/models/letter_receivable.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index fe598e50..18485f01 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -215,7 +215,7 @@ class SuratPiutang(models.Model): attachment_base64 = base64.b64encode(pdf_content) attachment = self.env['ir.attachment'].create({ - 'name': f"Surat Piutang {self.name}.pdf", + 'name': f"{self.perihal_label} - {self.partner_id.name}.pdf", 'type': 'binary', 'datas': attachment_base64, 'res_model': 'surat.piutang', @@ -224,7 +224,8 @@ class SuratPiutang(models.Model): }) values = { - 'subject': template.subject.replace('${object.name}', self.name or ''), + # 'subject': template.subject.replace('${object.name}', self.name or ''), + 'subject': perihal_map.get(self.perihal, self.perihal or '') + " - " + (self.partner_id.name or ''), 'email_to': self.tujuan_email, 'email_from': 'finance@indoteknik.co.id', 'body_html': body_html, -- cgit v1.2.3 From 60ef4aa56889985028a75cc927db80c36bc21338 Mon Sep 17 00:00:00 2001 From: AndriFP <113114423+andrifp@users.noreply.github.com> Date: Mon, 15 Sep 2025 15:31:31 +0700 Subject: (andri) add message post after refresh inv --- indoteknik_custom/models/letter_receivable.py | 3 +++ 1 file changed, 3 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 18485f01..79a4a3e0 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -333,6 +333,9 @@ class SuratPiutang(models.Model): # Hapus line yang tidak ada lagi di unpaid view rec.line_ids.filtered(lambda l: l.invoice_id.id not in new_invoice_ids).unlink() + rec.message_post( + body=f"Line Invoices diperbarui. Total line saat ini: {len(rec.line_ids)}" + ) @api.model def create(self, vals): -- cgit v1.2.3 From 540136f8096f090b481d139c58c3f36c28aa69bb Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 16 Sep 2025 15:01:02 +0700 Subject: (andri) add payment diff + fix onchange ketika pilih partner --- indoteknik_custom/models/down_payment.py | 1088 ----------------------- indoteknik_custom/models/letter_receivable.py | 12 +- indoteknik_custom/models/unpaid_invoice_view.py | 7 + 3 files changed, 14 insertions(+), 1093 deletions(-) delete mode 100644 indoteknik_custom/models/down_payment.py (limited to 'indoteknik_custom/models') 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 dikonfirmasi oleh AP.", - message_type="comment", - subtype_xmlid="mail.mt_note", - ) - - - - # def action_approval_check(self): - # for record in self: - # # user = record.user_id - # user = self.env['res.users'].browse(3401) - # roles = sorted(set( - # f"{group - # .name} (Category: {group.category_id.name})" - # for group in user.groups_id - # if group.category_id.name == 'Roles' - # )) - # _logger.info(f"[ROLE CHECK] User: {user.name} (Login: {user.login}) Roles: {roles}") - # return - - def action_approval_check(self): - jakarta_tz = pytz.timezone('Asia/Jakarta') - now = datetime.now(jakarta_tz).replace(tzinfo=None) - formatted_date = now.strftime('%d %B %Y %H:%M') - - for rec in self: - if not rec.departement_type: - raise UserError("Field 'departement_type' wajib diisi sebelum approval.") - - approver_id = rec._get_departement_approver() - - if rec.status == 'pengajuan1': - if self.env.user.id != approver_id: - raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") - rec.name_approval_departement = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement - rec.date_approved_department = now - - # Mapping posisi berdasarkan departement_type - department_titles = { - 'sales': 'Sales Manager', - 'merchandiser': 'Merchandiser Manager', - 'marketing': 'Marketing Manager', - 'logistic': 'Logistic Manager', - 'procurement': 'Procurement Manager', - 'fat': 'Finance & Accounting Manager', - 'hr_ga': 'HR & GA Manager', - } - rec.position_department = department_titles.get(rec.departement_type, 'Departement Manager') - - rec.status = 'pengajuan2' - - rec.message_post( - body=f"Approval Departement oleh {self.env.user.name} " - f"pada {formatted_date}." - ) - - elif rec.status == 'pengajuan2': - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - if self.env.user.id not in ap_user_ids: - raise UserError("Hanya AP yang berhak menyetujui tahap ini.") - rec.name_approval_ap = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap - rec.email_ap = self.env.user.email - rec.date_approved_ap = now - rec.position_ap = 'Finance AP' - rec.status = 'pengajuan3' - - rec.message_post( - body=f"Approval AP oleh {self.env.user.name} " - f"pada {formatted_date}." - ) - - elif rec.status == 'pengajuan3': - if self.env.user.id != 7: # ID user Pimpinan - raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") - rec.name_approval_pimpinan = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan - rec.date_approved_pimpinan = now - rec.position_pimpinan = 'Pimpinan' - rec.status = 'approved' - - rec.message_post( - body=f"Approval Pimpinan oleh {self.env.user.name} " - f"pada {formatted_date}." - ) - - else: - raise UserError("Status saat ini tidak bisa di-approve lagi.") - - # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap {rec.status}.") - - - def action_reject(self): - return { - 'type': 'ir.actions.act_window', - 'name': 'Alasan Penolakan', - 'res_model': '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 {new_status.capitalize()} oleh {self.env.user.name}.") - - - def action_ap_only(self): - self.ensure_one() - - ap_user_ids = [23, 9468] # Ganti sesuai kebutuhan - # if self.env.user.id not in ap_user_ids: - # raise UserError('Hanya User AP yang dapat menggunakan fitur ini.') - - if self.move_id: - raise UserError('CAB / Jurnal sudah pernah dibuat untuk PUM ini.') - - return { - 'name': 'Create CAB AP Only', - 'type': 'ir.actions.act_window', - 'res_model': '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 {dict(self._fields['done_status'].selection).get(self.done_status)} oleh {self.env.user.name}.") - - def action_cab(self): - self.ensure_one() - - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - if self.env.user.id not in ap_user_ids: - raise UserError('Hanya User AP yang dapat menggunakan ini.') - if self.move_id: - raise UserError("CAB / Jurnal sudah pernah dibuat untuk Realisasi ini.") - - if not self.pum_id or not self.pum_id.move_id: - raise UserError("PUM terkait atau CAB belum tersedia.") - - partner_id = self.pum_id.user_id.partner_id.id - cab_move = self.pum_id.move_id - - # Account Bank Intransit dari CAB: - bank_intransit_line = cab_move.line_ids.filtered(lambda l: l.account_id.id in [573, 389, 392]) - if not bank_intransit_line: - raise UserError("Account Bank Intransit dengan tidak ditemukan di CAB terkait.") - account_sisa_pum = bank_intransit_line[0].account_id.id - - # Account Uang Muka Operasional - account_uang_muka = 403 - - # Tanggal pakai create_date atau hari ini - account_date = self.date_done_not_realized or fields.Date.today() - - ref_label = f"Realisasi {self.pum_id.number} Biaya {self.pum_id.detail_note} ({cab_move.name})" - - label_sisa_pum = f"Sisa PUM {self.pum_id.detail_note} {self.pum_id.number} ({cab_move.name})" - - lines = [] - - # Sisa PUM (Debit) - if self.remaining_value > 0: - lines.append((0, 0, { - 'account_id': account_sisa_pum, - 'partner_id': partner_id, - 'name': label_sisa_pum, - 'debit': self.remaining_value, - 'credit': 0, - })) - - # Biaya Penggunaan (Debit) - total_biaya = 0 - for line in self.penggunaan_line_ids: - lines.append((0, 0, { - 'account_id': line.account_id.id, - 'partner_id': partner_id, - 'name': f"{line.description} ({line.date})", - 'debit': line.nominal, - 'credit': 0, - })) - total_biaya += line.nominal - - # Uang Muka Operasional (Credit) - total_credit = self.remaining_value + total_biaya - if total_credit > 0: - lines.append((0, 0, { - 'account_id': account_uang_muka, - 'partner_id': partner_id, - 'name': ref_label, - 'debit': 0, - 'credit': total_credit, - })) - - move = self.env['account.move'].create({ - 'ref': ref_label, - 'date': account_date, - 'journal_id': 11, # MISC - 'line_ids': lines, - }) - - # self.message_post(body=f"Jurnal CAB telah dibuat dengan nomor: {move.name}.") - - self.move_id = move.id - - return { - 'name': _('Journal Entry'), - 'view_mode': 'form', - 'res_model': 'account.move', - 'type': 'ir.actions.act_window', - 'res_id': move.id, - 'target': 'current', - } - - def action_approval_check(self): - jakarta_tz = pytz.timezone('Asia/Jakarta') - now = datetime.now(jakarta_tz).replace(tzinfo=None) - formatted_date = now.strftime('%d %B %Y %H:%M') - - for rec in self: - if not rec.pum_id.departement_type: - raise UserError("Field 'departement_type' wajib diisi sebelum approval.") - - approver_id = rec.pum_id._get_departement_approver() - - if rec.status == 'pengajuan1': - if self.env.user.id != approver_id: - raise UserError("Hanya approver departement yang berhak menyetujui tahap ini.") - rec.name_approval_departement = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_departement - rec.date_approved_department = now - - # Mapping posisi berdasarkan departement_type - department_titles = { - 'sales': 'Sales Manager', - 'merchandiser': 'Merchandiser Manager', - 'marketing': 'Marketing Manager', - 'logistic': 'Logistic Manager', - 'procurement': 'Procurement Manager', - 'fat': 'Finance & Accounting Manager', - 'hr_ga': 'HR & GA Manager', - } - rec.position_department = department_titles.get(rec.pum_id.departement_type, 'Departement Manager') - - rec.status = 'pengajuan2' - - rec.message_post( - body=f"Approval Departement oleh {self.env.user.name} " - f"pada {formatted_date}." - ) - - elif rec.status == 'pengajuan2': - ap_user_ids = [23, 9468] # List user ID yang boleh approve sebagai Finance AP - if self.env.user.id not in ap_user_ids: - raise UserError("Hanya AP yang berhak menyetujui tahap ini.") - rec.name_approval_ap = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_ap - rec.date_approved_ap = now - rec.position_ap = 'Finance AP' - rec.status = 'pengajuan3' - - rec.message_post( - body=f"Approval AP oleh {self.env.user.name} " - f"pada {formatted_date}." - ) - - elif rec.status == 'pengajuan3': - if self.env.user.id != 7: # ID user Pimpinan - raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") - rec.name_approval_pimpinan = self.env.user.name - rec.approved_by = (rec.approved_by + ', ' if rec.approved_by else '') + rec.name_approval_pimpinan - rec.date_approved_pimpinan = now - rec.position_pimpinan = 'Pimpinan' - rec.status = 'approved' - rec.done_status = 'done_not_realized' # Set status done untuk realisasi - - rec.message_post( - body=f"Approval Pimpinan oleh {self.env.user.name} " - f"pada {formatted_date}." - ) - - else: - raise UserError("Status saat ini tidak bisa di-approve lagi.") - - # rec.message_post(body=f"Approval oleh {self.env.user.name} pada tahap {rec.status}.") - - def _check_remaining_value(self): - for rec in self: - # Cek sisa PUM - if rec.remaining_value < 0: - raise ValidationError( - "Sisa uang PUM tidak boleh kurang dari 0.\n" - "Jika ada penggunaan uang pribadi, maka ajukan dengan sistem reimburse." - ) - - @api.model - def create(self, vals): - rec = super().create(vals) - rec._check_remaining_value() - return rec - - def write(self, vals): - res = super().write(vals) - self._check_remaining_value() - return res - -class 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 { -- cgit v1.2.3 From 5c69ea958bd5c5611601a3af0f76ac9c462b2667 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 16 Sep 2025 15:45:14 +0700 Subject: (andri) fix layout --- indoteknik_custom/models/letter_receivable.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 63d7e726..e4886258 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -60,6 +60,8 @@ class SuratPiutang(models.Model): payment_difficulty = fields.Selection(string="Payment Difficulty", related='partner_id.payment_difficulty', readonly=True) + sales_person_id = fields.Many2one('res.users', string='Salesperson', related='partner_id.user_id', readonly=True) + def _compute_perihal_label(self): for rec in self: rec.perihal_label = dict(self._fields['perihal'].selection).get(rec.perihal, '') -- cgit v1.2.3 From 6695da56e3f9fe575f7c855b60da31ceb4dd5129 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 16 Sep 2025 15:55:40 +0700 Subject: (andri) add filter hanya menampilkan partner yang unpaid --- indoteknik_custom/models/letter_receivable.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index e4886258..1deeda11 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -62,6 +62,14 @@ class SuratPiutang(models.Model): sales_person_id = fields.Many2one('res.users', string='Salesperson', related='partner_id.user_id', readonly=True) + @api.onchange('partner_id') + def _onchange_partner_id_domain(self): + unpaid_partner_ids = self.env['unpaid.invoice.view'].search([]).mapped('partner_id.id') + return { + 'domain': { + 'partner_id': [('id', 'in', unpaid_partner_ids)] + } + } def _compute_perihal_label(self): for rec in self: rec.perihal_label = dict(self._fields['perihal'].selection).get(rec.perihal, '') -- cgit v1.2.3 From 9dc31bc66d3d5fd3464e6b731cabe15c3c52b558 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 17 Sep 2025 09:23:12 +0700 Subject: (andri) add validasi sales manager --- indoteknik_custom/models/letter_receivable.py | 41 +++++++++++++++++++++------ 1 file changed, 33 insertions(+), 8 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 1deeda11..84a3fc35 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -136,18 +136,41 @@ class SuratPiutang(models.Model): wib = pytz.timezone('Asia/Jakarta') now_wib = datetime.now(wib) - pimpinan_user_ids = [7] # Pak Akbar - # if self.env.user.id not in pimpinan_user_ids: - # raise UserError("Hanya Pimpinan yang berhak menyetujui tahap ini.") + sales_manager_ids = [10] # ganti dengan ID user Sales Manager + pimpinan_user_ids = [7] # ganti dengan ID user Pimpinan + for rec in self: - if rec.state == "waiting_approval": - if rec.perihal in ("sp1", "sp2", "sp3"): + # === SP1 s/d SP3 butuh dua tahap approval === + if rec.perihal in ("sp1", "sp2", "sp3"): + + # Tahap 1: Sales Manager approval + if rec.state == "waiting_approval_sales": + if self.env.user.id not in sales_manager_ids: + raise UserError("Hanya Sales Manager yang boleh menyetujui tahap ini.") + rec.state = "waiting_approval_pimpinan" + rec.message_post(body="Disetujui oleh Sales Manager. Menunggu Approval Pimpinan.") + continue + + # Tahap 2: Pimpinan approval + if rec.state == "waiting_approval_pimpinan": if self.env.user.id not in pimpinan_user_ids: - raise UserError("Hanya Pimpinan yang berhak menyetujui surat peringatan piutang (SP1, SP2, SP3).") + raise UserError("Hanya Pimpinan yang berhak menyetujui surat ini.") + rec.state = "sent" + now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None) + rec.send_date = now_utc + rec.action_send_letter() + rec.message_post(body="Surat Piutang disetujui oleh Pimpinan dan berhasil dikirim.") + continue + + # === Surat penagihan biasa (langsung Pimpinan approve) === + if rec.perihal == "penagihan": + # if self.env.user.id not in pimpinan_user_ids: + # raise UserError("Hanya Pimpinan yang boleh menyetujui surat penagihan.") rec.state = "sent" now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None) rec.send_date = now_utc rec.action_send_letter() + rec.message_post(body="Surat Penagihan disetujui Pimpinan dan berhasil dikirim.") self.env.user.notify_info( message=f"Surat piutang {rec.name} berhasil dikirim ke {rec.partner_id.name} ({rec.tujuan_email})", @@ -358,8 +381,10 @@ class SuratPiutang(models.Model): bulan_romawi = ["I","II","III","IV","V","VI","VII","VIII","IX","X","XI","XII"][today.month-1] tahun = today.strftime("%y") vals["name"] = f"{seq}/LO/FAT/IDG/{bulan_romawi}/{tahun}" - - vals["state"] = "waiting_approval" + if vals.get("perihal") == "penagihan": + vals["state"] = "waiting_approval_pimpinan" + else: + vals["state"] = "waiting_approval_sales" return super().create(vals) class SuratPiutangLine(models.Model): -- cgit v1.2.3 From aaff9c4fa47ca2ee114ab1dc3a8140aec300ce26 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 17 Sep 2025 16:29:04 +0700 Subject: (andri) button surat lanjutan + fix bug --- indoteknik_custom/models/letter_receivable.py | 83 ++++++++++++++++++++++++--- 1 file changed, 76 insertions(+), 7 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index 84a3fc35..b1c82b12 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -32,11 +32,11 @@ class SuratPiutang(models.Model): state = fields.Selection([ ("draft", "Draft"), ("waiting_approval_sales", "Menunggu Approval Sales Manager"), - ("waiting_approval_pimpinan", "Menunggu Approval Pimpinan"), + ("waiting_approval_pimpinan", "Menunggu Approval Pimpinan / Kirim Surat"), ("sent", "Approved & Sent") ], default="draft", tracking=True) send_date = fields.Datetime(string="Tanggal Kirim", tracking=True) - seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim", tracking=True) + seven_days_after_sent_date = fields.Char(string="7 Hari Setelah Tanggal Kirim") periode_invoices_terpilih = fields.Char( string="Periode Invoices Terpilih", compute="_compute_periode_invoices", @@ -62,6 +62,12 @@ class SuratPiutang(models.Model): sales_person_id = fields.Many2one('res.users', string='Salesperson', related='partner_id.user_id', readonly=True) + PERIHAL_SEQUENCE = { + "penagihan": "sp1", + "sp1": "sp2", + "sp2": "sp3", + } + @api.onchange('partner_id') def _onchange_partner_id_domain(self): unpaid_partner_ids = self.env['unpaid.invoice.view'].search([]).mapped('partner_id.id') @@ -74,9 +80,68 @@ class SuratPiutang(models.Model): for rec in self: rec.perihal_label = dict(self._fields['perihal'].selection).get(rec.perihal, '') - # def action_print_surat_piutang(self): - # self.ensure_one() - # return self.env.ref('indoteknik_custom.report_surat_piutang_formal').report_action(self) + def action_create_next_letter(self): + for rec in self: + if rec.state != "sent": + raise UserError("Surat harus sudah terkirim sebelum bisa membuat surat lanjutan.") + + next_perihal = self.PERIHAL_SEQUENCE.get(rec.perihal) + if not next_perihal: + raise UserError("Surat ini sudah pada tahap terakhir (SP3). Tidak bisa membuat lanjutan lagi.") + + existing = self.search([ + ('partner_id', '=', rec.partner_id.id), + ('perihal', '=', next_perihal), + ('state', '!=', 'draft') # optional: cek hanya yang sudah dikirim + ]) + if existing: + raise UserError(f"Surat lanjutan {dict(self._fields['perihal'].selection).get(next_perihal)} " + f"untuk customer ini sudah dibuat: {', '.join(existing.mapped('name'))}") + + # copy surat lama + new_vals = { + "tujuan_nama": rec.tujuan_nama, + "tujuan_email": rec.tujuan_email, + "perihal": next_perihal, + "partner_id": rec.partner_id.id, + "line_ids": [(0, 0, { + 'invoice_id': line.invoice_id.id, + 'invoice_number': line.invoice_number, + 'invoice_date': line.invoice_date, + 'invoice_date_due': line.invoice_date_due, + 'invoice_day_to_due': line.invoice_day_to_due, + 'new_invoice_day_to_due': line.new_invoice_day_to_due, + 'ref': line.ref, + 'amount_residual': line.amount_residual, + 'currency_id': line.currency_id.id, + 'payment_term_id': line.payment_term_id.id, + 'date_kirim_tukar_faktur': line.date_kirim_tukar_faktur, + 'date_terima_tukar_faktur': line.date_terima_tukar_faktur, + 'invoice_user_id': line.invoice_user_id.id, + 'sale_id': line.sale_id.id, + "selected": line.selected, + }) for line in rec.line_ids], + } + new_letter = self.create(new_vals) + self.env.user.notify_info( + message=f"Surat lanjutan {dict(self._fields['perihal'].selection).get(next_perihal)} berhasil dibuat ({new_letter.name}).", + title="Informasi", + sticky=False + ) + new_letter.message_post( + body= + f"Surat lanjutan dengan perihal {dict(self._fields['perihal'].selection).get(next_perihal)} " + f"berhasil dibuat berdasarkan surat sebelumnya.
" + f"Nomor Surat: {new_letter.name}" + ) + rec.message_post( + body=( + f"Surat lanjutan dengan perihal {dict(self._fields['perihal'].selection).get(next_perihal)} " + f"telah dibuat sebagai kelanjutan dari surat ini.
" + f"Nomor Surat Baru: {new_letter.name}" + ) + ) + return True @api.depends("line_ids.selected", "line_ids.invoice_date") @@ -170,7 +235,7 @@ class SuratPiutang(models.Model): now_utc = now_wib.astimezone(pytz.UTC).replace(tzinfo=None) rec.send_date = now_utc rec.action_send_letter() - rec.message_post(body="Surat Penagihan disetujui Pimpinan dan berhasil dikirim.") + rec.message_post(body="Surat Penagihan disetujui dan berhasil dikirim.") self.env.user.notify_info( message=f"Surat piutang {rec.name} berhasil dikirim ke {rec.partner_id.name} ({rec.tujuan_email})", @@ -268,7 +333,11 @@ class SuratPiutang(models.Model): 'reply_to': 'finance@indoteknik.co.id', } - template.send_mail(self.id, force_send=True, email_values=values) + template.with_context(mail_post_autofollow=False).send_mail( + self.id, + force_send=True, + email_values=values + ) _logger.info( f"Surat Piutang {self.name} terkirim ke {self.tujuan_email} " -- cgit v1.2.3 From eada77c97134dcbbbbbff43b4195a0e07cda4b86 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 17 Sep 2025 16:31:58 +0700 Subject: (andri) fix --- indoteknik_custom/models/letter_receivable.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py index b1c82b12..1445800f 100644 --- a/indoteknik_custom/models/letter_receivable.py +++ b/indoteknik_custom/models/letter_receivable.py @@ -124,13 +124,13 @@ class SuratPiutang(models.Model): } new_letter = self.create(new_vals) self.env.user.notify_info( - message=f"Surat lanjutan {dict(self._fields['perihal'].selection).get(next_perihal)} berhasil dibuat ({new_letter.name}).", + message=f"{dict(self._fields['perihal'].selection).get(next_perihal)} berhasil dibuat ({new_letter.name}).", title="Informasi", sticky=False ) new_letter.message_post( body= - f"Surat lanjutan dengan perihal {dict(self._fields['perihal'].selection).get(next_perihal)} " + f"{dict(self._fields['perihal'].selection).get(next_perihal)} " f"berhasil dibuat berdasarkan surat sebelumnya.
" f"Nomor Surat: {new_letter.name}" ) -- cgit v1.2.3 From 886c28f6ebf20dcca5252341a8f6b61cd4d89d71 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 17 Sep 2025 18:25:29 +0700 Subject: fix --- indoteknik_custom/models/account_move.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py index 9061f541..ec23c626 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -218,8 +218,7 @@ class AccountMove(models.Model): ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']), ('invoice_date_due', '<', today), ('date_terima_tukar_faktur', '!=', False), - ('invoice_payment_term_id.name', 'ilike', 'tempo'), - ('partner_id', 'in', [94603])]) + ('invoice_payment_term_id.name', 'ilike', 'tempo')]) _logger.info(f"Found {len(invoices)} invoices overdue for reminder {invoices}.") if not invoices: _logger.info("Tidak ada invoice yang overdue") @@ -423,25 +422,20 @@ class AccountMove(models.Model): # Siapkan email values values = { - 'subject': f"Reminder Invoice Due Test - {partner.name}", + 'subject': f"Reminder Invoice Due - {partner.name}", 'email_to': 'andrifebriyadiputra@gmail.com', - # 'email_to': email_to, + 'email_to': email_to, 'email_from': 'finance@indoteknik.co.id', - # 'email_cc': ",".join(sorted(set(cc_list))), + 'email_cc': ",".join(sorted(set(cc_list))), 'body_html': body_html, - # 'reply_to': 'finance@indoteknik.co.id', + 'reply_to': 'finance@indoteknik.co.id', } -<<<<<<< HEAD - # template.send_mail(invs[0].id, force_send=True, email_values=values) - _logger.info(f"Mengirim email ke: {values['email_to']} > email CC: {values['email_cc']}") -======= template.send_mail(invs[0].id, force_send=True, email_values=values) - # _logger.info(f"Mengirim email ke: {values['email_to']} > email CC: {values['email_cc']}") ->>>>>>> 23d713fc686d56ef5b5e8004b91b3f4fe54117e6 + _logger.info(f"Mengirim email ke: {values['email_to']} > email CC: {values['email_cc']}") _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})") # flag - # invs.write({'reminder_sent_date': today}) + invs.write({'reminder_sent_date': today}) # Post ke chatter user_system = self.env['res.users'].browse(25) system_id = user_system.partner_id.id if user_system else False -- cgit v1.2.3