From c652ee3e1f652d23e37833f01e3fdd7aa8e52021 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 31 May 2025 11:03:06 +0700 Subject: (andri) add tracking biteship pada BU OUT & fix delivery departure & arrival date --- indoteknik_custom/models/stock_picking.py | 55 ++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 54da700b..4d38e5b3 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1551,7 +1551,60 @@ class StockPicking(models.Model): except Exception as e : _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") return { 'error': str(e) } - + + def action_sync_biteship_tracking(self): + for picking in self: + if not picking.biteship_id: + raise UserError("Tracking Biteship tidak tersedia.") + + histori = picking.get_manifest_biteship() + updated_fields = {} + seen_logs = set() + + manifests = sorted(histori.get("manifests", []), key=lambda m: m.get("datetime") or "") + + for manifest in manifests: + status = manifest.get("status", "").lower() + dt_str = manifest.get("datetime") + desc = manifest.get("description") + dt = False + + try: + local_dt_str = picking._convert_to_local_time(dt_str) + dt = fields.Datetime.from_string(local_dt_str) + except Exception as e: + _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") + continue + + # Update tanggal ke field + if status == "picked" and dt and not picking.driver_departure_date: + updated_fields["driver_departure_date"] = dt + + if status == "delivered" and dt and not picking.driver_arrival_date: + updated_fields["driver_arrival_date"] = dt + + # Buat log unik + if dt and desc: + desc_clean = ' '.join(desc.strip().split()) + log_line = f"[TRACKING] {status} - {dt.strftime('%d %b %Y %H:%M')}: {desc_clean}" + if not picking._has_existing_log(log_line): + picking.message_post(body=log_line) + seen_logs.add(log_line) + + if updated_fields: + picking.write(updated_fields) + + def _has_existing_log(self, log_line): + self.ensure_one() + self.env.cr.execute(""" + SELECT 1 FROM mail_message + WHERE model = %s AND res_id = %s + AND subtype_id IS NOT NULL + AND body ILIKE %s + LIMIT 1 + """, (self._name, self.id, f"%{log_line}%")) + return self.env.cr.fetchone() is not None + def _convert_to_local_time(self, iso_date): try: dt_with_tz = waktu.fromisoformat(iso_date) -- cgit v1.2.3 From b6928c9bffc486b471d4c335c2550cbc1bf7d841 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sat, 31 May 2025 12:32:45 +0700 Subject: (andri) fix bug datetime departure&arrival --- indoteknik_custom/models/stock_picking.py | 34 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 4d38e5b3..4517a941 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -1570,23 +1570,23 @@ class StockPicking(models.Model): dt = False try: - local_dt_str = picking._convert_to_local_time(dt_str) - dt = fields.Datetime.from_string(local_dt_str) + dt = picking._convert_to_local_time(dt_str) except Exception as e: _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") continue - # Update tanggal ke field + # Update tanggal ke field (pastikan naive datetime UTC) if status == "picked" and dt and not picking.driver_departure_date: - updated_fields["driver_departure_date"] = dt + updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt) if status == "delivered" and dt and not picking.driver_arrival_date: - updated_fields["driver_arrival_date"] = dt + updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt) - # Buat log unik + # Buat log unik dengan waktu lokal Asia/Jakarta if dt and desc: + dt_local = pytz.utc.localize(dt).astimezone(pytz.timezone("Asia/Jakarta")) desc_clean = ' '.join(desc.strip().split()) - log_line = f"[TRACKING] {status} - {dt.strftime('%d %b %Y %H:%M')}: {desc_clean}" + log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}: {desc_clean}" if not picking._has_existing_log(log_line): picking.message_post(body=log_line) seen_logs.add(log_line) @@ -1607,15 +1607,21 @@ class StockPicking(models.Model): def _convert_to_local_time(self, iso_date): try: - dt_with_tz = waktu.fromisoformat(iso_date) - utc_dt = dt_with_tz.astimezone(pytz.utc) - + from dateutil import parser + import pytz + if isinstance(iso_date, str): + waktu = parser.parse(iso_date) + else: + waktu = iso_date + if waktu.tzinfo is None: + waktu = waktu.replace(tzinfo=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") + local_dt = waktu.astimezone(local_tz) + utc_dt = local_dt.astimezone(pytz.utc).replace(tzinfo=None) + return utc_dt except Exception as e: - return str(e) + _logger.warning(f"[Biteship] Gagal konversi waktu lokal: {e}") + return False def _map_status_biteship(self, status): status_mapping = { -- cgit v1.2.3 From 03d52ddceacea1939aef8ee4c571cacdb8b2c055 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sun, 1 Jun 2025 13:12:27 +0700 Subject: (andri) fix bug selected shipping service --- indoteknik_custom/models/sale_order.py | 78 ++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 37 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index f1280b37..490e4581 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -249,7 +249,7 @@ class SaleOrder(models.Model): string="Attachment Bukti Cancel", readonly=False, ) nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) - shipping_option_id = fields.Many2one("shipping.option", string="Selected Service Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") + shipping_option_id = fields.Many2one("shipping.option", string="Selected Service Option", help="Selected shipping option for delivery", tracking=True, domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), @@ -306,20 +306,13 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): + self.shipping_option_id = False + # self.delivery_amt = 0 + # self.delivery_service_type = False + if not self.carrier_id: - self.shipping_option_id = False - self.delivery_amt = 0 return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - # ✅ Lewati validasi jika carrier bukan Biteship - if self.carrier_id.delivery_type != 'biteship': - _logger.info(f"Carrier {self.carrier_id.name} bertipe custom ({self.carrier_id.delivery_type}), tidak divalidasi.") - self.shipping_option_id = False - self.delivery_amt = 0 - return { - 'domain': {'shipping_option_id': [('id', '=', -1)]} - } - # Ambil provider dari rajaongkir_kurir self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir @@ -329,56 +322,67 @@ class SaleOrder(models.Model): result = self.env.cr.fetchone() provider = result[0].lower() if result and result[0] else False - # Fallback ke nama carrier jika tidak ada di rajaongkir_kurir + # Fallback dari nama carrier if not provider: provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False - _logger.info(f"Carrier changed to {self.carrier_id.name}, provider: {provider}") + _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}") - sale_order_id = self._origin.id if self._origin and self._origin.id else None - - # Cek shipping option untuk provider ini - if sale_order_id: - self.env.cr.execute(""" - SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s AND sale_order_id = %s - """, (f'%{provider}%', sale_order_id)) - else: - self.env.cr.execute(""" - SELECT COUNT(*) FROM shipping_option - WHERE LOWER(provider) LIKE %s AND sale_order_id IS NULL - """, (f'%{provider}%',)) + # Ambil ID SO + sale_order_id = self._origin.id if self._origin else False + # Hitung jumlah shipping_option yang cocok + self.env.cr.execute(""" + SELECT COUNT(*) FROM shipping_option + WHERE LOWER(provider) LIKE %s AND sale_order_id = %s + """, (f'%{provider}%', sale_order_id)) count = self.env.cr.fetchone()[0] - _logger.info(f"Found {count} shipping options for provider {provider}") - # VALIDASI GAGAL + _logger.info(f"[Shipping Option Count] Provider: {provider} | SO ID: {sale_order_id} | Count: {count}") + + # Jika tidak ditemukan shipping option if count == 0: previous_carrier = self._origin.carrier_id if self._origin else False + previous_provider = False + self.carrier_id = previous_carrier + self.shipping_option_id = self._origin.shipping_option_id if self._origin else False + + # Rehitung provider untuk domain fallback + if previous_carrier: + self.env.cr.execute(""" + SELECT name FROM rajaongkir_kurir + WHERE delivery_carrier_id = %s LIMIT 1 + """, (previous_carrier.id,)) + prev_row = self.env.cr.fetchone() + previous_provider = prev_row[0].lower() if prev_row and prev_row[0] else previous_carrier.name.lower().split()[0] + + fallback_domain = [('id', '=', -1)] + if sale_order_id and previous_provider: + fallback_domain = [ + '|', + '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{previous_provider}%'), + '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{previous_provider}%') + ] return { 'warning': { - 'title': "Shipping Method Tidak Tersedia", + 'title': "Shipping Option Tidak Ditemukan", 'message': ( - f"Shipping method '{self.carrier_id.name}' tidak tersedia pada pengiriman ini.\n" + f"Layanan kurir tidak tersedia untuk pengiriman ini.\n" f"Pilihan dikembalikan ke sebelumnya." ) }, - 'domain': {'shipping_option_id': [('id', '=', -1)]} + 'domain': {'shipping_option_id': fallback_domain} } - # ✅ Valid, baru reset shipping_option dan delivery amount - self.shipping_option_id = False - self.delivery_amt = 0 - + # Jika ditemukan, set domain normal domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), '&', ('sale_order_id', '=', False), ('provider', 'ilike', f'%{provider}%') ] - _logger.info(f"Final domain for shipping_option_id: {domain}") return {'domain': {'shipping_option_id': domain}} -- cgit v1.2.3 From e5a3d37ca40127a6bd2a31f08d94704103a1ac11 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sun, 1 Jun 2025 17:23:56 +0700 Subject: (andri) fix bug shipping method --- indoteknik_custom/models/sale_order.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 490e4581..453406c4 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -306,6 +306,10 @@ class SaleOrder(models.Model): @api.onchange('carrier_id') def _onchange_carrier_id(self): + # Jika record masih baru (belum disimpan), jangan jalankan onchange + if not self._origin or not self._origin.id: + return + self.shipping_option_id = False # self.delivery_amt = 0 # self.delivery_service_type = False @@ -313,7 +317,7 @@ class SaleOrder(models.Model): if not self.carrier_id: return {'domain': {'shipping_option_id': [('id', '=', -1)]}} - # Ambil provider dari rajaongkir_kurir + # Ambil provider dari mapping self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir WHERE delivery_carrier_id = %s @@ -322,16 +326,14 @@ class SaleOrder(models.Model): result = self.env.cr.fetchone() provider = result[0].lower() if result and result[0] else False - # Fallback dari nama carrier if not provider: provider = self.carrier_id.name.lower().split()[0] if self.carrier_id.name else False _logger.info(f"[Carrier Changed] {self.carrier_id.name}, Detected Provider: {provider}") - # Ambil ID SO - sale_order_id = self._origin.id if self._origin else False + sale_order_id = self._origin.id - # Hitung jumlah shipping_option yang cocok + # Cek apakah ada shipping_option yang cocok self.env.cr.execute(""" SELECT COUNT(*) FROM shipping_option WHERE LOWER(provider) LIKE %s AND sale_order_id = %s @@ -340,7 +342,6 @@ class SaleOrder(models.Model): _logger.info(f"[Shipping Option Count] Provider: {provider} | SO ID: {sale_order_id} | Count: {count}") - # Jika tidak ditemukan shipping option if count == 0: previous_carrier = self._origin.carrier_id if self._origin else False previous_provider = False @@ -348,7 +349,7 @@ class SaleOrder(models.Model): self.carrier_id = previous_carrier self.shipping_option_id = self._origin.shipping_option_id if self._origin else False - # Rehitung provider untuk domain fallback + # Ambil kembali domain provider sebelumnya if previous_carrier: self.env.cr.execute(""" SELECT name FROM rajaongkir_kurir @@ -358,7 +359,7 @@ class SaleOrder(models.Model): previous_provider = prev_row[0].lower() if prev_row and prev_row[0] else previous_carrier.name.lower().split()[0] fallback_domain = [('id', '=', -1)] - if sale_order_id and previous_provider: + if previous_provider: fallback_domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{previous_provider}%'), @@ -376,7 +377,7 @@ class SaleOrder(models.Model): 'domain': {'shipping_option_id': fallback_domain} } - # Jika ditemukan, set domain normal + # Jika data ada, kembalikan domain biasa domain = [ '|', '&', ('sale_order_id', '=', sale_order_id), ('provider', 'ilike', f'%{provider}%'), @@ -385,7 +386,6 @@ class SaleOrder(models.Model): return {'domain': {'shipping_option_id': domain}} - @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): if not self.shipping_option_id: -- cgit v1.2.3 From 1c0bec8dadc593348df9ca585dae13b8ff65c316 Mon Sep 17 00:00:00 2001 From: "Indoteknik ." Date: Sun, 1 Jun 2025 20:29:57 +0700 Subject: (andri) fix bug price estimate biteship --- indoteknik_custom/models/sale_order.py | 57 ++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 27 deletions(-) (limited to 'indoteknik_custom/models') diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py index 453406c4..ec4b55e7 100755 --- a/indoteknik_custom/models/sale_order.py +++ b/indoteknik_custom/models/sale_order.py @@ -696,10 +696,12 @@ class SaleOrder(models.Model): if weight_gram < 100: weight_gram = 100 + value = int(self.amount_untaxed or sum(line.price_subtotal for line in self.order_line)) + items = [{ "name": "Paket Pesanan", "description": f"Sale Order {self.name}", - "value": int(self.amount_untaxed), + "value": value, "weight": weight_gram, "quantity": 1, }] @@ -811,6 +813,7 @@ class SaleOrder(models.Model): if not shipping_options: raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data.get('destination_postal_code', '')}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") + # Temukan shipping option yang cocok berdasarkan carrier_id selected_option = None if self.carrier_id: @@ -822,44 +825,44 @@ class SaleOrder(models.Model): courier_code = rajaongkir_kurir.name.lower() carrier_name = self.carrier_id.name.lower() - possible_codes = [ + possible_codes = list({ courier_code, carrier_name, carrier_name.split()[0] if ' ' in carrier_name else carrier_name - ] + }) - _logger.info(f"Mencari shipping option untuk kurir: {possible_codes}") + _logger.info(f"[MATCHING] Mencari shipping option untuk kurir: {possible_codes}") for option in shipping_options: - option_provider = option.provider.lower() if option.provider else '' - option_name = option.name.lower() if option.name else '' - - for code in possible_codes: - if code in option_provider or code in option_name: - selected_option = option - _logger.info(f"Menemukan shipping option yang cocok: {option.name}") - break + option_provider = (option.provider or '').lower() + option_name = (option.name or '').lower() - if selected_option: + if any(code in option_provider or code in option_name for code in possible_codes): + selected_option = option + _logger.info(f"[MATCHED] Shipping option cocok: {option.name}") break if not selected_option and shipping_options: selected_option = shipping_options[0] - _logger.info(f"Menggunakan opsi pertama: {selected_option.name}") - - # Ganti carrier_id otomatis sesuai provider dari shipping option - provider = selected_option.provider.lower() - self.env.cr.execute(""" - SELECT delivery_carrier_id FROM rajaongkir_kurir - WHERE LOWER(name) = %s AND delivery_carrier_id IS NOT NULL - LIMIT 1 - """, (provider,)) - row = self.env.cr.fetchone() - matched_carrier_id = row[0] if row else False - if matched_carrier_id: - self.carrier_id = matched_carrier_id - _logger.info(f"Carrier diganti otomatis ke ID {matched_carrier_id} berdasarkan provider {provider}") + _logger.info(f"[DEFAULT] Tidak ada yang cocok, pakai opsi pertama: {selected_option.name}") + + # ❗ Ganti carrier_id hanya jika BELUM terisi sama sekali (contoh: user dari backend) + if not self.carrier_id: + provider = selected_option.provider.lower() + self.env.cr.execute(""" + SELECT delivery_carrier_id FROM rajaongkir_kurir + WHERE LOWER(name) = %s AND delivery_carrier_id IS NOT NULL + LIMIT 1 + """, (provider,)) + row = self.env.cr.fetchone() + matched_carrier_id = row[0] if row else False + if matched_carrier_id: + self.carrier_id = matched_carrier_id + _logger.info(f"[AUTO-SET] Carrier diisi otomatis ke ID {matched_carrier_id} (provider: {provider})") + else: + _logger.warning(f"[WARNING] Provider {provider} tidak ditemukan di rajaongkir_kurir") + # Set shipping option dan nilai ongkir if selected_option: self.shipping_option_id = selected_option.id self.delivery_amt = selected_option.price -- cgit v1.2.3