summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorit-fixcomart <it@fixcomart.co.id>2024-12-31 09:44:11 +0700
committerit-fixcomart <it@fixcomart.co.id>2024-12-31 09:44:11 +0700
commit8d00df73e76162d624d2f32eefdd47ca68ca154c (patch)
treeef2b9706de3bbe895709502bf60506bca8f20a91
parent6a7b2e28c9c1612ac3e91ac321b72e3400fdb5a3 (diff)
parentd35c2dce88a87bc05d30c4935d51d7d58aa5d37d (diff)
Merge branch 'production' into iman/telegram
# Conflicts: # indoteknik_custom/models/stock_picking.py
-rw-r--r--indoteknik_api/controllers/api_v1/flash_sale.py2
-rw-r--r--indoteknik_api/controllers/controller.py30
-rw-r--r--indoteknik_api/models/res_users.py2
-rwxr-xr-xindoteknik_custom/models/product_template.py52
-rwxr-xr-xindoteknik_custom/models/purchase_order.py4
-rw-r--r--indoteknik_custom/models/requisition.py6
-rwxr-xr-xindoteknik_custom/models/sale_order.py32
-rw-r--r--indoteknik_custom/models/stock_move.py21
-rw-r--r--indoteknik_custom/models/stock_picking.py151
-rw-r--r--indoteknik_custom/models/stock_picking_return.py14
-rw-r--r--indoteknik_custom/models/wati.py63
-rwxr-xr-xindoteknik_custom/views/product_template.xml6
-rwxr-xr-xindoteknik_custom/views/purchase_order.xml1
-rw-r--r--indoteknik_custom/views/res_partner.xml2
-rw-r--r--indoteknik_custom/views/stock_picking.xml38
15 files changed, 375 insertions, 49 deletions
diff --git a/indoteknik_api/controllers/api_v1/flash_sale.py b/indoteknik_api/controllers/api_v1/flash_sale.py
index 00b1f2e0..6c4ad8c0 100644
--- a/indoteknik_api/controllers/api_v1/flash_sale.py
+++ b/indoteknik_api/controllers/api_v1/flash_sale.py
@@ -20,6 +20,7 @@ class FlashSale(controller.Controller):
query = [
('pricelist_id', '=', pricelist.id)
]
+ formatted_end_date = pricelist.end_date.strftime('%Y-%m-%dT%H:%M:%S.%f')[:-3] + 'Z' if pricelist.end_date else None
data.append({
'pricelist_id': pricelist.id,
'option': pricelist.flashsale_option,
@@ -29,6 +30,7 @@ class FlashSale(controller.Controller):
'banner_mobile': request.env['ir.attachment'].api_image('product.pricelist', 'banner_mobile', pricelist.id),
'banner_top': request.env['ir.attachment'].api_image('product.pricelist', 'banner_top', pricelist.id),
'duration': pricelist._remaining_time_in_second(),
+ 'end_date': formatted_end_date,
'product_total': request.env['product.pricelist.item'].search_count(query),
})
return self.response(data)
diff --git a/indoteknik_api/controllers/controller.py b/indoteknik_api/controllers/controller.py
index a34a2688..80f45074 100644
--- a/indoteknik_api/controllers/controller.py
+++ b/indoteknik_api/controllers/controller.py
@@ -4,6 +4,7 @@ import functools
import io
import json
from array import array
+from io import BytesIO
import jwt
from odoo import http
@@ -11,6 +12,8 @@ from odoo.http import request
from odoo.modules import get_module_resource
from odoo.tools.config import config
from PIL import Image
+from PIL.WebPImagePlugin import Image
+from PIL import features
from pytz import timezone
@@ -204,6 +207,8 @@ class Controller(http.Controller):
if not variant:
image = self.add_watermark_to_image(image, ratio, version)
+ # image = self.convert_to_webp(image)
+
response_headers = [
('Content-Type', 'image/jpg'),
('Cache-Control', 'public, max-age=3600')
@@ -214,6 +219,31 @@ class Controller(http.Controller):
response_headers
)
+ def convert_to_webp(self, image_base64):
+ """Convert image from base64 to WebP format and return base64 WebP."""
+ try:
+ print(f"Image base64 length: {len(image_base64)}")
+
+ # Decode Base64 to Bytes
+ image_data = base64.b64decode(image_base64)
+ image = Image.open(BytesIO(image_data))
+
+ if image.format == "PNG" and image.mode != "RGBA":
+ image = image.convert("RGBA")
+
+ # Convert to WebP
+ with BytesIO() as output:
+ image.save(output, format="WEBP", quality=85)
+ webp_data = output.getvalue()
+
+ # Encode back to Base64
+ return base64.b64encode(webp_data).decode('utf-8')
+ except Exception as e:
+ print(f"Error details: {e}")
+ # If conversion fails, return the original image
+ request.env.cr.rollback() # Rollback any transactions
+ return image_base64
+
def add_watermark_to_image(self, image, ratio, version = '1'):
if not image: return ''
diff --git a/indoteknik_api/models/res_users.py b/indoteknik_api/models/res_users.py
index 52a044dc..77aeeef7 100644
--- a/indoteknik_api/models/res_users.py
+++ b/indoteknik_api/models/res_users.py
@@ -72,7 +72,7 @@ class ResUsers(models.Model):
data['state_id'] = {
'id': user.state_id.id,
'name': user.state_id.name
- } or None
+ } or 0
if user.kota_id:
data['city'] = {
diff --git a/indoteknik_custom/models/product_template.py b/indoteknik_custom/models/product_template.py
index 4d186568..9007dd71 100755
--- a/indoteknik_custom/models/product_template.py
+++ b/indoteknik_custom/models/product_template.py
@@ -5,7 +5,9 @@ import logging
import requests
import json
import re
+import qrcode, base64
from bs4 import BeautifulSoup
+from io import BytesIO
_logger = logging.getLogger(__name__)
@@ -58,15 +60,35 @@ class ProductTemplate(models.Model):
('sp', 'Spare Part'),
('acc', 'Accessories')
], string='Kind of', copy=False)
- sni = fields.Boolean(string='SNI')
+ sni = fields.Boolean(string='SNI')
tkdn = fields.Boolean(string='TKDN')
short_spesification = fields.Char(string='Short Spesification')
merchandise_ok = fields.Boolean(string='Product Promotion')
+ print_barcode = fields.Boolean(string='Print Barcode', default=True)
+ # qr_code = fields.Binary("QR Code", compute='_compute_qr_code')
+
+ # def _compute_qr_code(self):
+ # for rec in self.product_variant_ids:
+ # qr = qrcode.QRCode(
+ # version=1,
+ # error_correction=qrcode.constants.ERROR_CORRECT_L,
+ # box_size=5,
+ # border=4,
+ # )
+ # qr.add_data(rec.display_name)
+ # qr.make(fit=True)
+ # img = qr.make_image(fill_color="black", back_color="white")
+
+ # buffer = BytesIO()
+ # img.save(buffer, format="PNG")
+ # qr_code_img = base64.b64encode(buffer.getvalue()).decode()
+
+ # rec.qr_code = qr_code_img
@api.constrains('name', 'internal_reference', 'x_manufacture')
def required_public_categ_ids(self):
for rec in self:
- if not rec.public_categ_ids:
+ if not rec.public_categ_ids and rec.type == 'product':
raise UserError('Field Categories harus diisi')
def _get_qty_sold(self):
@@ -379,6 +401,30 @@ class ProductProduct(models.Model):
qty_rpo = fields.Float(string='Qty RPO', compute='_get_qty_rpo')
plafon_qty = fields.Float(string='Max Plafon', compute='_get_plafon_qty_product')
merchandise_ok = fields.Boolean(string='Product Promotion')
+ qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant')
+
+ def _compute_qr_code_variant(self):
+ for rec in self:
+ # Skip inactive variants
+ if not rec.active:
+ rec.qr_code_variant = False # Clear the QR Code for archived variants
+ continue
+
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
+ box_size=5,
+ border=4,
+ )
+ qr.add_data(rec.display_name)
+ qr.make(fit=True)
+ img = qr.make_image(fill_color="black", back_color="white")
+
+ buffer = BytesIO()
+ img.save(buffer, format="PNG")
+ qr_code_img = base64.b64encode(buffer.getvalue()).decode()
+
+ rec.qr_code_variant = qr_code_img
def _get_clean_website_description(self):
for rec in self:
@@ -388,7 +434,7 @@ class ProductProduct(models.Model):
@api.constrains('name', 'internal_reference', 'x_manufacture')
def required_public_categ_ids(self):
for rec in self:
- if not rec.public_categ_ids:
+ if not rec.public_categ_ids and rec.type == 'product':
raise UserError('Field Categories harus diisi')
@api.constrains('active')
diff --git a/indoteknik_custom/models/purchase_order.py b/indoteknik_custom/models/purchase_order.py
index 9388ae4c..0e39d12a 100755
--- a/indoteknik_custom/models/purchase_order.py
+++ b/indoteknik_custom/models/purchase_order.py
@@ -74,6 +74,7 @@ class PurchaseOrder(models.Model):
approve_by = fields.Many2one('res.users', string='Approve By')
exclude_incoming = fields.Boolean(string='Exclude Incoming', default=False,
help='Centang jika tidak mau masuk perhitungan Incoming Qty')
+ not_update_purchasepricelist = fields.Boolean(string='Not Update Purchase Pricelist?')
def _compute_total_margin_match(self):
for purchase in self:
@@ -620,7 +621,8 @@ class PurchaseOrder(models.Model):
raise UserError("Tidak ada link dengan SO, harus approval Merchandise")
send_email = False
- self.add_product_to_pricelist()
+ if not self.not_update_purchasepricelist:
+ self.add_product_to_pricelist()
for line in self.order_line:
if not line.product_id.purchase_ok:
raise UserError("Terdapat barang yang tidak bisa diproses")
diff --git a/indoteknik_custom/models/requisition.py b/indoteknik_custom/models/requisition.py
index 32a9f94f..c972b485 100644
--- a/indoteknik_custom/models/requisition.py
+++ b/indoteknik_custom/models/requisition.py
@@ -82,11 +82,11 @@ class Requisition(models.Model):
state = ['done', 'sale']
if self.sale_order_id.state in state:
raise UserError('SO sudah Confirm, akan berakibat double Purchase melalui PJ')
- if self.env.user.id not in [377, 19]:
+ if self.env.user.id not in [377, 19, 28]:
raise UserError('Hanya Vita dan Darren Yang Bisa Approve')
- if self.env.user.id == 377:
+ if self.env.user.id == 377 or self.env.user.id == 28:
self.sales_approve = True
- elif self.env.user.id == 19:
+ elif self.env.user.id == 19 or self.env.user.id == 28:
if not self.sales_approve:
raise UserError('Vita Belum Approve')
self.merchandise_approve = True
diff --git a/indoteknik_custom/models/sale_order.py b/indoteknik_custom/models/sale_order.py
index 7fc6d96a..f5e7e8a1 100755
--- a/indoteknik_custom/models/sale_order.py
+++ b/indoteknik_custom/models/sale_order.py
@@ -758,6 +758,7 @@ class SaleOrder(models.Model):
raise UserError("Salesperson sudah tidak aktif, mohon diisi yang benar pada data SO dan Contact")
def sale_order_approve(self):
+ self.check_credit_limit()
if self.validate_different_vendor() and not self.vendor_approval:
return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor')
self.check_due()
@@ -890,6 +891,37 @@ class SaleOrder(models.Model):
'email_to': salesperson_email,
}).send()
+ def check_credit_limit(self):
+ for rec in self:
+ outstanding_amount = rec.outstanding_amount
+ check_credit_limit = False
+ ######
+ block_stage = 0
+ if rec.partner_id.parent_id:
+ if rec.partner_id.parent_id.active_limit and rec.partner_id.parent_id.enable_credit_limit:
+ check_credit_limit = True
+ else:
+ if rec.partner_id.active_limit and rec.partner_id.enable_credit_limit:
+ check_credit_limit = True
+
+ term_days = 0
+ for term_line in rec.payment_term_id.line_ids:
+ term_days += term_line.days
+ if term_days == 0:
+ check_credit_limit = False
+
+ if check_credit_limit:
+ if rec.partner_id.parent_id:
+ block_stage = rec.partner_id.parent_id.blocking_stage or 0
+ else:
+ block_stage = rec.partner_id.blocking_stage or 0
+
+ if (outstanding_amount + rec.amount_total) >= block_stage:
+ if block_stage != 0:
+ remaining_credit_limit = block_stage - outstanding_amount
+ raise UserError(_("%s is in Blocking Stage, Remaining credit limit is %s, from %s and outstanding %s")
+ % (rec.partner_id.name, remaining_credit_limit, block_stage, outstanding_amount))
+
def validate_different_vendor(self):
if self.vendor_approval_id.filtered(lambda v: v.state == 'draft'):
draft_names = ", ".join(self.vendor_approval_id.filtered(lambda v: v.state == 'draft').mapped('number'))
diff --git a/indoteknik_custom/models/stock_move.py b/indoteknik_custom/models/stock_move.py
index ac2e3cc0..e1d4e74c 100644
--- a/indoteknik_custom/models/stock_move.py
+++ b/indoteknik_custom/models/stock_move.py
@@ -7,6 +7,27 @@ class StockMove(models.Model):
line_no = fields.Integer('No', default=0)
sale_id = fields.Many2one('sale.order', string='SO')
+ print_barcode = fields.Boolean(
+ string="Print Barcode",
+ default=lambda self: self.product_id.print_barcode,
+ )
+ qr_code_variant = fields.Binary("QR Code Variant", compute='_compute_qr_code_variant')
+
+ def _compute_qr_code_variant(self):
+ for rec in self:
+ if rec.print_barcode and rec.print_barcode == True and rec.product_id and rec.product_id.qr_code_variant:
+ rec.qr_code_variant = rec.product_id.qr_code_variant
+ else:
+ rec.qr_code_variant = False
+
+
+ def write(self, vals):
+ res = super(StockMove, self).write(vals)
+ if 'print_barcode' in vals:
+ for line in self:
+ if line.product_id:
+ line.product_id.print_barcode = vals['print_barcode']
+ return res
def _do_unreserve(self, product=None, quantity=False):
moves_to_unreserve = OrderedSet()
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 31c45531..e6506a0b 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -1,10 +1,11 @@
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 datetime
+from datetime import timedelta, datetime
from itertools import groupby
-import pytz, datetime, requests, json
-from odoo.http import request
+import pytz, requests, json, requests
+from dateutil import parser
+import datetime
class StockPicking(models.Model):
@@ -105,7 +106,7 @@ class StockPicking(models.Model):
('no', 'Nothing to Invoice')
], string='Invoice Status', related="sale_id.invoice_status")
note_return = fields.Text(string="Note Return", help="Catatan untuk kirim barang kembali")
-
+
state_reserve = fields.Selection([
('waiting', 'Waiting For Fullfilment'),
('ready', 'Ready to Ship'),
@@ -114,6 +115,120 @@ class StockPicking(models.Model):
], string='Status Reserve', readonly=True, tracking=True, help="The current state of the stock picking.")
notee = fields.Text(string="Note")
+ # Envio Tracking Section
+ envio_id = fields.Char(string="Envio ID", readonly=True)
+ envio_code = fields.Char(string="Envio Code", readonly=True)
+ envio_ref_code = fields.Char(string="Envio Reference Code", readonly=True)
+ envio_eta_at = fields.Datetime(string="Estimated Time of Arrival (ETA)", readonly=True)
+ envio_ata_at = fields.Datetime(string="Actual Time of Arrival (ATA)", readonly=True)
+ envio_etd_at = fields.Datetime(string="Estimated Time of Departure (ETD)", readonly=True)
+ envio_atd_at = fields.Datetime(string="Actual Time of Departure (ATD)", readonly=True)
+ envio_received_by = fields.Char(string="Received By", readonly=True)
+ envio_status = fields.Char(string="Status", readonly=True)
+ envio_cod_value = fields.Float(string="COD Value", readonly=True)
+ envio_cod_status = fields.Char(string="COD Status", readonly=True)
+ envio_logs = fields.Text(string="Logs", readonly=True)
+ envio_latest_message = fields.Text(string="Latest Log Message", readonly=True)
+ envio_latest_recorded_at = fields.Datetime(string="Log Recorded At", readonly=True)
+ envio_latest_latitude = fields.Float(string="Log Latitude", readonly=True)
+ envio_latest_longitude = fields.Float(string="Log Longitude", readonly=True)
+ tracking_by = fields.Many2one('res.users', string='Tracking By', readonly=True, tracking=True)
+
+ def _convert_to_wib(self, date_str):
+ """
+ Mengonversi string waktu ISO 8601 ke format waktu Indonesia (WIB)
+ """
+ if not date_str:
+ return False
+ try:
+ utc_time = datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%SZ')
+ wib_time = utc_time + timedelta(hours=7)
+ return wib_time.strftime('%d-%m-%Y %H:%M:%S')
+ except ValueError:
+ raise UserError(f"Format waktu tidak sesuai: {date_str}")
+
+ def _convert_to_datetime(self, date_str):
+ """Mengonversi string waktu dari API ke datetime."""
+ if not date_str:
+ return False
+ try:
+ # Format waktu dengan milidetik
+ date = datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%S.%fZ')
+ return date
+ except ValueError:
+ try:
+ # Format waktu tanpa milidetik
+ date = datetime.datetime.strptime(date_str, '%Y-%m-%dT%H:%M:%SZ')
+ return date
+ except ValueError:
+ raise UserError(f"Format waktu tidak sesuai: {date_str}")
+
+ def track_envio_shipment(self):
+ pickings = self.env['stock.picking'].search([
+ ('picking_type_code', '=', 'outgoing'),
+ ('state', '=', 'done'),
+ ('carrier_id', '=', 151)
+ ])
+ for picking in pickings:
+ if not picking.name:
+ raise UserError("Name pada stock.picking tidak ditemukan.")
+
+ # API URL dan headers
+ url = f"https://api.envio.co.id/v1/tracking/distribution?code={picking.name}"
+ headers = {
+ 'Authorization': 'Bearer JZ0Seh6qpYJAC3CJHdhF7sPqv8B/uSSfZe1VX5BL?vPYdo',
+ 'Content-Type': 'application/json',
+ }
+
+ try:
+ # Request ke API
+ response = requests.get(url, headers=headers, timeout=10)
+ response.raise_for_status() # Raise error jika status code bukan 200
+ response_data = response.json()
+
+ # Validasi jika respons tidak sesuai format yang diharapkan
+ if not response_data or "data" not in response_data:
+ raise UserError("Respons API tidak sesuai format yang diharapkan.")
+
+ data = response_data.get("data")
+ if not data:
+ continue
+
+ # Menyimpan data ke field masing-masing
+ picking.envio_id = data.get("id")
+ picking.envio_code = data.get("code")
+ picking.envio_ref_code = data.get("ref_code")
+ picking.envio_eta_at = self._convert_to_datetime(data.get("eta_at"))
+ picking.envio_ata_at = self._convert_to_datetime(data.get("ata_at"))
+ picking.envio_etd_at = self._convert_to_datetime(data.get("etd_at"))
+ picking.envio_atd_at = self._convert_to_datetime(data.get("atd_at"))
+ picking.envio_received_by = data.get("received_by")
+ picking.envio_status = data.get("status")
+ picking.envio_cod_value = data.get("cod_value", 0.0)
+ picking.envio_cod_status = data.get("cod_status")
+
+ # Menyimpan log terbaru
+ logs = data.get("logs", [])
+ if logs and isinstance(logs, list) and logs[0]:
+ latest_log = logs[0]
+ picking.envio_latest_message = latest_log.get("message", "Log kosong.")
+ picking.envio_latest_recorded_at = self._convert_to_datetime(latest_log.get("recorded_at"))
+ picking.envio_latest_latitude = latest_log.get("latitude", 0.0)
+ picking.envio_latest_longitude = latest_log.get("longitude", 0.0)
+
+ 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
+ picking.envio_ata_at = False
+ except requests.exceptions.RequestException as e:
+ raise UserError(f"Terjadi kesalahan saat menghubungi API Envio: {str(e)}")
+ except Exception as e:
+ 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"
@@ -142,7 +257,7 @@ class StockPicking(models.Model):
('order_id', '=', self.sale_id.id),
('product_id', '=', move_line.product_id.id)
], limit=1)
-
+
if order_line:
items_data_instant.append({
"name": order_line.product_id.name,
@@ -193,10 +308,10 @@ class StockPicking(models.Model):
return response.json()
else:
raise UserError(f"Error saat mengirim ke Biteship: {response.content}")
-
+
@api.constrains('driver_departure_date')
def constrains_driver_departure_date(self):
- if not self.date_doc_kirim:
+ if not self.date_doc_kirim:
self.date_doc_kirim = self.driver_departure_date
@api.constrains('arrival_time')
@@ -229,7 +344,7 @@ class StockPicking(models.Model):
current_time = datetime.datetime.utcnow()
self.date_unreserve = current_time
# self.check_state_reserve()
-
+
return res
# def check_state_reserve(self):
@@ -241,7 +356,7 @@ class StockPicking(models.Model):
# for rec in do:
# rec.state_reserve = 'ready'
# rec.date_reserved = datetime.datetime.utcnow()
-
+
# for line in rec.move_ids_without_package:
# if line.product_uom_qty > line.reserved_availability:
# rec.state_reserve = 'waiting'
@@ -253,19 +368,19 @@ class StockPicking(models.Model):
('state', 'not in', ['cancel', 'draft', 'done']),
('picking_type_code', '=', 'outgoing')
])
-
+
for picking in pickings:
fullfillments = self.env['sales.order.fullfillment'].search([
('sales_order_id', '=', picking.sale_id.id)
])
-
+
picking.state_reserve = 'ready'
picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow()
-
+
if any(rec.reserved_from not in ['Inventory On Hand', 'Reserved from stock', 'Free Stock'] for rec in fullfillments):
picking.state_reserve = 'waiting'
picking.date_reserved = ''
-
+
def _create_approval_notification(self, approval_role):
title = 'Warning'
message = f'Butuh approval sales untuk unreserved'
@@ -470,7 +585,7 @@ class StockPicking(models.Model):
raise UserError('Harus Purchasing yang Ask Return')
def calculate_line_no(self):
-
+
for picking in self:
name = picking.group_id.name
for move in picking.move_ids_without_package:
@@ -524,7 +639,7 @@ class StockPicking(models.Model):
and quant.inventory_quantity < line.product_uom_qty
):
raise UserError('Quantity reserved lebih besar dari quantity onhand di product')
-
+
def check_qty_done_stock(self):
for line in self.move_line_ids_without_package:
def check_qty_per_inventory(self, product, location):
@@ -537,16 +652,16 @@ class StockPicking(models.Model):
return quant.quantity
return 0
-
+
qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id)
if line.qty_done > qty_onhand:
- raise UserError(f'{line.product_id.display_name} : Quantity Done melebihi Quantity Onhand')
+ raise UserError('Quantity Done melebihi Quantity Onhand')
def button_validate(self):
if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking':
if self.origin and 'Return of' in self.origin:
raise UserError("Button ini hanya untuk Logistik")
-
+
if self.picking_type_code == 'internal':
self.check_qty_done_stock()
diff --git a/indoteknik_custom/models/stock_picking_return.py b/indoteknik_custom/models/stock_picking_return.py
index 91a3a9fd..d4347235 100644
--- a/indoteknik_custom/models/stock_picking_return.py
+++ b/indoteknik_custom/models/stock_picking_return.py
@@ -14,14 +14,14 @@ class ReturnPicking(models.TransientModel):
('id', '=', res['picking_id']),
])
- sale_id = stock_picking.group_id.sale_id
- if not stock_picking.approval_return_status == 'approved' and sale_id.invoice_ids:
+ # sale_id = stock_picking.group_id.sale_id
+ if not stock_picking.approval_return_status == 'approved':
raise UserError('Harus Approval Accounting AR untuk melakukan Retur')
- purchase = self.env['purchase.order'].search([
- ('name', '=', stock_picking.group_id.name),
- ])
- if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids:
- raise UserError('Harus Approval Accounting AP untuk melakukan Retur')
+ # purchase = self.env['purchase.order'].search([
+ # ('name', '=', stock_picking.group_id.name),
+ # ])
+ # if not stock_picking.approval_return_status == 'approved' and purchase.invoice_ids:
+ # raise UserError('Harus Approval Accounting AP untuk melakukan Retur')
return res \ No newline at end of file
diff --git a/indoteknik_custom/models/wati.py b/indoteknik_custom/models/wati.py
index f3632334..a0619f83 100644
--- a/indoteknik_custom/models/wati.py
+++ b/indoteknik_custom/models/wati.py
@@ -32,28 +32,43 @@ class WatiNotification(models.Model):
]).unlink()
_logger.info('Success Cleanup WATI Notification')
- def _parse_notification(self, limit = 0):
+ def _parse_notification(self, limit=0):
domain = [('is_parsed', '=', False)]
notifications = self.search(domain, order='id', limit=limit)
notification_not_parsed_count = self.search_count(domain)
i = 0
for notification in notifications:
i += 1
- _logger.info('[Parse Notification][%s] Process: %s/%s | Not Parsed: %s' % (notification.id, i, str(limit), str(notification_not_parsed_count)))
+ _logger.info('[Parse Notification][%s] Process: %s/%s | Not Parsed: %s' %
+ (notification.id, i, str(limit), str(notification_not_parsed_count)))
+
notification_json = json.loads(notification.json_raw)
sender_name = 'Indoteknik'
if 'senderName' in notification_json:
sender_name = notification_json['senderName']
- ticket_id = notification_json['ticketId']
- date_wati = float(notification_json['timestamp'])
- date_wati = datetime.fromtimestamp(date_wati)
+ ticket_id = notification_json.get('ticketId')
+ timestamp = notification_json.get('timestamp')
+
+ if not timestamp:
+ _logger.warning('[Parse Notification][%s] Missing timestamp in notification JSON: %s' %
+ (notification.id, notification.json_raw))
+ continue # Skip this notification
+
+ try:
+ date_wati = datetime.fromtimestamp(float(timestamp))
+ except ValueError as e:
+ _logger.error('[Parse Notification][%s] Invalid timestamp format: %s. Error: %s' %
+ (notification.id, timestamp, str(e)))
+ continue
+
wati_history = self.env['wati.history'].search([('ticket_id', '=', ticket_id)], limit=1)
if wati_history:
self._create_wati_history_line(wati_history, ticket_id, sender_name, notification_json, date_wati)
else:
new_header = self._create_wati_history_header(ticket_id, sender_name, notification_json, date_wati)
self._create_wati_history_line(new_header, ticket_id, sender_name, notification_json, date_wati)
+
notification.is_parsed = True
return
@@ -217,26 +232,42 @@ class WatiHistory(models.Model):
limit = 50
wati_histories = self.env['wati.history'].search(domain, limit=limit)
count = 0
+
for wati_history in wati_histories:
count += 1
- _logger.info('[Parse Notification] Process: %s/%s' % (str(count), str(limit)))
+ _logger.info('[Parse Notification] Processing: %s/%s', count, limit)
wati_api = self.env['wati.api']
- # Perbaikan pada params 'attribute' untuk menghindari masalah "type object is not subscriptable"
+ # Perbaikan pada parameter JSON
params = {
'pageSize': 1,
'pageNumber': 1,
- 'attribute': json.dumps([{'name': "phone", 'operator': "contain", 'value': wati_history.wa_id}]),
+ 'attribute': json.dumps([
+ {'name': "phone", 'operator': "contain", 'value': wati_history.wa_id}
+ ]),
}
- wati_contacts = wati_api.http_get('/api/v1/getContacts', params)
+ try:
+ wati_contacts = wati_api.http_get('/api/v1/getContacts', params)
+ except Exception as e:
+ _logger.error('Error while calling WATI API: %s', str(e))
+ continue
+ # Validasi respons dari API
+ if not isinstance(wati_contacts, dict):
+ _logger.error('Invalid response format from WATI API: %s', wati_contacts)
+ continue
+
if wati_contacts.get('result') != 'success':
- return
+ _logger.warning('WATI API request failed with result: %s', wati_contacts.get('result'))
+ continue
contact_list = wati_contacts.get('contact_list', [])
-
+ if not contact_list:
+ _logger.info('No contacts found for WA ID: %s', wati_history.wa_id)
+ continue
+
perusahaan = email = ''
for data in contact_list:
custom_params = data.get('customParams', [])
@@ -247,12 +278,14 @@ class WatiHistory(models.Model):
perusahaan = value
elif name == 'email':
email = value
- # End inner loop
# Update wati_history fields
- wati_history.perusahaan = perusahaan
- wati_history.email = email
- wati_history.is_get_attribute = True
+ wati_history.write({
+ 'perusahaan': perusahaan,
+ 'email': email,
+ 'is_get_attribute': True,
+ })
+ _logger.info('Wati history updated: %s', wati_history.id)
# @api.onchange('last_reply_date')
# def _compute_expired_date(self):
diff --git a/indoteknik_custom/views/product_template.xml b/indoteknik_custom/views/product_template.xml
index b6599137..af21984a 100755
--- a/indoteknik_custom/views/product_template.xml
+++ b/indoteknik_custom/views/product_template.xml
@@ -20,6 +20,7 @@
<field name="unpublished" />
<field name="desc_update_solr" readonly="1" />
<field name="last_update_solr" readonly="1" />
+ <!-- <field name="qr_code" widget="image" invisible="1"/> -->
</field>
<field name="public_categ_ids" position="attributes">
<attribute name="required">0</attribute>
@@ -29,6 +30,10 @@
<field name="merchandise_ok"/>
<label for="merchandise_ok"/>
</div>
+ <div>
+ <field name="print_barcode"/>
+ <label for="print_barcode"/>
+ </div>
</div>
<field name="public_categ_ids" position="attributes">
<attribute name="options">{'no_create': True}</attribute>
@@ -58,6 +63,7 @@
<field name="arch" type="xml">
<field name="last_update_solr" position="after">
<field name="clean_website_description" />
+ <field name="qr_code_variant" widget="image" readonly="True"/>
</field>
</field>
</record>
diff --git a/indoteknik_custom/views/purchase_order.xml b/indoteknik_custom/views/purchase_order.xml
index 0e6b6792..d22c3b5c 100755
--- a/indoteknik_custom/views/purchase_order.xml
+++ b/indoteknik_custom/views/purchase_order.xml
@@ -41,6 +41,7 @@
</field>
<field name="approval_status" position="after">
<field name="revisi_po"/>
+ <field name="not_update_purchasepricelist"/>
</field>
<field name="incoterm_id" position="after">
<field name="amount_total_without_service"/>
diff --git a/indoteknik_custom/views/res_partner.xml b/indoteknik_custom/views/res_partner.xml
index 712ebdd2..472569eb 100644
--- a/indoteknik_custom/views/res_partner.xml
+++ b/indoteknik_custom/views/res_partner.xml
@@ -76,7 +76,7 @@
<field name="site_id" attrs="{'readonly': [('parent_id', '=', False)]}" domain="[('partner_id', '=', main_parent_id)]" context="{'default_partner_id': active_id}" />
</xpath>
<xpath expr="//field[@name='property_payment_term_id']" position="attributes">
- <attribute name="readonly">1</attribute>
+ <attribute name="readonly">0</attribute>
</xpath>
<xpath expr="//field[@name='property_supplier_payment_term_id']" position="attributes">
<attribute name="readonly">1</attribute>
diff --git a/indoteknik_custom/views/stock_picking.xml b/indoteknik_custom/views/stock_picking.xml
index 1893fcaf..fab83885 100644
--- a/indoteknik_custom/views/stock_picking.xml
+++ b/indoteknik_custom/views/stock_picking.xml
@@ -56,6 +56,11 @@
string="Biteship"
type="object"
/>
+ <button name="track_envio_shipment"
+ string="Tracking Envio"
+ type="object"
+ attrs="{'invisible': [('carrier_id', '!=', 151)]}"
+ />
</button>
<field name="backorder_id" position="after">
<field name="summary_qty_detail"/>
@@ -115,6 +120,8 @@
</field>
<field name="product_uom" position="after">
<field name="sale_id" attrs="{'readonly': 1}" optional="hide"/>
+ <field name="print_barcode" optional="hide"/>
+ <field name="qr_code_variant" widget="image" optional="hide"/>
</field>
<page name="note" position="after">
<page string="E-Faktur" name="efaktur" attrs="{'invisible': [['is_internal_use', '=', False]]}">
@@ -143,13 +150,44 @@
<field name="sj_documentation" widget="image" />
<field name="paket_documentation" widget="image" />
</group>
+ <group>
+ <field name="envio_id" invisible="1"/>
+ <field name="envio_code"/>
+ <field name="envio_ref_code"/>
+ <field name="envio_eta_at"/>
+ <field name="envio_ata_at"/>
+ <field name="envio_etd_at" invisible="1"/>
+ <field name="envio_atd_at" invisible="1"/>
+ <field name="envio_received_by"/>
+ <field name="envio_status"/>
+ <field name="envio_cod_value" invisible="1"/>
+ <field name="envio_cod_status" invisible="1"/>
+ <field name="envio_latest_message"/>
+ <field name="envio_latest_recorded_at"/>
+ <field name="envio_latest_latitude" invisible="1"/>
+ <field name="envio_latest_longitude" invisible="1"/>
+ <field name="tracking_by" invisible="1"/>
+ </group>
</group>
</page>
+ <!-- <page string="Check Product" name="check_product">
+ <field name="check_product_lines"/>
+ </page> -->
</page>
</field>
</record>
+ <!-- <record id="check_product_tree" model="ir.ui.view">
+ <field name="name">check.product.tree</field>
+ <field name="model">check.product</field>
+ <field name="arch" type="xml">
+ <tree editable="bottom">
+ <field name="product_id"/>
+ </tree>
+ </field>
+ </record> -->
+
<record id="view_stock_move_line_detailed_operation_tree_inherit" model="ir.ui.view">
<field name="name">stock.move.line.operations.tree.inherit</field>
<field name="model">stock.move.line</field>