summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/stock_picking.py
diff options
context:
space:
mode:
authorIndoteknik . <it@fixcomart.co.id>2025-06-09 15:11:05 +0700
committerIndoteknik . <it@fixcomart.co.id>2025-06-09 15:11:05 +0700
commit1bc03c1482a664ffcd58f19022a40e65e21774c6 (patch)
treef491d00cd1d371c8fd76ad25e014ac8d662c3d02 /indoteknik_custom/models/stock_picking.py
parent1bd3a91889f8616d7042c0d15315c2f25c974ed3 (diff)
parentf43855aa55265794c7774af79089258e830b0df4 (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.py319
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