summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--indoteknik_api/controllers/api_v1/product.py90
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py42
-rwxr-xr-xindoteknik_custom/__manifest__.py2
-rwxr-xr-xindoteknik_custom/models/__init__.py2
-rw-r--r--indoteknik_custom/models/product_sla.py133
-rwxr-xr-xindoteknik_custom/models/product_template.py12
-rw-r--r--indoteknik_custom/models/public_holiday.py11
-rwxr-xr-xindoteknik_custom/models/sale_order.py75
-rw-r--r--indoteknik_custom/models/stock_picking.py166
-rw-r--r--indoteknik_custom/models/vendor_sla.py95
-rwxr-xr-xindoteknik_custom/security/ir.model.access.csv5
-rw-r--r--indoteknik_custom/views/product_product.xml8
-rw-r--r--indoteknik_custom/views/product_sla.xml7
-rw-r--r--indoteknik_custom/views/public_holiday.xml55
-rwxr-xr-xindoteknik_custom/views/sale_order.xml1
-rw-r--r--indoteknik_custom/views/stock_picking.xml5
-rw-r--r--indoteknik_custom/views/vendor_sla.xml42
-rwxr-xr-xindoteknik_custom/views/x_banner_category.xml2
18 files changed, 655 insertions, 98 deletions
diff --git a/indoteknik_api/controllers/api_v1/product.py b/indoteknik_api/controllers/api_v1/product.py
index 32362582..93ef305c 100644
--- a/indoteknik_api/controllers/api_v1/product.py
+++ b/indoteknik_api/controllers/api_v1/product.py
@@ -1,6 +1,6 @@
from .. import controller
from odoo import http
-from odoo.http import request
+from odoo.http import request, Response
from datetime import datetime, timedelta
import ast
import logging
@@ -9,6 +9,17 @@ import json
_logger = logging.getLogger(__name__)
+def get_days_until_next_business_day(start_date=None, *args, **kwargs):
+ today = start_date or datetime.today().date()
+ offset = 0 # Counter jumlah hari yang ditambahkan
+
+ while today.weekday() >= 5 :
+ today += timedelta(days=1)
+ offset += 1
+
+ return offset
+
+
class Product(controller.Controller):
prefix = '/api/v1/'
@@ -33,9 +44,64 @@ class Product(controller.Controller):
categories.reverse()
return self.response(categories, headers=[('Cache-Control', 'max-age=3600, public')])
-
- @http.route(prefix + 'product_variant/<id>/stock', auth='public', methods=['GET', 'OPTIONS'])
+
+ @http.route(prefix + 'product/variants/sla', auth='public', methods=['GET', 'OPTIONS'])
@controller.Controller.must_authorized()
+ def get_product_template_sla_by_id(self, **kwargs):
+ body_params = kwargs.get('ids')
+
+ if not body_params:
+ return self.response('Failed', code=400, description='id is required')
+
+ ids = [int(id.strip()) for id in body_params.split(',') if id.strip().isdigit()]
+
+ sla_duration = 0
+ sla_unit = 'Hari'
+ include_instant = True
+ products = request.env['product.product'].search([('id', 'in', ids)])
+ if len(products) < 1:
+ return self.response(
+ 'Failed',
+ code=400,
+ description='Produk Tidak Di Temukan.'
+ )
+
+ product_slas = request.env['product.sla'].search([('product_variant_id', 'in', ids)])
+ if len(product_slas) < 1:
+ return self.response(
+ 'Failed',
+ code=400,
+ description='SLA Tidak Di Temukan.'
+ )
+
+ # Mapping SLA untuk mempermudah lookup
+ sla_map = {sla.product_variant_id.id: sla for sla in product_slas}
+
+ for product in products:
+ product_sla = sla_map.get(product.id)
+ if product_sla:
+ sla_duration = max(sla_duration, int(product_sla.sla))
+ sla_unit = product_sla.sla_vendor_id.unit
+ if product.qty_free_bandengan < 1 :
+ if product_sla.sla_vendor_id.unit != 'jam':
+ include_instant = False
+ break
+
+ start_date = datetime.today().date()
+ additional_days = get_days_until_next_business_day(start_date)
+
+ # Jika semua loop selesai tanpa include_instant menjadi False
+ return self.response({
+ 'include_instant': include_instant,
+ 'sla_duration': sla_duration,
+ 'sla_additional_days': additional_days,
+ 'sla_total' : int(sla_duration) + int(additional_days),
+ 'sla_unit': 'Hari' if additional_days > 0 else sla_unit
+ }
+ )
+
+ @http.route(prefix + 'product_variant/<id>/stock', auth='public', methods=['GET', 'OPTIONS'])
+ @controller.Controller.must_authorized()
def get_product_template_stock_by_id(self, **kw):
id = int(kw.get('id'))
date_7_days_ago = datetime.now() - timedelta(days=7)
@@ -49,10 +115,11 @@ class Product(controller.Controller):
], limit=1)
qty_available = product.qty_free_bandengan
-
- if qty_available < 0:
- qty_available = 0
-
+
+
+ if qty_available < 1 :
+ qty_available = 0
+
qty = 0
sla_date = '-'
@@ -74,24 +141,25 @@ class Product(controller.Controller):
if qty_available > 0:
qty = qty_available + total_adem + total_excell
+ sla_date = product_sla.sla or 1
elif qty_altama > 0 or qty_vendor > 0:
qty = total_adem if qty_altama > 0 else total_excell
- sla_date = '2-4 Hari'
+ sla_date = product_sla.sla
else:
- sla_date = '3-7 Hari'
+ sla_date = product_sla.sla
except:
print('error')
else:
if qty_available > 0:
qty = qty_available
- sla_date = product_sla.sla or '-'
+ sla_date = product_sla.sla or 'Indent'
elif qty_vendor > 0:
qty = total_excell
sla_date = '2-4 Hari'
data = {
'qty': qty,
- 'sla_date': sla_date,
+ 'sla_date': sla_date
}
return self.response(data, headers=[('Cache-Control', 'max-age=600, private')])
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index 2e0c4ad0..9da9575b 100644
--- a/indoteknik_api/controllers/api_v1/stock_picking.py
+++ b/indoteknik_api/controllers/api_v1/stock_picking.py
@@ -101,7 +101,7 @@ class StockPicking(controller.Controller):
picking = picking_model.browse(id)
if not picking:
return self.response(None)
-
+ hostori = picking.get_tracking_detail()
return self.response(picking.get_tracking_detail())
@http.route(prefix + 'stock-picking/<id>/tracking', auth='public', method=['GET', 'OPTIONS'])
@@ -136,4 +136,42 @@ class StockPicking(controller.Controller):
return self.response({
'name': picking_data.name
- }) \ No newline at end of file
+ })
+
+ @http.route(prefix + 'webhook/biteship', type='json', auth='public', methods=['POST'], csrf=False)
+ def udpate_status_from_bitehsip(self, **kw):
+ try:
+ data = request.jsonrequest # Ambil data JSON dari request
+ event = data.get('event')
+
+ # Log Webhook ke Model Odoo
+ request.env['webhook.logs'].sudo().create({
+ 'event': event,
+ 'order_id': data.get('order_id'),
+ 'courier_tracking_id': data.get('courier_tracking_id'),
+ 'courier_waybill_id': data.get('courier_waybill_id'),
+ 'status': data.get('status'),
+ 'order_price': data.get('price'),
+ 'cash_on_delivery_fee': data.get('cash_on_delivery_fee'),
+ 'proof_of_delivery_fee': data.get('proof_of_delivery_fee'),
+ 'shippment_fee': data.get('shippment_fee'),
+ })
+
+ # Handle Event Berdasarkan Jenisnya
+ if event == "order.status":
+ self.process_order_status(data)
+ elif event == "order.price":
+ self.process_order_price(data)
+ elif event == "order.waybill_id":
+ self.process_order_waybill(data)
+
+ return {'success': True, 'message': f'Webhook {event} received'}
+ except Exception as e:
+ return {'success': False, 'message': str(e)}
+
+ def process_order_status(self, data):
+ """Update status order di Odoo"""
+ order = request.env['sale.order'].sudo().search([('id', '=', data.get('order_id'))], limit=1)
+ if order:
+ order.write({'state': data.get('status')})
+ \ No newline at end of file
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index e74efb95..38c60f50 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -160,7 +160,9 @@
'report/report_invoice.xml',
'report/report_picking.xml',
'report/report_sale_order.xml',
+ 'views/vendor_sla.xml',
'views/coretax_faktur.xml',
+ 'views/public_holiday.xml',
],
'demo': [],
'css': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index dea3eeea..ed9d69be 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -138,8 +138,10 @@ from . import user_pengajuan_tempo
from . import approval_retur_picking
from . import va_multi_approve
from . import va_multi_reject
+from . import vendor_sla
from . import stock_immediate_transfer
from . import coretax_fatur
+from . import public_holiday
from . import ir_actions_report
from . import barcoding_product
from . import account_payment_register
diff --git a/indoteknik_custom/models/product_sla.py b/indoteknik_custom/models/product_sla.py
index 2e663d30..04ad2ffd 100644
--- a/indoteknik_custom/models/product_sla.py
+++ b/indoteknik_custom/models/product_sla.py
@@ -12,73 +12,110 @@ class ProductSla(models.Model):
_rec_name = 'product_variant_id'
product_variant_id = fields.Many2one('product.product',string='Product')
- avg_leadtime = fields.Char(string='AVG Leadtime', readonly=True)
- leadtime = fields.Char(string='Leadtime', readonly=True)
+ sla_vendor_id = fields.Many2one('vendor.sla',string='Vendor', readonly=True)
+ sla_vendor_duration = fields.Char(string='AVG Leadtime', related='sla_vendor_id.duration_unit')
+ sla_logistic = fields.Char(string='SLA Logistic', readonly=True)
+ sla_logistic_unit = fields.Selection(
+ [('jam', 'Jam'),('hari', 'Hari')],
+ string="SLA Logistic Time"
+ )
+ sla_logistic_duration_unit = fields.Char(string="SLA Logistic Duration (Unit)")
sla = fields.Char(string='SLA', readonly=True)
version = fields.Integer(string="Version", compute="_compute_version")
def _compute_version(self):
for sla in self:
sla.version = sla.product_variant_id.sla_version
+
+
- def generate_product_variant_id_sla(self, limit=5000):
- # Filter produk non-Altama
+ def generate_product_variant_id_sla(self, limit=500):
+ offset = 0
+ # while True:
products = self.env['product.product'].search([
- ('x_manufacture', 'not in', [10, 122, 89]),
- ('location_id', '=', 57),
- ('stock_move_ids', '!=', False),
- ], order='sla_version asc', limit=limit)
+ ('active', '=', True),
+ ('sale_ok', '=', True),
+ ], order='sla_version asc', limit=limit, offset=offset)
+
+ # if not products:
+ # break
- i = 1
for product in products:
- _logger.info(f'Product SLA: {i}/{len(products)}')
- i += 1
- product.sla_version += 1
+ _logger.info(f'Memproses SLA untuk produk ID {product.id}, versi {product.sla_version}')
product_sla = self.search([('product_variant_id', '=', product.id)], limit=1)
if not product_sla:
- product_sla = self.env['product.sla'].create({
- 'product_variant_id': product.id,
- })
-
+ product_sla = self.create({'product_variant_id': product.id})
+
product_sla.generate_product_sla()
+ # Tandai produk sebagai sudah diproses
+ product.sla_version += 1
+
+ offset += limit
+
+
def generate_product_sla(self):
- self.avg_leadtime = '-'
- self.sla = '-'
+ # self.sla_logistic = '-'
+ # self.sla_logistic_duration_unit = '-'
+ # self.sla = '-'
product = self.product_variant_id
-
- qty_available = 0
- qty_available = product.qty_onhand_bandengan
+
+ q_vendor = [
+ ('product_id', '=', product.id),
+ ('is_winner', '=', True)
+ ]
+
+ vendor = self.env['purchase.pricelist'].search(q_vendor)
+ vendor_duration = 0
- if qty_available > 0:
- self.sla = '1 Hari'
+ #SLA Vendor
+ if vendor:
+ vendor_sla = self.env['vendor.sla'].search([('id_vendor', '=', vendor.vendor_id.id)], limit=1)
+ sla_vendor = int(vendor_sla.duration) if vendor_sla else 0
+ if sla_vendor > 0:
+ if vendor_sla.unit == 'hari':
+ vendor_duration = vendor_sla.duration * 24 * 60
+ else :
+ vendor_duration = vendor_sla.duration * 60
+
+ self.sla_vendor_id = vendor_sla.id if vendor_sla else False
+ #SLA Logistik selalu 1 hari
+ estimation_sla = (1 * 24 * 60) + vendor_duration
+ estimation_sla_days = estimation_sla / (24 * 60)
+ self.sla = math.ceil(estimation_sla_days)
+ self.sla_logistic = int(1)
+ self.sla_logistic_unit = 'hari'
+ self.sla_logistic_duration_unit = '1 hari'
+ else:
+ self.unlink()
+ else:
+ self.unlink()
+
- query = [
- ('product_id', '=', product.id),
- ('picking_id', '!=', False),
- ('picking_id.location_id', '=', 57),
- ('picking_id.state', 'not in', ['cancel'])
- ]
- picking = self.env['stock.move.line'].search(query)
- leadtimes=[]
- for stock in picking:
- date_delivered = stock.picking_id.driver_departure_date
- date_so_confirmed = stock.picking_id.sale_id.date_order
- if date_delivered and date_so_confirmed:
- leadtime = date_delivered - date_so_confirmed
- leadtime_in_days = leadtime.days
- leadtimes.append(leadtime_in_days)
+ # query = [
+ # ('product_id', '=', product.id),
+ # ('picking_id', '!=', False),
+ # ('picking_id.location_id', '=', 57),
+ # ('picking_id.state', 'not in', ['cancel'])
+ # ]
+ # picking = self.env['stock.move.line'].search(query)
+
+ # leadtimes=[]
+ # for stock in picking:
+ # date_delivered = stock.picking_id.driver_departure_date
+ # date_do_ready = stock.picking_id.date_reserved
+ # if date_delivered and date_do_ready:
+ # leadtime = date_delivered - date_do_ready
+ # leadtime_in_days = leadtime.days
+ # leadtimes.append(leadtime_in_days)
- if len(leadtimes) > 0:
- avg_leadtime = sum(leadtimes) / len(leadtimes)
- rounded_leadtime = math.ceil(avg_leadtime)
- self.avg_leadtime = rounded_leadtime
- if rounded_leadtime >= 1 and rounded_leadtime <= 5:
- self.sla = '3-7 Hari'
- elif rounded_leadtime >= 6 and rounded_leadtime <= 10:
- self.sla = '4-12 Hari'
- elif rounded_leadtime >= 11:
- self.sla = 'Indent' \ No newline at end of file
+ # if len(leadtimes) > 0:
+ # avg_leadtime = sum(leadtimes) / len(leadtimes)
+ # rounded_leadtime = math.ceil(avg_leadtime)
+ # estimation_sla = ((rounded_leadtime * 24 * 60) + vendor_duration)/2
+ # estimation_sla_days = estimation_sla / (24 * 60)
+ # self.sla = math.ceil(estimation_sla_days)
+ # self.avg_leadtime = int(rounded_leadtime) \ No newline at end of file
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index efacb95f..600dd90e 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -422,6 +422,18 @@ class ProductProduct(models.Model):
merchandise_ok = fields.Boolean(string='Product Promotion')
qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant')
+ def generate_product_sla(self):
+ product_variant_ids = self.env.context.get('active_ids', [])
+ product_variant = self.search([('id', 'in', product_variant_ids)])
+ sla_record = self.env['product.sla'].search([('product_variant_id', '=', product_variant.id)], limit=1)
+
+ if sla_record:
+ sla_record.generate_product_sla()
+ else:
+ new_sla_record = self.env['product.sla'].create({
+ 'product_variant_id': product_variant.id,
+ })
+ new_sla_record.generate_product_sla()
@api.model
def create(self, vals):
group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id
diff --git a/indoteknik_custom/models/public_holiday.py b/indoteknik_custom/models/public_holiday.py
new file mode 100644
index 00000000..851d9080
--- /dev/null
+++ b/indoteknik_custom/models/public_holiday.py
@@ -0,0 +1,11 @@
+from odoo import api, fields, models
+from datetime import timedelta, datetime
+
+class PublicHoliday(models.Model):
+ _name = 'hr.public.holiday'
+ _description = 'Public Holidays'
+
+ name = fields.Char(string='Holiday Name', required=True)
+ start_date = fields.Date('Date Holiday', required=True)
+ # end_date = fields.Date('End Holiday Date', required=True)
+ # company_id = fields.Many2one('res.company', 'Company')
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 430b4526..d0b57a3d 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -144,6 +144,17 @@ class SaleOrder(models.Model):
('PNR', 'Pareto Non Repeating'),
('NP', 'Non Pareto')
])
+ estimated_ready_ship_date = fields.Datetime(
+ string='ET Ready to Ship compute',
+ compute='_compute_etrts_date',
+ store=True
+
+ )
+ expected_ready_to_ship = fields.Datetime(
+ string='ET Ready to Ship',
+ copy=False,
+ store=True
+ )
shipping_method_picking = fields.Char(string='Shipping Method Picking', compute='_compute_shipping_method_picking')
def _compute_shipping_method_picking(self):
@@ -372,20 +383,53 @@ class SaleOrder(models.Model):
rec.compute_fullfillment = True
+ @api.depends('date_order', 'estimated_arrival_days', 'state')
def _compute_eta_date(self):
- max_leadtime = 0
-
- for line in self.order_line:
- leadtime = line.vendor_id.leadtime
- max_leadtime = max(max_leadtime, leadtime)
-
- for rec in self:
- if rec.date_order and rec.state not in ['cancel', 'draft']:
- eta_date = datetime.now() + timedelta(days=max_leadtime)
- rec.eta_date = eta_date
+ for rec in self:
+ if rec.date_order and rec.state not in ['cancel'] and rec.estimated_arrival_days:
+ rec.eta_date = rec.date_order + timedelta(days=rec.estimated_arrival_days)
else:
rec.eta_date = False
+ @api.depends("order_line.product_id")
+ def _compute_etrts_date(self): #Function to calculate Estimated Ready To Ship Date
+ for rec in self:
+ max_slatime = 1 # Default SLA jika tidak ada
+ for line in rec.order_line:
+ product_sla = self.env['product.sla'].search([('product_variant_id', '=', line.product_id.id)], limit=1)
+ slatime = int(product_sla.sla) if product_sla and product_sla.sla else 1
+ max_slatime = max(max_slatime, slatime)
+
+ if rec.date_order:
+ eta_date = datetime.now() + timedelta(days=max_slatime)
+ rec.estimated_ready_ship_date = eta_date
+ rec.commitment_date = eta_date
+ # Jika expected_ready_to_ship kosong, set nilai default
+ if not rec.expected_ready_to_ship:
+ rec.expected_ready_to_ship = eta_date
+
+ @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship
+ def _onchange_expected_ready_ship_date(self):
+ for rec in self:
+ if rec.expected_ready_to_ship and rec.estimated_ready_ship_date:
+ # Hanya membandingkan tanggal saja, tanpa jam
+ expected_date = rec.expected_ready_to_ship.date()
+ estimated_date = rec.estimated_ready_ship_date.date()
+
+ if expected_date < estimated_date:
+ rec.expected_ready_to_ship = rec.estimated_ready_ship_date
+ rec.commitment_date = rec.estimated_ready_ship_date
+ raise ValidationError(
+ "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}."
+ .format(estimated_date.strftime('%d-%m-%Y'), estimated_date.strftime('%d-%m-%Y'))
+ )
+
+ def _set_etrts_date(self):
+ for order in self:
+ if order.state in ('done', 'cancel', 'sale'):
+ raise UserError(_("You cannot change the Estimated Ready To Ship Date on a done, sale or cancelled order."))
+ # order.move_lines.write({'estimated_ready_ship_date': order.estimated_ready_ship_date})
+
def _prepare_invoice(self):
"""
Prepare the dict of values to create the new invoice for a sales order. This method may be
@@ -579,15 +623,16 @@ class SaleOrder(models.Model):
if line.product_id.type == 'product':
line_no += 1
line.line_no = line_no
+
def write(self, vals):
res = super(SaleOrder, self).write(vals)
-
+ # self._compute_etrts_date()
if 'carrier_id' in vals:
for picking in self.picking_ids:
if picking.state == 'assigned':
picking.carrier_id = self.carrier_id
-
+
return res
def calculate_so_status(self):
@@ -1045,6 +1090,7 @@ class SaleOrder(models.Model):
order._set_sppkp_npwp_contact()
order.calculate_line_no()
order.send_notif_to_salesperson()
+ order._compute_etrts_date()
# order.order_line.get_reserved_from()
res = super(SaleOrder, self).action_confirm()
@@ -1424,13 +1470,14 @@ class SaleOrder(models.Model):
def create(self, vals):
# Ensure partner details are updated when a sale order is created
order = super(SaleOrder, self).create(vals)
+ # order._compute_etrts_date()
# order._update_partner_details()
return order
def write(self, vals):
# Call the super method to handle the write operation
res = super(SaleOrder, self).write(vals)
-
+ # self._compute_etrts_date()
# Check if the update is coming from a save operation
# if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']):
# self._update_partner_details()
@@ -1465,4 +1512,6 @@ class SaleOrder(models.Model):
raise UserError(
"SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.")
res = super(SaleOrder, self).write(vals)
+ if 'order_line' in vals:
+ self._compute_etrts_date()
return res \ No newline at end of file
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 49e66786..1690a4ed 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -1,7 +1,7 @@
from odoo import fields, models, api, _
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.float_utils import float_is_zero
-from datetime import timedelta, datetime
+from datetime import timedelta, datetime as waktu
from itertools import groupby
import pytz, requests, json, requests
from dateutil import parser
@@ -12,8 +12,15 @@ import base64
import requests
import time
import logging
+import re
+from deep_translator import GoogleTranslator
_logger = logging.getLogger(__name__)
+_biteship_url = "https://api.biteship.com/v1"
+_biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
+
+
+
class StockPicking(models.Model):
_inherit = 'stock.picking'
check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True)
@@ -166,6 +173,39 @@ class StockPicking(models.Model):
lalamove_image_url = fields.Char(string="Lalamove Image URL")
lalamove_image_html = fields.Html(string="Lalamove Image", compute="_compute_lalamove_image_html")
+ # Biteship Section
+ 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, store=True, related='sale_id.estimated_ready_ship_date')
+ countdown_hours = fields.Float(string='Countdown in Hours', compute='_compute_countdown_ready_to_ship', store=True, default=False)
+ countdown_ready_to_ship = fields.Char(string='Countdown Ready to Ship', compute='_compute_countdown_ready_to_ship')
+
+ # @api.depends('estimated_ready_ship_date', 'state')
+ # def _compute_countdown_hours(self):
+ # for record in self:
+ # if record.state in ('cancel', 'done') or not record.estimated_ready_ship_date:
+ # # Gunakan nilai yang sangat besar sebagai placeholder
+ # record.countdown_hours = 999999
+ # else:
+ # delta = record.estimated_ready_ship_date - waktu.now()
+ # record.countdown_hours = delta.total_seconds() / 3600
+
+ @api.depends('estimated_ready_ship_date', 'state')
+ def _compute_countdown_ready_to_ship(self):
+ for record in self:
+ if record.state in ('cancel', 'done'):
+ record.countdown_ready_to_ship = False
+ else:
+ if record.estimated_ready_ship_date:
+ delta = record.estimated_ready_ship_date - waktu.now()
+ days = delta.days
+ hours, remainder = divmod(delta.seconds, 3600)
+ record.countdown_ready_to_ship = f"{days} days, {hours} hours"
+ record.countdown_hours = delta.total_seconds() / 3600
+ else:
+ record.countdown_ready_to_ship = False
+
def _compute_lalamove_image_html(self):
for record in self:
if record.lalamove_image_url:
@@ -321,7 +361,7 @@ class StockPicking(models.Model):
picking.tracking_by = self.env.user.id
ata_at_str = data.get("ata_at")
envio_ata = self._convert_to_datetime(data.get("ata_at"))
-
+
picking.driver_arrival_date = envio_ata
if data.get("status") != 'delivered':
picking.driver_arrival_date = False
@@ -332,12 +372,13 @@ class StockPicking(models.Model):
raise UserError(f"Kesalahan tidak terduga: {str(e)}")
def action_send_to_biteship(self):
- url = "https://api.biteship.com/v1/orders"
- api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA"
-
+
+ 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 [{
@@ -370,6 +411,7 @@ class StockPicking(models.Model):
})
payload = {
+ "reference_id " : self.sale_id.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,
@@ -381,7 +423,8 @@ class StockPicking(models.Model):
"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,
- "courier_type": "reg",
+ "origin_note": "BELAKANG INDOMARET",
+ "courier_type": self.sale_id.delivery_service_type or "reg",
"courier_company": self.carrier_id.name.lower(),
"delivery_type": "now",
"destination_postal_code": self.real_shipping_id.zip,
@@ -389,27 +432,42 @@ class StockPicking(models.Model):
}
# Cek jika pengiriman instant atau same_day
- if "instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type:
+ 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_note": "BELAKANG INDOMARET",
- "courier_company": self.carrier_id.name.lower(),
- "courier_type": self.sale_id.delivery_service_type,
- "delivery_type": "now",
- "items": items_data_instant # Gunakan items untuk instant
+ "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
})
-
+
+ api_key = _biteship_api_key
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
# Kirim request ke Biteship
- response = requests.post(url, headers=headers, json=payload)
+ response = requests.post(_biteship_url+'/orders', headers=headers, json=payload)
+
+ if response.status_code == 200:
+ data = response.json()
+
+ 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", "")
- if response.status_code == 201:
- return response.json()
+ return data
else:
- raise UserError(f"Error saat mengirim ke Biteship: {response.content}")
+ error_data = response.json()
+ error_message = error_data.get("error", "Unknown error")
+ error_code = error_data.get("code", "No code provided")
+ raise UserError(f"Error saat mengirim ke Biteship: {error_message} (Code: {error_code})")
@api.constrains('driver_departure_date')
def constrains_driver_departure_date(self):
@@ -1052,8 +1110,19 @@ class StockPicking(models.Model):
'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()
}
+
+ if self.biteship_id :
+ histori = self.get_manifest_biteship()
+ response['manifests'] = histori.get("manifests", [])
+ response['delivered'] = histori.get("delivered", False) or self.sj_return_date != False or self.driver_arrival_date != False
+ response['status'] = self._map_status_biteship(histori.get("delivered"))
+
+ response
+
+ return response
if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0:
response['delivered'] = self.sj_return_date != False or self.driver_arrival_date != False
@@ -1066,6 +1135,67 @@ class StockPicking(models.Model):
return response
+ def get_manifest_biteship(self):
+ api_key = _biteship_api_key
+ headers = {
+ "Authorization": f"Bearer {api_key}",
+ "Content-Type": "application/json"
+ }
+
+
+ manifests = []
+
+ try:
+ # Kirim request ke Biteship
+ response = requests.get(_biteship_url+'/trackings/'+self.biteship_tracking_id, headers=headers, json=manifests)
+ result = response.json()
+ if(result.get('success') == True):
+ history = result.get("history", [])
+ status = result.get("status", "")
+
+ for entry in reversed(history):
+ 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": GoogleTranslator(source='auto', target='id').translate(entry["note"]),
+ })
+
+ return {
+ "manifests": manifests,
+ "delivered": status
+ }
+
+ return manifests
+ 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):
+ try:
+ dt_with_tz = waktu.fromisoformat(iso_date)
+ utc_dt = dt_with_tz.astimezone(pytz.utc)
+
+ local_tz = pytz.timezone("Asia/Jakarta")
+ local_dt = utc_dt.astimezone(local_tz)
+
+ return local_dt.strftime("%Y-%m-%d %H:%M:%S")
+ except Exception as e:
+ return str(e)
+
+ def _map_status_biteship(self, status):
+ status_mapping = {
+ "confirmed": "pending",
+ "scheduled": "pending",
+ "allocated": "pending",
+ "picking_up": "pending",
+ "picked": "shipment",
+ "cancelled": "cancelled",
+ "on_hold": "on_hold",
+ "dropping_off": "shipment",
+ "delivered": "completed"
+ }
+ return status_mapping.get(status, "Hubungi Admin")
+
def generate_eta_delivery(self):
current_date = datetime.datetime.now()
prepare_days = 3
diff --git a/indoteknik_custom/models/vendor_sla.py b/indoteknik_custom/models/vendor_sla.py
new file mode 100644
index 00000000..67b6ffc3
--- /dev/null
+++ b/indoteknik_custom/models/vendor_sla.py
@@ -0,0 +1,95 @@
+from odoo import models, fields, api
+import logging
+import math
+_logger = logging.getLogger(__name__)
+
+class VendorSLA(models.Model):
+ _name = 'vendor.sla'
+ _description = 'Vendor SLA'
+ _rec_name = 'id_vendor'
+
+ id_vendor = fields.Many2one('res.partner', string='Name', domain="[('industry_id', '=', 46)]")
+ duration = fields.Integer(string='Duration', description='SLA Duration')
+ unit = fields.Selection(
+ [('jam', 'Jam'),('hari', 'Hari')],
+ string="SLA Time"
+ )
+ duration_unit = fields.Char(string="Duration (Unit)", compute="_compute_duration_unit")
+
+ # pertama, lakukan group by vendor pada modul purchase.order
+ # kedua, pada setiap Purchase order pada group by vendor tersebut, lakukan penghitungan penjumlahan setiap nilai datetime field date_planed dikurangi date_approve purchase order
+ # dibagi jumlah data dari setiap Purchase order pada group by vendor tersebut. hasilnya lalu di gunakan untuk mengset nilai duration
+ def generate_vendor_id_sla(self):
+ # Step 1: Group purchase orders by vendor (partner_id)
+ po_env = self.env['purchase.order']
+ pos = po_env.read_group(
+ domain=[('state', '=', 'done')],
+ fields=['partner_id', 'date_planned', 'date_approve'],
+ groupby=['partner_id'],
+ lazy=False
+ )
+
+ for group in pos:
+ partner_id = group['partner_id'][0]
+ total_duration = 0
+ count = 0
+
+ # Step 2: Calculate the average duration for each vendor
+ pos_for_vendor = po_env.search([
+ ('partner_id', '=', partner_id),
+ ('state', '=', 'done'),
+ ('date_planned', '>=', '2023-01-01')
+ ])
+
+ for po in pos_for_vendor:
+ if po.date_planned and po.date_approve:
+ date_planned = fields.Datetime.to_datetime(po.date_planned)
+ date_approve = fields.Datetime.to_datetime(po.date_approve)
+ if date_planned < date_approve: continue
+ duration = (date_planned - date_approve).total_seconds() / 3600 # Convert to hours
+ total_duration += duration
+ count += 1
+
+ if count > 0:
+ average_duration = total_duration / count
+
+ # Step 3: Update the duration field in the corresponding res.partner record
+ vendor_sla = self.search([('id_vendor', '=', partner_id)], limit=1)
+
+ # Konversi jam ke hari jika diperlukan
+ if average_duration >= 24:
+ days = average_duration / 24
+ if days - int(days) > 0.5: # Jika sisa lebih dari 0,5, bulatkan ke atas
+ days = int(days) + 1
+ else: # Jika sisa 0,5 atau kurang, bulatkan ke bawah
+ days = int(days)
+ duration_to_save = days
+ unit_to_save = 'hari'
+ else:
+ duration_to_save = round(average_duration)
+ unit_to_save = 'jam'
+
+ # Update atau create vendor SLA record
+ if vendor_sla:
+ vendor_sla.write({
+ 'duration': duration_to_save,
+ 'unit': unit_to_save
+ })
+ else:
+ self.create({
+ 'id_vendor': partner_id,
+ 'duration': duration_to_save,
+ 'unit': unit_to_save
+ })
+ _logger.info(f'Proses SLA untuk Vendor selesai dilakukan')
+
+ @api.depends('duration', 'unit')
+ def _compute_duration_unit(self):
+ for record in self:
+ if record.duration and record.unit:
+ record.duration_unit = f"{record.duration} {record.unit}"
+ else:
+ record.duration_unit = "-"
+
+
+ \ No newline at end of file
diff --git a/indoteknik_custom/security/ir.model.access.csv b/indoteknik_custom/security/ir.model.access.csv
index de29468d..37f5aeab 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -150,16 +150,17 @@ access_sales_order_fulfillment_v2,access.sales.order.fulfillment.v2,model_sales_
access_v_move_outstanding,access.v.move.outstanding,model_v_move_outstanding,,1,1,1,1
access_va_multi_approve,access.va.multi.approve,model_va_multi_approve,,1,1,1,1
access_va_multi_reject,access.va.multi.reject,model_va_multi_reject,,1,1,1,1
+access_vendor_sla,access.vendor_sla,model_vendor_sla,,1,1,1,1
access_check_product,access.check.product,model_check_product,,1,1,1,1
access_stock_immediate_transfer,access.stock.immediate.transfer,model_stock_immediate_transfer,,1,1,1,1
access_coretax_faktur,access.coretax.faktur,model_coretax_faktur,,1,1,1,1
access_purchase_order_unlock_wizard,access.purchase.order.unlock.wizard,model_purchase_order_unlock_wizard,,1,1,1,1
-
access_User_pengajuan_tempo_line,access.user.pengajuan.tempo.line,model_user_pengajuan_tempo_line,,1,1,1,1
access_user_pengajuan_tempo,access.user.pengajuan.tempo,model_user_pengajuan_tempo,,1,1,1,1
access_reject_reason_wizard,reject.reason.wizard,model_reject_reason_wizard,,1,1,1,0
access_confirm_approval_wizard,confirm.approval.wizard,model_confirm_approval_wizard,,1,1,1,0
+access_hr_public_holiday,confirm.hr.public.holiday,model_hr_public_holiday,,1,1,1,0
access_barcode_product,access.barcode.product,model_barcode_product,,1,1,1,1
access_barcoding_product,access.barcoding.product,model_barcoding_product,,1,1,1,1
access_barcoding_product_line,access.barcoding.product.line,model_barcoding_product_line,,1,1,1,1
-access_account_payment_register,access.account.payment.register,model_account_payment_register,,1,1,1,1 \ No newline at end of file
+access_account_payment_register,access.account.payment.register,model_account_payment_register,,1,1,1,1
diff --git a/indoteknik_custom/views/product_product.xml b/indoteknik_custom/views/product_product.xml
index 71748e44..b214dc87 100644
--- a/indoteknik_custom/views/product_product.xml
+++ b/indoteknik_custom/views/product_product.xml
@@ -31,6 +31,14 @@
<field name="code">model.action_sync_to_solr()</field>
</record>
+ <record id="ir_actions_server_product_sla_generate" model="ir.actions.server">
+ <field name="name">Generate Product SLA</field>
+ <field name="model_id" ref="product.model_product_product"/>
+ <field name="binding_model_id" ref="product.model_product_product"/>
+ <field name="state">code</field>
+ <field name="code">model.generate_product_sla()</field>
+ </record>
+
<data noupdate="1">
<record id="cron_variant_solr_flag_solr" model="ir.cron">
<field name="name">Sync Variant To Solr: Solr Flag 2</field>
diff --git a/indoteknik_custom/views/product_sla.xml b/indoteknik_custom/views/product_sla.xml
index 8b0e874b..9179730f 100644
--- a/indoteknik_custom/views/product_sla.xml
+++ b/indoteknik_custom/views/product_sla.xml
@@ -6,7 +6,9 @@
<field name="arch" type="xml">
<tree create="false">
<field name="product_variant_id"/>
- <field name="avg_leadtime"/>
+ <field name="sla_vendor_id" string="Name Vendor"/>
+ <field name="sla_vendor_duration" string="SLA Vendor"/>
+ <field name="sla_logistic_duration_unit" string="SLA Logistic"/>
<field name="sla"/>
</tree>
</field>
@@ -21,7 +23,8 @@
<group>
<group>
<field name="product_variant_id"/>
- <field name="avg_leadtime"/>
+ <field name="sla_logistic"/>
+ <field name="sla_logistic_unit"/>
<field name="sla"/>
<field name="version"/>
</group>
diff --git a/indoteknik_custom/views/public_holiday.xml b/indoteknik_custom/views/public_holiday.xml
new file mode 100644
index 00000000..146c5b0b
--- /dev/null
+++ b/indoteknik_custom/views/public_holiday.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<odoo>
+ <data>
+ <!-- Security Access Rights -->
+ <record id="model_hr_public_holiday_access" model="ir.model.access">
+ <field name="name">hr.public.holiday access</field>
+ <field name="model_id" ref="model_hr_public_holiday"/>
+ <field name="group_id" eval="False"/>
+ <field name="perm_read" eval="True"/>
+ <field name="perm_write" eval="True"/>
+ <field name="perm_create" eval="True"/>
+ <field name="perm_unlink" eval="True"/>
+ </record>
+
+ <!-- Public Holiday Form View -->
+ <record id="view_hr_public_holiday_form" model="ir.ui.view">
+ <field name="name">hr.public.holiday.form</field>
+ <field name="model">hr.public.holiday</field>
+ <field name="arch" type="xml">
+ <form string="Public Holiday">
+ <sheet>
+ <group>
+ <field name="name"/>
+ <field name="start_date"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <record id="view_hr_public_holiday_tree" model="ir.ui.view">
+ <field name="name">hr.public.holiday.tree</field>
+ <field name="model">hr.public.holiday</field>
+ <field name="arch" type="xml">
+ <tree string="Public Holidays">
+ <field name="name"/>
+ <field name="start_date"/>
+ </tree>
+ </field>
+ </record>
+ <record id="action_hr_public_holiday" model="ir.actions.act_window">
+ <field name="name">Public Holidays</field>
+ <field name="res_model">hr.public.holiday</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <menuitem
+ id="hr_public_holiday"
+ name="Public Holiday"
+ parent="website_sale.menu_orders"
+ sequence="1"
+ action="action_hr_public_holiday"
+ />
+ </data>
+</odoo>
diff --git a/indoteknik_custom/views/sale_order.xml b/indoteknik_custom/views/sale_order.xml
index 2a46901a..b267eee4 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -69,6 +69,7 @@
</field>
<field name="tag_ids" position="after">
<field name="eta_date" readonly="1"/>
+ <field name="expected_ready_to_ship" />
<field name="flash_sale"/>
<field name="margin_after_delivery_purchase"/>
<field name="percent_margin_after_delivery_purchase"/>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 50ea40bf..1832c31e 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -7,7 +7,8 @@
<field name="inherit_id" ref="stock.vpicktree"/>
<field name="arch" type="xml">
<tree position="attributes">
- <attribute name="default_order">create_date desc</attribute>
+ <attribute name="default_order">countdown_hours asc</attribute>
+<!-- <attribute name="default_order">create_date desc</attribute>-->
</tree>
<field name="json_popover" position="after">
<field name="date_done" optional="hide"/>
@@ -18,6 +19,8 @@
<field name="note" optional="hide"/>
<field name="date_reserved" optional="hide"/>
<field name="state_reserve" optional="hide"/>
+ <field name="countdown_hours" optional="hide"/>
+ <field name="countdown_ready_to_ship" />
</field>
<field name="partner_id" position="after">
<field name="purchase_representative_id"/>
diff --git a/indoteknik_custom/views/vendor_sla.xml b/indoteknik_custom/views/vendor_sla.xml
new file mode 100644
index 00000000..cf4425eb
--- /dev/null
+++ b/indoteknik_custom/views/vendor_sla.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<odoo>
+ <record id="vendor_action" model="ir.actions.act_window">
+ <field name="name">Vendor SLA</field>
+ <field name="res_model">vendor.sla</field>
+ <field name="view_mode">tree,form</field>
+ </record>
+
+ <record id="vendor_tree" model="ir.ui.view">
+ <field name="name">Vendor SLA</field>
+ <field name="model">vendor.sla</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="id_vendor" string="Vendor Name" />
+ <field name="duration_unit" string="Duration" />
+ </tree>
+ </field>
+ </record>
+
+ <record id="vendor_sla_view" model="ir.ui.view">
+ <field name="name">Vendor SLA</field>
+ <field name="model">vendor.sla</field>
+ <field name="arch" type="xml">
+ <form>
+ <sheet>
+ <group>
+ <field name="id_vendor" string="Vendor Name" />
+ <field name="duration" string="SLA Duration" />
+ <field name="unit" string="SLA Time" />
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+
+ <menuitem id="menu_vendor_sla"
+ name="Vendor SLA"
+ parent="menu_monitoring_in_purchase"
+ sequence="1"
+ action="vendor_action"
+ />
+</odoo> \ No newline at end of file
diff --git a/indoteknik_custom/views/x_banner_category.xml b/indoteknik_custom/views/x_banner_category.xml
index 11feb207..a83c4129 100755
--- a/indoteknik_custom/views/x_banner_category.xml
+++ b/indoteknik_custom/views/x_banner_category.xml
@@ -23,7 +23,7 @@
<group>
<field name="x_name"/>
<field name="x_banner_subtitle"/>
- <field name="x_studio_field_KKVl4"/>
+ <field name="x_studio_field_KKVl4"/>
<field name="last_update_solr" readonly="1"/>
</group>
<group></group>