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 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 _logger = logging.getLogger(__name__) class StockPicking(models.Model): _inherit = 'stock.picking' # check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True) is_internal_use = fields.Boolean('Internal Use', help='flag which is internal use or not') account_id = fields.Many2one('account.account', string='Account') efaktur_id = fields.Many2one('vit.efaktur', string='Faktur Pajak') is_efaktur_exported = fields.Boolean(string='Is eFaktur Exported') date_efaktur_exported = fields.Datetime(string='eFaktur Exported Date') delivery_status = fields.Char(string='Delivery Status', compute='compute_delivery_status', readonly=True) summary_qty_detail = fields.Float('Total Qty Detail', compute='_compute_summary_qty') summary_qty_operation = fields.Float('Total Qty Operation', compute='_compute_summary_qty') count_line_detail = fields.Float('Total Item Detail', compute='_compute_summary_qty') count_line_operation = fields.Float('Total Item Operation', compute='_compute_summary_qty') real_shipping_id = fields.Many2one( 'res.partner', string='Real Delivery Address', domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Dipakai untuk alamat tempel") # Delivery Order driver_departure_date = fields.Datetime( string='Delivery Departure Date', copy=False ) arrival_time = fields.Datetime( string='Jam Kedatangan', copy=False ) driver_arrival_date = fields.Datetime( string='Delivery Arrival Date', copy=False ) delivery_tracking_no = fields.Char( string='Delivery Tracking Number', readonly=True, copy=False ) driver_id = fields.Many2one( comodel_name='res.users', string='Driver', readonly=True, copy=False ) picking_code = fields.Char( string="Picking Code", readonly=True, copy=False ) sj_documentation = fields.Binary(string="Dokumentasi Surat Jalan", ) paket_documentation = fields.Binary(string="Dokumentasi Paket", ) sj_return_date = fields.Datetime(string="SJ Return Date", ) responsible = fields.Many2one('res.users', string='Responsible', tracking=True) approval_status = fields.Selection([ ('pengajuan1', 'Approval Accounting'), ('approved', 'Approved'), ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Internal Use") approval_receipt_status = fields.Selection([ ('pengajuan1', 'Approval Logistic'), ('approved', 'Approved'), ], string='Approval Receipt Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Receipt") approval_return_status = fields.Selection([ ('pengajuan1', 'Approval Finance'), ('approved', 'Approved'), ], string='Approval Return Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Return") date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ', help="Tanggal Kirim di cetakan SJ, tidak berpengaruh ke Accounting", tracking=True) note_logistic = fields.Selection([ ('hold', 'Hold by Sales'), ('not_paid', 'Customer belum bayar'), ('partial', 'Kirim Parsial'), ('indent', 'Indent'), ('self_pickup', 'Barang belum di pickup Customer'), ('expedition_closed', 'Eskpedisi belum buka') ], string='Note Logistic', help='jika field ini diisi maka tidak akan dihitung ke lead time') waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill') purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id', string="Purchase Representative") carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method') shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status") date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya') status_printed = fields.Selection([ ('not_printed', 'Belum Print'), ('printed', 'Printed') ], string='Printed?', copy=False, tracking=True) date_printed_sj = fields.Datetime(string='Status Printed Surat Jalan', copy=False, tracking=True) date_printed_list = fields.Datetime(string='Status Printed Picking List', copy=False, tracking=True) date_unreserve = fields.Datetime(string="Date Unreserved", copy=False, tracking=True) date_availability = fields.Datetime(string="Date Availability", copy=False, tracking=True) sale_order = fields.Char(string='Matches SO', copy=False) printed_sj = fields.Boolean('Printed Surat Jalan', help='flag which is internal use or not') printed_sj_retur = fields.Boolean('Printed Surat Jalan Retur', help='flag which is internal use or not') date_printed_sj_retur = fields.Datetime(string='Status Printed Surat Jalan Retur', copy=False, tracking=True) invoice_status = fields.Selection([ ('upselling', 'Upselling Opportunity'), ('invoiced', 'Fully Invoiced'), ('to invoice', 'To Invoice'), ('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'), ('done', 'Done'), ('cancel', 'Cancelled'), ], string='Status Reserve', readonly=True, tracking=True, help="The current state of the stock picking.") notee = fields.Text(string="Note") @api.model def _compute_dokumen_tanda_terima(self): for picking in self: picking.dokumen_tanda_terima = picking.partner_id.dokumen_pengiriman @api.model def _compute_dokumen_pengiriman(self): for picking in self: picking.dokumen_pengiriman = picking.partner_id.dokumen_pengiriman_input dokumen_tanda_terima = fields.Char(string='Dokumen Tanda Terima yang Diberikan Pada Saat Pengiriman Barang', readonly=True, compute=_compute_dokumen_tanda_terima) dokumen_pengiriman = fields.Char(string='Dokumen yang Dibawa Saat Pengiriman Barang', readonly=True, compute=_compute_dokumen_pengiriman) # 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) # Lalamove Section lalamove_order_id = fields.Char(string="Lalamove Order ID", copy=False) lalamove_address = fields.Char(string="Lalamove Address") lalamove_name = fields.Char(string="Lalamove Name") lalamove_phone = fields.Char(string="Lalamove Phone") lalamove_status = fields.Char(string="Lalamove Status") lalamove_delivered_at = fields.Datetime(string="Lalamove Delivered At") lalamove_data = fields.Text(string="Lalamove Data", readonly=True) lalamove_image_url = fields.Char(string="Lalamove Image URL") lalamove_image_html = fields.Html(string="Lalamove Image", compute="_compute_lalamove_image_html") def _compute_lalamove_image_html(self): for record in self: if record.lalamove_image_url: record.lalamove_image_html = f'' else: record.lalamove_image_html = "No image available." def action_fetch_lalamove_order(self): pickings = self.env['stock.picking'].search([ ('picking_type_code', '=', 'outgoing'), ('state', '=', 'done'), ('carrier_id', '=', 9) ]) for picking in pickings: try: order_id = picking.lalamove_order_id apikey = self.env['ir.config_parameter'].sudo().get_param('lalamove.apikey') secret = self.env['ir.config_parameter'].sudo().get_param('lalamove.secret') market = self.env['ir.config_parameter'].sudo().get_param('lalamove.market', default='ID') order_data = picking.get_lalamove_order(order_id, apikey, secret, market) picking.lalamove_data = order_data except Exception as e: _logger.error(f"Error fetching Lalamove order for picking {picking.id}: {str(e)}") continue def get_lalamove_order(self, order_id, apikey, secret, market): timestamp = str(int(time.time() * 1000)) message = f"{timestamp}\r\nGET\r\n/v3/orders/{order_id}\r\n\r\n" signature = hmac.new(secret.encode('utf-8'), message.encode('utf-8'), hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "Authorization": f"hmac {apikey}:{timestamp}:{signature}", "Market": market } url = f"https://rest.lalamove.com/v3/orders/{order_id}" response = requests.get(url, headers=headers) if response.status_code == 200: data = response.json() stops = data.get("data", {}).get("stops", []) for stop in stops: pod = stop.get("POD", {}) if pod.get("status") == "DELIVERED": image_url = pod.get("image") # Sesuaikan jika key berbeda self.lalamove_image_url = image_url address = stop.get("address") name = stop.get("name") phone = stop.get("phone") delivered_at = pod.get("deliveredAt") delivered_at_dt = self._convert_to_datetime(delivered_at) self.lalamove_address = address self.lalamove_name = name self.lalamove_phone = phone self.lalamove_status = pod.get("status") self.lalamove_delivered_at = delivered_at_dt return data raise UserError("No delivered data found in Lalamove response.") else: raise UserError(f"Error {response.status_code}: {response.text}") 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" # 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 [{ "name": line.product_id.name, "description": line.name, "value": line.price_unit, "quantity": line.product_uom_qty, "weight": line.weight } for line in lines] # Items untuk pengiriman standard items_data_standard = build_items_data(products) # Items untuk pengiriman instant, mengambil product_id dari move_line_ids_without_package items_data_instant = [] for move_line in self.move_line_ids_without_package: # Mencari baris di sale.order.line berdasarkan product_id dari move_line order_line = self.env['sale.order.line'].search([ ('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, "description": order_line.name, "value": order_line.price_unit, "quantity": move_line.qty_done, # Menggunakan qty_done dari move_line "weight": order_line.weight }) payload = { "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, "origin_contact_name": "PT. Indoteknik Dotcom Gemilang", "origin_contact_phone": "081717181922", "origin_address": "Jl. Bandengan Utara Komp A & BRT. Penjaringan, Kec. Penjaringan, Jakarta (BELAKANG INDOMARET) KOTA JAKARTA UTARA PENJARINGAN", "origin_postal_code": 14440, "destination_contact_name": self.real_shipping_id.name, "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", "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", "destination_postal_code": self.real_shipping_id.zip, "items": items_data_standard } # 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: 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 }) headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } # Kirim request ke Biteship response = requests.post(url, headers=headers, json=payload) if response.status_code == 201: 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: self.date_doc_kirim = self.driver_departure_date @api.constrains('arrival_time') def constrains_arrival_time(self): for record in self: if record.arrival_time and record.arrival_time > datetime.datetime.utcnow(): raise UserError('Jam kedatangan harus kurang dari Effective Date') def reset_status_printed(self): for rec in self: rec.status_printed = 'not_printed' rec.printed_sj = False rec.date_printed_list = False rec.date_printed_sj = False @api.onchange('carrier_id') def constrains_carrier_id(self): if self.carrier_id: if not self.env.user.is_logistic_approver: raise UserError('Hanya Logistic yang bisa mengubah shipping method') def do_unreserve(self): group_id = self.env.ref('indoteknik_custom.group_role_it').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) if not self._context.get('darimana') == 'sale.order' and self.env.user.id not in users_in_group.mapped('id'): self.sale_id.unreserve_id = self.id return self._create_approval_notification('Logistic') res = super(StockPicking, self).do_unreserve() current_time = datetime.datetime.utcnow() self.date_unreserve = current_time # self.check_state_reserve() return res # def check_state_reserve(self): # do = self.search([ # ('state', 'not in', ['cancel', 'draft', 'done']), # ('picking_type_code', '=', 'outgoing') # ]) # 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' # rec.date_reserved = '' # break def check_state_reserve(self): pickings = self.search([ ('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' return self._create_notification_action(title, message) def _create_notification_action(self, title, message): return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'} }, } def _compute_shipping_status(self): for rec in self: status = 'pending' if rec.driver_departure_date and not (rec.sj_return_date or rec.driver_arrival_date): status = 'shipment' elif rec.driver_departure_date and (rec.sj_return_date or rec.driver_arrival_date): status = 'completed' rec.shipping_status = status def action_create_invoice_from_mr(self): """Create the invoice associated to the PO. """ if not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa membuat Bill') precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') #custom here po = self.env['purchase.order'].search([ ('name', '=', self.group_id.name) ]) # 1) Prepare invoice vals and clean-up the section lines invoice_vals_list = [] for order in po: if order.invoice_status != 'to invoice': continue order = order.with_company(order.company_id) pending_section = None # Invoice values. invoice_vals = order._prepare_invoice() # Invoice line values (keep only necessary sections). for line in self.move_ids_without_package: po_line = self.env['purchase.order.line'].search([('order_id', '=', po.id), ('product_id', '=', line.product_id.id)], limit=1) qty = line.product_uom_qty if po_line.display_type == 'line_section': pending_section = line continue if not float_is_zero(po_line.qty_to_invoice, precision_digits=precision): if pending_section: invoice_vals['invoice_line_ids'].append((0, 0, pending_section._prepare_account_move_line_from_mr(po_line, qty))) pending_section = None invoice_vals['invoice_line_ids'].append((0, 0, line._prepare_account_move_line_from_mr(po_line, qty))) invoice_vals_list.append(invoice_vals) if not invoice_vals_list: raise UserError(_('There is no invoiceable line. If a product has a control policy based on received quantity, please make sure that a quantity has been received.')) # 2) group by (company_id, partner_id, currency_id) for batch creation new_invoice_vals_list = [] for grouping_keys, invoices in groupby(invoice_vals_list, key=lambda x: (x.get('company_id'), x.get('partner_id'), x.get('currency_id'))): origins = set() payment_refs = set() refs = set() ref_invoice_vals = None for invoice_vals in invoices: if not ref_invoice_vals: ref_invoice_vals = invoice_vals else: ref_invoice_vals['invoice_line_ids'] += invoice_vals['invoice_line_ids'] origins.add(invoice_vals['invoice_origin']) payment_refs.add(invoice_vals['payment_reference']) refs.add(invoice_vals['ref']) ref_invoice_vals.update({ 'ref': ', '.join(refs)[:2000], 'invoice_origin': ', '.join(origins), 'payment_reference': len(payment_refs) == 1 and payment_refs.pop() or False, }) new_invoice_vals_list.append(ref_invoice_vals) invoice_vals_list = new_invoice_vals_list # 3) Create invoices. moves = self.env['account.move'] AccountMove = self.env['account.move'].with_context(default_move_type='in_invoice') for vals in invoice_vals_list: moves |= AccountMove.with_company(vals['company_id']).create(vals) # 4) Some moves might actually be refunds: convert them if the total amount is negative # We do this after the moves have been created since we need taxes, etc. to know if the total # is actually negative or not moves.filtered(lambda m: m.currency_id.round(m.amount_total) < 0).action_switch_invoice_into_refund_credit_note() return self.action_view_invoice_from_mr(moves) def action_view_invoice_from_mr(self, invoices=False): """This function returns an action that display existing vendor bills of given purchase order ids. When only one found, show the vendor bill immediately. """ if not invoices: # Invoice_ids may be filtered depending on the user. To ensure we get all # invoices related to the purchase order, we read them in sudo to fill the # cache. self.sudo()._read(['invoice_ids']) invoices = self.invoice_ids result = self.env['ir.actions.act_window']._for_xml_id('account.action_move_in_invoice_type') # choose the view_mode accordingly if len(invoices) > 1: result['domain'] = [('id', 'in', invoices.ids)] elif len(invoices) == 1: res = self.env.ref('account.view_move_form', False) form_view = [(res and res.id or False, 'form')] if 'views' in result: result['views'] = form_view + [(state, view) for state, view in result['views'] if view != 'form'] else: result['views'] = form_view result['res_id'] = invoices.id else: result = {'type': 'ir.actions.act_window_close'} return result @api.onchange('date_doc_kirim') def update_date_doc_kirim_so(self): if not self.sale_id: return self.sale_id.date_doc_kirim = self.date_doc_kirim def action_assign(self): res = super(StockPicking, self).action_assign() current_time = datetime.datetime.utcnow() self.real_shipping_id = self.sale_id.real_shipping_id self.date_availability = current_time # self.check_state_reserve() return res def ask_approval(self): if self.env.user.is_accounting: raise UserError("Bisa langsung Validate") # for calendar distribute only # if self.is_internal_use: # stock_move_lines = self.env['stock.move.line'].search([ # ('picking_id', '!=', False), # ('product_id', '=', 236805), # ('picking_id.partner_id', '=', self.partner_id.id), # ('qty_done', '>', 0), # ]) # list_state = ['confirmed', 'done'] # for stock_move_line in stock_move_lines: # if stock_move_line.picking_id.state not in list_state: # continue # raise UserError('Sudah pernah dikirim kalender') for pick in self: if not pick.is_internal_use: raise UserError("Selain Internal Use bisa langsung Validate") for line in pick.move_line_ids_without_package: if line.qty_done <= 0: raise UserError("Qty tidak boleh 0") pick.approval_status = 'pengajuan1' def ask_receipt_approval(self): if self.env.user.is_logistic_approver: raise UserError('Bisa langsung validate tanpa Ask Receipt') else: self.approval_receipt_status = 'pengajuan1' def ask_return_approval(self): for pick in self: if self.env.user.is_accounting: pick.approval_return_status = 'approved' continue action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_stock_return_note_wizard') if self.picking_type_code == 'outgoing': if self.env.user.id in [3988, 3401, 20] or ( self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin ): action['context'] = {'picking_ids': [x.id for x in self]} return action elif not self.env.user.has_group('indoteknik_custom.group_role_purchasing') and 'Return of' in self.origin: raise UserError('Harus Purchasing yang Ask Return') else: raise UserError('Harus Sales Admin yang Ask Return') elif self.picking_type_code == 'incoming': if self.env.user.has_group('indoteknik_custom.group_role_purchasing') or ( self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin ): action['context'] = {'picking_ids': [x.id for x in self]} return action elif not self.env.user.id in [3988, 3401, 20] and 'Return of' in self.origin: raise UserError('Harus Sales Admin yang Ask Return') else: 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: if picking.group_id.sale_id: order = self.env['sale.order'].search([('name', '=', name)], limit=1) else: order = self.env['purchase.order'].search([('name', '=', name)], limit=1) order_lines = order.order_line set_line = 0 for order_line in order_lines: if move.product_id == order_line.product_id: set_line = order_line.line_no break else: continue move.line_no = set_line for line in move.move_line_ids: line.line_no = set_line def _compute_summary_qty(self): for picking in self: sum_qty_detail = sum_qty_operation = count_line_detail = count_line_operation = 0 for detail in picking.move_line_ids_without_package: # detailed operations sum_qty_detail += detail.qty_done count_line_detail += 1 for operation in picking.move_ids_without_package: # operations sum_qty_operation += operation.product_uom_qty count_line_operation += 1 picking.summary_qty_detail = sum_qty_detail picking.count_line_detail = count_line_detail picking.summary_qty_operation = sum_qty_operation picking.count_line_operation = count_line_operation @api.onchange('picking_type_id') def _onchange_operation_type(self): self.is_internal_use = self.picking_type_id.is_internal_use return def validation_minus_onhand_quantity(self): bu_location_id = 49 for line in self.move_line_ids_without_package: quant = self.env['stock.quant'].search([ ('product_id', '=', line.product_id.id), ('location_id', '=', bu_location_id), ]) if ( self.picking_type_id.id == 29 and quant and line.location_id.id == bu_location_id 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): quant = self.env['stock.quant'].search([ ('product_id', '=', product.id), ('location_id', '=', location.id), ]) if quant: 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('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() if self._name != 'stock.picking': return super(StockPicking, self).button_validate() if not self.picking_code: self.picking_code = self.env['ir.sequence'].next_by_code('stock.picking.code') or '0' if not self.arrival_time and 'BU/IN/' in self.name: raise UserError('Jam Kedatangan harus diisi') if self.picking_type_id.code == 'incoming' and self.group_id.id == False and self.is_internal_use == False: raise UserError(_('Tidak bisa Validate jika tidak dari Document SO / PO')) if self.is_internal_use and not self.env.user.is_accounting: raise UserError("Harus di Approve oleh Accounting") if self.picking_type_id.id == 28 and not self.env.user.is_logistic_approver: raise UserError("Harus di Approve oleh Logistik") if self.location_dest_id.id == 47 and not self.env.user.is_purchasing_manager: raise UserError("Transfer ke gudang selisih harus di approve Rafly Hanggara") if self.is_internal_use: self.approval_status = 'approved' elif self.picking_type_id.code == 'incoming': self.approval_receipt_status = 'approved' for product in self.move_line_ids_without_package.product_id: if product: product.product_tmpl_id._create_solr_queue('_sync_product_stock_to_solr') for move_line in self.move_line_ids_without_package: if move_line.product_id: move_line.product_id.product_tmpl_id._create_solr_queue('_sync_product_stock_to_solr') if not self.date_reserved: current_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') self.date_reserved = current_time self.validation_minus_onhand_quantity() self.responsible = self.env.user.id res = super(StockPicking, self).button_validate() self.calculate_line_no() self.date_done = datetime.datetime.utcnow() self.state_reserve = 'done' return res def send_mail_bills(self): if self.picking_type_code == 'incoming' and self.purchase_id: template = self.env.ref('indoteknik_custom.mail_template_invoice_po_document') if template and self.purchase_id: # Render email body email_values = template.sudo().generate_email( res_ids=[self.purchase_id.id], fields=['body_html'] ) rendered_body = email_values.get(self.purchase_id.id, {}).get('body_html', '') # Render report dengan XML ID report = self.env.ref('purchase.action_report_purchase_order') # Gunakan XML ID laporan if not report: raise UserError("Laporan dengan XML ID 'purchase.action_report_purchase_order' tidak ditemukan.") # Render laporan ke PDF pdf_content, _ = report._render_qweb_pdf([self.purchase_id.id]) report_content = base64.b64encode(pdf_content).decode('utf-8') # Kirim email menggunakan template email_sent = template.sudo().send_mail(self.purchase_id.id, force_send=True) if email_sent: # Buat attachment untuk laporan attachment = self.env['ir.attachment'].create({ 'name': self.purchase_id.name or "Laporan Invoice.pdf", 'type': 'binary', 'datas': report_content, 'res_model': 'purchase.order', 'res_id': self.purchase_id.id, 'mimetype': 'application/pdf', }) # Siapkan data untuk mail.compose.message compose_values = { 'subject': "Pengiriman Email Invoice", 'body': rendered_body, 'attachment_ids': [(4, attachment.id)], 'res_id': self.purchase_id.id, 'model': 'purchase.order', } # Buat mail.compose.message compose_message = self.env['mail.compose.message'].create(compose_values) # Kirim pesan melalui wizard compose_message.action_send_mail() return True def action_cancel(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") res = super(StockPicking, self).action_cancel() return res @api.model def create(self, vals): self._use_faktur(vals) return super(StockPicking, self).create(vals) def write(self, vals): self._use_faktur(vals) return super(StockPicking, self).write(vals) def _use_faktur(self, vals): if vals.get('efaktur_id', False): self.env['vit.efaktur'].search( [ ('id', '=', vals['efaktur_id']) ], limit=1 ).is_used = True if self.efaktur_id.id != vals['efaktur_id']: self.efaktur_id.is_used = False return True def compute_delivery_status(self): for picking in self: if not picking.driver_departure_date and picking.picking_code: picking.delivery_status = "Sedang Dikemas" elif picking.driver_departure_date and not picking.driver_arrival_date: picking.delivery_status = "Dalam Perjalanan" elif picking.driver_departure_date and picking.driver_arrival_date and picking.carrier_id == 1: picking.delivery_status = "Diterima Konsumen" elif picking.driver_departure_date and picking.driver_arrival_date and picking.carrier_id != 1: picking.delivery_status = "Diterima Ekspedisi" else: picking.delivery_status = "Diterima Konsumen" def create_manifest_data(self, description, object): datetime_str = '' if isinstance(object, datetime.datetime): jakarta_timezone = pytz.timezone('Asia/Jakarta') datetime_str = object.replace(tzinfo=pytz.utc).astimezone(jakarta_timezone).strftime('%Y-%m-%d %H:%M:%S') return { 'description': description, 'datetime': datetime_str } def get_manifests(self): if self.waybill_id and len(self.waybill_id.manifest_ids) > 0: return [self.create_manifest_data(x.description, x.datetime) for x in self.waybill_id.manifest_ids] status_mapping = { 'pickup': { 'arrival': 'Sudah diambil', 'departure': 'Siap diambil', 'prepare': 'Sedang disiapkan' }, 'delivery': { 'arrival': 'Sudah sampai', 'departure': 'Sedang dikirim', 'prepare': 'Menunggu pickup', } } status_key = 'delivery' if self.carrier_id.id == 32: status_key = 'pickup' manifest_datas = [] departure_date = self.driver_departure_date arrival_date = self.sj_return_date if self.sj_return_date else self.driver_arrival_date status = status_mapping.get(status_key) if not status: return manifest_datas if arrival_date or self.sj_return_date: manifest_datas.append(self.create_manifest_data(status['arrival'], arrival_date)) if departure_date: manifest_datas.append(self.create_manifest_data(status['departure'], departure_date)) manifest_datas.append(self.create_manifest_data(status['prepare'], self.create_date)) return manifest_datas def get_tracking_detail(self): self.ensure_one() response = { 'delivery_order': { 'name': self.name, 'carrier': self.carrier_id.name or '', 'receiver_name': '', 'receiver_city': '' }, 'delivered': False, 'status': self.shipping_status, 'waybill_number': self.delivery_tracking_no or '', 'delivery_status': None, 'eta': self.generate_eta_delivery(), 'manifests': self.get_manifests() } 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 return response response['delivery_order']['receiver_name'] = self.waybill_id.receiver_name response['delivery_order']['receiver_city'] = self.waybill_id.receiver_city response['delivery_status'] = self.waybill_id._get_history('delivery_status') response['delivered'] = self.waybill_id.delivered return response def generate_eta_delivery(self): current_date = datetime.datetime.now() prepare_days = 3 start_date = self.driver_departure_date or self.create_date ead = self.sale_id.estimated_arrival_days or 0 if not self.driver_departure_date: ead += prepare_days ead_datetime = datetime.timedelta(days=ead) fastest_eta = start_date + ead_datetime if not self.driver_departure_date and fastest_eta < current_date: fastest_eta = current_date + ead_datetime longest_days = 3 longest_eta = fastest_eta + datetime.timedelta(days=longest_days) format_time = '%d %b %Y' format_time_fastest = '%d %b' if fastest_eta.year == longest_eta.year else format_time formatted_fastest_eta = fastest_eta.strftime(format_time_fastest) formatted_longest_eta = longest_eta.strftime(format_time) return f'{formatted_fastest_eta} - {formatted_longest_eta}' # 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') # @api.constrains('product_id') # def check_product_validity(self): # """ # Validate if the product exists in the related stock.picking's move_ids_without_package # and ensure that the product's quantity does not exceed the available product_uom_qty. # """ # for record in self: # if not record.picking_id or not record.product_id: # continue # # Filter move lines in the related picking for 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' is not available in the related stock picking's moves. " # "Please check and try again." # ) % record.product_id.display_name) # # Calculate the total entries for the product in check.product for the same picking # product_entries_count = self.search_count([ # ('picking_id', '=', record.picking_id.id), # ('product_id', '=', record.product_id.id) # ]) # # Sum the product_uom_qty for all relevant moves # total_qty_in_moves = sum(moves.mapped('product_uom_qty')) # # Compare the count of entries against the available quantity # if product_entries_count > total_qty_in_moves: # raise UserError(( # "The product '%s' exceeds the allowable quantity (%s) in the related stock picking's moves. " # "You can only add it %s times." # ) % (record.product_id.display_name, total_qty_in_moves, total_qty_in_moves))