diff options
| -rw-r--r-- | indoteknik_api/controllers/api_v1/product.py | 90 | ||||
| -rw-r--r-- | indoteknik_api/controllers/api_v1/stock_picking.py | 42 | ||||
| -rwxr-xr-x | indoteknik_custom/__manifest__.py | 2 | ||||
| -rwxr-xr-x | indoteknik_custom/models/__init__.py | 2 | ||||
| -rw-r--r-- | indoteknik_custom/models/product_sla.py | 133 | ||||
| -rwxr-xr-x | indoteknik_custom/models/product_template.py | 12 | ||||
| -rw-r--r-- | indoteknik_custom/models/public_holiday.py | 11 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 75 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 166 | ||||
| -rw-r--r-- | indoteknik_custom/models/vendor_sla.py | 95 | ||||
| -rwxr-xr-x | indoteknik_custom/security/ir.model.access.csv | 5 | ||||
| -rw-r--r-- | indoteknik_custom/views/product_product.xml | 8 | ||||
| -rw-r--r-- | indoteknik_custom/views/product_sla.xml | 7 | ||||
| -rw-r--r-- | indoteknik_custom/views/public_holiday.xml | 55 | ||||
| -rwxr-xr-x | indoteknik_custom/views/sale_order.xml | 1 | ||||
| -rw-r--r-- | indoteknik_custom/views/stock_picking.xml | 5 | ||||
| -rw-r--r-- | indoteknik_custom/views/vendor_sla.xml | 42 | ||||
| -rwxr-xr-x | indoteknik_custom/views/x_banner_category.xml | 2 |
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> |
