from odoo import fields, models, api, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.float_utils import float_is_zero from collections import defaultdict from datetime import timedelta, datetime from datetime import timedelta, datetime as waktu from itertools import groupby import pytz, requests, json, requests from dateutil import parser import datetime import hmac import hashlib import base64 import requests import time import logging import re from hashlib import sha256 _logger = logging.getLogger(__name__) Request_URI = '/openapi/order/v1/print' ACCESS_KEY = '24bb6a1ec618ec6a' SECRET_KEY = '32e4a78ad05ee230' class StockPicking(models.Model): _inherit = 'stock.picking' check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True, copy=False) order_reference = fields.Char('Order Reference') provider_name = fields.Char('Provider Name') tracking_number = fields.Char('Tracking Number') invoice_number = fields.Char('Invoice Number') pdf_label_url = fields.Char('PDF Label URL') invoice_mp = fields.Char(string='Invoice Marketplace') address = fields.Char('Address') note_by_buyer = fields.Char('Note By Buyer') carrier = fields.Char(string='Shipping Method') shipment_group_id = fields.Many2one('shipment.group', string='Shipment Group', copy=False) pdf_label_preview = fields.Binary( string="PDF Preview", compute="_compute_pdf_binary", store=False ) def _compute_pdf_binary(self): for record in self: record.pdf_label_preview = False if record.pdf_label_url: try: response = requests.get(record.pdf_label_url, timeout=10) if response.status_code == 200 and response.headers.get('Content-Type') == 'application/pdf': record.pdf_label_preview = base64.b64encode(response.content) except Exception as e: _logger.warning(f"Gagal mengambil PDF dari URL: {e}") @api.constrains('sale_id') def _check_sale_order(self): for picking in self: if picking.sale_id: picking.order_reference = picking.sale_id.name self.order_reference = picking.sale_id.order_reference self.invoice_mp = picking.sale_id.invoice_mp self.carrier = picking.sale_id.carrier self.address = picking.sale_id.address self.note_by_buyer = picking.sale_id.note_by_buyer def open_form_shipment_group(self): return { 'name': _('Create Shipment Group'), 'type': 'ir.actions.act_window', 'res_model': 'stock.picking.shipment_group', 'view_mode': 'form', 'target': 'new', 'context': { 'picking_ids': self.ids, } } def open_form_print_picking_list(self): return { 'name': _('Create Print Picking List'), 'type': 'ir.actions.act_window', 'res_model': 'stock.picking.print_picking_list', 'view_mode': 'form', 'target': 'new', 'context': { 'picking_ids': self.ids, } } def label_ginee(self): try: order_id = self.order_reference authorization = self.sign_request() headers = { 'Content-Type': 'application/json', 'X-Advai-Country': 'ID', 'Authorization': authorization } payload = { "orderId": order_id, "documentType": "LABEL" } url = "https://api.ginee.com/openapi/order/v1/print" response = requests.post( url, headers=headers, data=json.dumps(payload) ) if response.status_code == 200: data = response.json() if data.get('code') == 'SUCCESS' and data.get('message') == 'OK': logistic_info_list = data.get('data', {}).get('logisticsInfos') # Check if logistic_info exists and has at least one item if not logistic_info_list: raise UserError(_("No logistic information found in response")) logistic_info = logistic_info_list[0] self.pdf_label_url = data.get('data', {}).get('pdfUrl') or '' self.tracking_number = logistic_info.get('logisticsTrackingNumber') or '' self.provider_name = logistic_info.get('logisticsProviderName') or '' self.invoice_number = logistic_info.get('invoiceNumber') or '' else: raise UserError(_("API Error: %s - %s") % (data.get('code', 'UNKNOWN'), data.get('message', 'No error message'))) else: raise UserError(_("API request failed with status code: %s") % response.status_code) except Exception as e: raise UserError(_("Error: %s") % str(e)) def sign_request(self): signData = '$'.join(['POST', Request_URI]) + '$' authorization = ACCESS_KEY + ':' + base64.b64encode( hmac.new(SECRET_KEY.encode('utf-8'), signData.encode('utf-8'), digestmod=sha256).digest() ).decode('ascii') return authorization # def sync_qty_reserved_qty_done(self): # for picking in self: # for line in picking.move_line_ids_without_package: # line.qty_done = line.product_uom_qty # picking.button_validate() class CheckProduct(models.Model): _name = 'check.product' _description = 'Check Product' _order = 'picking_id, id' picking_id = fields.Many2one( 'stock.picking', string='Picking Reference', required=True, ondelete='cascade', index=True, copy=False, ) product_id = fields.Many2one('product.product', string='Product') quantity = fields.Float(string='Quantity') status = fields.Char(string='Status', compute='_compute_status') code_product = fields.Char(string='Code Product') @api.onchange('code_product') def _onchange_code_product(self): if not self.code_product: return # Cari product berdasarkan default_code, barcode, atau barcode_box product = self.env['product.product'].search([ '|', ('default_code', '=', self.code_product), '|', ('barcode', '=', self.code_product), ('barcode_box', '=', self.code_product) ], limit=1) if not product: raise UserError("Product tidak ditemukan") # Jika scan barcode_box, set quantity sesuai qty_pcs_box if product.barcode_box == self.code_product: self.product_id = product.id self.quantity = product.qty_pcs_box self.code_product = product.default_code or product.barcode # return { # 'warning': { # 'title': 'Info',8994175025871 # 'message': f'Product box terdeteksi. Quantity di-set ke {product.qty_pcs_box}' # } # } else: # Jika scan biasa self.product_id = product.id self.code_product = product.default_code or product.barcode self.quantity = 1 def unlink(self): # Get all affected pickings before deletion pickings = self.mapped('picking_id') # Store product_ids that will be deleted deleted_product_ids = self.mapped('product_id') # Perform the deletion result = super(CheckProduct, self).unlink() # After deletion, update moves for affected pickings for picking in pickings: # For products that were completely removed (no remaining check.product lines) remaining_product_ids = picking.check_product_lines.mapped('product_id') removed_product_ids = deleted_product_ids - remaining_product_ids # Set quantity_done to 0 for moves of completely removed products moves_to_reset = picking.move_ids_without_package.filtered( lambda move: move.product_id in removed_product_ids ) for move in moves_to_reset: move.quantity_done = 0.0 # Also sync remaining products in case their totals changed self._sync_check_product_to_moves(picking) return result @api.depends('quantity') def _compute_status(self): for record in self: moves = record.picking_id.move_ids_without_package.filtered( lambda move: move.product_id.id == record.product_id.id ) total_qty_in_moves = sum(moves.mapped('product_uom_qty')) if record.quantity < total_qty_in_moves: record.status = 'Pending' else: record.status = 'Done' def create(self, vals): # Create the record record = super(CheckProduct, self).create(vals) # Ensure uniqueness after creation if not self.env.context.get('skip_consolidate'): record.with_context(skip_consolidate=True)._consolidate_duplicate_lines() return record def write(self, vals): # Write changes to the record result = super(CheckProduct, self).write(vals) # Ensure uniqueness after writing if not self.env.context.get('skip_consolidate'): self.with_context(skip_consolidate=True)._consolidate_duplicate_lines() return result def _sync_check_product_to_moves(self, picking): """ Sinkronisasi quantity_done di move_ids_without_package dengan total quantity dari check.product berdasarkan product_id. """ for product_id in picking.check_product_lines.mapped('product_id'): # Totalkan quantity dari semua baris check.product untuk product_id ini total_quantity = sum( line.quantity for line in picking.check_product_lines.filtered(lambda line: line.product_id == product_id) ) # Update quantity_done di move yang relevan moves = picking.move_ids_without_package.filtered(lambda move: move.product_id == product_id) for move in moves: move.quantity_done = total_quantity def _consolidate_duplicate_lines(self): """ Consolidate duplicate lines with the same product_id under the same picking_id and sync the total quantity to related moves. """ for picking in self.mapped('picking_id'): lines_to_remove = self.env['check.product'] # Recordset untuk menyimpan baris yang akan dihapus product_lines = picking.check_product_lines.filtered(lambda line: line.product_id) # Group lines by product_id product_groups = {} for line in product_lines: product_groups.setdefault(line.product_id.id, []).append(line) for product_id, lines in product_groups.items(): if len(lines) > 1: # Consolidate duplicate lines first_line = lines[0] total_quantity = sum(line.quantity for line in lines) # Update the first line's quantity first_line.with_context(skip_consolidate=True).write({'quantity': total_quantity}) # Add the remaining lines to the lines_to_remove recordset lines_to_remove |= self.env['check.product'].browse([line.id for line in lines[1:]]) # Perform unlink after consolidation if lines_to_remove: lines_to_remove.unlink() # Sync total quantities to moves self._sync_check_product_to_moves(picking) @api.onchange('product_id', 'quantity') def check_product_validity(self): for record in self: if not record.picking_id or not record.product_id: continue # Filter moves related to the selected product moves = record.picking_id.move_ids_without_package.filtered( lambda move: move.product_id.id == record.product_id.id ) if not moves: raise UserError(( "The product '%s' tidak ada di operations. " ) % record.product_id.display_name) total_qty_in_moves = sum(moves.mapped('product_uom_qty')) # Find existing lines for the same product, excluding the current line existing_lines = record.picking_id.check_product_lines.filtered( lambda line: line.product_id == record.product_id ) if existing_lines: # Get the first existing line first_line = existing_lines[0] # Calculate the total quantity after addition total_quantity = sum(existing_lines.mapped('quantity')) if total_quantity > total_qty_in_moves: raise UserError(( "Quantity Product '%s' sudah melebihi quantity demand." ) % (record.product_id.display_name)) else: # Check if the quantity exceeds the allowed total if record.quantity == total_qty_in_moves: raise UserError(( "Quantity Product '%s' sudah melebihi quantity demand." ) % (record.product_id.display_name)) # Set the quantity to the entered value record.quantity = record.quantity