from odoo import models, fields, api, _ from datetime import datetime import base64 import xlrd from odoo.exceptions import ValidationError, UserError import requests import json import hmac from hashlib import sha256 class UploadBills(models.Model): _name = "upload.bills" _description = "Upload Bills" _order = "create_date desc" _rec_name = "number" bills_lines = fields.One2many('upload.bills.line', 'upload_bills_id', string='Lines', copy=False, auto_join=True) number = fields.Char('Number', copy=False) date_upload = fields.Datetime('Upload Date', copy=False) user_id = fields.Many2one('res.users', 'Created By', default=lambda self: self.env.user.id) excel_file = fields.Binary('Excel File', attachment=True) filename = fields.Char('File Name') @api.model def create(self, vals): vals['number'] = self.env['ir.sequence'].next_by_code('upload.bills') or '/' return super().create(vals) def action_import_excel(self): self.ensure_one() if not self.excel_file: raise ValidationError(_("Please upload an Excel file first.")) try: file_content = base64.b64decode(self.excel_file) workbook = xlrd.open_workbook(file_contents=file_content) sheet = workbook.sheet_by_index(0) except Exception: raise ValidationError(_("Invalid Excel file format.")) if sheet.ncols < 3: raise ValidationError(_("Excel format tidak valid. Minimal ada 3 kolom: No BU IN, Tanggal, dan Faktur Pajak.")) rows_data = [] for row_idx in range(sheet.nrows): try: no_bu = str(sheet.cell(row_idx, 0).value).strip() tanggal_raw = sheet.cell(row_idx, 1).value faktur_pajak = str(sheet.cell(row_idx, 2).value).strip() if isinstance(tanggal_raw, float): tanggal = datetime(*xlrd.xldate_as_tuple(tanggal_raw, workbook.datemode)).date() else: tanggal = datetime.strptime(tanggal_raw, '%Y-%m-%d').date() rows_data.append({ 'row_num': row_idx + 1, 'no_bu': no_bu, 'date': tanggal, 'faktur_pajak': faktur_pajak, }) except Exception: continue faktur_list = [row['faktur_pajak'] for row in rows_data] existing_faktur = set() no_bu_list = [row['no_bu'] for row in rows_data] existing_no_bu = set() chunk_size = 500 for i in range(0, len(faktur_list), chunk_size): chunk_faktur = faktur_list[i:i + chunk_size] chunk_no_bu = no_bu_list[i:i + chunk_size] existing_lines = self.env['upload.bills.line'].search([ '|', ('faktur_pajak', 'in', chunk_faktur), ('no_bu', 'in', chunk_no_bu) ]) existing_faktur.update(existing_lines.mapped('faktur_pajak')) existing_no_bu.update(existing_lines.mapped('no_bu')) duplicate_rows = [] for row in rows_data: if row['faktur_pajak'] in existing_faktur or row['no_bu'] in existing_no_bu: duplicate_rows.append(str(row['row_num'])) if duplicate_rows: raise ValidationError(_("Data duplikat ditemukan di baris: %s\nPeriksa 'No BU IN' atau 'Faktur Pajak' yang sudah pernah diupload.") % ", ".join(duplicate_rows)) line_vals_list = [] for row in rows_data: line_vals = { 'no_bu': row['no_bu'], 'date': row['date'], 'faktur_pajak': row['faktur_pajak'], 'upload_bills_id': self.id, } line_vals_list.append((0, 0, line_vals)) self.bills_lines.unlink() self.write({'bills_lines': line_vals_list}) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Success'), 'message': _('Berhasil mengimpor %s baris dari Excel.') % len(line_vals_list), 'sticky': False, 'next': {'type': 'ir.actions.act_window_close'}, } } def _show_notification(self, message): return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Success'), 'message': message, 'sticky': False, } } def queue_job(self): self.date_upload = datetime.utcnow() for line in self.bills_lines: queue_job = self.env['queue.job'].search([('res_id', '=', line.id), ('method_name', '=', 'action_create_bills'), ('state', '!=', 'error')], limit=1) if queue_job: continue self.env['queue.job'].create({ 'name': f'Upload Bills {line.no_bu}', 'model_name': 'upload.bills.line', 'method_name': 'action_create_bills', 'res_id': line.id, }) # def action_create_bills(self): # all_invoice_ids = [] # failed_bus = [] # for line in self.bills_lines: # bu_in = self.env['stock.picking'].search([ # ('name', '=', line.no_bu), # ('picking_type_code', '=', 'incoming') # ], limit=1) # if not bu_in: # failed_bus.append(f"{line.no_bu} (BU tidak ditemukan)") # continue # try: # created_invoices = bu_in.action_create_invoice_from_mr() # except Exception as e: # failed_bus.append(f"{bu_in.name} ({str(e)})") # continue # # Sinkron faktur dan tanggal ke account.move # if isinstance(created_invoices, dict) and 'res_id' in created_invoices: # try: # invoice = self.env['account.move'].browse(created_invoices['res_id']) # invoice.write({ # 'faktur_pajak': line.faktur_pajak, # 'invoice_date': line.date # }) # invoice.action_post() # all_invoice_ids.append(invoice.id) # except Exception as e: # failed_bus.append(f"{bu_in.name} ({str(e)})") # continue # elif isinstance(created_invoices, dict) and 'domain' in created_invoices: # try: # # Banyak invoice # invoice_ids = created_invoices.get('domain', [])[0][2] or [] # invoices = self.env['account.move'].browse(invoice_ids) # invoices.write({ # 'faktur_pajak': line.faktur_pajak, # 'invoice_date': line.date # }) # invoices.action_post() # all_invoice_ids.extend(invoices.ids) # except Exception as e: # failed_bus.append(f"{bu_in.name} ({str(e)})") # continue # else: # raise UserError("Gagal menemukan invoice yang baru dibuat.") # if failed_bus: # raise UserError( # "Beberapa BU IN gagal diproses:\n- " + "\n- ".join(failed_bus) # ) # if not all_invoice_ids: # return {'type': 'ir.actions.act_window_close'} # action = self.env.ref('account.action_move_in_invoice_type').read()[0] # if len(all_invoice_ids) == 1: # action.update({ # 'view_mode': 'tree,form', # 'res_id': all_invoice_ids, # }) # else: # action.update({ # 'domain': [('id', 'in', all_invoice_ids)], # 'view_mode': 'tree,form', # }) # return action class UploadBillsLine(models.Model): _name = "upload.bills.line" _description = "Upload Bills Line" _inherit = ['mail.thread'] upload_bills_id = fields.Many2one('upload.bills', string='Upload') no_bu = fields.Char('No BU IN') date = fields.Date('Date') faktur_pajak = fields.Char('Faktur Pajak') state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting Another Operation'), ('confirmed', 'Waiting'), ('assigned', 'Ready'), ('done', 'Done'), ('cancel', 'Cancelled'), ], string='Status', compute='_compute_state', copy=False, readonly=True, tracking=True) def _compute_state(self): for line in self: picking = self.env['stock.picking'].search([ ('name', '=', line.no_bu), ('picking_type_code', '=', 'incoming') ], limit=1) line.state = picking.state def action_create_bills(self): # all_invoice_ids = [] failed_bus = [] for line in self: bu_in = self.env['stock.picking'].search([ ('name', '=', line.no_bu), ('picking_type_code', '=', 'incoming') ], limit=1) if not bu_in: failed_bus.append(f"{line.no_bu} (BU tidak ditemukan)") continue try: created_invoices = bu_in.action_create_invoice_from_mr() except Exception as e: failed_bus.append(f"{bu_in.name} ({str(e)})") continue # Sinkron faktur dan tanggal ke account.move if isinstance(created_invoices, dict) and 'res_id' in created_invoices: try: invoice = self.env['account.move'].browse(created_invoices['res_id']) invoice.write({ 'faktur_pajak': line.faktur_pajak, 'invoice_date': line.date }) invoice.action_post() # all_invoice_ids.append(invoice.id) except Exception as e: failed_bus.append(f"{bu_in.name} ({str(e)})") continue elif isinstance(created_invoices, dict) and 'domain' in created_invoices: try: # Banyak invoice invoice_ids = created_invoices.get('domain', [])[0][2] or [] invoices = self.env['account.move'].browse(invoice_ids) invoices.write({ 'faktur_pajak': line.faktur_pajak, 'invoice_date': line.date }) invoices.action_post() # all_invoice_ids.extend(invoices.ids) except Exception as e: failed_bus.append(f"{bu_in.name} ({str(e)})") continue else: raise UserError("Gagal menemukan invoice yang baru dibuat.") if failed_bus: raise UserError( "Beberapa BU IN gagal diproses:\n- " + "\n- ".join(failed_bus) ) # if not all_invoice_ids: # return {'type': 'ir.actions.act_window_close'} # action = self.env.ref('account.action_move_in_invoice_type').read()[0] # if len(all_invoice_ids) == 1: # action.update({ # 'view_mode': 'tree,form', # 'res_id': all_invoice_ids, # }) # else: # action.update({ # 'domain': [('id', 'in', all_invoice_ids)], # 'view_mode': 'tree,form', # }) # return action