from odoo import models, api, fields from odoo.exceptions import AccessError, UserError, ValidationError from markupsafe import escape as html_escape from datetime import timedelta, date, datetime from pytz import timezone, utc import logging, json import base64 import PyPDF2 import os import re from terbilang import Terbilang from collections import defaultdict from odoo.tools.misc import formatLang import socket _logger = logging.getLogger(__name__) class AccountMove(models.Model): _inherit = 'account.move' _description = 'Account Move' invoice_day_to_due = fields.Integer(string="Day to Due", compute="_compute_invoice_day_to_due") bill_day_to_due = fields.Date(string="Day to Due", compute="_compute_bill_day_to_due") date_send_fp = fields.Datetime(string="Tanggal Kirim Faktur Pajak") last_log_fp = fields.Char(string="Log Terakhir Faktur Pajak") # use for industry business date_kirim_tukar_faktur = fields.Date(string='Kirim Faktur') resi_tukar_faktur = fields.Char(string='Resi Faktur') date_terima_tukar_faktur = fields.Date(string='Terima Faktur') payment_schedule = fields.Date(string='Jadwal Pembayaran') shipper_faktur_id = fields.Many2one('delivery.carrier', string='Shipper Faktur') due_extension = fields.Integer(string='Due Extension', default=0) new_due_date = fields.Date(string='New Due') counter = fields.Integer(string="Counter", default=0) cost_centre_id = fields.Many2one('cost.centre', string='Cost Centre') due_line = fields.One2many('due.extension.line', 'invoice_id', compute='_compute_due_line', string='Due Extension Lines') no_faktur_pajak = fields.Char(string='No Faktur Pajak') date_completed = fields.Datetime(string='Date Completed') mark_upload_efaktur = fields.Selection([ ('belum_upload', 'Belum Upload FP'), ('sudah_upload', 'Sudah Upload FP'), ], 'Mark Upload Faktur', compute='_compute_mark_upload_efaktur', default='belum_upload') sale_id = fields.Many2one('sale.order', string='Sale Order') reklas_id = fields.Many2one('account.move', string='Nomor CAB', domain="[('partner_id', '=', partner_id)]") new_invoice_day_to_due = fields.Integer(string="New Day Due", compute="_compute_invoice_day_to_due") date_efaktur_upload = fields.Datetime(string='eFaktur Upload Date', tracking=True) real_invoice_id = fields.Many2one( 'res.partner', string='Delivery Invoice Address', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Dipakai untuk alamat tempel") address_invoice = fields.Char(related='real_invoice_id.street', string='Invoice Address', readonly=True) bills_efaktur_exporter = fields.Many2one('res.users', string='Efaktur Exporter') bills_date_efaktur = fields.Datetime(string="eFaktur Exported Date", required=False) bills_efaktur_document = fields.Binary(string="eFaktur", required=False) bills_invoice_exporter =fields.Many2one('res.users', string='Invoice Exporter') bills_date_invoice = fields.Datetime(string="Invoice Exported Date", required=False) bills_invoice_document = fields.Binary(string="Invoice", required=False) is_invoice_uploaded = fields.Boolean(string="Is Invoice Uploaded", default=False) is_efaktur_uploaded = fields.Boolean(string="Is eFaktur Uploaded", default=False) already_paid = fields.Boolean(string="Sudah Dibayar?", default=False) delivery_amt_text = fields.Char(string="Delivery Amt Terbilang", compute='compute_delivery_amt_text') so_shipping_paid_by = fields.Char(string="SO Shipping Paid By", compute='compute_so_shipping_paid_by') so_shipping_covered_by = fields.Char(string="SO Shipping Covered By", compute='compute_so_shipping_paid_by') so_delivery_amt = fields.Char(string="SO Delivery Amount", compute='compute_so_shipping_paid_by') flag_delivery_amt = fields.Boolean(string="Flag Delivery Amount", compute='compute_flag_delivery_amt') nomor_kwitansi = fields.Char(string="Nomor Kwitansi") other_subtotal = fields.Float(string="Other Subtotal", compute='compute_other_subtotal') other_taxes = fields.Float(string="Other Taxes", compute='compute_other_taxes') is_hr = fields.Boolean(string="Is HR?", default=False) purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order') length_of_payment = fields.Integer(string="Length of Payment", compute='compute_length_of_payment') reklas_misc_id = fields.Many2one('account.move', string='Journal Entries Reklas') # Di model account.move bill_id = fields.Many2one('account.move', string='Vendor Bill', domain=[('move_type', '=', 'in_invoice')], help='Bill asal dari proses reklas ini') down_payment = fields.Boolean('Down Payments?') refund_id = fields.Many2one('refund.sale.order', string='Refund Reference') refund_so_ids = fields.Many2many( 'sale.order', 'account_move_sale_order_rel', 'move_id', 'sale_order_id', string='Group SO Number' ) refund_so_links = fields.Html( string="Group SO Numbers", compute="_compute_refund_so_links", ) has_refund_so = fields.Boolean( string='Has Refund SO', compute='_compute_has_refund_so', ) payment_date = fields.Date(string="Payment Date", compute='_compute_payment_date') partial_payment = fields.Float(string="Partial Payment", compute='compute_partial_payment') 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.", tracking=True ) internal_notes_contact = fields.Text(related='partner_id.comment', string="Internal Notes", readonly=True, help="Internal Notes dari contact utama customer.") payment_info = fields.Text( string="Payment Info", compute='_compute_payment_info', store=False, help="Informasi pembayaran yang diambil dari payment yang sudah direkonsiliasi ke invoice ini." ) def _compute_payment_info(self): for rec in self: summary = "" try: widget_data = rec.invoice_payments_widget if widget_data: data = json.loads(widget_data) lines = [] for item in data.get('content', []): amount = item.get('amount', 0.0) date = item.get('date') or item.get('payment_date') or '' formatted_amount = formatLang(self.env, amount, currency_obj=rec.currency_id) lines.append(f"
  • Paid on {date} - {formatted_amount}
  • ") summary = f"" if lines else (data.get('title', '') or "") except Exception: summary = "" rec.payment_info = summary # def _check_and_lock_cbd(self): # cbd_term = self.env['account.payment.term'].browse(26) # today = date.today() # # Cari semua invoice overdue # overdue_invoices = self.search([ # ('move_type', '=', 'out_invoice'), # ('state', '=', 'posted'), # ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']), # ('invoice_date_due', '!=', False), # ('invoice_date_due', '<=', today - timedelta(days=30)), # ], limit=3) # _logger.info(f"Found {len(overdue_invoices)} overdue invoices for CBD lock check.") # _logger.info(f"Overdue Invoices: {overdue_invoices.mapped('name')}") # # Ambil partner unik dari invoice # partners_to_lock = overdue_invoices.mapped('partner_id').filtered(lambda p: not p.is_cbd_locked) # _logger.info(f"Partners to lock: {partners_to_lock.mapped('name')}") # # Lock hanya partner yang belum locked # if partners_to_lock: # partners_to_lock.write({ # 'is_cbd_locked': True, # 'property_payment_term_id': cbd_term.id, # }) def compute_partial_payment(self): for move in self: if move.amount_total_signed > 0 and move.amount_residual_signed > 0 and move.payment_state == 'partial': move.partial_payment = move.amount_total_signed - move.amount_residual_signed else: move.partial_payment = 0 def _compute_payment_date(self): for move in self: accountPayment = self.env['account.payment'] payment = accountPayment.search([]).filtered( lambda p: move.id in p.reconciled_invoice_ids.ids ) if payment: move.payment_date = payment[0].date elif move.reklas_misc_id: move.payment_date = move.reklas_misc_id.date else: move.payment_date = False def action_sync_promise_date(self): self.ensure_one() finance_user_ids = [688] is_it = self.env.user.has_group('indoteknik_custom.group_role_it') if self.env.user.id not in finance_user_ids and not is_it: raise UserError('Hanya Finance (Widya) yang dapat menggunakan fitur ini.') if not self.customer_promise_date: raise UserError("Isi Janji Bayar terlebih dahulu sebelum melakukan sinkronisasi.") other_invoices = self.env['account.move'].search([ ('id', '!=', self.id), ('partner_id', '=', self.partner_id.id), ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('date_terima_tukar_faktur', '!=', False), ('invoice_payment_term_id.name', 'ilike', 'tempo') ]) lines = [] for inv in other_invoices: lines.append((0, 0, {'invoice_id': inv.id, 'sync_check': True})) # default dicentang semua wizard = self.env['sync.promise.date.wizard'].create({ 'invoice_id': self.id, 'line_ids': lines, }) return { 'name': 'Sync Janji Bayar', 'type': 'ir.actions.act_window', 'res_model': 'sync.promise.date.wizard', 'view_mode': 'form', 'res_id': wizard.id, 'target': 'new', } @staticmethod def is_local_env(): hostname = socket.gethostname().lower() keywords = ['andri', 'miqdad', 'fin', 'stephan', 'hafid', 'nathan'] return any(keyword in hostname for keyword in keywords) def send_due_invoice_reminder(self): if self.is_local_env(): _logger.warning("📪 Local environment detected — skip sending email reminders.") return today = fields.Date.today() target_dates = [ today + timedelta(days=7), today + timedelta(days=3), today, ] 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), ('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 self._send_invoice_reminders(invoices, mode='due') def send_overdue_invoice_reminder(self): if self.is_local_env(): _logger.warning("📪 Local environment detected — skip sending email reminders.") return 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 overdue") return self._send_invoice_reminders(invoices, mode='overdue') def _send_invoice_reminders(self, invoices, mode): if self.is_local_env(): _logger.warning("📪 Local environment detected — skip sending email reminders.") return 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 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 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 if today <= earliest_promise: _logger.info( f"Skip reminder untuk {partner.name} karena ada Janji Bayar sampai {earliest_promise}" ) continue emails = [] # skip semua jika partner centang dont_send_reminder_inv_all if partner.dont_send_reminder_inv_all: _logger.info(f"Partner {partner.name} skip karena dont_send_reminder_inv_all aktif") continue # cek parent hanya dengan flag dont_sent_reminder_inv_parent if not partner.dont_send_reminder_inv_parent and partner.email: emails.append(partner.email) # Ambil child contact yang di-checklist reminder_invoices reminder_contacts = self.env['res.partner'].search([ ('parent_id', '=', partner.id), ('reminder_invoices', '=', True), ('email', '!=', False), ]) _logger.info(f"Email Reminder Child {reminder_contacts}") emails += reminder_contacts.mapped('email') if reminder_contacts: _logger.info(f"Email Reminder Child {reminder_contacts}") else: _logger.info(f"Tidak ada child contact reminder, gunakan email utama partner") if not emails: _logger.info(f"Partner {partner.name} tidak memiliki email yang bisa dikirimi") continue email_to = ",".join(emails) _logger.info(f"Email tujuan: {email_to}") invoice_table_rows = "" grand_total = 0 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 grand_total += inv.amount_residual invoice_table_rows += f""" {idx} {inv.partner_id.name} {inv.ref or '-'} {inv.name} {fields.Date.to_string(inv.invoice_date) or '-'} {fields.Date.to_string(inv.invoice_date_due) or '-'} {formatLang(self.env, inv.amount_total, currency_obj=inv.currency_id)} {inv.invoice_payment_term_id.name or '-'} {days_to_due} """ invoice_table_footer = f""" Grand Total {formatLang(self.env, grand_total, currency_obj=invs[0].currency_id)} """ blocking_limit = partner.blocking_stage or 0.0 # semua invoice tempo yang masih open outstanding_invoices = self.env['account.move'].search([ ('move_type', '=', 'out_invoice'), ('state', '=', 'posted'), ('payment_state', 'not in', ['paid', 'in_payment', 'reversed']), ('partner_id', '=', partner.id), ('invoice_payment_term_id.name', 'ilike', 'tempo') ]) outstanding_amount = sum(outstanding_invoices.mapped('amount_total')) # invoice tempo yang sudah jatuh tempo overdue_invoices = outstanding_invoices.filtered( lambda inv: inv.invoice_date_due and inv.invoice_date_due < fields.Date.today() ) overdue_amount = sum(overdue_invoices.mapped('amount_total')) currency = invs[0].currency_id if invs else partner.company_id.currency_id tempo_link = 'https://indoteknik.com/my/tempo' # tempo_link = 'http://localhost:2100/my/tempo' # 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"""

    Informasi Tambahan:

    Cek Selengkapnya

    """ days_to_due_message = "" closing_message = "" 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.
    " "Mohon konfirmasi apabila pembayaran telah dijadwalkan. " "Terima kasih atas perhatian dan kerja samanya." ) 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.
    " "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 beberapa tagihan anda telah jatuh tempo, " "dengan rincian sebagai berikut:" ) closing_message = ( "Mohon kesediaan Bapak/Ibu untuk segera melakukan pembayaran guna menghindari keterlambatan " "dan menjaga kelancaran kerja sama yang telah terjalin dengan baik.
    " "Apabila pembayaran sudah dilakukan, mohon konfirmasi dan lampirkan bukti transfer agar dapat kami proses lebih lanjut. " "Terima kasih atas perhatian dan kerja samanya." ) body_html = re.sub( r"]*>.*?", f"{invoice_table_rows}{invoice_table_footer}", template.body_html, flags=re.DOTALL ).replace('${object.name}', partner.name) \ .replace('${object.partner_id.name}', partner.name) \ .replace('${days_to_due_message}', days_to_due_message) \ .replace('${closing_message}', closing_message) \ .replace('${limit_info_html}', limit_info_html) cc_list = [ 'finance@indoteknik.co.id', 'akbar@indoteknik.co.id', 'stephan@indoteknik.co.id', # 'darren@indoteknik.co.id' ] sales_email = invs[0].invoice_user_id.partner_id.email if invs[0].invoice_user_id else None if sales_email and sales_email not in cc_list: cc_list.append(sales_email) # Siapkan email values values = { 'subject': f"Reminder Invoice Due - {partner.name}", # 'email_to': 'andrifebriyadiputra@gmail.com', 'email_to': email_to, 'email_from': 'finance@indoteknik.co.id', 'email_cc': ",".join(sorted(set(cc_list))), 'body_html': body_html, 'reply_to': 'finance@indoteknik.co.id', } template.send_mail(invs[0].id, force_send=True, email_values=values) _logger.info(f"Mengirim email ke: {values['email_to']} > email CC: {values['email_cc']}") _logger.info(f"Reminder terkirim ke {partner.name} ({values['email_to']}) → {len(invs)} invoice (dtd = {dtd})") # flag invs.write({'reminder_sent_date': today}) # Post ke chatter user_system = self.env['res.users'].browse(25) system_id = user_system.partner_id.id if user_system else False for inv in invs: inv.message_post( subject=values['subject'], body=body_html, subtype_id=self.env.ref('mail.mt_note').id, author_id=system_id, ) @api.onchange('invoice_date') def _onchange_invoice_date(self): if self.invoice_date: self.date = self.invoice_date @api.onchange('date') def _onchange_date(self): if self.date: self.invoice_date = self.date @api.depends('refund_so_ids') def _compute_refund_so_links(self): for rec in self: links = [] for so in rec.refund_so_ids: url = f"/web#id={so.id}&model=sale.order&view_type=form" name = html_escape(so.name or so.display_name) links.append(f'{name}') rec.refund_so_links = ', '.join(links) if links else "-" @api.depends('refund_so_ids') def _compute_has_refund_so(self): for rec in self: rec.has_refund_so = bool(rec.refund_so_ids) # def compute_length_of_payment(self): # for rec in self: # payment_term = rec.invoice_payment_term_id.line_ids[0].days # terima_faktur = rec.date_terima_tukar_faktur # payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1) # if payment and terima_faktur: # date_diff = terima_faktur - payment.date # rec.length_of_payment = date_diff.days + payment_term # else: # rec.length_of_payment = 0 def compute_length_of_payment(self): for rec in self: payment_term = 0 if rec.invoice_payment_term_id and rec.invoice_payment_term_id.line_ids: payment_term = rec.invoice_payment_term_id.line_ids[0].days terima_faktur = rec.date_terima_tukar_faktur payment = self.search([('ref', '=', rec.name), ('move_type', '=', 'entry')], limit=1) if payment and terima_faktur: date_diff = terima_faktur - payment.date rec.length_of_payment = date_diff.days + payment_term else: rec.length_of_payment = 0 def _update_line_name_from_ref(self): """Update all account.move.line name fields with ref from account.move""" for move in self: if move.move_type == 'entry' and move.ref and move.line_ids: for line in move.line_ids: line.name = move.ref def compute_other_taxes(self): for rec in self: rec.other_taxes = round(rec.other_subtotal * 0.12, 2) def compute_other_subtotal(self): for rec in self: rec.other_subtotal = round(rec.amount_untaxed * (11 / 12)) @api.model def generate_attachment(self, record): # Fetch the binary field file_content = record.efaktur_document file_name = "efaktur_document_{}.pdf".format(record.id) # Adjust the file extension if necessary attachment = self.env['ir.attachment'].create({ 'name': file_name, 'type': 'binary', 'datas': file_content, 'res_model': record._name, 'res_id': record.id, }) return attachment @api.constrains('efaktur_document') def send_scheduled_email(self): # Get the records for which emails need to be sent records = self.search([('id', 'in', self.ids)]) template = self.env.ref('indoteknik_custom.mail_template_efaktur_document') ICP = self.env['ir.config_parameter'].sudo() special_partner_ids = set( int(x) for x in (ICP.get_param('efaktur.special_partner_ids') or '').split(',') if x ) for record in records: if record.invoice_payment_term_id.id == 26: attachment = self.generate_attachment(record) email_values = { 'attachment_ids': [(4, attachment.id)] } template.send_mail(record.id, email_values=email_values, force_send=True) elif record.partner_id.id in special_partner_ids: cust_ref = record.sale_id.client_order_ref if record.sale_id and record.sale_id.client_order_ref else '' attachment = self.generate_attachment(record) email_list = [record.partner_id.email] if record.partner_id.email else [] if record.real_invoice_id and record.real_invoice_id.email: email_list.append(record.real_invoice_id.email) email_values = { 'email_to': ",".join(set(email_list)), 'attachment_ids': [(4, attachment.id)] } template.with_context(cust_ref=cust_ref).send_mail(record.id, email_values=email_values, force_send=True) # @api.model # def create(self, vals): # vals['nomor_kwitansi'] = self.env['ir.sequence'].next_by_code('nomor.kwitansi') or '0' # result = super(AccountMove, self).create(vals) # # result._update_line_name_from_ref() # return result @api.model def create(self, vals): vals['nomor_kwitansi'] = self.env['ir.sequence'].next_by_code('nomor.kwitansi') or '0' result = super(AccountMove, self).create(vals) # Tambahan: jika ini Vendor Bill dan tanggal belum diisi if result.move_type == 'in_invoice' and not vals.get('invoice_date') and not vals.get('date'): po = result.purchase_order_id if po: # Cari receipt dari PO picking = self.env['stock.picking'].search([ ('purchase_id', '=', po.id), ('picking_type_code', '=', 'incoming'), ('state', '=', 'done'), ('date_done', '!=', False), ], order='date_done desc', limit=1) if picking: receipt_date = picking.date_done result.invoice_date = receipt_date result.date = receipt_date return result def compute_so_shipping_paid_by(self): for record in self: record.so_shipping_paid_by = record.sale_id.shipping_paid_by record.so_shipping_covered_by = record.sale_id.shipping_cost_covered record.so_delivery_amt = record.sale_id.delivery_amt def compute_flag_delivery_amt(self): for record in self: if record.sale_id.delivery_amt > 0: record.flag_delivery_amt = True else: record.flag_delivery_amt = False def compute_delivery_amt_text(self): tb = Terbilang() for record in self: res = '' try: if record.sale_id.delivery_amt > 0: tb.parse(int(record.sale_id.delivery_amt)) res = tb.getresult().title() record.delivery_amt_text = res + ' Rupiah' except: record.delivery_amt_text = res @api.constrains('bills_efaktur_document') def _constrains_efaktur_document(self): for move in self: current_time = datetime.utcnow() move.bills_date_efaktur = current_time move.bills_efaktur_exporter = self.env.user.id move.is_efaktur_uploaded = True @api.constrains('bills_invoice_document') def _constrains_invoice_efaktur(self): for move in self: current_time = datetime.utcnow() move.bills_date_invoice = current_time move.bills_invoice_exporter = self.env.user.id move.is_invoice_uploaded = True @api.constrains('partner_id') def _constrains_real_invoice(self): for move in self: move.real_invoice_id = move.sale_id.real_invoice_id @api.constrains('efaktur_document') def _constrains_date_efaktur(self): for move in self: current_time = datetime.utcnow() move.date_efaktur_upload = current_time def open_form_multi_create_reklas_penjualan(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_view_invoice_reklas_penjualan') invoice = self.env['invoice.reklas.penjualan'].create([{ 'name': '-', }]) for move in self: sale_id = move.sale_id.id self.env['invoice.reklas.penjualan.line'].create([{ 'invoice_reklas_id': invoice.id, 'name': move.name, 'partner_id': move.partner_id.id, 'sale_id': move.sale_id.id, 'amount_untaxed_signed': move.amount_untaxed_signed, 'amount_total_signed': move.amount_total_signed, }]) action['res_id'] = invoice.id return action def _compute_mark_upload_efaktur(self): for move in self: if move.efaktur_document: move.mark_upload_efaktur = 'sudah_upload' else: move.mark_upload_efaktur = 'belum_upload' def _compute_due_line(self): for invoice in self: invoice.due_line = self.env['due.extension.line'].search([ ('invoice_id', '=', invoice.id), ('due_id.approval_status', '=', 'approved') ]) def unlink(self): res = super(AccountMove, self).unlink() for rec in self: if rec.state == 'posted': raise UserError('Data Hanya Bisa Di Cancel') return res def copy(self, default=None): default = dict(default or {}) today = fields.Date.context_today(self) if self.invoice_date_due: default.update({ 'invoice_date_due': today, }) move = super().copy(default) for line in move.line_ids: if ( line.account_id.user_type_id.type in ('receivable', 'payable') and line.date_maturity ): line.date_maturity = today return move def button_cancel(self): res = super(AccountMove, self).button_cancel() if self.move_type == 'entry': po = self.env['purchase.order'].search([ ('move_id', 'in', [self.id]) ]) for order in po: if order: order.is_create_uangmuka = False if self.id and not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa Cancel') return res def button_draft(self): res = super(AccountMove, self).button_draft() if not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa Reset to Draft') for rec in self.line_ids: if rec.write_date != rec.create_date: if rec.statement_line_id and not rec.statement_line_id.statement_id.is_edit and rec.statement_line_id.statement_id.state == 'confirm': raise UserError('Bank Statement di Lock, Minta admin reconcile untuk unlock') return res def action_post(self): if self._name != 'account.move': return super(AccountMove, self).action_post() # validation cant qty invoice greater than qty order if self.move_type == 'out_invoice': query = ["&",("name","=",self.invoice_origin),"|",("state","=","sale"),("state","=","done")] sale_order = self.env['sale.order'].search(query, limit=1) sum_qty_invoice = sum_qty_order = 0 for line in sale_order.order_line: sum_qty_invoice += line.qty_invoiced sum_qty_order += line.product_uom_qty if sum_qty_invoice > sum_qty_order: raise UserError('Error Qty Invoice akan lebih besar dari Qty Order jika lanjut Posting') elif self.move_type == 'in_invoice': query = ["&",("name","=",self.invoice_origin),"|",("state","=","purchase"),("state","=","done")] purchase_order = self.env['purchase.order'].search(query, limit=1) sum_qty_invoice = sum_qty_order = 0 for line in purchase_order.order_line: sum_qty_invoice += line.qty_invoiced sum_qty_order += line.product_qty if sum_qty_invoice > sum_qty_order: raise UserError('Error Qty Invoice akan lebih besar dari Qty Order jika lanjut Posting') res = super(AccountMove, self).action_post() # if not self.env.user.is_accounting: # raise UserError('Hanya Accounting yang bisa Posting') # if self._name == 'account.move': # for entry in self: # entry.date_completed = datetime.utcnow() # for line in entry.line_ids: # line.date_maturity = entry.date return res def button_draft(self): res = super(AccountMove, self).button_draft() if not self.env.user.is_accounting: raise UserError("Hanya Finence yang bisa ubah data") return res def _compute_invoice_day_to_due(self): for invoice in self: invoice_day_to_due = 0 new_invoice_day_to_due = 0 if invoice.payment_state not in ['paid', 'in_payment', 'reversed'] and invoice.invoice_date_due: invoice_day_to_due = invoice.invoice_date_due - date.today() new_invoice_day_to_due = invoice.invoice_date_due - date.today() if invoice.new_due_date: invoice_day_to_due = invoice.new_due_date - date.today() invoice_day_to_due = invoice_day_to_due.days new_invoice_day_to_due = new_invoice_day_to_due.days invoice.invoice_day_to_due = invoice_day_to_due invoice.new_invoice_day_to_due = new_invoice_day_to_due def _compute_bill_day_to_due(self): for rec in self: rec.bill_day_to_due = rec.payment_schedule or rec.invoice_date_due @api.onchange('date_kirim_tukar_faktur') def change_date_kirim_tukar_faktur(self): for invoice in self: if not invoice.date_kirim_tukar_faktur: return tukar_date = invoice.date_kirim_tukar_faktur term = invoice.invoice_payment_term_id add_days = 0 for line in term.line_ids: add_days += line.days due_date = tukar_date + timedelta(days=add_days) invoice.invoice_date_due = due_date @api.constrains('date_terima_tukar_faktur') def change_date_terima_tukar_faktur(self): for invoice in self: if not invoice.date_terima_tukar_faktur: return tukar_date = invoice.date_terima_tukar_faktur term = invoice.invoice_payment_term_id add_days = 0 for line in term.line_ids: add_days += line.days due_date = tukar_date + timedelta(days=add_days) invoice.invoice_date_due = due_date def open_form_multi_update(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_account_move_multi_update') action['context'] = { 'move_ids': [x.id for x in self] } return action def open_form_multi_update_bills(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_account_move_multi_update_bills') action['context'] = { 'move_ids': [x.id for x in self] } return action @api.constrains('efaktur_id', 'ref', 'date', 'journal_id', 'name') def constrains_edit(self): for rec in self.line_ids: if rec.statement_line_id and not rec.statement_line_id.statement_id.is_edit and rec.statement_line_id.statement_id.state == 'confirm': raise UserError('Bank Statement di Lock, Minta admin reconcile untuk unlock') # def write(self, vals): # res = super(AccountMove, self).write(vals) # for rec in self.line_ids: # if rec.write_date != rec.create_date: # if rec.statement_line_id and not rec.statement_line_id.statement_id.is_edit and rec.statement_line_id.statement_id.state == 'confirm': # raise UserError('Bank Statement di Lock, Minta admin reconcile untuk unlock') # return res def validate_faktur_for_export(self): invoices = self.filtered(lambda inv: not inv.is_efaktur_exported and inv.state == 'posted' and inv.move_type == 'out_invoice') invalid_invoices = self - invoices if invalid_invoices: invalid_ids = ", ".join(str(inv.id) for inv in invalid_invoices) raise UserError(( "Faktur dengan ID berikut tidak valid untuk diekspor: {}.\n" "Pastikan faktur dalam status 'posted', belum diekspor, dan merupakan 'out_invoice'.".format(invalid_ids) )) return invoices def export_faktur_to_xml(self): valid_invoices = self coretax_faktur = self.env['coretax.faktur'].create({}) response = coretax_faktur.export_to_download( invoices=valid_invoices, down_payments=[inv.down_payment for inv in valid_invoices], ) valid_invoices.write({ 'is_efaktur_exported': True, 'date_efaktur_exported': datetime.utcnow(), }) return response class SyncPromiseDateWizard(models.TransientModel): _name = "sync.promise.date.wizard" _description = "Sync Janji Bayar Wizard" invoice_id = fields.Many2one('account.move', string="Invoice Utama", required=True) promise_date = fields.Date(string="Janji Bayar", related="invoice_id.customer_promise_date", readonly=True) line_ids = fields.One2many('sync.promise.date.wizard.line', 'wizard_id', string="Invoices Terkait") def action_check_all(self): for line in self.line_ids: line.sync_check = True return { 'type': 'ir.actions.act_window', 'res_model': 'sync.promise.date.wizard', 'view_mode': 'form', 'res_id': self.id, 'target': 'new', } def action_uncheck_all(self): for line in self.line_ids: line.sync_check = False return { 'type': 'ir.actions.act_window', 'res_model': 'sync.promise.date.wizard', 'view_mode': 'form', 'res_id': self.id, 'target': 'new', } def action_confirm(self): self.ensure_one() selected_lines = self.line_ids.filtered(lambda l: l.sync_check) selected_invoices = selected_lines.mapped('invoice_id') if not selected_invoices: raise UserError("Tidak ada invoice dipilih untuk sinkronisasi.") # Update hanya invoice yang dipilih for inv in selected_invoices: inv.write({'customer_promise_date': self.promise_date}) inv.message_post( body=f"Janji Bayar {self.promise_date} disinkronkan dari invoice {self.invoice_id.name}." ) # Log di invoice utama self.invoice_id.message_post( body=f"Janji Bayar {self.promise_date} disinkronkan ke {len(selected_invoices)} invoice lain: {', '.join(selected_invoices.mapped('name'))}." ) return {'type': 'ir.actions.act_window_close'} class SyncPromiseDateWizardLine(models.TransientModel): _name = "sync.promise.date.wizard.line" _description = "Sync Janji Bayar Wizard Line" wizard_id = fields.Many2one('sync.promise.date.wizard', string="Wizard") invoice_id = fields.Many2one('account.move', string="Invoice") sync_check = fields.Boolean(string="Sync?") invoice_name = fields.Char(related="invoice_id.name", string="Nomor Invoice", readonly=True) invoice_date_due = fields.Date(related="invoice_id.invoice_date_due", string="Due Date", readonly=True) invoice_day_to_due = fields.Integer(related="invoice_id.invoice_day_to_due", string="Day to Due", readonly=True) new_invoice_day_to_due = fields.Integer(related="invoice_id.new_invoice_day_to_due", string="New Day Due", readonly=True) date_terima_tukar_faktur = fields.Date(related="invoice_id.date_terima_tukar_faktur", string="Tanggal Terima Tukar Faktur", readonly=True) amount_total = fields.Monetary(related="invoice_id.amount_total", string="Total", readonly=True) currency_id = fields.Many2one(related="invoice_id.currency_id", readonly=True)