diff options
| author | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2025-09-23 11:31:14 +0700 |
|---|---|---|
| committer | HafidBuroiroh <hafidburoiroh09@gmail.com> | 2025-09-23 11:31:14 +0700 |
| commit | e1678372f8af653d30d49f38abe8ca3129e29a03 (patch) | |
| tree | 2375aac399c56d917d0ed6d419b182fdaa50897e /indoteknik_custom/models | |
| parent | 6d50b35724592c4f8c302204adcfbc0f5db3727f (diff) | |
| parent | f58e6e2fa013789bfa8ac8456cd29735a83a56d0 (diff) | |
Merge branch 'odoo-backup' of https://bitbucket.org/altafixco/indoteknik-addons into refund_system
Diffstat (limited to 'indoteknik_custom/models')
19 files changed, 990 insertions, 223 deletions
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py index 3a9f9312..6dc61277 100755 --- a/indoteknik_custom/models/__init__.py +++ b/indoteknik_custom/models/__init__.py @@ -156,4 +156,7 @@ 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 +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..44b3cb76 100644 --- a/indoteknik_custom/models/account_move.py +++ b/indoteknik_custom/models/account_move.py @@ -99,6 +99,8 @@ class AccountMove(models.Model): reminder_sent_date = fields.Date(string="Tanggal Reminder Terkirim") + payment_difficulty = fields.Selection(string="Payment Difficulty", related='partner_id.payment_difficulty', readonly=True) + customer_promise_date = fields.Date( string="Janji Bayar", help="Tanggal janji bayar dari customer setelah reminder dikirim.", @@ -192,49 +194,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 +285,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 +301,7 @@ class AccountMove(models.Model): <td>{days_to_due}</td> </tr> """ + invoice_table_footer = f""" <tfoot> <tr style="font-weight:bold; background-color:#f9f9f9;"> @@ -324,12 +335,14 @@ class AccountMove(models.Model): 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' + # payment_term = partner.previous_payment_term_id if partner.is_cbd_locked else partner.property_payment_term_id + payment_term = invs[0].invoice_payment_term_id 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>Status Detail Tempo: {payment_term.name or ''}</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> @@ -354,33 +367,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 = ( diff --git a/indoteknik_custom/models/approval_payment_term.py b/indoteknik_custom/models/approval_payment_term.py index 08d91738..449bd90b 100644 --- a/indoteknik_custom/models/approval_payment_term.py +++ b/indoteknik_custom/models/approval_payment_term.py @@ -69,8 +69,8 @@ class ApprovalPaymentTerm(models.Model): return res def _track_changes_for_user_688(self, vals, old_values_dict): - if self.env.user.id != 688: - return + # if self.env.user.id != 688: + # return tracked_fields = {"blocking_stage", "warning_stage", "property_payment_term_id"} @@ -106,7 +106,8 @@ class ApprovalPaymentTerm(models.Model): if changes: timestamp = fields.Datetime.now().strftime('%Y-%m-%d %H:%M:%S') - rec.change_log_688 = f"{timestamp} - Perubahan oleh Widya:\n" + "\n".join(changes) + user = self.env.user + rec.change_log_688 = f"{timestamp} - Perubahan oleh {user.name}:\n" + "\n".join(changes) @staticmethod @@ -172,7 +173,7 @@ class ApprovalPaymentTerm(models.Model): 'warning_stage': self.warning_stage, 'active_limit': self.active_limit, 'property_payment_term_id': self.property_payment_term_id.id, - 'is_locked_cbd': False, + 'is_cbd_locked': False, }) self.approve_date = datetime.utcnow() self.state = 'approved' diff --git a/indoteknik_custom/models/dunning_run.py b/indoteknik_custom/models/dunning_run.py index 5a6aebac..9feea1d1 100644 --- a/indoteknik_custom/models/dunning_run.py +++ b/indoteknik_custom/models/dunning_run.py @@ -1,6 +1,6 @@ from odoo import models, api, fields from odoo.exceptions import AccessError, UserError, ValidationError -from datetime import timedelta +from datetime import timedelta, date import logging @@ -149,4 +149,5 @@ class DunningRunLine(models.Model): total_amt = fields.Float(string='Total Amount') open_amt = fields.Float(string='Open Amount') due_date = fields.Date(string='Due Date') + payment_term = fields.Many2one('account.payment.term', related='invoice_id.invoice_payment_term_id', string='Payment Term') diff --git a/indoteknik_custom/models/letter_receivable.py b/indoteknik_custom/models/letter_receivable.py new file mode 100644 index 00000000..16034938 --- /dev/null +++ b/indoteknik_custom/models/letter_receivable.py @@ -0,0 +1,508 @@ +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', + }) + + cc_list = [ + 'finance@indoteknik.co.id', + 'akbar@indoteknik.co.id', + 'stephan@indoteknik.co.id', + 'darren@indoteknik.co.id' + ] + + sales_email = self.sales_person_id.email if self.sales_person_id else None + if sales_email and sales_email not in cc_list: + cc_list.append(sales_email) + + 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', + 'email_cc': ",".join(sorted(set(cc_list))), + '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/logbook_sj.py b/indoteknik_custom/models/logbook_sj.py index 75b2622f..0cda9c8b 100644 --- a/indoteknik_custom/models/logbook_sj.py +++ b/indoteknik_custom/models/logbook_sj.py @@ -24,6 +24,7 @@ class LogbookSJ(models.TransientModel): } report_logbook = self.env['report.logbook.sj'].create([parameters_header]) + seq=1 for line in logbook_line: picking = self.env['stock.picking'].search([('picking_code', '=', line.name)], limit=1) if not picking: @@ -43,9 +44,11 @@ class LogbookSJ(models.TransientModel): 'tracking_no': stock.delivery_tracking_no, 'partner_id': parent_id, 'report_logbook_sj_id': report_logbook.id, - 'note': line.note + 'note': line.note, + 'line_num': seq } self.env['report.logbook.sj.line'].create([data]) + seq += 1 report_logbook_ids.append(report_logbook.id) line.unlink() diff --git a/indoteknik_custom/models/manufacturing.py b/indoteknik_custom/models/manufacturing.py index aea01362..f986fd4f 100644 --- a/indoteknik_custom/models/manufacturing.py +++ b/indoteknik_custom/models/manufacturing.py @@ -4,54 +4,56 @@ import logging _logger = logging.getLogger(__name__) + class Manufacturing(models.Model): _inherit = 'mrp.production' unbuild_counter = fields.Integer(string='Unbuild Counter', default=0, help='For restrict unbuild more than once') - + def action_confirm(self): if self._name != 'mrp.production': return super(Manufacturing, self).action_confirm() if not self.env.user.is_purchasing_manager: raise UserError("Hanya bisa di confirm oleh Purchasing Manager") - + # if self.location_src_id.id != 75: # raise UserError('Component Location hanya bisa di AS/Stock') # elif self.location_dest_id.id != 75: # raise UserError('Finished Product Location hanya bisa di AS/Stock') - + result = super(Manufacturing, self).action_confirm() return result - + def button_mark_done(self): if self._name != 'mrp.production': return super(Manufacturing, self).button_mark_done() # Check product category if self.product_id.categ_id.name != 'Finish Good': raise UserError('Tidak bisa di complete karna product category bukan Unit / Finish Good') - + if self.sale_order and self.sale_order.state != 'sale': raise UserError( ('Tidak bisa Mark as Done.\nSales Order "%s" (Nomor: %s) belum dikonfirmasi.') % (self.sale_order.partner_id.name, self.sale_order.name) ) - + for line in self.move_raw_ids: # if line.quantity_done > 0 and line.quantity_done != self.product_uom_qty: # raise UserError('Qty Consume per Line tidak sama dengan Qty to Produce') if line.forecast_availability != line.product_uom_qty: - raise UserError('Qty Reserved belum sesuai dengan yang seharusnya, product: %s' % line.product_id.display_name) + raise UserError( + 'Qty Reserved belum sesuai dengan yang seharusnya, product: %s' % line.product_id.display_name) result = super(Manufacturing, self).button_mark_done() return result - + def button_unbuild(self): if self._name != 'mrp.production': return super(Manufacturing, self).button_unbuild() - + if self.unbuild_counter >= 1: raise UserError('Tidak bisa unbuild lebih dari 1 kali') - + self.unbuild_counter = self.unbuild_counter + 1 result = super(Manufacturing, self).button_unbuild() diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py index 18811b85..b34ec926 100755 --- a/indoteknik_custom/models/purchase_order.py +++ b/indoteknik_custom/models/purchase_order.py @@ -6,6 +6,7 @@ import logging from pytz import timezone, utc import io import base64 +from odoo.tools import lazy_property try: from odoo.tools.misc import xlsxwriter except ImportError: @@ -115,6 +116,20 @@ class PurchaseOrder(models.Model): compute='_compute_complete_bu_in_count' ) + show_description = fields.Boolean( + string='Show Description', + default=True + ) + + @api.onchange('show_description') + def onchange_show_description(self): + if self.show_description == True: + for line in self.order_line: + line.show_description = True + else: + for line in self.order_line: + line.show_description = False + def _compute_complete_bu_in_count(self): for order in self: if order.state not in ['done', 'cancel']: @@ -137,7 +152,7 @@ class PurchaseOrder(models.Model): def _compute_date_planned(self): """ date_planned = the earliest date_planned across all order lines. """ for order in self: - order.date_planned = False + order.date_planned = order.date_planned @api.constrains('date_planned') def constrains_date_planned(self): @@ -183,8 +198,11 @@ class PurchaseOrder(models.Model): # Ambil semua BU awal dari PO base_bu = StockPicking.search([ + '|', + '&', ('name', 'ilike', 'BU/'), - ('origin', 'ilike', order.name) + ('group_id.id', '=', order.group_id.id), + ('origin', '=', order.name), ]) all_bu = base_bu @@ -214,10 +232,12 @@ class PurchaseOrder(models.Model): # Step 1: cari semua BU pertama (PUT, INT) yang berasal dari PO ini base_bu = StockPicking.search([ + '|', + '&', ('name', 'ilike', 'BU/'), - ('origin', 'ilike', self.name) + ('group_id.id', '=', self.group_id.id), + ('origin', '=', self.name), ]) - all_bu = base_bu seen_names = set(base_bu.mapped('name')) @@ -228,10 +248,10 @@ class PurchaseOrder(models.Model): ('origin', 'in', ['Return of %s' % name for name in seen_names]) ]) next_names = set(next_bu.mapped('name')) - + if not next_names - seen_names: break - + all_bu |= next_bu seen_names |= next_names @@ -1037,8 +1057,19 @@ class PurchaseOrder(models.Model): message="Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")", sticky=True ) + + def _check_assets_note(self): + for order in self: + # Cari apakah ada line dengan produk ID 614469 ('Assets Mesin & Peralatan') + asset_line = order.order_line.filtered(lambda l: l.product_id.id == 595346) + if asset_line and not order.notes: + raise UserError(_( + "%s berisi produk 'Assets Mesin & Peralatan'. " + "Harap isi Notes untuk menjelaskan kebutuhan dan divisi terkait." + ) % order.name) def button_confirm(self): + self._check_assets_note() # self._check_payment_term() # check payment term res = super(PurchaseOrder, self).button_confirm() current_time = datetime.now() @@ -1066,8 +1097,11 @@ class PurchaseOrder(models.Model): # sticky=True # ) + has_bom = self.product_bom_id.id + has_manufacturing = self.manufacturing_id.id + if not self.from_apo: - if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader: + if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not has_bom and not has_manufacturing: raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager") send_email = False diff --git a/indoteknik_custom/models/purchase_order_line.py b/indoteknik_custom/models/purchase_order_line.py index 315795d5..a3c3a33b 100755 --- a/indoteknik_custom/models/purchase_order_line.py +++ b/indoteknik_custom/models/purchase_order_line.py @@ -50,6 +50,7 @@ class PurchaseOrderLine(models.Model): cost_service_per_item = fields.Float(string='Biaya Jasa Per Item', compute='_compute_doc_delivery_amt') contribution_cost_service = fields.Float(string='Contribution Cost Service', compute='_compute_doc_delivery_amt') ending_price = fields.Float(string='Ending Price', compute='_compute_doc_delivery_amt') + show_description = fields.Boolean(string='Show Description', help="Show Description when print po", default=True) def _compute_doc_delivery_amt(self): for line in self: diff --git a/indoteknik_custom/models/refund_sale_order.py b/indoteknik_custom/models/refund_sale_order.py index d89954cc..d4702210 100644 --- a/indoteknik_custom/models/refund_sale_order.py +++ b/indoteknik_custom/models/refund_sale_order.py @@ -1177,7 +1177,7 @@ class RefundSaleOrder(models.Model): 'origin': ','.join(refund.sale_order_ids.mapped('name')), 'origin_so': refund.sale_order_ids.id, 'operations': picking.id, - 'return_type': 'revisi_so', + 'return_type': 'retur_so', 'invoice_id': [(6, 0, refund.invoice_ids.ids)], 'refund_id': refund.id, 'line_ids': line_vals, diff --git a/indoteknik_custom/models/report_logbook_sj.py b/indoteknik_custom/models/report_logbook_sj.py index 17119c12..3b07ff02 100644 --- a/indoteknik_custom/models/report_logbook_sj.py +++ b/indoteknik_custom/models/report_logbook_sj.py @@ -1,7 +1,14 @@ +from operator import index + from odoo import models, fields, api from odoo.exceptions import UserError from pytz import timezone from datetime import datetime +import requests +import json +import logging + +_logger = logging.getLogger(__name__) class ReportLogbookSJ(models.Model): _name = 'report.logbook.sj' @@ -60,9 +67,28 @@ class ReportLogbookSJ(models.Model): self.state = 'terima_semua' else: raise UserError('Hanya Accounting yang bisa Approve') - + + + def write(self, vals): + res = super(ReportLogbookSJ, self).write(vals) + if 'report_logbook_sj_line' in vals or any(f in vals for f in ()): + self._resequence_lines() + return res + + def _resequence_lines(self): + for rec in self: + lines = rec.report_logbook_sj_line.sorted(key=lambda l: (l.line_num or 0, l.id)) + for idx, line in enumerate(lines, start=1): + if line.line_num != idx: + line.line_num = idx + + @api.onchange('report_logbook_sj_line') + def _onchange_report_logbook_sj_line(self): + self._resequence_lines() + class ReportLogbookSJLine(models.Model): _name = 'report.logbook.sj.line' + _order = 'sequence, id' # urut default di UI & ORM (drag pakai sequence) name = fields.Char(string='SJ Number') driver_id = fields.Many2one(comodel_name='res.users', string='Driver') @@ -70,10 +96,41 @@ class ReportLogbookSJLine(models.Model): arrival_date = fields.Char(string='Arrival Date') carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method') tracking_no = fields.Char(string='Tracking No') - logbook_sj_id = fields.Many2one('report.logbook.sj', string='Logbook SJ') # Corrected model name + + # NOTE: field ini duplikat relasi; pakai salah satu saja. + # kamu boleh hapus logbook_sj_id kalau tidak dipakai di tempat lain. + logbook_sj_id = fields.Many2one('report.logbook.sj', string='Logbook SJ') + partner_id = fields.Many2one('res.partner', string='Customer') picking_id = fields.Many2one('stock.picking', string='Picking') sale_id = fields.Many2one('sale.order', string='Sale Order') + report_logbook_sj_id = fields.Many2one('report.logbook.sj', string='Logbook SJ') not_exist = fields.Boolean(string='Not Exist') note = fields.Char(string='Note') + + sequence = fields.Integer(string='Sequence', default=0, index=True) + + line_num = fields.Integer(string='No', compute='_compute_line_num', store=False) + + @api.depends( + 'report_logbook_sj_id.report_logbook_sj_line', + 'report_logbook_sj_id.report_logbook_sj_line.sequence' + ) + def _compute_line_num(self): + for parent in self.mapped('report_logbook_sj_id'): + lines = parent.report_logbook_sj_line.sorted(key=lambda l: (l.sequence or 0, l.id)) + for i, l in enumerate(lines, start=1): + l.line_num = i + for rec in self.filtered(lambda r: not r.report_logbook_sj_id): + rec.line_num = 0 + + @api.model + def create(self, vals): + if not vals.get('sequence') and vals.get('report_logbook_sj_id'): + last = self.search( + [('report_logbook_sj_id', '=', vals['report_logbook_sj_id'])], + order='sequence desc, id desc', limit=1 + ) + vals['sequence'] = (last.sequence or 0) + 1 + return super().create(vals) diff --git a/indoteknik_custom/models/res_partner.py b/indoteknik_custom/models/res_partner.py index 36570e8f..8aaee47e 100644 --- a/indoteknik_custom/models/res_partner.py +++ b/indoteknik_custom/models/res_partner.py @@ -194,6 +194,12 @@ class ResPartner(models.Model): default=_default_payment_term, tracking=3 ) + previous_payment_term_id = fields.Many2one( + 'account.payment.term', + string='Previous Payment Term', + readonly=True + ) + @api.depends("street", "street2", "city", "state_id", "country_id", "blok", "nomor", "rt", "rw", "kelurahan_id", "kecamatan_id") diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 35d1f087..39830ffc 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -234,9 +234,9 @@ class SaleOrder(models.Model): customer_type = fields.Selection([ ('pkp', 'PKP'), ('nonpkp', 'Non PKP') - ], required=True, compute='_compute_partner_field') - sppkp = fields.Char(string="SPPKP", required=True, tracking=True, compute='_compute_partner_field') - npwp = fields.Char(string="NPWP", required=True, tracking=True, compute='_compute_partner_field') + ], related="partner_id.customer_type", string="Customer Type", readonly=True) + sppkp = fields.Char(string="SPPKP", related="partner_id.sppkp") + npwp = fields.Char(string="NPWP", related="partner_id.npwp") purchase_total = fields.Monetary(string='Purchase Total', compute='_compute_purchase_total') voucher_id = fields.Many2one(comodel_name='voucher', string='Voucher', copy=False) applied_voucher_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False) @@ -1674,7 +1674,7 @@ class SaleOrder(models.Model): rec.expected_ready_to_ship = eta_date @api.depends("order_line.product_id", "date_order") - def _compute_etrts_date(self): # Function to calculate Estimated Ready To Ship Date + def _compute_etrts_date(self): self._calculate_etrts_date() @@ -1844,11 +1844,12 @@ class SaleOrder(models.Model): def override_allow_create_invoice(self): if not self.env.user.is_accounting: raise UserError('Hanya Finance Accounting yang dapat klik tombol ini') - for term in self.payment_term_id.line_ids: - if term.days > 0: - raise UserError('Hanya dapat digunakan pada Cash Before Delivery') + # for term in self.payment_term_id.line_ids: + # 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'] @@ -1876,6 +1877,8 @@ class SaleOrder(models.Model): }) def open_form_multi_update_status(self): + if self.env.user.id != 688 or self.env.user.has_group('indoteknik_custom.group_role_it'): + raise UserError("Hanya Finance nya yang bisa approve.") action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_sale_orders_multi_update') action['context'] = { 'sale_ids': [x.id for x in self] @@ -2049,22 +2052,22 @@ class SaleOrder(models.Model): # return [('id', 'not in', order_ids)] # return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)] - @api.depends('partner_id') - def _compute_partner_field(self): - for order in self: - partner = order.partner_id.parent_id or order.partner_id - order.npwp = partner.npwp - order.sppkp = partner.sppkp - order.customer_type = partner.customer_type + # @api.depends('partner_id') + # def _compute_partner_field(self): + # for order in self: + # partner = order.partner_id.parent_id or order.partner_id + # order.npwp = partner.npwp + # order.sppkp = partner.sppkp + # order.customer_type = partner.customer_type @api.onchange('partner_id') def onchange_partner_contact(self): parent_id = self.partner_id.parent_id parent_id = parent_id if parent_id else self.partner_id - self.npwp = parent_id.npwp - self.sppkp = parent_id.sppkp - self.customer_type = parent_id.customer_type + # self.npwp = parent_id.npwp + # self.sppkp = parent_id.sppkp + # self.customer_type = parent_id.customer_type self.email = parent_id.email self.pareto_status = parent_id.pareto_status self.user_id = parent_id.user_id @@ -2141,9 +2144,15 @@ class SaleOrder(models.Model): if self.state not in ['draft', 'sent']: raise UserError("Status harus draft atau sent") - self._validate_npwp() - def _validate_npwp(self): + if not self.npwp: + raise UserError("NPWP partner kosong, silahkan isi terlebih dahulu npwp nya di contact partner") + + if not self.customer_type: + raise UserError("Customer Type partner kosong, silahkan isi terlebih dahulu Customer Type nya di contact partner") + + if not self.sppkp: + raise UserError("SPPKP partner kosong, silahkan isi terlebih dahulu SPPKP nya di contact partner") num_digits = sum(c.isdigit() for c in self.npwp) if num_digits < 10: @@ -2157,6 +2166,7 @@ class SaleOrder(models.Model): self._validate_order() for order in self: + order._validate_npwp() order._validate_uniform_taxes() order.order_line.validate_line() @@ -2211,9 +2221,8 @@ class SaleOrder(models.Model): if self.validate_different_vendor() and not self.vendor_approval: return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor') self.check_due() - - self._validate_order() for order in self: + order._validate_npwp() order._validate_delivery_amt() order._validate_uniform_taxes() order.order_line.validate_line() @@ -2406,17 +2415,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)) @@ -2480,6 +2494,7 @@ class SaleOrder(models.Model): order.check_data_real_delivery_address() order.sale_order_check_approve() order._validate_order() + order._validate_npwp() order.order_line.validate_line() main_parent = order.partner_id.get_main_parent() @@ -2637,7 +2652,7 @@ class SaleOrder(models.Model): if user.is_leader or user.is_sales_manager: return True - if user.id in (3401, 20, 3988): # admin (fida, nabila, ninda) + if user.id in (3401, 20, 3988, 17340): # admin (fida, nabila, ninda) return True if self.env.context.get("ask_approval") and user.id in (3401, 20, 3988): @@ -2665,23 +2680,17 @@ class SaleOrder(models.Model): def _set_sppkp_npwp_contact(self): partner = self.partner_id.parent_id or self.partner_id - if not partner.sppkp: - partner.sppkp = self.sppkp - if not partner.npwp: - partner.npwp = self.npwp + # if not partner.sppkp: + # partner.sppkp = self.sppkp + # if not partner.npwp: + # partner.npwp = self.npwp if not partner.email: partner.email = self.email - if not partner.customer_type: - partner.customer_type = self.customer_type + # if not partner.customer_type: + # partner.customer_type = self.customer_type if not partner.user_id: partner.user_id = self.user_id.id - # if not partner.sppkp or not partner.npwp or not partner.email or partner.customer_type: - # partner.customer_type = self.customer_type - # partner.npwp = self.npwp - # partner.sppkp = self.sppkp - # partner.email = self.email - def _compute_total_margin(self): for order in self: total_margin = sum(line.item_margin for line in order.order_line if line.product_id) @@ -3123,52 +3132,6 @@ class SaleOrder(models.Model): # order._update_partner_details() return order - # def write(self, vals): - # Call the super method to handle the write operation - # res = super(SaleOrder, self).write(vals) - # self._compute_etrts_date() - # Check if the update is coming from a save operation - # if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']): - # self._update_partner_details() - - # return res - - def _update_partner_details(self): - for order in self: - partner = order.partner_id.parent_id or order.partner_id - if partner: - # Update partner details - partner.sppkp = order.sppkp - partner.npwp = order.npwp - partner.email = order.email - partner.customer_type = order.customer_type - - # Save changes to the partner record - partner.write({ - 'sppkp': partner.sppkp, - 'npwp': partner.npwp, - 'email': partner.email, - 'customer_type': partner.customer_type, - }) - - # def write(self, vals): - # for order in self: - # if order.state in ['sale', 'cancel']: - # if 'order_line' in vals: - # new_lines = vals.get('order_line', []) - # for command in new_lines: - # if command[0] == 0: # A new line is being added - # raise UserError( - # "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") - # - # res = super(SaleOrder, self).write(vals) - # # self._check_total_margin_excl_third_party() - # if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): - # self._validate_delivery_amt() - # if any(field in vals for field in ["order_line", "client_order_ref"]): - # self._calculate_etrts_date() - # return res - # @api.depends('commitment_date') def _compute_ready_to_ship_status_detail(self): def is_empty(val): diff --git a/indoteknik_custom/models/sale_order_line.py b/indoteknik_custom/models/sale_order_line.py index 47a24264..1f2ea1fb 100644 --- a/indoteknik_custom/models/sale_order_line.py +++ b/indoteknik_custom/models/sale_order_line.py @@ -71,23 +71,17 @@ class SaleOrderLine(models.Model): if order_qty > 0: for move in line.move_ids: - # --- CASE 1: Move belum selesai --- if move.state not in ('done', 'cancel'): reserved_qty += move.reserved_availability or 0.0 continue - # --- CASE 2: Move sudah done --- if move.location_dest_id.usage == 'customer': - # Barang dikirim ke customer delivered_qty += move.quantity_done or 0.0 elif move.location_id.usage == 'customer': - # Barang balik dari customer (retur) delivered_qty -= move.quantity_done or 0.0 - # Clamp supaya delivered gak minus delivered_qty = max(delivered_qty, 0) - # Hitung persen line.reserved_percent = min((reserved_qty / order_qty) * 100, 100) if order_qty else 0 line.delivered_percent = min((delivered_qty / order_qty) * 100, 100) if order_qty else 0 line.unreserved_percent = max(100 - line.reserved_percent - line.delivered_percent, 0) diff --git a/indoteknik_custom/models/sj_tele.py b/indoteknik_custom/models/sj_tele.py new file mode 100644 index 00000000..d44aa338 --- /dev/null +++ b/indoteknik_custom/models/sj_tele.py @@ -0,0 +1,102 @@ +from odoo import models, fields, api +from odoo.exceptions import UserError +import requests +import json +import logging, subprocess +import time +from collections import OrderedDict + +_logger = logging.getLogger(__name__) + +class SjTele(models.Model): + _name = 'sj.tele' + _description = 'sj.tele' + + picking_id = fields.Many2one('stock.picking', string='Picking') + sale_id = fields.Many2one('sale.order', string='Sales Order') + picking_name = fields.Char(string='Picking Name') + sale_name = fields.Char(string='Sale Name') + create_date = fields.Datetime(string='Create Date') + date_doc_kirim = fields.Datetime(string='Tanggal Kirim SJ') + + # @api.model + # def run_pentaho_carte(self): + # carte = "http://127.0.0.1:8080" + # job_kjb = r"C:/Users/Indoteknik/Desktop/tes.kjb" + # params = {"job": job_kjb, "level": "Basic", "block": "Y"} + # try: + # r = requests.get( + # f"{carte}/kettle/executeJob/", + # params=params, + # auth=("cluster", "cluster"), + # timeout=900, + # ) + # r.raise_for_status() + # # kalau Carte mengembalikan <result>ERROR</result>, anggap gagal + # if "<result>ERROR</result>" in r.text: + # raise UserError(f"Carte error: {r.text}") + # except Exception as e: + # _logger.exception("Carte call failed: %s", e) + # raise UserError(f"Gagal memanggil Carte: {e}") + + # time.sleep(3) + + # self.env['sj.tele'].sudo().woi() + + # return True + + def woi(self): + bot_mqdd = '8203414501:AAHy_XwiUAVrgRM2EJzW7sZx9npRLITZpb8' + chat_id_mqdd = '-1003087280519' + api_base = f'https://api.telegram.org/bot{bot_mqdd}' + + data = self.search([], order='create_date asc', limit=15) + + if not data: + text = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\nā
tidak ada data (semua sudah tercatat)." + try: + r = requests.post(api_base + "/sendMessage", + json={'chat_id': chat_id_mqdd, 'text': text}, + timeout=20) + r.raise_for_status() + except Exception as e: + _logger.exception("Gagal kirim Telegram (no data): %s", e) + return True + + + lines = [] + groups = OrderedDict() + + for rec in data: + name = rec.picking_name or (rec.picking_id.name if rec.picking_id else '') + pid = rec.picking_id.id if rec.picking_id else '' + so = rec.sale_id.name or rec.sale_name or '' + dttm = (rec.picking_id.date_doc_kirim if (rec.picking_id and rec.picking_id.date_doc_kirim) + else getattr(rec, 'date_doc_kirim', None)) + + # format header tanggal (string), tanpa konversi Waktu/WIB + if dttm: + date_header = dttm if isinstance(dttm, str) else fields.Datetime.to_string(dttm) + date_header = date_header[:10] + else: + date_header = '(Tidak ada tanggal kirim SJ)' + + if name: + groups.setdefault(date_header, []).append(f"- ({pid}) - {name} - {so}") + + # build output berurutan per tanggal + for header_date, items in groups.items(): + lines.append(header_date) + lines.extend(items) + + + header = "Berikut merupakan nomor BU/OUT yang belum ada di Logbook SJ report:\n" + text = header + "\n".join(lines) + + try: + r = requests.post(api_base + "/sendMessage", + json={'chat_id': chat_id_mqdd, 'text': text}) + r.raise_for_status() + except Exception as e: + _logger.exception("Gagal kirim Telegram: %s", e) + return True
\ No newline at end of file diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index a48e0ed1..67106073 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""" @@ -1351,6 +1352,19 @@ class StockPicking(models.Model): if self.picking_type_code == 'outgoing' and 'BU/OUT/' in self.name: self.check_koli() res = super(StockPicking, self).button_validate() + + # Penambahan link PO di Stock Journal untuk Picking BD + for picking in self: + if picking.name and 'BD/' in picking.name and picking.purchase_id: + stock_journal = self.env['account.move'].search([ + ('ref', 'ilike', picking.name + '%'), + ('journal_id', '=', 3) # Stock Journal ID + ], limit = 1) + if stock_journal: + stock_journal.write({ + 'purchase_order_id': picking.purchase_id.id + }) + self.date_done = datetime.datetime.utcnow() self.state_reserve = 'done' self.final_seq = 0 @@ -1743,27 +1757,37 @@ class StockPicking(models.Model): } if self.biteship_id: - histori = self.get_manifest_biteship() - day_start = order.estimated_arrival_days_start - day_end = order.estimated_arrival_days - if sale_order_delay: - if sale_order_delay.status == 'delayed': - day_start = day_start + sale_order_delay.days_delayed - day_end = day_end + sale_order_delay.days_delayed - elif sale_order_delay.status == 'early': - day_start = day_start - sale_order_delay.days_delayed - day_end = day_end - sale_order_delay.days_delayed - - eta_start = order.date_order + timedelta(days=day_start) - eta_end = order.date_order + timedelta(days=day_end) - formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" - response['eta'] = formatted_eta - response['manifests'] = histori.get("manifests", []) - response['delivered'] = histori.get("delivered", - False) or self.sj_return_date != False or self.driver_arrival_date != False - response['status'] = self._map_status_biteship(histori.get("delivered")) + try: + histori = self.get_manifest_biteship() + day_start = order.estimated_arrival_days_start + day_end = order.estimated_arrival_days + if sale_order_delay: + if sale_order_delay.status == 'delayed': + day_start += sale_order_delay.days_delayed + day_end += sale_order_delay.days_delayed + elif sale_order_delay.status == 'early': + day_start -= sale_order_delay.days_delayed + day_end -= sale_order_delay.days_delayed + + eta_start = order.date_order + timedelta(days=day_start) + eta_end = order.date_order + timedelta(days=day_end) + formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" + + response['eta'] = formatted_eta + response['manifests'] = histori.get("manifests", []) + response['delivered'] = ( + histori.get("delivered", False) + or self.sj_return_date != False + or self.driver_arrival_date != False + ) + response['status'] = self._map_status_biteship(histori.get("delivered")) - return response + return response + + except Exception as e: + # Kalau ada error di biteship, log dan fallback ke Odoo + _logger.warning("Biteship error pada DO %s: %s", self.name, str(e)) + # biarkan lanjut ke kondisi di bawah (pakai Odoo waybill_id) if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0: response['delivered'] = self.sj_return_date != False or self.driver_arrival_date != False diff --git a/indoteknik_custom/models/tukar_guling.py b/indoteknik_custom/models/tukar_guling.py index 6e839bf0..c683f75a 100644 --- a/indoteknik_custom/models/tukar_guling.py +++ b/indoteknik_custom/models/tukar_guling.py @@ -61,7 +61,7 @@ class TukarGuling(models.Model): notes = fields.Text('Notes') return_type = fields.Selection(String='Return Type', selection=[ ('tukar_guling', 'Tukar Guling'), # -> barang yang sama - ('revisi_so', 'Revisi SO')], required=True, tracking=3) + ('retur_so', 'Retur SO')], required=True, tracking=3, help='Retur SO (ORT-SRT),\n Tukar Guling (ORT-SRT-PICK-OUT)') state = fields.Selection(string='Status', selection=[ ('draft', 'Draft'), ('approval_sales', ' Approval Sales'), @@ -169,7 +169,7 @@ class TukarGuling(models.Model): raise UserError("ā Picking type harus BU/OUT atau BU/PICK") for rec in self: if rec.operations and rec.operations.picking_type_id.id == 30: - rec.return_type = 'revisi_so' + rec.return_type = 'retur_so' if self.operations: from_return_picking = self.env.context.get('from_return_picking', False) or \ @@ -315,7 +315,7 @@ class TukarGuling(models.Model): @api.constrains('return_type', 'operations') def _check_required_bu_fields(self): for record in self: - if record.return_type in ['revisi_so', 'tukar_guling'] and not record.operations: + if record.return_type in ['retur_so', 'tukar_guling'] and not record.operations: raise ValidationError("Operations harus diisi") @api.constrains('line_ids', 'state') @@ -352,16 +352,16 @@ class TukarGuling(models.Model): # ('state', '!=', 'cancel') # ]) > 0 - # def _check_invoice_on_revisi_so(self): + # def _check_invoice_on_retur_so(self): # for record in self: - # if record.return_type == 'revisi_so' and record.origin: + # if record.return_type == 'retur_so' and record.origin: # invoices = self.env['account.move'].search([ # ('invoice_origin', 'ilike', record.origin), # ('state', 'not in', ['draft', 'cancel']) # ]) # if invoices: # raise ValidationError( - # _("Tidak bisa memilih Return Type 'Revisi SO' karena dokumen %s sudah dibuat invoice.") % record.origin + # _("Tidak bisa memilih Return Type 'Retur SO' karena dokumen %s sudah dibuat invoice.") % record.origin # ) @@ -414,7 +414,7 @@ class TukarGuling(models.Model): self.ensure_one() if self.operations.picking_type_id.id not in [29, 30]: raise UserError("ā Picking type harus BU/OUT atau BU/PICK") - # self._check_invoice_on_revisi_so() + # self._check_invoice_on_retur_so() operasi = self.operations.picking_type_id.id tipe = self.return_type pp = vals.get('return_type', tipe) @@ -530,7 +530,7 @@ class TukarGuling(models.Model): raise UserError( _("Qty di Koli tidak sesuai dengan qty retur untuk produk %s") % line.product_id.display_name ) - # self._check_invoice_on_revisi_so() + # self._check_invoice_on_retur_so() self._validate_product_lines() if self.state != 'draft': @@ -553,7 +553,7 @@ class TukarGuling(models.Model): self.state = 'done' # OUT revisi SO - elif self.operations.picking_type_id.id == 29 and self.return_type == 'revisi_so': + elif self.operations.picking_type_id.id == 29 and self.return_type == 'retur_so': total_ort = self.env['stock.picking'].search_count([ ('tukar_guling_id', '=', self.id), ('picking_type_id', '=', 74), @@ -567,7 +567,7 @@ class TukarGuling(models.Model): self.state = 'done' # PICK revisi SO - elif self.operations.picking_type_id.id == 30 and self.return_type == 'revisi_so': + elif self.operations.picking_type_id.id == 30 and self.return_type == 'retur_so': done_ort = self.env['stock.picking'].search([ ('tukar_guling_id', '=', self.id), ('picking_type_id', '=', 74), @@ -581,7 +581,7 @@ class TukarGuling(models.Model): def action_approve(self): self.ensure_one() self._validate_product_lines() - # self._check_invoice_on_revisi_so() + # self._check_invoice_on_retur_so() self._check_not_allow_tukar_guling_on_bu_pick() operasi = self.operations.picking_type_id.id @@ -631,7 +631,7 @@ class TukarGuling(models.Model): elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance Manager yang boleh approve tahap ini.") - # rec._check_invoice_on_revisi_so() + # rec._check_invoice_on_retur_so() rec.set_opt() rec.state = 'approval_logistic' rec.date_finance = now @@ -710,7 +710,7 @@ class TukarGuling(models.Model): ### ======== SRT dari BU/OUT ========= srt_return_lines = [] - if mapping_koli: + if mapping_koli and record.operations.picking_type_id.id == 29: for prod in mapping_koli.mapped('product_id'): qty_total = sum(mk.qty_return for mk in mapping_koli.filtered(lambda m: m.product_id == prod)) move = bu_out.move_lines.filtered(lambda m: m.product_id == prod) @@ -723,7 +723,7 @@ class TukarGuling(models.Model): })) _logger.info(f"š SRT line: {prod.display_name} | qty={qty_total}") - elif not mapping_koli: + elif not mapping_koli and record.operations.picking_type_id.id == 29: for line in record.line_ids: move = bu_out.move_lines.filtered(lambda m: m.product_id == line.product_id) if not move: diff --git a/indoteknik_custom/models/tukar_guling_po.py b/indoteknik_custom/models/tukar_guling_po.py index f2f37606..2a5ca3dd 100644 --- a/indoteknik_custom/models/tukar_guling_po.py +++ b/indoteknik_custom/models/tukar_guling_po.py @@ -38,9 +38,9 @@ class TukarGulingPO(models.Model): ) ba_num = fields.Char('Nomor BA', tracking=3) return_type = fields.Selection([ - ('revisi_po', 'Revisi PO'), + ('retur_po', 'Retur PO'), ('tukar_guling', 'Tukar Guling'), - ], string='Return Type', required=True, tracking=3) + ], string='Return Type', required=True, tracking=3, help='Retur PO (VRT-PRT),\n Tukar Guling (VRT-PRT-INPUT-PUT') notes = fields.Text('Notes', tracking=3) tukar_guling_po_id = fields.Many2one('tukar.guling.po', string='Tukar Guling PO', ondelete='cascade') line_ids = fields.One2many('tukar.guling.line.po', 'tukar_guling_po_id', string='Product Lines', tracking=3) @@ -143,9 +143,9 @@ class TukarGulingPO(models.Model): return res - # def _check_bill_on_revisi_po(self): + # def _check_bill_on_retur_po(self): # for record in self: - # if record.return_type == 'revisi_po' and record.origin: + # if record.return_type == 'retur_po' and record.origin: # bills = self.env['account.move'].search([ # ('invoice_origin', 'ilike', record.origin), # ('move_type', '=', 'in_invoice'), # hanya vendor bill @@ -153,7 +153,7 @@ class TukarGulingPO(models.Model): # ]) # if bills: # raise ValidationError( - # _("Tidak bisa memilih Return Type 'Revisi PO' karena PO %s sudah dibuat vendor bill. Harus Cancel Jika ingin melanjutkan") % record.origin + # _("Tidak bisa memilih Return Type 'Retur PO' karena PO %s sudah dibuat vendor bill. Harus Cancel Jika ingin melanjutkan") % record.origin # ) @api.onchange('operations') @@ -284,7 +284,7 @@ class TukarGulingPO(models.Model): @api.constrains('return_type', 'operations') def _check_required_bu_fields(self): for record in self: - if record.return_type in ['revisi_po', 'tukar_guling'] and not record.operations: + if record.return_type in ['retur_po', 'tukar_guling'] and not record.operations: raise ValidationError("Operations harus diisi") @api.constrains('line_ids', 'state') @@ -350,21 +350,21 @@ class TukarGulingPO(models.Model): def write(self, vals): if self.operations.picking_type_id.id not in [75, 28]: raise UserError("ā Tidak bisa retur bukan BU/INPUT atau BU/PUT!") - # self._check_bill_on_revisi_po() + # self._check_bill_on_retur_po() tipe = vals.get('return_type', self.return_type) - if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': - group = self.operations.group_id - if group: - # Cari BU/PUT dalam group yang sama - bu_put = self.env['stock.picking'].search([ - ('group_id', '=', group.id), - ('picking_type_id.id', '=', 75), # 75 = ID BU/PUT - ('state', '=', 'done') - ], limit=1) - - if bu_put: - raise UserError("ā Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") + # if self.operations and self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': + # group = self.operations.group_id + # if group: + # # Cari BU/PUT dalam group yang sama + # bu_put = self.env['stock.picking'].search([ + # ('group_id', '=', group.id), + # ('picking_type_id.id', '=', 75), # 75 = ID BU/PUT + # ('state', '=', 'done') + # ], limit=1) + # + # if bu_put: + # raise UserError("ā Tidak bisa retur BU/INPUT karena BU/PUT sudah Done!") if self.operations.picking_type_id.id == 28 and tipe == 'tukar_guling': raise UserError("ā BU/INPUT tidak boleh di retur tukar guling") @@ -418,7 +418,7 @@ class TukarGulingPO(models.Model): def action_submit(self): self.ensure_one() - # self._check_bill_on_revisi_po() + # self._check_bill_on_retur_po() self._validate_product_lines() self._check_not_allow_tukar_guling_on_bu_input() @@ -463,7 +463,7 @@ class TukarGulingPO(models.Model): def action_approve(self): self.ensure_one() self._validate_product_lines() - # self._check_bill_on_revisi_po() + # self._check_bill_on_retur_po() self._check_not_allow_tukar_guling_on_bu_input() if not self.operations: @@ -485,7 +485,7 @@ class TukarGulingPO(models.Model): elif rec.state == 'approval_finance': if not rec.env.user.has_group('indoteknik_custom.group_role_fat'): raise UserError("Hanya Finance yang boleh approve tahap ini.") - # rec._check_bill_on_revisi_po() + # rec._check_bill_on_retur_po() rec.set_opt() rec.state = 'approval_logistic' rec.date_finance = now @@ -501,7 +501,7 @@ class TukarGulingPO(models.Model): def update_doc_state(self): # bu input rev po - if self.operations.picking_type_id.id == 28 and self.return_type == 'revisi_po': + if self.operations.picking_type_id.id == 28 and self.return_type == 'retur_po': prt = self.env['stock.picking'].search([ ('tukar_guling_po_id', '=', self.id), ('state', '=', 'done'), @@ -510,7 +510,7 @@ class TukarGulingPO(models.Model): if self.state == 'approved' and prt: self.state = 'done' # bu put rev po - elif self.operations.picking_type_id.id == 75 and self.return_type == 'revisi_po': + elif self.operations.picking_type_id.id == 75 and self.return_type == 'retur_po': total_prt = self.env['stock.picking'].search_count([ ('tukar_guling_po_id', '=', self.id), ('picking_type_id.id', '=', 76) 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, + } + } |
