diff options
| author | Indoteknik . <it@fixcomart.co.id> | 2025-06-09 15:11:05 +0700 |
|---|---|---|
| committer | Indoteknik . <it@fixcomart.co.id> | 2025-06-09 15:11:05 +0700 |
| commit | 1bc03c1482a664ffcd58f19022a40e65e21774c6 (patch) | |
| tree | f491d00cd1d371c8fd76ad25e014ac8d662c3d02 /indoteknik_custom/models/stock_picking.py | |
| parent | 1bd3a91889f8616d7042c0d15315c2f25c974ed3 (diff) | |
| parent | f43855aa55265794c7774af79089258e830b0df4 (diff) | |
(andri) fix merge biteship with odoo-backup
Diffstat (limited to 'indoteknik_custom/models/stock_picking.py')
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 319 |
1 files changed, 216 insertions, 103 deletions
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py index 3921ed5a..71eca020 100644 --- a/indoteknik_custom/models/stock_picking.py +++ b/indoteknik_custom/models/stock_picking.py @@ -271,14 +271,14 @@ class StockPicking(models.Model): 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') - shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO', - related='sale_id.carrier_id') - state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], - string='Packing Status') + shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO', related='sale_id.carrier_id') + shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option SO', related='sale_id.shipping_option_id') + select_shipping_option_so = fields.Selection([ + ('biteship', 'Biteship'), + ('custom', 'Custom'), + ], string='Shipping Type SO', related='sale_id.select_shipping_option') + state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status') approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim', copy=False) update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD') @@ -688,46 +688,52 @@ class StockPicking(models.Model): raise UserError(f"Kesalahan tidak terduga: {str(e)}") def action_send_to_biteship(self): - if self.biteship_tracking_id: raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") + + def is_courier_need_coordinates(service_code): + return service_code in [ + "instant", "same_day", "instant_car", + "instant_bike", "motorcycle", "mpv", "van", "truck", + "cdd_bak", "cdd_box", "engkel_box", "engkel_bak" + ] + + # ✅ Ambil item dari move_line_ids_with_package (qty_done > 0) + items = [] + for ml in self.move_line_ids_without_package: + if ml.qty_done <= 0: + continue - # Mencari data sale.order.line berdasarkan sale_id - products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)]) + product = ml.product_id + weight = product.weight or 0.1 # default minimal + line = ml.move_id.sale_line_id or self.env['sale.order.line'].search([ + ('order_id', '=', self.sale_id.id), + ('product_id', '=', ml.product_id.id) + ], limit=1) - # Fungsi untuk membangun items_data dari order lines - def build_items_data(lines): - return [{ - "name": line.product_id.name, - "description": line.name, - "value": line.price_unit, - "quantity": line.product_uom_qty, - "weight": line.weight - } for line in lines] + value = line.price_unit if line else 0 + description = line.name if line else product.name - # Items untuk pengiriman standard - items_data_standard = build_items_data(products) + items.append({ + "name": product.name, + "description": description, + "value": value, + "quantity": ml.qty_done, + "weight": int(weight * 1000), + }) - # Items untuk pengiriman instant, mengambil product_id dari move_line_ids_without_package - items_data_instant = [] - for move_line in self.move_line_ids_without_package: - # Mencari baris di sale.order.line berdasarkan product_id dari move_line - order_line = self.env['sale.order.line'].search([ - ('order_id', '=', self.sale_id.id), - ('product_id', '=', move_line.product_id.id) - ], limit=1) + if not items: + raise UserError("Pengiriman tidak dapat dilakukan karena tidak ada barang yang divalidasi (qty_done = 0).") - if order_line: - items_data_instant.append({ - "name": order_line.product_id.name, - "description": order_line.name, - "value": order_line.price_unit, - "quantity": move_line.qty_done, - "weight": order_line.weight - }) + shipping_partner = self.real_shipping_id + courier_service_code = self.sale_id.delivery_service_type or "reg" payload = { - "reference_id ": self.sale_id.name, + "origin_coordinate": { + "latitude": -6.3031123, + "longitude": 106.7794934999 + }, + "reference_id": self.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, @@ -735,41 +741,39 @@ class StockPicking(models.Model): "origin_contact_phone": "081717181922", "origin_address": "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN", "origin_postal_code": 14440, - "destination_contact_name": self.real_shipping_id.name, - "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, + "destination_contact_name": shipping_partner.name, + "destination_contact_phone": shipping_partner.phone or shipping_partner.mobile, + "destination_address": shipping_partner.street, + "destination_postal_code": shipping_partner.zip, "origin_note": "BELAKANG INDOMARET", - "courier_type": self.sale_id.delivery_service_type or "reg", + "destination_note": f"SO: {self.sale_id.name}", + "order_note": f"SO: {self.sale_id.name}", + "courier_type": courier_service_code, "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", - "destination_postal_code": self.real_shipping_id.zip, - "items": items_data_standard + "items": items } - # Cek jika pengiriman instant atau same_day - 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_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 - }) + if is_courier_need_coordinates(courier_service_code): + if not shipping_partner.latitude or not shipping_partner.longtitude: + raise UserError("Alamat tujuan tidak memiliki koordinat (latitude/longitude).") + payload["destination_coordinate"] = { + "latitude": shipping_partner.latitude, + "longitude": shipping_partner.longtitude, + } + + _logger.info(f"Payload untuk Biteship: {payload}") + + # Kirim ke Biteship api_key = _biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } - # Kirim request ke Biteship response = requests.post(_biteship_url + '/orders', headers=headers, json=payload) + _logger.info(f"Response dari Biteship: {response.text}") if response.status_code == 200: data = response.json() @@ -777,17 +781,27 @@ class StockPicking(models.Model): 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", "") - - waybill_id = data.get("courier", {}).get("waybill_id", "") + self.delivery_tracking_no = self.biteship_waybill_id + + waybill_id = self.biteship_waybill_id + + self.message_post( + body=f"Biteship berhasil dilakukan.<br/>" + f"Kurir: {self.carrier_id.name}<br/>" + f"Tracking ID: {self.biteship_tracking_id or '-'}<br/>" + f"Resi: {waybill_id or '-'}<br/>" + f"Reference: {self.name}<br/>" + f"SO: {self.sale_id.name}", + message_type="comment" + ) message = f"✅ Berhasil Order ke Biteship! Resi: {waybill_id}" if waybill_id else "⚠️ Order berhasil, tetapi tidak ada nomor resi." return { 'effect': { - 'fadeout': 'slow', # Efek menghilang perlahan - 'message': message, # Pesan sukses - 'type': 'rainbow_man', # Efek animasi lucu Odoo + 'fadeout': 'slow', + 'message': message, + 'type': 'rainbow_man', } } else: @@ -1629,28 +1643,42 @@ class StockPicking(models.Model): self.ensure_one() order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1) + + sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', order.name)], limit=1) response = { 'delivery_order': { 'name': self.name, - 'carrier': self.carrier_id.name or '', - 'service': order.delivery_service_type or '', + 'carrier': self.carrier_id.name or '-', + 'service' : order.delivery_service_type or '-', 'receiver_name': '', 'receiver_city': '' }, + 'delivered_date': self.driver_departure_date.strftime('%d %b %Y') if self.driver_departure_date != False else '-', 'delivered': False, 'status': self.shipping_status, - 'waybill_number': self.delivery_tracking_no or '', + '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() + 'manifests': self.get_manifests(), + 'is_delay': True if sale_order_delay and sale_order_delay.status == 'delayed' else False } 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) + day_start = order.estimated_arrival_days_start + day_end = order.estimated_arrival_days + if sale_order_delay: + if sale_order_delay.status == 'delayed': + day_start = day_start + sale_order_delay.days_delayed + day_end = day_end + sale_order_delay.days_delayed + elif sale_order_delay.status == 'early': + day_start = day_start - sale_order_delay.days_delayed + day_end = day_end - sale_order_delay.days_delayed + + eta_start = order.date_order + timedelta(days=day_start) + eta_end = order.date_order + timedelta(days=day_end) formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" response['eta'] = formatted_eta response['manifests'] = histori.get("manifests", []) @@ -1677,7 +1705,7 @@ class StockPicking(models.Model): "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } - + manifests = [] try: @@ -1686,14 +1714,13 @@ class StockPicking(models.Model): 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", "") + '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("company", ""), + 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', + 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', + 'delivered' : f'Pesanan telah sampai dan diterima oleh <span style="color:#DC2626;">{result.get("destination", {}).get("contact_name", "")}</span>' } if (result.get('success') == True): history = result.get("history", []) @@ -1711,19 +1738,99 @@ class StockPicking(models.Model): "delivered": status } - return manifests - except Exception as e: + return { + "manifests": [], + "delivered": False + } + except Exception as e : _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") - return {'error': str(e)} + return { 'error': str(e) } - def _convert_to_local_time(self, iso_date): + 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: + dt = picking._convert_to_utc_datetime(dt_str) + _logger.info(f"[Biteship Sync] Berhasil parse datetime: {dt_str} -> {dt}") + except Exception as e: + _logger.warning(f"[Biteship Sync] Gagal parse datetime: {e}") + continue + + # Update tanggal ke field (pastikan naive datetime UTC) + if status == "picked" and dt and not picking.driver_departure_date: + 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"] = fields.Datetime.to_string(dt) + + # Buat log unik dengan waktu lokal Asia/Jakarta + if dt and desc: + try: + dt_local = parser.parse(dt_str).replace(tzinfo=None) + except Exception as e: + _logger.warning(f"[Biteship Sync] Gagal parse dt_str untuk log: {e}") + dt_local = dt # fallback + + desc_clean = ' '.join(desc.strip().split()) + 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) + + 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 + + # Untuk internal Odoo (mengembalikan naive UTC datetime untuk disimpan ke DB) + def _convert_to_utc_datetime(self, iso_date): try: - dt_with_tz = waktu.fromisoformat(iso_date) - utc_dt = dt_with_tz.astimezone(pytz.utc) + 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) + utc_dt = waktu.astimezone(pytz.utc).replace(tzinfo=None) + return utc_dt + except Exception as e: + _logger.warning(f"[Biteship] Gagal konversi waktu UTC: {e}") + return False + # Untuk tampilan di API atau kebutuhan web (mengembalikan string waktu lokal) + def _convert_to_local_time(self, iso_date): + try: + 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) - + local_dt = waktu.astimezone(local_tz) return local_dt.strftime("%Y-%m-%d %H:%M:%S") except Exception as e: return str(e) @@ -1744,20 +1851,26 @@ class StockPicking(models.Model): def generate_eta_delivery(self): current_date = datetime.datetime.now() - prepare_days = 3 - start_date = self.driver_departure_date or self.create_date - - ead = self.sale_id.estimated_arrival_days or 0 - if not self.driver_departure_date: - ead += prepare_days - - ead_datetime = datetime.timedelta(days=ead) - fastest_eta = start_date + ead_datetime - if not self.driver_departure_date and fastest_eta < current_date: - fastest_eta = current_date + ead_datetime - - longest_days = 3 - longest_eta = fastest_eta + datetime.timedelta(days=longest_days) + days_start = self.sale_id.estimated_arrival_days_start or self.sale_id.estimated_arrival_days + days_end = self.sale_id.estimated_arrival_days or (self.sale_id.estimated_arrival_days + 3) + start_date = self.sale_id.create_date + datetime.timedelta(days=days_start) + end_date = self.sale_id.create_date + datetime.timedelta(days=days_end) + + + add_day_start = 0 + add_day_end = 0 + sale_order_delay = self.env['sale.order.delay'].search([('so_number', '=', self.sale_id.name)], limit=1) + if sale_order_delay: + if sale_order_delay.status == 'delayed': + add_day_start = sale_order_delay.days_delayed + add_day_end = sale_order_delay.days_delayed + elif sale_order_delay.status == 'early': + add_day_start = -abs(sale_order_delay.days_delayed) + add_day_end = -abs(sale_order_delay.days_delayed) + + fastest_eta = start_date +datetime.timedelta(days=add_day_start + add_day_start) + + longest_eta = end_date + datetime.timedelta(days=add_day_end) format_time = '%d %b %Y' format_time_fastest = '%d %b' if fastest_eta.year == longest_eta.year else format_time |
