diff options
Diffstat (limited to 'indoteknik_custom/models/stock_picking.py')
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 212 |
1 files changed, 194 insertions, 18 deletions
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 36d9f63d..b8bdcd94 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1,7 +1,7 @@ from odoo import fields, models, api, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.float_utils import float_is_zero -from datetime import timedelta, datetime +from datetime import timedelta, datetime as waktu from itertools import groupby import pytz, requests, json, requests from dateutil import parser @@ -12,10 +12,19 @@ import base64 import requests import time import logging +import re +from deep_translator import GoogleTranslator _logger = logging.getLogger(__name__) +_biteship_url = "https://api.biteship.com/v1" +_biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" + + + class StockPicking(models.Model): _inherit = 'stock.picking' + _order = 'final_seq ASC' + check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True) barcode_product_lines = fields.One2many('barcode.product', 'picking_id', string='Barcode Product', auto_join=True) is_internal_use = fields.Boolean('Internal Use', help='flag which is internal use or not') @@ -166,6 +175,68 @@ class StockPicking(models.Model): lalamove_image_url = fields.Char(string="Lalamove Image URL") lalamove_image_html = fields.Html(string="Lalamove Image", compute="_compute_lalamove_image_html") + # Biteship Section + biteship_id = fields.Char(string="Biteship Respon ID") + biteship_tracking_id = fields.Char(string="Biteship Trackcking ID") + biteship_waybill_id = fields.Char(string="Biteship Waybill ID") + # estimated_ready_ship_date = fields.Datetime(string='ET Ready to Ship', copy=False, related='sale_id.estimated_ready_ship_date') + # countdown_hours = fields.Float(string='Countdown in Hours', compute='_callculate_sequance', default=False, store=False, compute_sudo=False) + # countdown_ready_to_ship = fields.Char(string='Countdown Ready to Ship', compute='_callculate_sequance', store=False, compute_sudo=False) + final_seq = fields.Float(string='Remaining Time') + + + def schduled_update_sequance(self): + query = "SELECT update_sequance_stock_picking();" + self.env.cr.execute(query) + + + # @api.depends('estimated_ready_ship_date', 'state') + # def _callculate_sequance(self): + # for record in self: + # try : + # if record.estimated_ready_ship_date and record.state not in ('cancel', 'done'): + # rts = record.estimated_ready_ship_date - waktu.now() + # rts_days = rts.days + # rts_hours = divmod(rts.seconds, 3600) + + # estimated_by_erts = rts.total_seconds() / 3600 + + # record.countdown_ready_to_ship = f"{rts_days} days, {rts_hours} hours" + # record.countdown_hours = estimated_by_erts + # else: + # record.countdown_hours = 999999999999 + # record.countdown_ready_to_ship = False + # except Exception as e : + # _logger.error(f"Error calculating sequance {record.id}: {str(e)}") + # print(str(e)) + # return { 'error': str(e) } + + + # @api.depends('estimated_ready_ship_date', 'state') + # def _compute_countdown_hours(self): + # for record in self: + # if record.state in ('cancel', 'done') or not record.estimated_ready_ship_date: + # # Gunakan nilai yang sangat besar sebagai placeholder + # record.countdown_hours = 999999 + # else: + # delta = record.estimated_ready_ship_date - waktu.now() + # record.countdown_hours = delta.total_seconds() / 3600 + + # @api.depends('estimated_ready_ship_date', 'state') + # def _compute_countdown_ready_to_ship(self): + # for record in self: + # if record.state in ('cancel', 'done'): + # record.countdown_ready_to_ship = False + # else: + # if record.estimated_ready_ship_date: + # delta = record.estimated_ready_ship_date - waktu.now() + # days = delta.days + # hours, remainder = divmod(delta.seconds, 3600) + # record.countdown_ready_to_ship = f"{days} days, {hours} hours" + # record.countdown_hours = delta.total_seconds() / 3600 + # else: + # record.countdown_ready_to_ship = False + def _compute_lalamove_image_html(self): for record in self: if record.lalamove_image_url: @@ -321,7 +392,7 @@ class StockPicking(models.Model): picking.tracking_by = self.env.user.id ata_at_str = data.get("ata_at") envio_ata = self._convert_to_datetime(data.get("ata_at")) - + picking.driver_arrival_date = envio_ata if data.get("status") != 'delivered': picking.driver_arrival_date = False @@ -332,12 +403,13 @@ class StockPicking(models.Model): raise UserError(f"Kesalahan tidak terduga: {str(e)}") def action_send_to_biteship(self): - url = "https://api.biteship.com/v1/orders" - api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" - + + if self.biteship_tracking_id: + raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") + # Mencari data sale.order.line berdasarkan sale_id products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)]) - + # Fungsi untuk membangun items_data dari order lines def build_items_data(lines): return [{ @@ -370,6 +442,7 @@ class StockPicking(models.Model): }) payload = { + "reference_id " : self.sale_id.name, "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, @@ -381,7 +454,8 @@ class StockPicking(models.Model): "destination_contact_phone": self.real_shipping_id.phone or self.real_shipping_id.mobile, "destination_address": self.real_shipping_id.street, "destination_postal_code": self.real_shipping_id.zip, - "courier_type": "reg", + "origin_note": "BELAKANG INDOMARET", + "courier_type": self.sale_id.delivery_service_type or "reg", "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", "destination_postal_code": self.real_shipping_id.zip, @@ -389,27 +463,42 @@ class StockPicking(models.Model): } # Cek jika pengiriman instant atau same_day - if "instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type: + if self.sale_id.delivery_service_type and ("instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type): payload.update({ - "origin_note": "BELAKANG INDOMARET", - "courier_company": self.carrier_id.name.lower(), - "courier_type": self.sale_id.delivery_service_type, - "delivery_type": "now", - "items": items_data_instant # Gunakan items untuk instant + "origin_coordinate" :{ + "latitude": -6.3031123, + "longitude" : 106.7794934999 + }, + "destination_coordinate" : { + "latitude": self.real_shipping_id.latitude, + "longitude": self.real_shipping_id.longtitude, + }, + "items": items_data_instant }) - + + api_key = _biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } # Kirim request ke Biteship - response = requests.post(url, headers=headers, json=payload) + response = requests.post(_biteship_url+'/orders', headers=headers, json=payload) - if response.status_code == 201: - return response.json() + if response.status_code == 200: + data = response.json() + + self.biteship_id = data.get("id", "") + self.biteship_tracking_id = data.get("courier", {}).get("tracking_id", "") + self.biteship_waybill_id = data.get("courier", {}).get("waybill_id", "") + self.delivery_tracking_no = data.get("courier", {}).get("waybill_id", "") + + return data else: - raise UserError(f"Error saat mengirim ke Biteship: {response.content}") + error_data = response.json() + error_message = error_data.get("error", "Unknown error") + error_code = error_data.get("code", "No code provided") + raise UserError(f"Error saat mengirim ke Biteship: {error_message} (Code: {error_code})") @api.constrains('driver_departure_date') def constrains_driver_departure_date(self): @@ -1039,11 +1128,14 @@ class StockPicking(models.Model): def get_tracking_detail(self): self.ensure_one() + + order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1) response = { 'delivery_order': { 'name': self.name, 'carrier': self.carrier_id.name or '', + 'service' : order.delivery_service_type or '', 'receiver_name': '', 'receiver_city': '' }, @@ -1052,8 +1144,21 @@ class StockPicking(models.Model): 'waybill_number': self.delivery_tracking_no or '', 'delivery_status': None, 'eta': self.generate_eta_delivery(), + 'is_biteship': True if self.biteship_id else False, 'manifests': self.get_manifests() } + + if self.biteship_id : + histori = self.get_manifest_biteship() + eta_start = order.date_order + timedelta(days=order.estimated_arrival_days_start) + eta_end = order.date_order + timedelta(days=order.estimated_arrival_days) + 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 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 @@ -1066,6 +1171,77 @@ class StockPicking(models.Model): return response + def get_manifest_biteship(self): + api_key = _biteship_api_key + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + + manifests = [] + + try: + # Kirim request ke Biteship + response = requests.get(_biteship_url+'/trackings/'+self.biteship_tracking_id, headers=headers, json=manifests) + result = response.json() + description = { + 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up', + 'allocated' : 'Kurir akan melakukan pick-up pesanan', + 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', + 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("name", ""), + 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', + 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + 'delivered' : 'Pesanan telah sampai dan diterima oleh '+result.get("destination", {}).get("contact_name", "") + } + if(result.get('success') == True): + history = result.get("history", []) + status = result.get("status", "") + + for entry in reversed(history): + manifests.append({ + "status": re.sub(r'[^a-zA-Z0-9\s]', ' ', entry["status"]).lower().capitalize(), + "datetime": self._convert_to_local_time(entry["updated_at"]), + # "description": GoogleTranslator(source='auto', target='id').translate(entry["note"]), + "description": description[entry["status"]], + }) + + return { + "manifests": manifests, + "delivered": status + } + + return manifests + except Exception as e : + _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") + return { 'error': str(e) } + + def _convert_to_local_time(self, iso_date): + try: + dt_with_tz = waktu.fromisoformat(iso_date) + utc_dt = dt_with_tz.astimezone(pytz.utc) + + local_tz = pytz.timezone("Asia/Jakarta") + local_dt = utc_dt.astimezone(local_tz) + + return local_dt.strftime("%Y-%m-%d %H:%M:%S") + except Exception as e: + return str(e) + + def _map_status_biteship(self, status): + status_mapping = { + "confirmed": "pending", + "scheduled": "pending", + "allocated": "pending", + "picking_up": "pending", + "picked": "shipment", + "cancelled": "cancelled", + "on_hold": "on_hold", + "dropping_off": "shipment", + "delivered": "completed" + } + return status_mapping.get(status, "Hubungi Admin") + def generate_eta_delivery(self): current_date = datetime.datetime.now() prepare_days = 3 |
