from odoo import api, fields, models, _ from odoo.exceptions import UserError import time import requests import json import hmac import base64 from datetime import datetime, timezone, timedelta from hashlib import sha256 import logging import pytz _logger = logging.getLogger(__name__) Request_URI = '/openapi/order/v1/batch-get' ACCESS_KEY = '24bb6a1ec618ec6a' SECRET_KEY = '32e4a78ad05ee230' class DetailOrder(models.Model): _name = "detail.order" _inherit = ['mail.thread'] json_ginee = fields.Text('JSON Ginee') detail_order = fields.Text() execute_status = fields.Selection([ ('from_webhook', 'From Webhook'), ('detail_order', 'Detail Order'), ('so_confirm', 'SO Confirm'), ('so_draft', 'SO Draft'), ('done', 'Done'), ('failed', 'Failed'), ('already_so', 'SO Already Created'), ('processing_get_so', 'Processing Get SO'), ('cancelled_so', 'Cancelled SO'), ('cancelled_so_picking', 'Cancelled Picking'), ], 'Execute Status') source = fields.Selection([ ('webhook', 'From Webhook'), ('manual', 'Manual'), ], 'source') sale_id = fields.Many2one('sale.order', 'Sale Order') picking_id = fields.Many2one('stock.picking', 'Picking') invoice_id = fields.Many2one('account.move', 'Invoice') message_error = fields.Text('Message Error') is_grouped_order = fields.Boolean('Is Grouped Order', default=False) original_order_ids = fields.Char('Original Order IDs') # get detail order section def get_order_id(self): try: if self.json_ginee: json_data = json.loads(self.json_ginee) order_id = json_data.get('payload', {}).get('orderId') if not order_id: raise UserError(_("Order ID not found in JSON data")) return order_id raise UserError(_("No JSON data available")) except json.JSONDecodeError: raise UserError(_("Invalid JSON format in json_ginee field")) except Exception as e: raise UserError(_("Error extracting order ID: %s") % str(e)) # def process_queue_item(self, limit=100): # domain = [('create_date', '>', '2025-12-31 23:59:59')] # records = self.search(domain, order='create_date asc', limit=limit) # for rec in records: # rec.execute_queue() def process_queue_item(self, limit=100): domain = [('execute_status', '=', False), ('source', '=', 'webhook')] records = self.search(domain, order='create_date asc', limit=limit) for rec in records: rec.execute_queue() def execute_queue(self): try: order_id = self.get_order_id() authorization = self.sign_request() headers = { 'Content-Type': 'application/json', 'X-Advai-Country': 'ID', 'Authorization': authorization } payload = { "orderIds": [order_id] } # URL endpoint Ginee url = "https://api.ginee.com/openapi/order/v1/batch-get" # Melakukan POST request response = requests.post( url, headers=headers, data=json.dumps(payload) ) # Cek status response if response.status_code == 200: data = response.json() self.detail_order = json.dumps(data, indent=4, sort_keys=True) self.execute_status = 'detail_order' else: self.write({ 'message_error': json.dumps({ 'error': f"Request failed with status code {response.status_code}", 'response': response.text }) }) except Exception as e: self.write({ 'message_error': json.dumps({ 'error': str(e) }) }) # detail order to so section def get_order_id_detail(self): try: if self.detail_order: json_data = json.loads(self.detail_order) order_id = json_data.get('data', {})[0].get('orderId') order_status = json_data.get('data', {})[0].get('orderStatus') print_info = json_data.get('data', {})[0].get('printInfo', {}).get('labelPrintStatus') if not order_id: raise UserError(_("Order ID not found in JSON data")) return order_id, order_status, print_info raise UserError(_("No JSON data available")) except json.JSONDecodeError: raise UserError(_("Invalid JSON format in detail_order field")) except Exception as e: raise UserError(_("Error extracting order ID: %s") % str(e)) # def process_queue_item_detail(self, limit=100): # domain = [ # ('execute_status', '=', 'detail_order'), # '!', # '|', # ('json_ginee', 'like', '"orderStatus": "PENDING_PAYMENT"'), # ('json_ginee', 'like', '"channel":"BLIBLI_ID"'), # ] # records = self.search(domain, order='create_date asc', limit=limit) # for i, rec in enumerate(records, 1): # try: # rec.execute_queue_detail() # if i % 10 == 0: # self.env.cr.commit() # except Exception as e: # _logger.error("Failed to process record %s: %s", rec.id, str(e)) # self.env.cr.rollback() # self.env.cr.commit() def process_queue_item_detail(self, limit=100): domain = [ ('execute_status', '=', 'detail_order'), '!', '|', ('json_ginee', 'like', '"orderStatus": "PENDING_PAYMENT"'), ('json_ginee', 'like', '"channel":"BLIBLI_ID"'), ] records = self.search(domain, order='create_date asc', limit=limit) for i, rec in enumerate(records, 1): try: rec.execute_queue_detail() # ⏳ throttle API time.sleep(0.8) if i % 10 == 0: self.env.cr.commit() except Exception as e: msg = str(e) # 🎯 khusus rate limit if '429' in msg or 'SERVICE_BUSY' in msg: _logger.warning( "Rate limit hit. Sleep & retry later. Record ID %s", rec.id ) self.env.cr.rollback() time.sleep(5 + random.uniform(1, 3)) break # STOP loop, jangan maksa _logger.error("Failed record %s: %s", rec.id, msg) self.env.cr.rollback() self.env.cr.commit() def get_partner(self, shop_id): partner = self.env['res.partner'].search([('ginee_shop_id', '=', shop_id)], limit=1) if not partner: partner = self.env['res.partner'].browse(414) return partner.id def prepare_data_so(self, json_data): order = json_data.get('data', [{}])[0] date_str = json_data.get('data', [{}])[0].get('promisedToShipBefore') deadline_date = False partner = self.get_partner(json_data.get('data', {})[0].get('shopId')) if partner == 281: payment_info = order.get('paymentInfo', {}) total_discounts = payment_info.get('totalDiscounts', 0) or 0 discount_shipping = payment_info.get('totalDiscountShipping', 0) or 0 total_tax = payment_info.get('totalTaxation', 0) or 0 total_shipping_fee = payment_info.get('totalShippingFee', 0) or 0 if discount_shipping > 0: total_discounts += discount_shipping total_discounts = -total_discounts if date_str: # utc_dt = datetime.strptime( # date_str, # "%Y-%m-%dT%H:%M:%SZ" # ).replace(tzinfo=pytz.UTC) # wib_tz = pytz.timezone('Asia/Jakarta') # deadline_date = utc_dt.astimezone(wib_tz).replace(tzinfo=None) deadline_date = datetime.strptime( date_str, "%Y-%m-%dT%H:%M:%SZ" ) + timedelta(hours=1) data = { 'partner_id': self.get_partner(json_data.get('data', {})[0].get('shopId')), 'client_order_ref': json_data.get('data', {})[0].get('orderId'), 'warehouse_id': 4, 'picking_policy': 'direct', 'carrier': json_data.get('data', {})[0].get('logisticsInfos')[0].get('logisticsProviderName'), 'invoice_mp': json_data.get('data', {})[0].get('externalOrderId'), 'source': 'ginee', 'channel': json_data.get('data', {})[0].get('channel'), 'deadline_date': deadline_date, } if partner == 281: data['marketplace_discount'] = total_discounts # data['marketplace_tax'] = total_tax # data['delivery_amount'] = total_shipping_fee return data def _combine_order_items(self, items, lazada_id): """Combine quantities of the same products from multiple orders""" product_quantities = {} for item in items: key = item.get('sku', "") if key == "" or key == False: key = item.get('masterSku', "") qty = item.get('quantity', 0) price = item.get('actualPrice', 0) if key in product_quantities: if not lazada_id: # Normal mode → digabung product_quantities[key]['quantity'] += qty product_quantities[key]['actualPrice'] += price else: # Lazada mode → overwrite, bukan akumulasi product_quantities[key]['quantity'] += qty product_quantities[key]['actualPrice'] = price product_quantities[key]['item_data'] = item else: product_quantities[key] = { 'quantity': qty, 'actualPrice': price, 'productName': item.get('productName'), 'masterSkuType': item.get('masterSkuType'), 'item_data': item } return product_quantities def prepare_data_so_line(self, json_data): order_lines = [] product_not_found = False product_bundling_no_pricelist = False # Get all items (already combined if grouped) items = json_data.get('data', [{}])[0].get('items', []) lazada_id = json_data.get('data', [{}])[0].get('channel') # Combine quantities of the same products product_quantities = self._combine_order_items(items, lazada_id=True if lazada_id == 'LAZADA_ID' else False) # Process the combined items for sku, combined_item in product_quantities.items(): item = combined_item['item_data'] product = self.env['product.product'].search( [('default_code', '=', sku)], limit=1 ) if product.id == 6031: product.id = 5792 product_not_found = True if product and item.get('masterSkuType') == 'BUNDLE': order_lines.append((0, 0, { 'display_type': 'line_note', 'name': f"Bundle: {item.get('productName')}, Qty: {combined_item['quantity']}, Master SKU: {sku}", 'product_uom_qty': 0, 'price_unit': 0, })) bundling_lines = self.env['bundling.line'].search([('product_id', '=', product.id)]) bundling_variant_ids = bundling_lines.mapped('variant_id').ids sale_pricelist = self.env['product.pricelist.item'].search([ ('product_id', 'in', bundling_variant_ids), ('pricelist_id', '=', 17) ]) price_bundling_bottom = sum(item.fixed_price for item in sale_pricelist) for bline in bundling_lines: bottom_price = self.env['product.pricelist.item'].search([ ('product_id', '=', bline.variant_id.id), ('pricelist_id', '=', 17) ], limit=1) price = bottom_price.fixed_price if bottom_price else 0 if not price or price <= 0: price_unit = 0 product_bundling_no_pricelist = True else: price_unit = self.prorate_price_bundling( bline.variant_id, price_bundling_bottom, price, actual_price=combined_item['actualPrice'] ) order_lines.append((0, 0, { 'product_id': bline.variant_id.id if bline.variant_id else product.id, 'product_uom_qty': bline.product_uom_qty * combined_item['quantity'], 'price_unit': price_unit, 'name': f"{bline.variant_id.display_name} (Bundle Component)" if bline.variant_id.display_name else product.name, })) order_lines.append((0, 0, { 'display_type': 'line_note', 'name': f"End Of Bundling Product", 'product_uom_qty': 0, 'price_unit': 0, })) continue partner = self.get_partner(json_data.get('data', {})[0].get('shopId')) # Regular product line line_data = { 'product_id': product.id if product else 5792, 'product_uom_qty': combined_item['quantity'], 'price_unit': combined_item['actualPrice'], } if partner == 281: line_data['tax_id'] = [(5, 0, 0)] if not product: line_data['name'] = f"{sku} ({combined_item['productName']})" product_not_found = True order_lines.append((0, 0, line_data)) return order_lines, product_not_found, product_bundling_no_pricelist def execute_queue_detail(self): try: json_data = json.loads(self.detail_order) data = self.prepare_data_so(json_data) order_lines, product_not_found, product_bundling_no_pricelist = self.prepare_data_so_line(json_data) order_id, order_status, print_info = self.get_order_id_detail() # First check if a sale order with this reference already exists existing_order = self.env['sale.order'].search([('order_reference', '=', order_id)], limit=1) create_at_str = json_data.get('data', [{}])[0].get('createAt') if create_at_str: create_at = datetime.strptime( create_at_str, "%Y-%m-%dT%H:%M:%SZ" ).replace(tzinfo=timezone.utc) cutoff = datetime(2026, 1, 1, 0, 0, tzinfo=timezone.utc) if create_at >= cutoff: if order_status == 'CANCELLED': external_order_id = json_data.get('data', [{}])[0].get('externalOrderId') order_id = json_data.get('data', [{}])[0].get('orderId') # Try to find existing SO existing_order = self.env['sale.order'].search([ '|', ('invoice_mp', '=', external_order_id), ('client_order_ref', '=', order_id) ], limit=1) if existing_order: if existing_order.state == 'sale': # Cancel all pickings linked to this order for picking in existing_order.picking_ids: if picking.state not in ['cancel', 'done']: picking.action_cancel() self.sale_id = existing_order.id self.execute_status = 'cancelled_so_picking' existing_order.action_cancel() else: existing_order.action_cancel() self.sale_id = existing_order.id self.execute_status = 'cancelled_so' else: # If no existing SO, create one, then cancel data = self.prepare_data_so(json_data) order_lines, product_not_found, product_bundling_no_pricelist = self.prepare_data_so_line(json_data) data['order_line'] = order_lines sale_order = self.env['sale.order'].create(data) self.sale_id = sale_order.id sale_order.order_reference = order_id sale_order.address = json_data.get('data', [{}])[0].get('shippingAddressInfo', []).get('fullAddress', []) sale_order.note_by_buyer = json_data.get('data', [{}])[0].get('extraInfo', []).get('noteByBuyer', []) sale_order.action_cancel() self.execute_status = 'cancelled_so' return if existing_order: date_str = json_data.get('data', [{}])[0].get('promisedToShipBefore') deadline_date = False if date_str: # utc_dt = datetime.strptime( # date_str, # "%Y-%m-%dT%H:%M:%SZ" # ).replace(tzinfo=pytz.UTC) # wib_tz = pytz.timezone('Asia/Jakarta') # deadline_date = utc_dt.astimezone(wib_tz).replace(tzinfo=None) deadline_date = datetime.strptime( date_str, "%Y-%m-%dT%H:%M:%SZ" ) + timedelta(hours=1) self.sale_id = existing_order.id self.execute_status = 'already_so' existing_order.deadline_date = deadline_date picking = self.env['stock.picking'].search([('sale_id', '=', existing_order.id),('order_reference', '=', existing_order.client_order_ref), ('picking_type_code', '=', 'outgoing'), ('state', 'not in', ['cancel', 'done'])], limit=1) if picking: picking.date_deadline = deadline_date return # Exit early since we don't need to create anything if order_status != 'PENDING_PAYMENT': if order_status in ('PARTIALLY_PAID', 'PAID'): data['order_line'] = order_lines sale_order = self.env['sale.order'].create(data) self.sale_id = sale_order.id sale_order.order_reference = order_id sale_order.address = json_data.get('data', [{}])[0].get('shippingAddressInfo', []).get('fullAddress', []) sale_order.note_by_buyer = json_data.get('data', [{}])[0].get('extraInfo', []).get('noteByBuyer', []) if (not product_not_found and not product_bundling_no_pricelist) or sale_order.partner_id == 414: sale_order.action_confirm() # self.picking_id = sale_order.picking_ids[0].id # self.picking_id.order_reference = order_id # self.picking_id.invoice_mp = sale_order.invoice_mp # self.picking_id.carrier = sale_order.carrier # self.picking_id.address = json_data.get('data', [{}])[0].get('shippingAddressInfo', []).get('fullAddress', []) # self.picking_id.note_by_buyer = json_data.get('data', [{}])[0].get('extraInfo', []).get('noteByBuyer', []) self.execute_status = 'so_confirm' else: sale_order.message_post(body="Auto draft karena bundling price 0 / tidak ditemukan.") self.execute_status = 'so_draft' else: # For other statuses, create new order only if it doesn't exist data['order_line'] = order_lines sale_order = self.env['sale.order'].create(data) self.sale_id = sale_order.id sale_order.order_reference = order_id sale_order.address = json_data.get('data', [{}])[0].get('shippingAddressInfo', []).get('fullAddress', []) sale_order.note_by_buyer = json_data.get('data', [{}])[0].get('extraInfo', []).get('noteByBuyer', []) if (not product_not_found and not product_bundling_no_pricelist) or sale_order.partner_id == 414: sale_order.action_confirm() # self.picking_id = sale_order.picking_ids[0].id # self.picking_id.order_reference = order_id # self.picking_id.invoice_mp = sale_order.invoice_mp # self.picking_id.carrier = sale_order.carrier # self.picking_id.address = json_data.get('data', [{}])[0].get('shippingAddressInfo', []).get('fullAddress', []) # self.picking_id.note_by_buyer = json_data.get('data', [{}])[0].get('extraInfo', []).get('noteByBuyer', []) self.execute_status = 'so_confirm' else: sale_order.message_post(body="Auto draft karena bundling price 0 / tidak ditemukan.") self.execute_status = 'so_draft' else: self.execute_status = 'failed' self.message_error = "Tanggal order di bawah 1 januari 2026" except Exception as e: self.write({ 'message_error': json.dumps({ 'error': str(e) }) }) def sign_request(self): signData = '$'.join(['POST', Request_URI]) + '$' authorization = ACCESS_KEY + ':' + base64.b64encode( hmac.new(SECRET_KEY.encode('utf-8'), signData.encode('utf-8'), digestmod=sha256).digest() ).decode('ascii') return authorization def prorate_price_bundling(self, product, sum_bottom, price_bottom,actual_price): percent = price_bottom / sum_bottom real_price = percent * actual_price return real_price