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" _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([ ('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) 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', ) 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: rec.grand_total = sum( 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: 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 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 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}" vals["state"] = "approval_pimpinan" 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')