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 API_BASE_URL = "https://api.ginee.com" BATCH_GET_URI = '/openapi/order/v1/batch-get' LIST_ORDER_URI = '/openapi/order/v2/list-order' ACCESS_KEY = '24bb6a1ec618ec6a' SECRET_KEY = '32e4a78ad05ee230' class UploadGinee(models.Model): _name = "upload.ginee" _description = "Upload Ginee" _order = "create_date desc" _rec_name = "number" ginee_lines = fields.One2many('upload.ginee.line', 'upload_ginee_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') upload_type = fields.Selection([ ('rehit', 'Rehit'), ('blibli', 'Blibli'), ], 'Upload Type') @api.model def create(self, vals): vals['number'] = self.env['ir.sequence'].next_by_code('upload.ginee') 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: raise ValidationError(_("Invalid Excel file format.")) header = [str(sheet.cell(0, col).value).strip().lower() for col in range(sheet.ncols)] expected_headers = ['marketplace', 'shop', 'invoice'] if not all(h in header for h in expected_headers): raise ValidationError(_("Invalid Excel format. Expected columns: Marketplace, Shop, Invoice")) marketplace_col = header.index('marketplace') shop_col = header.index('shop') invoice_col = header.index('invoice') # Store rows for validation and processing rows_data = [] for row_idx in range(1, sheet.nrows): try: marketplace = str(sheet.cell(row_idx, marketplace_col).value).strip() shop = str(sheet.cell(row_idx, shop_col).value).strip() invoice_marketplace = str(sheet.cell(row_idx, invoice_col).value).strip() rows_data.append((row_idx + 1, marketplace, shop, invoice_marketplace)) # +1 for 1-based Excel row except Exception as e: continue # Validasi 1: Jika ada BLIBLI_ID di Marketplace, upload_type harus blibli has_blibli_id = any("BLIBLI_ID" in row[1].upper() for row in rows_data) if has_blibli_id and self.upload_type != 'blibli': raise ValidationError(_("Excel contains BLIBLI_ID in Marketplace. Please select Blibli as the upload type.")) # Validasi 2: Untuk upload_type blibli, semua Marketplace harus mengandung BLIBLI_ID if self.upload_type == 'blibli': invalid_rows = [] for row_data in rows_data: row_num = row_data[0] marketplace = row_data[1] if "BLIBLI_ID" not in marketplace.upper(): invalid_rows.append(str(row_num)) if invalid_rows: error_msg = _("For Blibli uploads, 'Marketplace' must contain 'BLIBLI_ID'. Errors in rows: %s") raise ValidationError(error_msg % ", ".join(invalid_rows)) # Validasi 3: Cek duplikat invoice_marketplace di sistem existing_invoices = set() invoice_to_check = [row_data[3] for row_data in rows_data] # Search in chunks to avoid too long SQL queries chunk_size = 500 for i in range(0, len(invoice_to_check), chunk_size): chunk = invoice_to_check[i:i+chunk_size] existing_records = self.env['upload.ginee.line'].search([ ('invoice_marketplace', 'in', chunk), ('upload_ginee_id', '!=', False) ]) existing_invoices.update(existing_records.mapped('invoice_marketplace')) duplicate_rows = [] for row_data in rows_data: row_num = row_data[0] invoice_marketplace = row_data[3] if invoice_marketplace in existing_invoices: duplicate_rows.append(str(row_num)) if duplicate_rows: error_msg = _("Invoice Marketplace already exists in the system. Duplicates found in rows: %s") raise ValidationError(error_msg % ", ".join(duplicate_rows)) # Prepare lines for import line_vals_list = [] for row_data in rows_data: marketplace = row_data[1] shop = row_data[2] invoice_marketplace = row_data[3] line_vals = { 'marketplace': marketplace, 'shop': shop, 'invoice_marketplace': invoice_marketplace, 'upload_ginee_id': self.id, } line_vals_list.append((0, 0, line_vals)) # Update record self.ginee_lines.unlink() self.write({'ginee_lines': line_vals_list}) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': _('Success'), 'message': _('Imported %s lines from 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 action_get_order_id(self): self.date_upload = datetime.utcnow() self.ginee_lines.get_order_id() def action_create_detail_order(self): self.date_upload = datetime.utcnow() self.ginee_lines.create_so_and_detail_order() # def action_get_order_id_and_create_detail_order(self): # self.date_upload = datetime.utcnow() # self.ginee_lines.get_order_id() # self.ginee_lines.create_so_and_detail_order() def action_get_order_id_and_create_detail_order(self): self.date_upload = datetime.utcnow() if self.upload_type == 'rehit': for line in self.ginee_lines: queue_job = self.env['queue.job'].search([('res_id', '=', line.id), ('method_name', '=', 'get_order_id_and_create_detail_order'), ('state', '!=', 'error')], limit=1) if queue_job: continue self.env['queue.job'].create({ 'name': f'Get Order Ginee {line.invoice_marketplace}', 'model_name': 'upload.ginee.line', 'method_name': 'get_order_id_and_create_detail_order', 'res_id': line.id, }) else: self.ginee_lines.get_order_id() self.ginee_lines.create_so_and_detail_order() class UploadGineeLine(models.Model): _name = "upload.ginee.line" _description = "Upload Ginee Line" _inherit = ['mail.thread'] upload_ginee_id = fields.Many2one('upload.ginee', string='Upload') marketplace = fields.Char('Marketplace') shop = fields.Char('Shop') invoice_marketplace = fields.Char('Invoice Marketplace') order_id = fields.Char('Order ID') message_error = fields.Text('Error Message') detail_order_id = fields.Many2one('detail.order', string='Detail Order') is_grouped = fields.Boolean('Is Grouped', default=False) group_key = fields.Char('Group Key') def get_order_id_and_create_detail_order(self): self.get_order_id() self.create_so_and_detail_order() def _process_grouped_blibli_orders(self, lines): """Process a group of BLIBLI orders with the same invoice prefix""" order_ids = [l.order_id for l in lines if l.order_id] if not order_ids: raise UserError(_('Order ID is empty for one or more records in group!')) # ===== 1. Fast duplicate check (still same behavior) ===== existing_detail = self.env['detail.order'].search( [('detail_order', 'ilike', order_ids[0])], limit=1 ) if existing_detail: return existing_detail # ===== 2. Single API call ===== data = lines[0]._call_api( BATCH_GET_URI, {"orderIds": order_ids} ) orders_data = data.get('data', []) if not orders_data: raise UserError(_('No data returned from BLIBLI API')) # ===== 3. Combine items (lebih ringkas) ===== combined_items = [ item for order in orders_data for item in order.get('items', []) ] base_order = orders_data[0] # ===== 4. Build grouped payload ===== combined_json_data = { 'data': [{ **base_order, 'items': combined_items, # 'externalOrderId': ', '.join(lines.mapped('invoice_marketplace')), # 'orderId': ', '.join(order_ids), 'externalOrderId': ', '.join(l.invoice_marketplace for l in lines), 'orderId': ', '.join(l.order_id for l in lines), }] } return self.env['detail.order'].create({ 'detail_order': json.dumps(combined_json_data, indent=4), 'source': 'manual', }) def _sign_request(self, uri): """Membuat tanda tangan sesuai format yang berhasil""" sign_data = f'POST${uri}$' signature = hmac.new( SECRET_KEY.encode('utf-8'), sign_data.encode('utf-8'), digestmod=sha256 ).digest() return f"{ACCESS_KEY}:{base64.b64encode(signature).decode('ascii')}" def _call_api(self, uri, payload): """Memanggil API dengan autentikasi yang benar""" headers = { 'Content-Type': 'application/json', 'X-Advai-Country': 'ID', 'Authorization': self._sign_request(uri) } response = requests.post( f"{API_BASE_URL}{uri}", headers=headers, data=json.dumps(payload) ) if response.status_code != 200: error_msg = f"API Error ({response.status_code}): {response.text}" raise UserError(_(error_msg)) return response.json() def create_so_and_detail_order(self): grouped_lines = {} # ===== 1. Grouping (lebih rapi & cepat) ===== for rec in self.filtered(lambda r: not r.detail_order_id): if rec.upload_ginee_id.upload_type == 'blibli' and '-' in rec.invoice_marketplace: key = rec.invoice_marketplace.split('-')[0] else: key = rec.id grouped_lines.setdefault(key, []).append(rec) # ===== 2. Preload sale.order (hindari query berulang) ===== invoice_list = [ line.invoice_marketplace for lines in grouped_lines.values() for line in lines if len(lines) == 1 ] existing_so_map = {} if invoice_list: so_records = self.env['sale.order'].search([ ('invoice_mp', 'in', invoice_list) ]) for so in so_records: existing_so_map.setdefault(so.invoice_mp, []).append(so.name) # ===== 3. Process per group ===== for _, lines in grouped_lines.items(): try: # ===== GROUPED BLIBLI ===== if len(lines) > 1: detail_order = self._process_grouped_blibli_orders(lines) detail_order.execute_queue_detail() for line in lines: line.write({ 'message_error': 'Success (grouped)', 'detail_order_id': detail_order.id }) # ===== SINGLE LINE ===== else: line = lines[0] if line.invoice_marketplace in existing_so_map: raise UserError(_( "Invoice Marketplace %s sudah terdaftar di Sale Order: %s" ) % ( line.invoice_marketplace, ', '.join(existing_so_map[line.invoice_marketplace]) )) if not line.order_id: raise UserError(_('Order ID is empty!')) data = line._call_api( BATCH_GET_URI, {"orderIds": [line.order_id]} ) detail_order = self.env['detail.order'].create({ 'detail_order': json.dumps(data, indent=4), 'source': 'manual', }) detail_order.execute_queue_detail() line.write({ 'message_error': 'Success', 'detail_order_id': detail_order.id }) except Exception as e: self.env['upload.ginee.line'].browse( [l.id for l in lines] ).write({ 'message_error': str(e) }) def get_order_id(self): for rec in self: try: if rec.order_id: continue if self.search_count([ ('marketplace', '=', rec.marketplace), ('invoice_marketplace', '=', rec.invoice_marketplace), ('id', '!=', rec.id) ]): raise UserError(_( "Invoice %s already exists") % rec.invoice_marketplace) data = rec._call_api( LIST_ORDER_URI, { "channel": rec.marketplace, "orderNumbers": [rec.invoice_marketplace] } ) orders = data.get('data', {}).get('content', []) if orders: rec.order_id = orders[0].get('orderId') rec.message_error = 'Success' else: raise UserError(_("No orders found for invoice: %s") % rec.invoice_marketplace) except Exception as e: rec.message_error = str(e) raise