summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/stock_picking.py
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2025-07-28 15:09:55 +0700
committerit-fixcomart <it@fixcomart.co.id>2025-07-28 15:09:55 +0700
commitd15ce4e186e2b77f01e8dfd03886298cc733d4c1 (patch)
tree1b32a4c29c4fcea85070fcecb5b77a7d55d30029 /indoteknik_custom/models/stock_picking.py
parentdeba962d7368a5c4e30441b5a640102608e3dde6 (diff)
parent36a53535dbdc5777266fd9276b4c557259dab6be (diff)
<hafid> merging odoo-backup
Diffstat (limited to 'indoteknik_custom/models/stock_picking.py')
-rw-r--r--indoteknik_custom/models/stock_picking.py678
1 files changed, 487 insertions, 191 deletions
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 0fcb7ca1..3e152f10 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -19,15 +19,19 @@ import re
_logger = logging.getLogger(__name__)
_biteship_url = "https://api.biteship.com/v1"
-_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
+biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
-# _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
+# biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
class StockPicking(models.Model):
_inherit = 'stock.picking'
_order = 'final_seq ASC'
+ tukar_guling_id = fields.Many2one(
+ 'tukar.guling',
+ string='Tukar Guling Reference'
+ )
konfirm_koli_lines = fields.One2many('konfirm.koli', 'picking_id', string='Konfirm Koli', auto_join=True,
copy=False)
scan_koli_lines = fields.One2many('scan.koli', 'picking_id', string='Scan Koli', auto_join=True, copy=False)
@@ -121,7 +125,7 @@ class StockPicking(models.Model):
waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill')
purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id',
string="Purchase Representative")
- carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method')
+ carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3)
shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status")
date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya', copy=False)
status_printed = fields.Selection([
@@ -170,6 +174,10 @@ class StockPicking(models.Model):
area_name = fields.Char(string="Area", compute="_compute_area_name")
+ # def _get_biteship_api_key(self):
+ # # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_test')
+ # return self.env['ir.config_parameter'].sudo().get_param('biteship.api_key_live')
+
@api.depends('real_shipping_id.kecamatan_id', 'real_shipping_id.kota_id')
def _compute_area_name(self):
for record in self:
@@ -269,14 +277,27 @@ 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")
- # 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)
+ 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_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method', related='sale_id.carrier_id',
+ help="Shipping Method yang digunakan di SO", tracking=3)
+ shipping_option_so_id = fields.Many2one('shipping.option', string='Shipping Option',
+ related='sale_id.shipping_option_id',
+ help="Shipping Option yang digunakan di SO", tracking=3)
+ select_shipping_option_so = fields.Selection([
+ ('biteship', 'Biteship'),
+ ('custom', 'Custom'),
+ ], string='Shipping Type', related='sale_id.select_shipping_option', help="Shipping Type yang digunakan di SO",
+ tracking=3)
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')
@@ -288,8 +309,8 @@ class StockPicking(models.Model):
self.ensure_one()
if not self.name or not self.origin:
return False
- return f"{self.name} {self.origin}"
-
+ return f"{self.name}"
+
def _download_pod_photo(self, url):
"""Mengunduh foto POD dari URL"""
try:
@@ -298,48 +319,53 @@ class StockPicking(models.Model):
return base64.b64encode(response.content)
except Exception as e:
raise UserError(f"Gagal mengunduh foto POD: {str(e)}")
-
+
def _parse_datetime(self, dt_str):
"""Parse datetime string dari format KGX"""
try:
from datetime import datetime
- # Hilangkan timezone jika ada masalah parsing
+
+ if not dt_str:
+ return False
+
if '+' in dt_str:
dt_str = dt_str.split('+')[0]
+
return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S')
except ValueError:
return False
-
- def action_get_kgx_pod(self):
+
+
+ def action_get_kgx_pod(self, shipment=False):
self.ensure_one()
-
- awb_number = self._get_kgx_awb_number()
+
+ awb_number = shipment or self._get_kgx_awb_number()
if not awb_number:
raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin")
-
+
url = "https://kgx.co.id/get_detail_awb"
headers = {'Content-Type': 'application/json'}
- payload = {"params" : {'awb_number': awb_number}}
-
+ payload = {"params": {'awb_number': awb_number}}
+
try:
response = requests.post(url, headers=headers, data=json.dumps(payload))
response.raise_for_status()
data = response.json()
-
+
if data.get('result', {}).get('data', []):
pod_data = data['result']['data'][0].get('connote_pod', {})
photo_url = pod_data.get('photo')
-
+
self.kgx_pod_photo_url = photo_url
self.kgx_pod_signature = pod_data.get('signature')
self.kgx_pod_receiver = pod_data.get('receiver')
self.kgx_pod_receive_time = self._parse_datetime(pod_data.get('timeReceive'))
self.driver_arrival_date = self._parse_datetime(pod_data.get('timeReceive'))
-
+
return data
else:
raise UserError(f"Tidak ditemukan data untuk AWB: {awb_number}")
-
+
except requests.exceptions.RequestException as e:
raise UserError(f"Gagal mengambil data POD: {str(e)}")
@@ -529,13 +555,7 @@ class StockPicking(models.Model):
record.kgx_pod_photo = "No image available."
def action_fetch_lalamove_order(self):
- pickings = self.env['stock.picking'].search([
- ('picking_type_code', '=', 'outgoing'),
- ('state', '=', 'done'),
- ('carrier_id', '=', 9),
- ('lalamove_order_id', '!=', False)
- ])
- for picking in pickings:
+ for picking in self:
try:
order_id = picking.lalamove_order_id
apikey = self.env['ir.config_parameter'].sudo().get_param('lalamove.apikey')
@@ -584,6 +604,7 @@ class StockPicking(models.Model):
self.lalamove_phone = phone
self.lalamove_status = pod.get("status")
self.lalamove_delivered_at = delivered_at_dt
+ self.driver_arrival_date = delivered_at_dt
return data
raise UserError("No delivered data found in Lalamove response.")
@@ -619,18 +640,19 @@ class StockPicking(models.Model):
except ValueError:
raise UserError(f"Format waktu tidak sesuai: {date_str}")
- def track_envio_shipment(self):
+ def track_envio_shipment(self, shipment=False):
pickings = self.env['stock.picking'].search([
('picking_type_code', '=', 'outgoing'),
('state', '=', 'done'),
('carrier_id', '=', 151)
])
- for picking in pickings:
+ for picking in self:
+ name = shipment or picking.name
if not picking.name:
raise UserError("Name pada stock.picking tidak ditemukan.")
# API URL dan headers
- url = f"https://api.envio.co.id/v1/tracking/distribution?code={picking.name}"
+ url = f"https://api.envio.co.id/v1/tracking/distribution?code={name}"
headers = {
'Authorization': 'Bearer JZ0Seh6qpYJAC3CJHdhF7sPqv8B/uSSfZe1VX5BL?vPYdo',
'Content-Type': 'application/json',
@@ -686,46 +708,56 @@ 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}")
- # 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 [{
- "name": line.product_id.name,
- "description": line.name,
- "value": line.price_unit,
- "quantity": line.product_uom_qty,
- "weight": line.weight
- } for line in lines]
-
- # Items untuk pengiriman standard
- items_data_standard = build_items_data(products)
+ if self.sale_id.select_shipping_option == 'custom':
+ raise UserError(
+ "Shipping Option pada Sales Order ini adalah *Custom*. Tidak dapat dikirim melalui Biteship.")
+
+ 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
- # 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([
+ 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', '=', move_line.product_id.id)
+ ('product_id', '=', ml.product_id.id)
], limit=1)
- 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
- })
+ value = line.price_unit if line else 0
+ description = line.name if line else product.name
+
+ items.append({
+ "name": product.name,
+ "description": description,
+ "value": value,
+ "quantity": ml.qty_done,
+ "weight": int(weight * 1000),
+ })
+
+ if not items:
+ raise UserError("Pengiriman tidak dapat dilakukan karena tidak ada barang yang divalidasi (qty_done = 0).")
+
+ 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,
@@ -733,41 +765,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,
+ }
- api_key = _biteship_api_key
+ _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()
@@ -775,17 +805,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:
@@ -917,6 +957,9 @@ class StockPicking(models.Model):
pending_section = None
# Invoice values.
invoice_vals = order._prepare_invoice()
+ invoice_date = self.date_done
+ invoice_vals['date'] = invoice_date
+ invoice_vals['invoice_date'] = invoice_date
# Invoice line values (keep only necessary sections).
for line in self.move_ids_without_package:
po_line = self.env['purchase.order.line'].search(
@@ -1055,38 +1098,40 @@ class StockPicking(models.Model):
self.approval_receipt_status = 'pengajuan1'
def ask_return_approval(self):
- for pick in self:
- if self.env.user.is_accounting:
- pick.approval_return_status = 'approved'
- continue
- else:
- pick.approval_return_status = 'pengajuan1'
-
- action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard')
-
- if self.picking_type_code == 'outgoing':
- if self.env.user.id in [3988, 3401, 20] or (
- self.env.user.has_group(
- 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin
- ):
- action['context'] = {'picking_ids': [x.id for x in self]}
- return action
- elif not self.env.user.has_group(
- 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin:
- raise UserError('Harus Purchasing yang Ask Return')
- else:
- raise UserError('Harus Sales Admin yang Ask Return')
-
- elif self.picking_type_code == 'incoming':
- if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or (
- self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin
- ):
- action['context'] = {'picking_ids': [x.id for x in self]}
- return action
- elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin:
- raise UserError('Harus Sales Admin yang Ask Return')
- else:
- raise UserError('Harus Purchasing yang Ask Return')
+ pass
+ raise UserError("Bisa langsung Validate")
+ # for pick in self:
+ # if self.env.user.is_accounting:
+ # pick.approval_return_status = 'approved'
+ # continue
+ # else:
+ # pick.approval_return_status = 'pengajuan1'
+ #
+ # action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard')
+ #
+ # if self.picking_type_code == 'outgoing':
+ # if self.env.user.id in [3988, 3401, 20] or (
+ # self.env.user.has_group(
+ # 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin
+ # ):
+ # action['context'] = {'picking_ids': [x.id for x in self]}
+ # return action
+ # elif not self.env.user.has_group(
+ # 'indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin:
+ # raise UserError('Harus Purchasing yang Ask Return')
+ # else:
+ # raise UserError('Harus Sales Admin yang Ask Return')
+ #
+ # elif self.picking_type_code == 'incoming':
+ # if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or (
+ # self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin
+ # ):
+ # action['context'] = {'picking_ids': [x.id for x in self]}
+ # return action
+ # elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin:
+ # raise UserError('Harus Sales Admin yang Ask Return')
+ # else:
+ # raise UserError('Harus Purchasing yang Ask Return')
def calculate_line_no(self):
@@ -1181,6 +1226,10 @@ class StockPicking(models.Model):
def button_validate(self):
self.check_invoice_date()
+ _logger.info("Kode Picking: %s", self.picking_type_id.code)
+ _logger.info("Group ID: %s", self.group_id)
+ _logger.info("Group ID ID: %s", self.group_id.id if self.group_id else None)
+ _logger.info("Is Internal Use: %s", self.is_internal_use)
threshold_datetime = waktu(2025, 4, 11, 6, 26)
group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])])
@@ -1269,6 +1318,19 @@ class StockPicking(models.Model):
current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
self.date_reserved = current_time
+ # Validate Qty Demand Can't higher than Qty Product
+ for move_line in self.move_line_ids_without_package:
+ purchase_line = move_line.move_id.purchase_line_id
+ if purchase_line:
+ if purchase_line.product_uom_qty < move_line.product_uom_qty:
+ raise UserError(
+ _("Quantity demand (%s) tidak bisa lebih besar dari qty product (%s) untuk produk %s") % (
+ move_line.product_uom_qty,
+ purchase_line.product_uom_qty,
+ move_line.product_id.display_name
+ )
+ )
+
self.validation_minus_onhand_quantity()
self.responsible = self.env.user.id
# self.send_koli_to_so()
@@ -1280,7 +1342,6 @@ class StockPicking(models.Model):
self.final_seq = 0
self.set_picking_code_out()
self.send_koli_to_so()
-
if (self.state_reserve == 'done' and self.picking_type_code == 'internal' and 'BU/PICK/' in self.name
and self.linked_manual_bu_out):
if not self.linked_manual_bu_out.date_reserved:
@@ -1317,26 +1378,40 @@ class StockPicking(models.Model):
'target': 'new',
}
self.send_mail_bills()
+ if 'BU/PUT' in self.name:
+ self.automatic_reserve_product()
return res
+ def automatic_reserve_product(self):
+ if self.state == 'done':
+ po = self.env['purchase.order'].search([
+ ('name', '=', self.group_id.name)
+ ])
+
+ for line in po.order_sales_match_line:
+ if not line.bu_pick:
+ continue
+ line.bu_pick.action_assign()
+
def check_invoice_date(self):
for picking in self:
if picking.picking_type_code != 'outgoing' or 'BU/OUT/' not in picking.name or picking.partner_id.id == 96868:
continue
invoice = self.env['account.move'].search(
- [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']), ('move_type', '=', 'out_invoice')], limit=1)
+ [('sale_id', '=', picking.sale_id.id), ('state', 'not in', ['draft', 'cancel']),
+ ('move_type', '=', 'out_invoice')], limit=1)
if not invoice:
continue
-
+
if not picking.so_lama and invoice and (not picking.date_doc_kirim or not invoice.invoice_date):
raise UserError("Tanggal Kirim atau Tanggal Invoice belum diisi!")
picking_date = fields.Date.to_date(picking.date_doc_kirim)
invoice_date = fields.Date.to_date(invoice.invoice_date)
- if picking_date != invoice_date and picking.update_date_doc_kirim_add:
+ if picking_date != invoice_date and picking.update_date_doc_kirim_add and not picking.so_lama:
raise UserError("Tanggal Kirim (%s) tidak sesuai dengan Tanggal Invoice (%s)!" % (
picking_date.strftime('%d-%m-%Y'),
invoice_date.strftime('%d-%m-%Y')
@@ -1515,25 +1590,25 @@ class StockPicking(models.Model):
new_picking.state_packing = 'packing_done'
self._use_faktur(vals)
self.sync_sale_line(vals)
- for picking in self:
- # Periksa apakah kondisi terpenuhi saat data diubah
- if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and
- vals.get('location_dest_id', picking.location_dest_id.id) == 58):
- if 'name' in vals or picking.name.startswith('BU/IN/'):
- name_to_modify = vals.get('name', picking.name)
- if name_to_modify.startswith('BU/IN/'):
- vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1)
-
- if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and
- vals.get('location_id', picking.location_id.id) == 58):
- name_to_modify = vals.get('name', picking.name)
- if name_to_modify.startswith('BU/INT'):
- new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1)
- # Periksa apakah nama sudah ada
- if self.env['stock.picking'].search_count(
- [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0:
- new_name = f"{new_name}-DUP"
- vals['name'] = new_name
+ # for picking in self:
+ # # Periksa apakah kondisi terpenuhi saat data diubah
+ # if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and
+ # vals.get('location_dest_id', picking.location_dest_id.id) == 58):
+ # if 'name' in vals or picking.name.startswith('BU/IN/'):
+ # name_to_modify = vals.get('name', picking.name)
+ # if name_to_modify.startswith('BU/IN/'):
+ # vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1)
+
+ # if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and
+ # vals.get('location_id', picking.location_id.id) == 58):
+ # name_to_modify = vals.get('name', picking.name)
+ # if name_to_modify.startswith('BU/INT'):
+ # new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1)
+ # # Periksa apakah nama sudah ada
+ # if self.env['stock.picking'].search_count(
+ # [('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0:
+ # new_name = f"{new_name}-DUP"
+ # vals['name'] = new_name
return super(StockPicking, self).write(vals)
def _use_faktur(self, vals):
@@ -1614,27 +1689,51 @@ class StockPicking(models.Model):
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)
+
+ product_shipped = []
+ for move_line in self.move_line_ids_without_package:
+ if move_line.qty_done > 0:
+ product_shipped.append({
+ 'name': move_line.product_id.name,
+ 'qty': move_line.qty_done
+ })
+
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,
+ 'products': product_shipped
}
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", [])
@@ -1656,7 +1755,7 @@ class StockPicking(models.Model):
return response
def get_manifest_biteship(self):
- api_key = _biteship_api_key
+ api_key = biteship_api_key
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
@@ -1669,16 +1768,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("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", "")
- }
+ # 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", "")
@@ -1687,7 +1785,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 {
@@ -1695,19 +1793,205 @@ class StockPicking(models.Model):
"delivered": status
}
- return manifests
+ 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)}
- def _convert_to_local_time(self, iso_date):
+ # 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',
+ }
+
+ 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',
+ }
+
+ return description_map.get(status, f"Status '{status}' diterima dari Biteship")
+
+ def log_biteship_event_from_webhook(self, status, timestamp, description, extra_data=None):
+ self.ensure_one()
+ updated_fields = {}
+
try:
- dt_with_tz = waktu.fromisoformat(iso_date)
- utc_dt = dt_with_tz.astimezone(pytz.utc)
+ dt = self._convert_to_utc_datetime(timestamp)
+ except Exception as e:
+ _logger.warning(f"[Webhook] Gagal konversi waktu: {e}")
+ dt = datetime.utcnow()
+
+ # Penanganan status pengiriman
+ 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
+
+ # Penanganan extra data dari webhook
+ if extra_data:
+ # Informasi kurir
+ 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"]
+ # Informasi harga
+ if extra_data.get("order_price"):
+ updated_fields["biteship_shipping_price"] = extra_data["order_price"]
+ # Status mentah dari Biteship
+ if extra_data.get("status"):
+ updated_fields["biteship_shipping_status"] = extra_data["status"]
+
+ # Tambahan untuk handle order.waybill_id
+ if extra_data.get("tracking_id"):
+ updated_fields["biteship_tracking_id"] = extra_data["tracking_id"]
+ updated_fields["delivery_tracking_no"] = extra_data["tracking_id"]
+ if extra_data.get("waybill_id"):
+ updated_fields["biteship_waybill_id"] = extra_data["waybill_id"]
+
+ # Konversi waktu lokal untuk log
+ 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"))
+
+ # Format pesan log
+ desc_clean = ' '.join(description.strip().split())
+ log_line = f"[TRACKING] {status} - {dt_local.strftime('%d %b %Y %H:%M')}:<br/>{desc_clean}"
+
+ # Hindari log duplikat
+ if not self._has_existing_log(log_line):
+ biteship_user = self.env['res.users'].sudo().browse(15710) # ID live
+ # biteship_user = self.env['res.users'].sudo().browse(15710) # ID user (cek di db)
+ self.sudo().message_post(
+ body=log_line,
+ author_id=biteship_user.partner_id.id
+ )
- local_tz = pytz.timezone("Asia/Jakarta")
- local_dt = utc_dt.astimezone(local_tz)
+ # Update field-field terkait
+ if updated_fields:
+ self.write(updated_fields)
+ _logger.info(f"[Webhook] Updated fields on picking {self.name}: {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:
+ 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 = waktu.astimezone(local_tz)
return local_dt.strftime("%Y-%m-%d %H:%M:%S")
except Exception as e:
return str(e)
@@ -1719,29 +2003,39 @@ 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")
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
@@ -2229,6 +2523,8 @@ class KonfirmKoli(models.Model):
copy=False,
)
pick_id = fields.Many2one('stock.picking', string='Pick')
+ product_id = fields.Many2one('product.product', string='Product')
+ qty_done = fields.Float(string='Qty Done')
@api.constrains('pick_id')
def _check_duplicate_pick_id(self):
@@ -2254,4 +2550,4 @@ class WarningModalWizard(models.TransientModel):
def action_continue(self):
if self.picking_id:
return self.picking_id.with_context(skip_koli_check=True).button_validate()
- return {'type': 'ir.actions.act_window_close'}
+ return {'type': 'ir.actions.act_window_close'} \ No newline at end of file