summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2025-09-17 11:28:03 +0000
committerIT Fixcomart <it@fixcomart.co.id>2025-09-17 11:28:03 +0000
commit701db966d5780ab0322e8cd9ca13acf06e10acdb (patch)
tree9fcaa8dfa5b671f95966c8e4e5e697de093c14ae /indoteknik_custom/models
parentfe75f5b4ad91ef9c5d54cd98449a53b8a40018bc (diff)
parent1fdeef8073eb35b407bb0b3cdb26bf635b3b1629 (diff)
Merged odoo-backup into cr_bu-related
Diffstat (limited to 'indoteknik_custom/models')
-rwxr-xr-xindoteknik_custom/models/__init__.py4
-rw-r--r--indoteknik_custom/models/account_move.py103
-rw-r--r--indoteknik_custom/models/letter_receivable.py496
-rwxr-xr-xindoteknik_custom/models/purchase_order.py3
-rwxr-xr-xindoteknik_custom/models/sale_order.py16
-rw-r--r--indoteknik_custom/models/stock_picking.py1
-rw-r--r--indoteknik_custom/models/unpaid_invoice_view.py55
7 files changed, 624 insertions, 54 deletions
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index c0aa7085..6dc61277 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -157,4 +157,6 @@ from . import refund_sale_order
from . import tukar_guling
from . import tukar_guling_po
from . import update_date_planned_po_wizard
-from . import sj_tele \ No newline at end of file
+from . import unpaid_invoice_view
+from . import letter_receivable
+from . import sj_tele
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 70cd07e4..ec23c626 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -192,49 +192,56 @@ class AccountMove(models.Model):
def send_due_invoice_reminder(self):
today = fields.Date.today()
target_dates = [
- today - timedelta(days=7),
- today - timedelta(days=3),
- today,
- today + timedelta(days=3),
today + timedelta(days=7),
+ today + timedelta(days=3),
+ today,
]
-
- for days_after_due in range(14, 181, 7):
- target_dates.append(today - timedelta(days=days_after_due))
-
invoices = self.env['account.move'].search([
('move_type', '=', 'out_invoice'),
('state', '=', 'posted'),
('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
('invoice_date_due', 'in', target_dates),
- ('date_terima_tukar_faktur', '!=', False)
- ])
- _logger.info(f"Invoices: {invoices}")
+ ('date_terima_tukar_faktur', '!=', False),
+ ('invoice_payment_term_id.name', 'ilike', 'tempo')])
+ _logger.info(f"Found {len(invoices)} invoices due for reminder {invoices}.")
+ if not invoices:
+ _logger.info("Tidak ada invoice yang due")
+ return
- invoices = invoices.filtered(
- lambda inv: inv.invoice_payment_term_id and 'tempo' in (inv.invoice_payment_term_id.name or '').lower()
- )
- # _logger.info(f"Invoices tahap 2: {invoices}")
+ self._send_invoice_reminders(invoices, mode='due')
+ def send_overdue_invoice_reminder(self):
+ today = fields.Date.today()
+ invoices = self.env['account.move'].search([
+ ('move_type', '=', 'out_invoice'),
+ ('state', '=', 'posted'),
+ ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']),
+ ('invoice_date_due', '<', today),
+ ('date_terima_tukar_faktur', '!=', False),
+ ('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 due")
+ _logger.info("Tidak ada invoice yang overdue")
return
+ self._send_invoice_reminders(invoices, mode='overdue')
+
+ def _send_invoice_reminders(self, invoices, mode):
+ today = fields.Date.today()
+ template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
invoice_group = {}
for inv in invoices:
dtd = (inv.invoice_date_due - today).days if inv.invoice_date_due else 0
- key = (inv.partner_id, dtd)
+ key = (inv.partner_id, dtd if mode == 'due' else "overdue")
if key not in invoice_group:
invoice_group[key] = self.env['account.move'] # recordset kosong
invoice_group[key] |= inv # gabung recordset
- template = self.env.ref('indoteknik_custom.mail_template_invoice_due_reminder')
-
for (partner, dtd), invs in invoice_group.items():
if all(inv.reminder_sent_date == today for inv in invs):
_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
@@ -276,11 +283,12 @@ class AccountMove(models.Model):
invoice_table_rows = ""
grand_total = 0
- for inv in invs:
+ for idx, inv in enumerate(invs, start=1): # numbering
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>{idx}</td>
<td>{inv.partner_id.name}</td>
<td>{inv.ref or '-'}</td>
<td>{inv.name}</td>
@@ -291,6 +299,7 @@ class AccountMove(models.Model):
<td>{days_to_due}</td>
</tr>
"""
+
invoice_table_footer = f"""
<tfoot>
<tr style="font-weight:bold; background-color:#f9f9f9;">
@@ -354,33 +363,33 @@ class AccountMove(models.Model):
days_to_due_message = ""
closing_message = ""
- if dtd > 0:
- days_to_due_message = (
- f"Kami ingin mengingatkan bahwa tagihan anda akan jatuh tempo dalam {dtd} hari ke depan, "
- "dengan rincian sebagai berikut:"
- )
- closing_message = (
- "Kami mengharapkan pembayaran dapat dilakukan tepat waktu untuk mendukung kelancaran "
- "hubungan kerja sama yang baik antara kedua belah pihak.<br/>"
- "Mohon konfirmasi apabila pembayaran telah dijadwalkan. "
- "Terima kasih atas perhatian dan kerja samanya."
- )
-
- if dtd == 0:
- days_to_due_message = (
- "Kami ingin mengingatkan bahwa tagihan anda telah memasuki tanggal jatuh tempo pada hari ini, "
- "dengan rincian sebagai berikut:"
- )
- closing_message = (
- "Mohon kesediaannya untuk segera melakukan pembayaran tepat waktu guna menghindari status "
- "keterlambatan dan menjaga kelancaran hubungan kerja sama yang telah terjalin dengan baik.<br/>"
- "Apabila pembayaran telah dijadwalkan atau diproses, mohon dapat dikonfirmasi kepada kami. "
- "Terima kasih atas perhatian dan kerja samanya."
- )
+ if mode == "due":
+ if dtd > 0:
+ days_to_due_message = (
+ f"Kami ingin mengingatkan bahwa tagihan anda akan jatuh tempo dalam {dtd} hari ke depan, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Kami mengharapkan pembayaran dapat dilakukan tepat waktu untuk mendukung kelancaran "
+ "hubungan kerja sama yang baik antara kedua belah pihak.<br/>"
+ "Mohon konfirmasi apabila pembayaran telah dijadwalkan. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
- if dtd < 0:
+ elif dtd == 0:
+ days_to_due_message = (
+ "Kami ingin mengingatkan bahwa tagihan anda telah memasuki tanggal jatuh tempo pada hari ini, "
+ "dengan rincian sebagai berikut:"
+ )
+ closing_message = (
+ "Mohon kesediaannya untuk segera melakukan pembayaran tepat waktu guna menghindari status "
+ "keterlambatan dan menjaga kelancaran hubungan kerja sama yang telah terjalin dengan baik.<br/>"
+ "Apabila pembayaran telah dijadwalkan atau diproses, mohon dapat dikonfirmasi kepada kami. "
+ "Terima kasih atas perhatian dan kerja samanya."
+ )
+ else: # mode overdue
days_to_due_message = (
- f"Kami ingin mengingatkan bahwa tagihan anda telah jatuh tempo selama {abs(dtd)} hari, "
+ f"Kami ingin mengingatkan bahwa beberapa tagihan anda telah jatuh tempo, "
"dengan rincian sebagai berikut:"
)
closing_message = (
@@ -414,7 +423,7 @@ class AccountMove(models.Model):
# Siapkan email values
values = {
'subject': f"Reminder Invoice Due - {partner.name}",
- # 'email_to': 'andrifebriyadiputra@gmail.com',
+ 'email_to': 'andrifebriyadiputra@gmail.com',
'email_to': email_to,
'email_from': 'finance@indoteknik.co.id',
'email_cc': ",".join(sorted(set(cc_list))),
diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py
new file mode 100644
index 00000000..1445800f
--- /dev/null
+++ b/indoteknik_custom/models/letter_receivable.py
@@ -0,0 +1,496 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+from odoo.exceptions import ValidationError
+from odoo.tools import mail, formatLang
+from terbilang import Terbilang
+import re
+import logging
+from datetime import datetime, timedelta
+import babel
+import base64
+import pytz
+
+_logger = logging.getLogger(__name__)
+
+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, 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'),
+ ('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"),
+ ("waiting_approval_sales", "Menunggu Approval Sales Manager"),
+ ("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")
+ periode_invoices_terpilih = fields.Char(
+ string="Periode Invoices Terpilih",
+ compute="_compute_periode_invoices",
+ )
+
+ 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",
+ )
+
+ 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)
+
+ 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')
+ 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, '')
+
+ 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"{dict(self._fields['perihal'].selection).get(next_perihal)} berhasil dibuat ({new_letter.name}).",
+ title="Informasi",
+ sticky=False
+ )
+ new_letter.message_post(
+ body=
+ f"<b>{dict(self._fields['perihal'].selection).get(next_perihal)}</b> "
+ f"berhasil dibuat berdasarkan surat sebelumnya.<br/>"
+ f"Nomor Surat: <b>{new_letter.name}</b>"
+ )
+ rec.message_post(
+ body=(
+ f"Surat lanjutan dengan perihal <b>{dict(self._fields['perihal'].selection).get(next_perihal)}</b> "
+ f"telah dibuat sebagai kelanjutan dari surat ini.<br/>"
+ f"Nomor Surat Baru: <a href='/web#id={new_letter.id}&model=surat.piutang&view_type=form'><b>{new_letter.name}</b></a>"
+ )
+ )
+ return True
+
+
+ @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:
+ 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):
+ wib = pytz.timezone('Asia/Jakarta')
+ now_wib = datetime.now(wib)
+
+ sales_manager_ids = [10] # ganti dengan ID user Sales Manager
+ pimpinan_user_ids = [7] # ganti dengan ID user Pimpinan
+
+ for rec in self:
+ # === 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 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 dan berhasil dikirim.")
+
+ 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()
+
+ 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:
+ # 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"""
+ <tr>
+ <td>{line.invoice_number or '-'}</td>
+ <td>{self.partner_id.name or '-'}</td>
+ <td>{fields.Date.to_string(line.invoice_date) or '-'}</td>
+ <td>{fields.Date.to_string(line.invoice_date_due) or '-'}</td>
+ <td>{line.new_invoice_day_to_due}</td>
+ <td>{line.ref or '-'}</td>
+ <td>{formatLang(self.env, line.amount_residual, currency_obj=line.currency_id)}</td>
+ <td>{line.payment_term_id.name or '-'}</td>
+ </tr>
+ """
+
+ invoice_table_footer = f"""
+ <tfoot>
+ <tr style="font-weight:bold; background-color:#f9f9f9;">
+ <td colspan="6" align="right">Grand Total</td>
+ <td>{formatLang(self.env, grand_total, currency_obj=self.currency_id, monetary=True)}</td>
+ <td colspan="2"></td>
+ </tr>
+ </tfoot>
+ """
+ # inject table rows ke template
+ body_html = re.sub(
+ r"<tbody[^>]*>.*?</tbody>",
+ f"<tbody>{invoice_table_rows}</tbody>{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 '')
+
+ 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"{self.perihal_label} - {self.partner_id.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 ''),
+ '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,
+ 'attachments': [(attachment.name, attachment.datas)],
+ 'reply_to': 'finance@indoteknik.co.id',
+ }
+
+ 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} "
+ f"({self.partner_id.name}), total {len(selected_lines)} invoice."
+ )
+
+ @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'
+ )
+ selected_invoice_id = self.env.context.get('default_selected_invoice_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,
+ 'invoice_date_due': inv.invoice_date_due,
+ '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': True if inv.invoice_id.id == selected_invoice_id else 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,
+ '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_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.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),
+ })
+
+ # 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):
+ # 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}"
+ if vals.get("perihal") == "penagihan":
+ vals["state"] = "waiting_approval_pimpinan"
+ else:
+ vals["state"] = "waiting_approval_sales"
+ 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')
+ 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')
+ 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')
+
+ 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
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 45fbe6e7..0304b5e2 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -117,7 +117,8 @@ class PurchaseOrder(models.Model):
)
show_description = fields.Boolean(
- string='Show Description'
+ string='Show Description',
+ default=True
)
@api.onchange('show_description')
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 484a9016..94c5f041 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -1848,7 +1848,8 @@ class SaleOrder(models.Model):
# if term.days > 0:
# raise UserError('Hanya dapat digunakan pada Cash Before Delivery')
for line in self.order_line:
- line.qty_to_invoice = line.product_uom_qty
+ if line.product_id.type == 'product':
+ line.qty_to_invoice = line.product_uom_qty
# def _get_pickings(self):
# state = ['assigned']
@@ -2412,17 +2413,22 @@ class SaleOrder(models.Model):
# Ambil blocking stage dari partner
block_stage = rec.partner_id.parent_id.blocking_stage if rec.partner_id.parent_id else rec.partner_id.blocking_stage or 0
is_cbd = rec.partner_id.parent_id.property_payment_term_id.id == 26 if rec.partner_id.parent_id else rec.partner_id.property_payment_term_id.id == 26 or False
+ partner_term = rec.partner_id.property_payment_term_id
+ partner_term_days_total = 0
+ if partner_term:
+ partner_term_days_total = sum((line.days or 0) for line in partner_term.line_ids)
+ is_partner_cbd = (partner_term_days_total == 0)
+ is_so_cbd = bool(rec.payment_term_id.id == 26)
- # Ambil jumlah nilai dari SO yang invoice_status masih 'to invoice'
so_to_invoice = 0
for sale in rec.partner_id.sale_order_ids:
if sale.invoice_status == 'to invoice':
so_to_invoice = so_to_invoice + sale.amount_total
- # Hitung remaining credit limit
- remaining_credit_limit = block_stage - current_total - so_to_invoice
+
+ remaining_credit_limit = block_stage - current_total - so_to_invoice if not is_cbd and not is_partner_cbd else 0
# Validasi limit
- if remaining_credit_limit <= 0 and block_stage > 0 and not is_cbd:
+ if remaining_credit_limit <= 0 and block_stage > 0 and not is_cbd and not is_so_cbd and not is_partner_cbd:
raise UserError(
_("The credit limit for %s will exceed the Blocking Stage if the Sale Order is confirmed. The remaining credit limit is %s, from %s and the outstanding amount is %s.")
% (rec.partner_id.name, block_stage - current_total, block_stage, outstanding_amount))
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 78a49ee4..35d408a1 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -307,6 +307,7 @@ class StockPicking(models.Model):
('delay', 'Delay By Vendor'),
('urgent', 'Urgent Delivery'),
], string='Reason Change Date Planned', tracking=True)
+ delivery_date = fields.Datetime(string='Delivery Date', copy=False)
def _get_kgx_awb_number(self):
"""Menggabungkan name dan origin untuk membuat AWB Number"""
diff --git a/indoteknik_custom/models/unpaid_invoice_view.py b/indoteknik_custom/models/unpaid_invoice_view.py
new file mode 100644
index 00000000..3eb6efc7
--- /dev/null
+++ b/indoteknik_custom/models/unpaid_invoice_view.py
@@ -0,0 +1,55 @@
+from odoo import models, fields
+
+class UnpaidInvoiceView(models.Model):
+ _name = 'unpaid.invoice.view'
+ _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')
+ # 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 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'),
+ ('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')
+
+ 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 {
+ '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,
+ }
+ }