summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAzka Nathan <darizkyfaz@gmail.com>2025-03-10 08:42:21 +0700
committerAzka Nathan <darizkyfaz@gmail.com>2025-03-10 08:42:21 +0700
commit5aeeba569652f15a2cc506a4820ec8dcbda74565 (patch)
treef2e9a927bf40f3bcca7737ed3c1bb4b1639a488e
parentad19154ae49ec5bc1178006344baf104154167bf (diff)
parent3b8b4ec20d523cedf00d0a343ffc244e0a43da58 (diff)
Merge branch 'odoo-backup' into dev/wms
# Conflicts: # indoteknik_custom/models/stock_picking.py # indoteknik_custom/security/ir.model.access.csv
-rw-r--r--indoteknik_api/controllers/api_v1/partner.py68
-rw-r--r--indoteknik_api/controllers/api_v1/product.py81
-rw-r--r--indoteknik_api/controllers/api_v1/sale_order.py4
-rw-r--r--indoteknik_api/controllers/api_v1/stock_picking.py50
-rwxr-xr-xindoteknik_custom/__manifest__.py2
-rwxr-xr-xindoteknik_custom/models/__init__.py2
-rw-r--r--indoteknik_custom/models/account_move.py3
-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
-rw-r--r--indoteknik_custom/models/res_partner.py7
-rwxr-xr-xindoteknik_custom/models/sale_order.py103
-rw-r--r--indoteknik_custom/models/stock_picking.py211
-rw-r--r--indoteknik_custom/models/vendor_sla.py102
-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.xml3
-rw-r--r--indoteknik_custom/views/stock_picking.xml6
-rw-r--r--indoteknik_custom/views/vendor_sla.xml42
-rwxr-xr-xindoteknik_custom/views/x_banner_category.xml2
22 files changed, 786 insertions, 131 deletions
diff --git a/indoteknik_api/controllers/api_v1/partner.py b/indoteknik_api/controllers/api_v1/partner.py
index 307165b3..d28aa8b4 100644
--- a/indoteknik_api/controllers/api_v1/partner.py
+++ b/indoteknik_api/controllers/api_v1/partner.py
@@ -64,37 +64,43 @@ class Partner(controller.Controller):
@http.route(prefix + 'partner/<id>/address', auth='public', methods=['PUT', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
def write_partner_address_by_id(self, **kw):
- params = self.get_request_params(kw, {
- 'id': ['required', 'number'],
- 'type': ['default:other'],
- 'name': ['required'],
- 'email': ['required'],
- 'mobile': ['required'],
- 'phone': [''],
- 'street': ['required'],
- 'state_id': ['required', 'number', 'alias:state_id'],
- 'city_id': ['required', 'number', 'alias:kota_id'],
- 'district_id': ['number', 'alias:kecamatan_id'],
- 'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'],
- 'zip': ['required'],
- 'longtitude': [],
- 'latitude': [],
- 'address_map': [],
- 'alamat_lengkap_text': []
- })
+ try:
+ params = self.get_request_params(kw, {
+ 'id': ['required', 'number'],
+ 'type': ['default:other'],
+ 'name': ['required'],
+ 'email': ['required'],
+ 'mobile': ['required'],
+ 'phone': [''],
+ 'street': ['required'],
+ 'state_id': ['required', 'number', 'alias:state_id'],
+ 'city_id': ['required', 'number', 'alias:kota_id'],
+ 'district_id': ['number', 'alias:kecamatan_id'],
+ 'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'],
+ 'zip': ['required'],
+ 'longitude': '', # Perbaikan dari longtitude ke longitude
+ 'latitude': '',
+ 'address_map': [],
+ 'alamat_lengkap_text': []
+ })
- if not params['valid']:
- return self.response(code=400, description=params)
-
- partner = request.env[self._name].search([('id', '=', params['value']['id'])], limit=1)
- if not partner:
- return self.response(code=404, description='User not found')
-
- partner.write(params['value'])
+ if not params['valid']:
+ return self.response(code=400, description=params)
- return self.response({
- 'id': partner.id
- })
+ partner = request.env[self._name].sudo().search([('id', '=', params['value']['id'])], limit=1)
+
+ if not partner:
+ return self.response(code=404, description='User not found')
+
+ try:
+ partner.write(params['value'])
+ except Exception as e:
+ return self.response(code=500, description=f'Error writing partner data: {str(e)}')
+
+ return self.response({'id': partner.id})
+
+ except Exception as e:
+ return self.response(code=500, description=f'Unexpected error: {str(e)}')
@http.route(prefix + 'partner/address', auth='public', methods=['POST', 'OPTIONS'], csrf=False)
@controller.Controller.must_authorized()
@@ -111,8 +117,8 @@ class Partner(controller.Controller):
'city_id': ['required', 'number', 'alias:kota_id'],
'district_id': ['number', 'alias:kecamatan_id'],
'sub_district_id': ['number', 'alias:kelurahan_id', 'exclude_if_null'],
- 'longtitude': [],
- 'latitude': [],
+ 'longtitude': '',
+ 'latitude': '',
'address_map': [],
'zip': ['required']
})
diff --git a/indoteknik_api/controllers/api_v1/product.py b/indoteknik_api/controllers/api_v1/product.py
index 32362582..557215ea 100644
--- a/indoteknik_api/controllers/api_v1/product.py
+++ b/indoteknik_api/controllers/api_v1/product.py
@@ -1,13 +1,13 @@
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
import math
import json
-_logger = logging.getLogger(__name__)
+_logger = logging.getLogger(__name__)
class Product(controller.Controller):
@@ -33,9 +33,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 = request.env['sale.order'].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 +104,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 +130,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/sale_order.py b/indoteknik_api/controllers/api_v1/sale_order.py
index a7e027c8..6815bf6c 100644
--- a/indoteknik_api/controllers/api_v1/sale_order.py
+++ b/indoteknik_api/controllers/api_v1/sale_order.py
@@ -386,7 +386,8 @@ class SaleOrder(controller.Controller):
'note_website': [],
'voucher': [],
'source': [],
- 'estimated_arrival_days': ['number', 'default:0']
+ 'estimated_arrival_days': ['number', 'default:0'],
+ 'estimated_arrival_days_start': ['number', 'default:0']
})
if not params['valid']:
@@ -417,6 +418,7 @@ class SaleOrder(controller.Controller):
'partner_purchase_order_file': params['value']['po_file'],
'delivery_amt': params['value']['delivery_amount'] * 1.10,
'estimated_arrival_days': params['value']['estimated_arrival_days'],
+ 'estimated_arrival_days_start': params['value']['estimated_arrival_days_start'],
'shipping_cost_covered': 'customer',
'shipping_paid_by': 'customer',
'carrier_id': params['value']['carrier_id'],
diff --git a/indoteknik_api/controllers/api_v1/stock_picking.py b/indoteknik_api/controllers/api_v1/stock_picking.py
index 2e0c4ad0..55e07152 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,50 @@ 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:
+ if not request.jsonrequest:
+ return "ok"
+
+ data = request.jsonrequest # Ambil data JSON dari request
+ event = data.get('event')
+
+ # 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):
+ 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()})
+
+ 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')
+ })
+
+ def process_order_waybill(self, data):
+ picking_model = request.env['stock.picking'].sudo().search([('biteship_id', '=', data.get('order_id'))], limit=1)
+ if picking_model:
+ picking_model.write({
+ 'biteship_waybill_id': data.get('courier_waybill_id'),
+ 'delivery_tracking_no': data.get('courier_waybill_id'),
+ 'biteship_tracking_id':data.get('courier_tracking_id')
+ })
+ \ No newline at end of file
diff --git a/indoteknik_custom/__manifest__.py b/indoteknik_custom/__manifest__.py
index f66314fa..a7096346 100755
--- a/indoteknik_custom/__manifest__.py
+++ b/indoteknik_custom/__manifest__.py
@@ -161,7 +161,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',
'views/stock_inventory.xml',
],
'demo': [],
diff --git a/indoteknik_custom/models/__init__.py b/indoteknik_custom/models/__init__.py
index 531767fe..935a0aa0 100755
--- a/indoteknik_custom/models/__init__.py
+++ b/indoteknik_custom/models/__init__.py
@@ -139,8 +139,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 sales_order_koli
diff --git a/indoteknik_custom/models/account_move.py b/indoteknik_custom/models/account_move.py
index 9aa0743b..45fdb8df 100644
--- a/indoteknik_custom/models/account_move.py
+++ b/indoteknik_custom/models/account_move.py
@@ -76,7 +76,8 @@ class AccountMove(models.Model):
def compute_other_taxes(self):
for rec in self:
- rec.other_taxes = rec.other_subtotal * 0.12
+ rec.other_taxes = round(rec.other_subtotal * 0.12, 2)
+
def compute_other_subtotal(self):
for rec in self:
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/res_partner.py b/indoteknik_custom/models/res_partner.py
index 7e574a72..fa2f6a81 100644
--- a/indoteknik_custom/models/res_partner.py
+++ b/indoteknik_custom/models/res_partner.py
@@ -470,4 +470,9 @@ class ResPartner(models.Model):
if not self.nitku.isdigit():
raise UserError("NITKU harus berupa angka.")
if len(self.nitku) != 22:
- raise UserError("NITKU harus memiliki tepat 22 angka.") \ No newline at end of file
+ raise UserError("NITKU harus memiliki tepat 22 angka.")
+
+ @api.onchange('name')
+ def _onchange_name(self):
+ if self.company_type == 'person':
+ self.nama_wajib_pajak = self.name \ No newline at end of file
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 3534306c..b4c3b1c2 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -138,11 +138,13 @@ class SaleOrder(models.Model):
applied_voucher_shipping_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False)
amount_voucher_shipping_disc = fields.Float(string='Voucher Discount')
source_id = fields.Many2one('utm.source', 'Source', domain="[('id', 'in', [32, 59, 60, 61])]", required=True)
- estimated_arrival_days = fields.Integer('Estimated Arrival Days', default=0)
+ estimated_arrival_days = fields.Integer('Estimated Arrival To', default=0)
+ estimated_arrival_days_start = fields.Integer('Estimated Arrival From', default=0)
email = fields.Char(string='Email')
picking_iu_id = fields.Many2one('stock.picking', 'Picking IU')
helper_by_id = fields.Many2one('res.users', 'Helper By')
- eta_date = fields.Datetime(string='ETA Date', copy=False, compute='_compute_eta_date')
+ eta_date_start = fields.Datetime(string='ETA Date start', copy=False, compute='_compute_eta_date')
+ eta_date = fields.Datetime(string='ETA Date end', copy=False, compute='_compute_eta_date')
flash_sale = fields.Boolean(string='Flash Sale', help='Data dari web')
is_continue_transaction = fields.Boolean(string='Button Transaction', help='Data dari web')
web_approval = fields.Selection([
@@ -189,7 +191,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'
+ )
+ 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')
+
reason_cancel = fields.Selection([
('harga_terlalu_mahal', 'Harga barang terlalu mahal'),
('harga_web_tidak_valid', 'Harga web tidak valid'),
@@ -436,19 +448,77 @@ class SaleOrder(models.Model):
rec.compute_fullfillment = True
+ @api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start')
def _compute_eta_date(self):
- max_leadtime = 0
+ 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)
+ rec.eta_date_start = rec.date_order + timedelta(days=rec.estimated_arrival_days_start)
+ else:
+ rec.eta_date = False
+ rec.eta_date_start = False
+
+
+ def get_days_until_next_business_day(self,start_date=None, *args, **kwargs):
+ today = start_date or datetime.today().date()
+ offset = 0 # Counter jumlah hari yang ditambahkan
+ holiday = self.env['hr.public.holiday']
+
+ while True :
+ today += timedelta(days=1)
+ offset += 1
+
+ if today.weekday() >= 5:
+ continue
- for line in self.order_line:
- leadtime = line.vendor_id.leadtime
- max_leadtime = max(max_leadtime, leadtime)
+ is_holiday = holiday.search([("start_date", "=", today)])
+ if is_holiday:
+ continue
+
+ break
+
+ return offset
+ @api.depends("order_line.product_id")
+ def _compute_etrts_date(self): #Function to calculate Estimated Ready To Ship Date
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
- else:
- rec.eta_date = False
+ 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)
+ # if(product_sla == False):
+ # continue
+ slatime = int(product_sla.sla) if product_sla and product_sla.sla and product_sla.sla != 'Indent' and "hari" in product_sla.sla.lower() else 15
+ max_slatime = max(max_slatime, slatime)
+
+ if rec.date_order:
+ eta_date = rec.date_order + timedelta(days=self.get_days_until_next_business_day(rec.date_order)) + 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):
"""
@@ -643,15 +713,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):
@@ -1109,6 +1180,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()
@@ -1504,13 +1576,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()
@@ -1545,4 +1618,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 327389cd..7b70c2b9 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -3,6 +3,7 @@ from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools.float_utils import float_is_zero
from collections import defaultdict
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
@@ -13,12 +14,21 @@ 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_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo"
+
+
+
class StockPicking(models.Model):
_inherit = 'stock.picking'
scan_koli_lines = fields.One2many('scan.koli', 'picking_id', string='Scan Koli', auto_join=True)
check_koli_lines = fields.One2many('check.koli', 'picking_id', string='Check Koli', auto_join=True)
+ _order = 'final_seq ASC'
+
check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True)
barcode_product_lines = fields.One2many('barcode.product', 'picking_id', string='Barcode Product', auto_join=True)
is_internal_use = fields.Boolean('Internal Use', help='flag which is internal use or not')
@@ -177,6 +187,16 @@ class StockPicking(models.Model):
linked_out_picking_id = fields.Many2one('stock.picking', string="Linked BU/OUT", copy=False)
total_so_koli = fields.Integer(compute='_compute_total_so_koli', string="Total SO Koli")
+ # 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, related='sale_id.estimated_ready_ship_date')
+ # countdown_hours = fields.Float(string='Countdown in Hours', compute='_callculate_sequance', default=False, store=False, compute_sudo=False)
+ # countdown_ready_to_ship = fields.Char(string='Countdown Ready to Ship', compute='_callculate_sequance', store=False, compute_sudo=False)
+ final_seq = fields.Float(string='Remaining Time')
+
+
@api.depends('total_so_koli')
def _compute_total_so_koli(self):
for picking in self:
@@ -220,6 +240,58 @@ class StockPicking(models.Model):
'koli': f"{self.name}/{str(i+1).zfill(3)}",
'picking_id': self.id,
}) for i in range(int(self.quantity_koli))]
+
+ def schduled_update_sequance(self):
+ query = "SELECT update_sequance_stock_picking();"
+ self.env.cr.execute(query)
+
+
+ @api.depends('estimated_ready_ship_date', 'state')
+ def _callculate_sequance(self):
+ for record in self:
+ try :
+ if record.estimated_ready_ship_date and record.state not in ('cancel', 'done'):
+ rts = record.estimated_ready_ship_date - waktu.now()
+ rts_days = rts.days
+ rts_hours = divmod(rts.seconds, 3600)
+
+ estimated_by_erts = rts.total_seconds() / 3600
+
+ record.countdown_ready_to_ship = f"{rts_days} days, {rts_hours} hours"
+ record.countdown_hours = estimated_by_erts
+ else:
+ record.countdown_hours = 999999999999
+ record.countdown_ready_to_ship = False
+ except Exception as e :
+ _logger.error(f"Error calculating sequance {record.id}: {str(e)}")
+ print(str(e))
+ return { 'error': str(e) }
+
+
+ # @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:
@@ -376,7 +448,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
@@ -387,12 +459,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 [{
@@ -425,6 +498,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,
@@ -436,7 +510,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,
@@ -444,27 +519,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):
@@ -1159,11 +1249,14 @@ class StockPicking(models.Model):
def get_tracking_detail(self):
self.ensure_one()
+
+ order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1)
response = {
'delivery_order': {
'name': self.name,
'carrier': self.carrier_id.name or '',
+ 'service' : order.delivery_service_type or '',
'receiver_name': '',
'receiver_city': ''
},
@@ -1172,8 +1265,21 @@ 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()
+ eta_start = order.date_order + timedelta(days=order.estimated_arrival_days_start)
+ eta_end = order.date_order + timedelta(days=order.estimated_arrival_days)
+ formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}"
+ response['eta'] = formatted_eta
+ 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"))
+
+ 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
@@ -1186,6 +1292,77 @@ 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()
+ 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", "")
+ }
+ 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"]),
+ "description": description[entry["status"]],
+ })
+
+ 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..b052e6cb
--- /dev/null
+++ b/indoteknik_custom/models/vendor_sla.py
@@ -0,0 +1,102 @@
+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 stock pickings by vendor based on operation type
+ stock_picking_env = self.env['stock.picking']
+ stock_moves = stock_picking_env.read_group(
+ domain=[
+ ('state', '=', 'done'),
+ ('location_id', '=', 4), # Partner Locations/Vendors
+ ('location_dest_id', '=', 57) # BU/Stock
+ ],
+ fields=['partner_id', 'date_done', 'scheduled_date'],
+ groupby=['partner_id'],
+ lazy=False
+ )
+
+ for group in stock_moves:
+ partner_id = group['partner_id'][0]
+ total_duration = 0
+ count = 0
+
+ # Step 2: Calculate the average duration for each vendor
+ pos_for_vendor = stock_picking_env.search([
+ ('partner_id', '=', partner_id),
+ ('state', '=', 'done'),
+ ])
+
+ for po in pos_for_vendor:
+ if po.date_done and po.purchase_id.date_approve:
+ date_of_transfer = fields.Datetime.to_datetime(po.date_done)
+ po_confirmation_date = fields.Datetime.to_datetime(po.purchase_id.date_approve)
+ if date_of_transfer < po_confirmation_date: continue
+
+ days_difference = (date_of_transfer - po_confirmation_date).days
+ if days_difference > 14:
+ continue
+ duration = (date_of_transfer - po_confirmation_date).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 18077441..4e580d03 100755
--- a/indoteknik_custom/security/ir.model.access.csv
+++ b/indoteknik_custom/security/ir.model.access.csv
@@ -151,20 +151,25 @@ 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_check_koli,access.check.koli,model_check_koli,,1,1,1,1
access_scan_koli,access.scan.koli,model_scan_koli,,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
+<<<<<<< HEAD
access_sales_order_koli,access.sales.order.koli,model_sales_order_koli,,1,1,1,1
access_stock_backorder_confirmation,access.stock.backorder.confirmation,model_stock_backorder_confirmation,,1,1,1,1
access_warning_modal_wizard,access.warning.modal.wizard,model_warning_modal_wizard,,1,1,1,1
+=======
+>>>>>>> odoo-backup
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
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 8158774b..1a007b25 100755
--- a/indoteknik_custom/views/sale_order.xml
+++ b/indoteknik_custom/views/sale_order.xml
@@ -68,7 +68,10 @@
<field name="compute_fullfillment" invisible="1"/>
</field>
<field name="tag_ids" position="after">
+ <field name="eta_date_start"/>
+ <t t-esc="' to '"/>
<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 03fc8f87..144ed820 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">final_seq 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,9 @@
<field name="note" optional="hide"/>
<field name="date_reserved" optional="hide"/>
<field name="state_reserve" optional="hide"/>
+ <field name="final_seq"/>
+ <!-- <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>