summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIT Fixcomart <it@fixcomart.co.id>2025-07-05 08:05:10 +0000
committerIT Fixcomart <it@fixcomart.co.id>2025-07-05 08:05:10 +0000
commit9357a194e8b85343160f30b131c5802be954650d (patch)
tree47337a703aafdb631216f2316c4985987727d770
parent5b1b45d46e34c6724572b9b3182813e0bfdea0a3 (diff)
parent75fd0f87c6d1f8c3b92450f9826daa74550a5577 (diff)
Merged in webhook_v1 (pull request #348)
Webhook v1
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py50
-rw-r--r--indoteknik_custom/models/stock_picking.py218
-rw-r--r--indoteknik_custom/views/stock_picking.xml22
3 files changed, 215 insertions, 75 deletions
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index 1b247c8a..0525adc3 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -1,6 +1,6 @@
from .. import controller
from odoo import http
-from odoo.http import request
+from odoo.http import request, Response
from pytz import timezone
from datetime import datetime
import json
@@ -8,6 +8,7 @@ import logging
_logger = logging.getLogger(__name__)
+_logger = logging.getLogger(__name__)
class StockPicking(controller.Controller):
prefix = '/api/v1/'
@@ -181,21 +182,46 @@ class StockPicking(controller.Controller):
return {'status': 'error', 'message': str(e)}
def process_order_status(self, data):
- picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))],
- limit=1)
- if data.get('status') == 'picked':
- picking_model.write({'driver_departure_date': datetime.utcnow()})
- elif data.get('status') == 'delivered':
- picking_model.write({'driver_arrival_date': datetime.utcnow()})
+ picking = request.env['stock.picking'].sudo().search([
+ ('biteship_id', '=', data.get('order_id'))
+ ], limit=1)
+
+ if not picking:
+ _logger.warning(f"[Webhook] Tidak ditemukan picking untuk order_id {data.get('order_id')}")
+ return
+
+ status = data.get('status')
+ timestamp = data.get('updated_at') or datetime.utcnow().isoformat()
+
+ description = picking._get_biteship_status_description(status, {
+ "courier": {"company": data.get("courier_company", "")},
+ "destination": {"contact_name": picking.partner_id.name or ""}
+ })
+
+ # Tambahkan extra data dari webhook
+ extra_data = {
+ "courier_driver_name": data.get("courier_driver_name"),
+ "courier_driver_phone": data.get("courier_driver_phone"),
+ "courier_driver_plate_number": data.get("courier_driver_plate_number"),
+ "courier_link": data.get("courier_link"),
+ "order_price": data.get("order_price"),
+ "status": data.get("status"),
+ }
+
+ picking.log_biteship_event_from_webhook(status, timestamp, description, extra_data=extra_data)
+
+
def process_order_price(self, data):
picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))],
limit=1)
- order = request.env['sale.order'].sudo().search([('name', '=', picking_model.sale_id.name)], limit=1)
- if order:
- order.write({
- 'delivery_amt': data.get('price')
- })
+ if not picking_model:
+ _logger.warning(f"Tidak ditemukan picking untuk order_id {data.get('order_id')}")
+ return
+
+ picking_model.write({
+ 'biteship_shipping_price': data.get('price')
+ })
def process_order_waybill(self, data):
picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))],
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index c884f97e..e411aee6 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -271,15 +271,22 @@ class StockPicking(models.Model):
# Biteship Section
biteship_id = fields.Char(string="Biteship Respon ID")
- biteship_tracking_id = fields.Char(string="Biteship Trackcking ID")
+ biteship_tracking_id = fields.Char(string="Biteship Tracking ID")
biteship_waybill_id = fields.Char(string="Biteship Waybill ID")
+ biteship_driver_name = fields.Char('Biteship Driver Name')
+ biteship_driver_phone = fields.Char('Biteship Driver Phone')
+ biteship_driver_plate_number = fields.Char('Biteship Driver Plate Number')
+ biteship_courier_link = fields.Char('Biteship Courier Link')
+ biteship_shipping_status = fields.Char('Biteship Shipping Status', help="Status pengiriman dari Biteship")
+ biteship_shipping_price = fields.Monetary('Biteship Shipping Price', currency_field='currency_id', help="Harga pengiriman dari Biteship")
+ currency_id = fields.Many2one('res.currency', related='sale_id.currency_id', string='Currency', readonly=True)
final_seq = fields.Float(string='Remaining Time')
- 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')
+ shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id', help="Shipping Method yang digunakan di SO")
+ shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option', related='sale_id.shipping_option_id' , help="Shipping Option yang digunakan di SO")
select_shipping_option_so = fields.Selection([
('biteship', 'Biteship'),
('custom', 'Custom'),
- ], string='Shipping Type SO', related='sale_id.select_shipping_option')
+ ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO")
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)
@@ -1733,15 +1740,15 @@ class StockPicking(models.Model):
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("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>'
- }
+ # 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("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", [])
status = result.get("status", "")
@@ -1750,7 +1757,7 @@ class StockPicking(models.Model):
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": description[entry["status"]],
+ "description": self._get_biteship_status_description(entry["status"], result),
})
return {
@@ -1766,53 +1773,141 @@ class StockPicking(models.Model):
_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:
- 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
+ # ACTION GET TRACKING MANUAL BITESHIP
+ # 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 action_open_biteship_tracking(self):
+ self.ensure_one()
+ if not self.biteship_courier_link:
+ raise UserError("Biteship tracking link tidak tersedia.")
+ return {
+ 'type': 'ir.actions.act_url',
+ 'url': self.biteship_courier_link,
+ 'target': 'new',
+ }
- # 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)
+ def _get_biteship_status_description(self, status, data=None):
+ data = data or {}
+ courier = data.get("courier", {}).get("company", "")
+ contact_name = data.get("destination", {}).get("contact_name", "")
+
+ description_map = {
+ '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': f'Pesanan sudah di pick-up kurir {courier}',
+ 'dropping_off': 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli',
+ 'delivered': f'Pesanan telah sampai dan diterima oleh <span style="color:#DC2626;">{contact_name}</span>',
+ 'return_in_transit': 'Pesanan dalam perjalanan kembali ke pengirim',
+ 'on_hold': 'Pesanan ditahan sementara karena masalah pengiriman',
+ 'rejected': 'Pesanan ditolak, silakan hubungi Biteship',
+ 'courier_not_found': 'Pesanan dibatalkan karena tidak ada kurir tersedia',
+ 'returned': 'Pesanan berhasil dikembalikan',
+ 'disposed': 'Pesanan sudah dimusnahkan',
+ 'cancelled': 'Pesanan dibatalkan oleh sistem atau pengguna',
+ }
- if status == "delivered" and dt and not picking.driver_arrival_date:
- updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt)
+ return description_map.get(status, f"Status '{status}' diterima dari Biteship")
- # 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)
+ def log_biteship_event_from_webhook(self, status, timestamp, description, extra_data=None):
+ self.ensure_one()
+ updated_fields = {}
+
+ try:
+ dt = self._convert_to_utc_datetime(timestamp)
+ except Exception as e:
+ _logger.warning(f"[Webhook] Gagal konversi waktu: {e}")
+ dt = datetime.utcnow()
+
+ if status == "picked" and not self.driver_departure_date:
+ updated_fields["driver_departure_date"] = fields.Datetime.to_string(dt)
+ if status == "delivered" and not self.driver_arrival_date:
+ updated_fields["driver_arrival_date"] = fields.Datetime.to_string(dt)
+
+ shipping_status = self._map_status_biteship(status)
+ if shipping_status and self.shipping_status != shipping_status:
+ updated_fields["shipping_status"] = shipping_status
+
+ if extra_data:
+ if extra_data.get("courier_driver_name"):
+ updated_fields["biteship_driver_name"] = extra_data["courier_driver_name"]
+ if extra_data.get("courier_driver_phone"):
+ updated_fields["biteship_driver_phone"] = extra_data["courier_driver_phone"]
+ if extra_data.get("courier_driver_plate_number"):
+ updated_fields["biteship_driver_plate_number"] = extra_data["courier_driver_plate_number"]
+ if extra_data.get("courier_link"):
+ updated_fields["biteship_courier_link"] = extra_data["courier_link"]
+ if extra_data.get("order_price"):
+ updated_fields["biteship_shipping_price"] = extra_data["order_price"]
+ if extra_data.get("status"):
+ updated_fields["biteship_shipping_status"] = extra_data["status"]
+
+ try:
+ dt_parsed = parser.parse(timestamp)
+ if dt_parsed.tzinfo is None:
+ dt_parsed = dt_parsed.replace(tzinfo=pytz.utc)
+ dt_local = dt_parsed.astimezone(pytz.timezone("Asia/Jakarta"))
+ except Exception:
+ dt_local = dt.astimezone(pytz.timezone("Asia/Jakarta"))
+
+ desc_clean = ' '.join(description.strip().split())
+ log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}:<br/>{desc_clean}"
+
+ if not self._has_existing_log(log_line):
+ self.with_user(15172).message_post(body=log_line) # user biteship test
+ # self.with_user(15710).message_post(body=log_line) # user biteship live
+
+ if updated_fields:
+ self.write(updated_fields)
+ _logger.info(f"[Webhook] Updated fields on picking {self.name}: {updated_fields}")
- if updated_fields:
- picking.write(updated_fields)
def _has_existing_log(self, log_line):
self.ensure_one()
@@ -1862,10 +1957,15 @@ class StockPicking(models.Model):
"allocated": "pending",
"picking_up": "pending",
"picked": "shipment",
- "cancelled": "cancelled",
- "on_hold": "on_hold",
"dropping_off": "shipment",
- "delivered": "completed"
+ "delivered": "completed",
+ "return_in_transit": "returning",
+ "on_hold": "on_hold",
+ "rejected": "cancelled",
+ "courier_not_found": "cancelled",
+ "returned": "returned",
+ "disposed": "disposed",
+ "cancelled": "cancelled"
}
return status_mapping.get(status, "Hubungi Admin")
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index c088e00c..5e33e4c7 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -64,12 +64,12 @@
string="Biteship"
type="object"
/>
- <button name="action_sync_biteship_tracking"
+ <!-- <button name="action_sync_biteship_tracking"
type="object"
string="Lacak dari Biteship"
class="btn-primary"
attrs="{'invisible': [('biteship_id', '=', False)]}"
- />
+ /> -->
<button name="track_envio_shipment"
string="Tracking Envio"
type="object"
@@ -192,19 +192,33 @@
<field name="note_logistic"/>
<field name="note_info"/>
<field name="responsible" />
- <field name="carrier_id"/>
+ <field name="carrier_id" attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}" />
<field name="biteship_id" invisible="1"/>
<field name="out_code" attrs="{'invisible': [['out_code', '=', False]]}"/>
<field name="picking_code" attrs="{'invisible': [['picking_code', '=', False]]}"/>
<field name="picking_code" string="Picking code (akan digenerate ketika sudah di-validate)" attrs="{'invisible': [['picking_code', '!=', False]]}"/>
<field name="driver_departure_date" attrs="{'readonly':[('invoice_status', '=', 'invoiced')]}"/>
<field name="driver_arrival_date"/>
- <field name="delivery_tracking_no"/>
+ <field name="delivery_tracking_no" attrs="{'invisible': [('select_shipping_option_so', '=', 'biteship')]}"/>
<field name="driver_id"/>
<field name='sj_return_date'/>
<field name="sj_documentation" widget="image" />
<field name="paket_documentation" widget="image" />
</group>
+ <!-- Biteship Group -->
+ <group attrs="{'invisible': [('select_shipping_option_so', '!=', 'biteship')]}">
+ <field name="delivery_tracking_no" />
+ <field name="shipping_method_so_id"/>
+ <field name="shipping_option_so_id"/>
+ <field name="biteship_shipping_price" readonly="1"/>
+ <field name="currency_id" invisible="1"/>
+ <field name="biteship_shipping_status" readonly="1"/>
+ <field name="biteship_driver_name" readonly="1"/>
+ <field name="biteship_driver_phone" readonly="1"/>
+ <field name="biteship_driver_plate_number" readonly="1"/>
+ <button name="action_open_biteship_tracking" string="Visit Biteship Tracking" type="object"/>
+ </group>
+
<group attrs="{'invisible': [('carrier_id', '!=', 151)]}">
<field name="envio_id" invisible="1"/>
<field name="envio_code"/>