diff options
| author | it-fixcomart <it@fixcomart.co.id> | 2024-12-31 09:44:11 +0700 |
|---|---|---|
| committer | it-fixcomart <it@fixcomart.co.id> | 2024-12-31 09:44:11 +0700 |
| commit | 8d00df73e76162d624d2f32eefdd47ca68ca154c (patch) | |
| tree | ef2b9706de3bbe895709502bf60506bca8f20a91 | |
| parent | 6a7b2e28c9c1612ac3e91ac321b72e3400fdb5a3 (diff) | |
| parent | d35c2dce88a87bc05d30c4935d51d7d58aa5d37d (diff) | |
Merge branch 'production' into iman/telegram
# Conflicts:
# indoteknik_custom/models/stock_picking.py
| -rw-r--r-- | indoteknik_api/controllers/api_v1/flash_sale.py | 2 | ||||
| -rw-r--r-- | indoteknik_api/controllers/controller.py | 30 | ||||
| -rw-r--r-- | indoteknik_api/models/res_users.py | 2 | ||||
| -rwxr-xr-x | indoteknik_custom/models/product_template.py | 52 | ||||
| -rwxr-xr-x | indoteknik_custom/models/purchase_order.py | 4 | ||||
| -rw-r--r-- | indoteknik_custom/models/requisition.py | 6 | ||||
| -rwxr-xr-x | indoteknik_custom/models/sale_order.py | 32 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_move.py | 21 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking.py | 151 | ||||
| -rw-r--r-- | indoteknik_custom/models/stock_picking_return.py | 14 | ||||
| -rw-r--r-- | indoteknik_custom/models/wati.py | 63 | ||||
| -rwxr-xr-x | indoteknik_custom/views/product_template.xml | 6 | ||||
| -rwxr-xr-x | indoteknik_custom/views/purchase_order.xml | 1 | ||||
| -rw-r--r-- | indoteknik_custom/views/res_partner.xml | 2 | ||||
| -rw-r--r-- | indoteknik_custom/views/stock_picking.xml | 38 |
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> |
