summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/account_move.py
diff options
context:
space:
mode:
Diffstat (limited to 'indoteknik_custom/models/account_move.py')
-rw-r--r--indoteknik_custom/models/account_move.py210
1 files changed, 197 insertions, 13 deletions
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index b0ffd8b9..c44cad78 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -99,6 +99,12 @@ class AccountMove(models.Model):
reminder_sent_date = fields.Date(string="Tanggal Reminder Terkirim")
+ customer_promise_date = fields.Date(
+ string="Janji Bayar",
+ help="Tanggal janji bayar dari customer setelah reminder dikirim.",
+ tracking=True
+ )
+
def compute_partial_payment(self):
for move in self:
if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial':
@@ -121,6 +127,39 @@ class AccountMove(models.Model):
else:
move.payment_date = False
+ def action_sync_promise_date(self):
+ self.ensure_one()
+ finance_user_ids = [688]
+ if self.env.user.id not in finance_user_ids:
+ raise UserError('Hanya Finance (Widya) yang dapat menggunakan fitur ini.')
+ if not self.customer_promise_date:
+ raise UserError("Isi Janji Bayar terlebih dahulu sebelum melakukan sinkronisasi.")
+
+ other_invoices = self.env['account.move'].search([
+ ('id', '!=', self.id),
+ ('partner_id', '=', self.partner_id.id),
+ ('invoice_date_due', '=', self.invoice_date_due),
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('date_terima_tukar_faktur', '!=', False)
+ ])
+ lines = []
+ for inv in other_invoices:
+ lines.append((0, 0, {'invoice_id': inv.id, 'sync_check': True})) # default dicentang semua
+
+ wizard = self.env['sync.promise.date.wizard'].create({
+ 'invoice_id': self.id,
+ 'line_ids': lines,
+ })
+
+ return {
+ 'name': 'Sync Janji Bayar',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sync.promise.date.wizard',
+ 'view_mode': 'form',
+ 'res_id': wizard.id,
+ 'target': 'new',
+ }
def send_due_invoice_reminder(self):
today = fields.Date.today()
@@ -140,8 +179,9 @@ class AccountMove(models.Model):
('state', '=', 'posted'),
('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
('invoice_date_due', 'in', target_dates),
- ('date_terima_tukar_faktur', '!=', False)
- ])
+ ('date_terima_tukar_faktur', '!=', False),
+ ('partner_id', 'in' , [94603])
+ ], limit=5)
_logger.info(f"Invoices: {invoices}")
invoices = invoices.filtered(
@@ -168,6 +208,24 @@ class AccountMove(models.Model):
_logger.info(f"Reminder untuk {partner.name} sudah terkirim hari ini, skip.")
continue
+ promise_dates = [inv.customer_promise_date for inv in invs if inv.customer_promise_date]
+ if promise_dates:
+ earliest_promise = min(promise_dates) # ambil janji paling awal
+ if today <= earliest_promise:
+ _logger.info(
+ f"Skip reminder untuk {partner.name} karena ada Janji Bayar sampai {earliest_promise}"
+ )
+ continue
+
+ emails = []
+ # skip semua jika partner centang dont_send_reminder_inv_all
+ if partner.dont_send_reminder_inv_all:
+ _logger.info(f"Partner {partner.name} skip karena dont_send_reminder_inv_all aktif")
+ continue
+ # cek parent hanya dengan flag dont_sent_reminder_inv_parent
+ if not partner.dont_send_reminder_inv_parent and partner.email:
+ emails.append(partner.email)
+
# Ambil child contact yang di-checklist reminder_invoices
reminder_contacts = self.env['res.partner'].search([
('parent_id', '=', partner.id),
@@ -175,12 +233,8 @@ class AccountMove(models.Model):
('email', '!=', False),
])
_logger.info(f"Email Reminder Child {reminder_contacts}")
-
- # if not reminder_contacts:
- # _logger.info(f"Partner {partner.name} tidak memiliki email yang sudah ceklis reminder")
- # continue
-
- emails = list(filter(None, [partner.email])) + reminder_contacts.mapped('email')
+
+ emails += reminder_contacts.mapped('email')
if reminder_contacts:
_logger.info(f"Email Reminder Child {reminder_contacts}")
else:
@@ -194,8 +248,10 @@ class AccountMove(models.Model):
_logger.info(f"Email tujuan: {email_to}")
invoice_table_rows = ""
+ grand_total = 0
for inv in invs:
days_to_due = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
+ grand_total += inv.amount_total
invoice_table_rows += f"""
<tr>
<td>{inv.partner_id.name}</td>
@@ -208,6 +264,66 @@ class AccountMove(models.Model):
<td>{days_to_due}</td>
</tr>
"""
+ invoice_table_footer = f"""
+ <tfoot>
+ <tr style="font-weight:bold; background-color:#f9f9f9;">
+ <td colspan="5" align="right">Grand Total</td>
+ <td>{formatLang(self.env, grand_total, currency_obj=invs[0].currency_id)}</td>
+ <td colspan="2"></td>
+ </tr>
+ </tfoot>
+ """
+
+ blocking_limit = partner.blocking_stage or 0.0
+
+ # semua invoice tempo yang masih open
+ outstanding_invoices = self.env['account.move'].search([
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ ('partner_id', '=', partner.id),
+ ('invoice_payment_term_id.name', 'ilike', 'tempo')
+ ])
+
+ outstanding_amount = sum(outstanding_invoices.mapped('amount_total'))
+
+ # invoice tempo yang sudah jatuh tempo
+ overdue_invoices = outstanding_invoices.filtered(
+ lambda inv: inv.invoice_date_due and inv.invoice_date_due < fields.Date.today()
+ )
+
+ overdue_amount = sum(overdue_invoices.mapped('amount_total'))
+
+ currency = invs[0].currency_id if invs else partner.company_id.currency_id
+ tempo_link = 'https://indoteknik.com/my/tempo'
+ # tempo_link = 'http://localhost:2100/my/tempo'
+
+ limit_info_html = f"""
+ <p><b>Informasi Tambahan:</b></p>
+ <ul style="font-size:12px; color:#333; line-height:1.4; margin:0; padding-left:0; list-style-position:inside;">
+ <li>Kredit Limit Anda: {formatLang(self.env, blocking_limit, currency_obj=currency)}</li>
+ <li>Status Detail Tempo: {partner.property_payment_term_id.name or 'Review'}</li>
+ <li style="color:{'red' if (blocking_limit - outstanding_amount) < 0 else 'green'};">
+ Sisa Kredit Limit: {formatLang(self.env, blocking_limit - outstanding_amount, currency_obj=currency)}
+ </li>
+ <li style="color:red;">
+ Kredit Limit Terpakai: {formatLang(self.env, outstanding_amount, currency_obj=currency)}
+ <span style="font-size:12px; color:#666;">({len(outstanding_invoices)} Transaksi)</span>
+ </li>
+ <li style="color:red;">
+ Jatuh Tempo: {formatLang(self.env, overdue_amount, currency_obj=currency)}
+ <span style="font-size:12px; color:#666;">({len(overdue_invoices)} Invoice)</span>
+ </li>
+ </ul>
+ <p style="margin-top:10px;">
+ <a href="{tempo_link or '#'}"
+ style="display:inline-block; padding:8px 16px;
+ background-color:#007bff; color:#fff; text-decoration:none;
+ border-radius:4px; font-size:12px;">
+ Cek Selengkapnya
+ </a>
+ </p>
+ """
days_to_due_message = ""
closing_message = ""
@@ -249,13 +365,14 @@ class AccountMove(models.Model):
body_html = re.sub(
r"<tbody[^>]*>.*?</tbody>",
- f"<tbody>{invoice_table_rows}</tbody>",
+ f"<tbody>{invoice_table_rows}</tbody>{invoice_table_footer}",
template.body_html,
flags=re.DOTALL
).replace('${object.name}', partner.name) \
.replace('${object.partner_id.name}', partner.name) \
.replace('${days_to_due_message}', days_to_due_message) \
- .replace('${closing_message}', closing_message)
+ .replace('${closing_message}', closing_message) \
+ .replace('${limit_info_html}', limit_info_html)
cc_list = [
'finance@indoteknik.co.id',
@@ -278,8 +395,9 @@ class AccountMove(models.Model):
'reply_to': 'finance@indoteknik.co.id',
}
- _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']}")
+ _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})")
# flag
invs.write({'reminder_sent_date': today})
# Post ke chatter
@@ -293,7 +411,6 @@ class AccountMove(models.Model):
author_id=system_id,
)
- _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})")
@api.onchange('invoice_date')
@@ -689,4 +806,71 @@ class AccountMove(models.Model):
'date_efaktur_exported': datetime.utcnow(),
})
- return response \ No newline at end of file
+ return response
+
+class SyncPromiseDateWizard(models.TransientModel):
+ _name = "sync.promise.date.wizard"
+ _description = "Sync Janji Bayar Wizard"
+
+ invoice_id = fields.Many2one('account.move', string="Invoice Utama", required=True)
+ promise_date = fields.Date(string="Janji Bayar", related="invoice_id.customer_promise_date", readonly=True)
+ line_ids = fields.One2many('sync.promise.date.wizard.line', 'wizard_id', string="Invoices Terkait")
+
+ def action_check_all(self):
+ for line in self.line_ids:
+ line.sync_check = True
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sync.promise.date.wizard',
+ 'view_mode': 'form',
+ 'res_id': self.id,
+ 'target': 'new',
+ }
+
+ def action_uncheck_all(self):
+ for line in self.line_ids:
+ line.sync_check = False
+ return {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'sync.promise.date.wizard',
+ 'view_mode': 'form',
+ 'res_id': self.id,
+ 'target': 'new',
+ }
+
+ def action_confirm(self):
+ self.ensure_one()
+ selected_lines = self.line_ids.filtered(lambda l: l.sync_check)
+ selected_invoices = selected_lines.mapped('invoice_id')
+
+ if not selected_invoices:
+ raise UserError("Tidak ada invoice dipilih untuk sinkronisasi.")
+
+ # Update hanya invoice yang dipilih
+ for inv in selected_invoices:
+ inv.write({'customer_promise_date': self.promise_date})
+ inv.message_post(
+ body=f"Janji Bayar {self.promise_date} disinkronkan dari invoice {self.invoice_id.name}."
+ )
+
+ # Log di invoice utama
+ self.invoice_id.message_post(
+ body=f"Janji Bayar {self.promise_date} disinkronkan ke {len(selected_invoices)} invoice lain: {', '.join(selected_invoices.mapped('name'))}."
+ )
+ return {'type': 'ir.actions.act_window_close'}
+
+
+class SyncPromiseDateWizardLine(models.TransientModel):
+ _name = "sync.promise.date.wizard.line"
+ _description = "Sync Janji Bayar Wizard Line"
+
+ wizard_id = fields.Many2one('sync.promise.date.wizard', string="Wizard")
+ invoice_id = fields.Many2one('account.move', string="Invoice")
+ sync_check = fields.Boolean(string="Sync?")
+ invoice_name = fields.Char(related="invoice_id.name", string="Nomor Invoice", readonly=True)
+ invoice_date_due = fields.Date(related="invoice_id.invoice_date_due", string="Due Date", readonly=True)
+ invoice_day_to_due = fields.Integer(related="invoice_id.invoice_day_to_due", string="Day to Due", readonly=True)
+ new_invoice_day_to_due = fields.Integer(related="invoice_id.new_invoice_day_to_due", string="New Day Due", readonly=True)
+ date_terima_tukar_faktur = fields.Date(related="invoice_id.date_terima_tukar_faktur", string="Tanggal Terima Tukar Faktur", readonly=True)
+ amount_total = fields.Monetary(related="invoice_id.amount_total", string="Total", readonly=True)
+ currency_id = fields.Many2one(related="invoice_id.currency_id", readonly=True) \ No newline at end of file