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 _logger = logging.getLogger(__name__) _biteship_url = "https://api.biteship.com/v1" _biteship_api_key = "biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA" # _biteship_api_key = "biteship_live.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiaW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTc0MTE1NTU4M30.pbFCai9QJv8iWhgdosf8ScVmEeP3e5blrn33CHe7Hgo" class StockPicking(models.Model): _inherit = 'stock.picking' _order = 'final_seq ASC' konfirm_koli_lines = fields.One2many('konfirm.koli', 'picking_id', string='Konfirm Koli', auto_join=True, copy=False) scan_koli_lines = fields.One2many('scan.koli', 'picking_id', string='Scan Koli', auto_join=True, copy=False) check_koli_lines = fields.One2many('check.koli', 'picking_id', string='Check Koli', auto_join=True, copy=False) check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True, copy=False) barcode_product_lines = fields.One2many('barcode.product', 'picking_id', string='Barcode 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 ) out_code = fields.Integer( string="Out Code", readonly=True, related="id", ) 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, copy=False) note_logistic = fields.Selection([ ('wait_so_together', 'Tunggu SO Barengan'), ('not_paid', 'Customer belum bayar'), ('reserve_stock', 'Reserve Stock'), ('waiting_schedule', 'Menunggu Jadwal Kirim'), ('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', copy=False) 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', tracking=True, copy=False, help="The current state of the stock picking.") notee = fields.Text(string="Note SJ", help="Catatan untuk kirim barang") note_info = fields.Text(string="Note Logistix (Text)", help="Catatan untuk pengiriman") state_approve_md = fields.Selection([ ('waiting', 'Waiting For Approve by MD'), ('pending', 'Pending (perlu koordinasi dengan MD)'), ('done', 'Approve by MD'), ], string='Approval MD Gudang Selisih', tracking=True, copy=False, help="The current state of the MD Approval transfer barang from gudang selisih.") # show_state_approve_md = fields.Boolean(compute="_compute_show_state_approve_md") # def _compute_show_state_approve_md(self): # for record in self: # record.show_state_approve_md = record.location_id.id == 47 or record.location_id.complete_name == "Virtual Locations/Gudang Selisih" quantity_koli = fields.Float(string="Quantity Koli", copy=False) total_mapping_koli = fields.Float(string="Total Mapping Koli", compute='_compute_total_mapping_koli') so_lama = fields.Boolean('SO LAMA', copy=False) linked_manual_bu_out = fields.Many2one('stock.picking', string='BU Out', copy=False) area_name = fields.Char(string="Area", compute="_compute_area_name") @api.depends('real_shipping_id.kecamatan_id', 'real_shipping_id.kota_id') def _compute_area_name(self): for record in self: district = record.real_shipping_id.kecamatan_id or '' city = record.real_shipping_id.kota_id or '' record.area_name = f"{district}, {city}".strip(', ') # def write(self, vals): # if 'linked_manual_bu_out' in vals: # for record in self: # if (record.picking_type_code == 'internal' # and 'BU/PICK/' in record.name): # # Jika menghapus referensi (nilai di-set False/None) # if record.linked_manual_bu_out and not vals['linked_manual_bu_out']: # record.linked_manual_bu_out.state_packing = 'not_packing' # # Jika menambahkan referensi baru # elif vals['linked_manual_bu_out']: # new_picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out']) # new_picking.state_packing = 'packing_done' # return super().write(vals) # @api.model # def create(self, vals): # record = super().create(vals) # if (record.picking_type_code == 'internal' # and 'BU/PICK/' in record.name # and vals.get('linked_manual_bu_out')): # picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out']) # picking.state_packing = 'packing_done' # return record @api.depends('konfirm_koli_lines', 'konfirm_koli_lines.pick_id', 'konfirm_koli_lines.pick_id.quantity_koli') def _compute_total_mapping_koli(self): for record in self: total = 0.0 for line in record.konfirm_koli_lines: if line.pick_id and line.pick_id.quantity_koli: total += line.pick_id.quantity_koli record.total_mapping_koli = total @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") total_koli = fields.Integer(compute='_compute_total_koli', string="Total Koli") total_koli_display = fields.Char(compute='_compute_total_koli_display', string="Total Koli Display") linked_out_picking_id = fields.Many2one('stock.picking', string="Linked BU/OUT", copy=False) total_so_koli = fields.Integer(compute='_compute_total_so_koli', string="Total SO Koli") # Biteship Section biteship_id = fields.Char(string="Biteship Respon ID") biteship_tracking_id = fields.Char(string="Biteship Trackcking ID") biteship_waybill_id = fields.Char(string="Biteship Waybill ID") # estimated_ready_ship_date = fields.Datetime(string='ET Ready to Ship', copy=False, related='sale_id.estimated_ready_ship_date') # countdown_hours = fields.Float(string='Countdown in Hours', compute='_callculate_sequance', default=False, store=False, compute_sudo=False) # countdown_ready_to_ship = fields.Char(string='Countdown Ready to Ship', compute='_callculate_sequance', store=False, compute_sudo=False) final_seq = fields.Float(string='Remaining Time') shipping_method_so_id = fields.Many2one('delivery.carrier', string='Shipping Method SO', related='sale_id.carrier_id') state_packing = fields.Selection([('not_packing', 'Belum Packing'), ('packing_done', 'Sudah Packing')], string='Packing Status') approval_invoice_date_id = fields.Many2one('approval.invoice.date', string='Approval Invoice Date') last_update_date_doc_kirim = fields.Datetime(string='Last Update Tanggal Kirim') update_date_doc_kirim_add = fields.Boolean(string='Update Tanggal Kirim Lewat ADD') def _check_date_doc_kirim_modification(self): for record in self: if record.last_update_date_doc_kirim and not self.env.context.get('from_button_approve'): kirim_date = fields.Datetime.from_string(record.last_update_date_doc_kirim) now = fields.Datetime.now() deadline = kirim_date + timedelta(days=1) deadline = deadline.replace(hour=10, minute=0, second=0) if now > deadline: raise ValidationError( _("Anda tidak dapat mengubah Tanggal Kirim setelah jam 10:00 pada hari berikutnya!") ) @api.constrains('date_doc_kirim') def _constrains_date_doc_kirim(self): for rec in self: rec.calculate_line_no() if rec.picking_type_code == 'outgoing' and 'BU/OUT/' in rec.name and rec.partner_id.id != 96868: invoice = self.env['account.move'].search([('sale_id', '=', rec.sale_id.id), ('move_type', '=', 'out_invoice'), ('state', '=', 'posted')], limit=1, order='create_date desc') if invoice and not self.env.context.get('active_model') == 'stock.picking': rec._check_date_doc_kirim_modification() if rec.date_doc_kirim != invoice.invoice_date and not self.env.context.get('from_button_approve'): get_approval_invoice_date = self.env['approval.invoice.date'].search([('picking_id', '=', rec.id),('state', '=', 'draft')], limit=1) if get_approval_invoice_date and get_approval_invoice_date.state == 'draft': get_approval_invoice_date.date_doc_do = rec.date_doc_kirim else: approval_invoice_date = self.env['approval.invoice.date'].create({ 'picking_id': rec.id, 'date_invoice': invoice.invoice_date, 'date_doc_do': rec.date_doc_kirim, 'sale_id': rec.sale_id.id, 'move_id': invoice.id, 'partner_id': rec.partner_id.id }) rec.approval_invoice_date_id = approval_invoice_date.id if approval_invoice_date: return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Notification', 'message': 'Invoice Date Tidak Sesuai, Document Approval Invoice Date Terbuat', 'next': {'type': 'ir.actions.act_window_close'} }, } rec.last_update_date_doc_kirim = datetime.datetime.utcnow() @api.constrains('scan_koli_lines') def _constrains_scan_koli_lines(self): now = datetime.datetime.utcnow() for picking in self: if len(picking.scan_koli_lines) > 0: if len(picking.scan_koli_lines) != picking.total_mapping_koli: raise UserError("Scan Koli Tidak Sesuai Dengan Total Mapping Koli") picking.driver_departure_date = now @api.depends('total_so_koli') def _compute_total_so_koli(self): for picking in self: if picking.state == 'done': picking.total_so_koli = self.env['sales.order.koli'].search_count([('picking_id.linked_out_picking_id', '=', picking.id), ('state', '=', 'delivered')]) else: picking.total_so_koli = self.env['sales.order.koli'].search_count([('picking_id.linked_out_picking_id', '=', picking.id), ('state', '!=', 'delivered')]) @api.depends('total_koli') def _compute_total_koli(self): for picking in self: picking.total_koli = self.env['scan.koli'].search_count([('picking_id', '=', picking.id)]) @api.depends('total_koli', 'total_so_koli') def _compute_total_koli_display(self): for picking in self: picking.total_koli_display = f"{picking.total_koli} / {picking.total_so_koli}" @api.constrains('quantity_koli') def _constrains_quantity_koli(self): for picking in self: if not picking.linked_out_picking_id: so_koli = self.env['sales.order.koli'].search([('picking_id', '=', picking.id)]) if so_koli: so_koli.unlink() for rec in picking.check_koli_lines: self.env['sales.order.koli'].create({ 'sale_order_id': picking.sale_id.id, 'picking_id': picking.id, 'koli_id': rec.id, }) else: raise UserError('Tidak Bisa Mengubah Quantity Koli Karena Koli Dari Picking Ini Sudah Dipakai Di BU/OUT!') @api.onchange('quantity_koli') def _onchange_quantity_koli(self): self.check_koli_lines = [(5, 0, 0)] self.check_koli_lines = [(0, 0, { 'koli': f"{self.name}/{str(i+1).zfill(3)}", 'picking_id': self.id, }) for i in range(int(self.quantity_koli))] def schduled_update_sequance(self): query = "SELECT update_sequance_stock_picking();" self.env.cr.execute(query) # @api.depends('estimated_ready_ship_date', 'state') # def _callculate_sequance(self): # for record in self: # try : # if record.estimated_ready_ship_date and record.state not in ('cancel', 'done'): # rts = record.estimated_ready_ship_date - waktu.now() # rts_days = rts.days # rts_hours = divmod(rts.seconds, 3600) # estimated_by_erts = rts.total_seconds() / 3600 # record.countdown_ready_to_ship = f"{rts_days} days, {rts_hours} hours" # record.countdown_hours = estimated_by_erts # else: # record.countdown_hours = 999999999999 # record.countdown_ready_to_ship = False # except Exception as e : # _logger.error(f"Error calculating sequance {record.id}: {str(e)}") # print(str(e)) # return { 'error': str(e) } # @api.depends('estimated_ready_ship_date', 'state') # def _compute_countdown_hours(self): # for record in self: # if record.state in ('cancel', 'done') or not record.estimated_ready_ship_date: # # Gunakan nilai yang sangat besar sebagai placeholder # record.countdown_hours = 999999 # else: # delta = record.estimated_ready_ship_date - waktu.now() # record.countdown_hours = delta.total_seconds() / 3600 # @api.depends('estimated_ready_ship_date', 'state') # def _compute_countdown_ready_to_ship(self): # for record in self: # if record.state in ('cancel', 'done'): # record.countdown_ready_to_ship = False # else: # if record.estimated_ready_ship_date: # delta = record.estimated_ready_ship_date - waktu.now() # days = delta.days # hours, remainder = divmod(delta.seconds, 3600) # record.countdown_ready_to_ship = f"{days} days, {hours} hours" # record.countdown_hours = delta.total_seconds() / 3600 # else: # record.countdown_ready_to_ship = False def _compute_lalamove_image_html(self): for record in self: if record.lalamove_image_url: 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), ('lalamove_order_id', '!=', False) ]) 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): if self.biteship_tracking_id: raise UserError(f"Order ini sudah dikirim ke Biteship. Dengan Tracking Id: {self.biteship_tracking_id}") # Mencari data sale.order.line berdasarkan sale_id products = self.env['sale.order.line'].search([('order_id', '=', self.sale_id.id)]) # Fungsi untuk membangun items_data dari order lines def build_items_data(lines): return [{ "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, "weight": order_line.weight }) payload = { "reference_id " : self.sale_id.name, "shipper_contact_name": self.carrier_id.pic_name or '', "shipper_contact_phone": self.carrier_id.pic_phone or '', "shipper_organization": self.carrier_id.name, "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, "origin_note": "BELAKANG INDOMARET", "courier_type": self.sale_id.delivery_service_type or "reg", "courier_company": self.carrier_id.name.lower(), "delivery_type": "now", "destination_postal_code": self.real_shipping_id.zip, "items": items_data_standard } # Cek jika pengiriman instant atau same_day if self.sale_id.delivery_service_type and ("instant" in self.sale_id.delivery_service_type or "same_day" in self.sale_id.delivery_service_type): payload.update({ "origin_coordinate" :{ "latitude": -6.3031123, "longitude" : 106.7794934999 }, "destination_coordinate" : { "latitude": self.real_shipping_id.latitude, "longitude": self.real_shipping_id.longtitude, }, "items": items_data_instant }) api_key = _biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } # Kirim request ke Biteship response = requests.post(_biteship_url+'/orders', headers=headers, json=payload) if response.status_code == 200: data = response.json() self.biteship_id = data.get("id", "") self.biteship_tracking_id = data.get("courier", {}).get("tracking_id", "") self.biteship_waybill_id = data.get("courier", {}).get("waybill_id", "") self.delivery_tracking_no = data.get("courier", {}).get("waybill_id", "") waybill_id = data.get("courier", {}).get("waybill_id", "") message = f"✅ Berhasil Order ke Biteship! Resi: {waybill_id}" if waybill_id else "⚠️ Order berhasil, tetapi tidak ada nomor resi." return { 'effect': { 'fadeout': 'slow', # Efek menghilang perlahan 'message': message, # Pesan sukses 'type': 'rainbow_man', # Efek animasi lucu Odoo } } else: error_data = response.json() error_message = error_data.get("error", "Unknown error") error_code = error_data.get("code", "No code provided") raise UserError(f"Error saat mengirim ke Biteship: {error_message} (Code: {error_code})") @api.constrains('driver_departure_date') def constrains_driver_departure_date(self): 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 return res def check_state_reserve(self): pickings = self.search([ ('state', 'not in', ['cancel', 'draft', 'done']), ('picking_type_code', '=', 'internal'), ('name', 'ilike', 'BU/PICK/'), ]) for picking in pickings: fullfillments = self.env['sales.order.fulfillment.v2'].search([ ('sale_order_id', '=', picking.sale_id.id) ]) picking.state_reserve = 'ready' picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow() if any(rec.so_qty != rec.reserved_stock_qty for rec in fullfillments): picking.state_reserve = 'waiting' picking.date_reserved = '' self.check_state_reserve_backorder() def check_state_reserve_backorder(self): pickings = self.search([ ('backorder_id', '!=', False), ('name', 'ilike', 'BU/PICK/'), ('picking_type_code', '=', 'internal'), ('state', 'not in', ['cancel', 'draft', 'done']) ]) for picking in pickings: fullfillments = self.env['sales.order.fulfillment.v2'].search([ ('sale_order_id', '=', picking.sale_id.id) ]) picking.state_reserve = 'ready' picking.date_reserved = picking.date_reserved or datetime.datetime.utcnow() if any(rec.so_qty != rec.reserved_stock_qty 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_state_approve_md(self): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) active_model = self.env.context.get('active_model') if self.env.user.id in users_in_group.mapped('id'): self.state_approve_md = 'done' else: raise UserError('Hanya MD yang bisa Approve') def button_state_pending_md(self): group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) active_model = self.env.context.get('active_model') if self.env.user.id in users_in_group.mapped('id'): self.state_approve_md = 'pending' else: raise UserError('Hanya MD yang bisa Approve') def button_validate(self): self.check_invoice_date() threshold_datetime = waktu(2025, 4, 11, 6, 26) group_id = self.env.ref('indoteknik_custom.group_role_merchandiser').id users_in_group = self.env['res.users'].search([('groups_id', 'in', [group_id])]) active_model = self.env.context.get('active_model') if self.location_id.id == 47 and self.env.user.id not in users_in_group.mapped('id') and self.state_approve_md != 'done': self.state_approve_md = 'waiting' if self.state_approve_md != 'pending' else 'pending' self.env.cr.commit() raise UserError("Transfer dari gudang selisih harus di approve MD, Hubungi MD agar bisa di Validate") else: if self.location_id.id == 47 and self.env.user.id in users_in_group.mapped('id'): self.state_approve_md = 'done' if (len(self.konfirm_koli_lines) == 0 and 'BU/OUT/' in self.name and self.picking_type_code == 'outgoing' and self.create_date > threshold_datetime and not self.so_lama): raise UserError(_("Tidak ada Mapping koli! Harap periksa kembali.")) if (len(self.scan_koli_lines) == 0 and 'BU/OUT/' in self.name and self.picking_type_code == 'outgoing' and self.create_date > threshold_datetime and not self.so_lama): raise UserError(_("Tidak ada scan koli! Harap periksa kembali.")) # if self.driver_departure_date == False and 'BU/OUT/' in self.name and self.picking_type_code == 'outgoing': # raise UserError(_("Isi Driver Departure Date dulu sebelum validate")) if len(self.check_koli_lines) == 0 and 'BU/PICK/' in self.name: raise UserError(_("Tidak ada koli! Harap periksa kembali.")) if not self.linked_manual_bu_out and 'BU/PICK/' in self.name: raise UserError(_("Isi BU Out terlebih dahulu!")) if len(self.check_product_lines) == 0 and 'BU/PICK/' in self.name: raise UserError(_("Tidak ada Check Product! Harap periksa kembali.")) if self.total_koli > self.total_so_koli: raise UserError(_("Total Koli (%s) dan Total SO Koli (%s) tidak sama! Harap periksa kembali.") % (self.total_koli, self.t1otal_so_koli)) 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 # self.send_koli_to_so() if self.picking_type_code == 'outgoing' and 'BU/OUT/' in self.name: self.check_koli() res = super(StockPicking, self).button_validate() self.date_done = datetime.datetime.utcnow() self.state_reserve = 'done' self.final_seq = 0 self.set_picking_code_out() self.send_koli_to_so() if not self.env.context.get('skip_koli_check'): for picking in self: if picking.sale_id: all_koli_ids = picking.sale_id.koli_lines.filtered(lambda k: k.state != 'delivered').ids scanned_koli_ids = picking.scan_koli_lines.mapped('koli_id.id') missing_koli_ids = set(all_koli_ids) - set(scanned_koli_ids) if len(missing_koli_ids) > 0 and picking.picking_type_code == 'outgoing' and 'BU/OUT/' in picking.name: missing_koli_names = picking.sale_id.koli_lines.filtered(lambda k: k.id in missing_koli_ids and k.state != 'delivered').mapped('display_name') missing_koli_list = "\n".join(f"- {name}" for name in missing_koli_names) # Buat wizard modal warning wizard = self.env['warning.modal.wizard'].create({ 'message': f"Berikut Koli yang belum discan:\n{missing_koli_list}", 'picking_id': picking.id, }) return { 'type': 'ir.actions.act_window', 'res_model': 'warning.modal.wizard', 'view_mode': 'form', 'res_id': wizard.id, 'target': 'new', } self.send_mail_bills() return res def check_invoice_date(self): for picking in self: if picking.picking_type_code != 'outgoing' or 'BU/OUT/' not in picking.name or picking.partner_id.id == 96868: continue invoice = self.env['account.move'].search([('sale_id', '=', picking.sale_id.id), ('state','not in',['draft','cancel'])], limit=1) if not invoice: continue if not picking.so_lama and not picking.date_doc_kirim or not invoice.invoice_date: raise UserError("Tanggal Kirim atau Tanggal Invoice belum diisi!") picking_date = fields.Date.to_date(picking.date_doc_kirim) invoice_date = fields.Date.to_date(invoice.invoice_date) if picking_date != invoice_date and picking.update_date_doc_kirim_add: raise UserError("Tanggal Kirim (%s) tidak sesuai dengan Tanggal Invoice (%s)!" % ( picking_date.strftime('%d-%m-%Y'), invoice_date.strftime('%d-%m-%Y') )) def set_picking_code_out(self): for picking in self: # Check if picking meets criteria is_bu_pick = picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name if not is_bu_pick: continue # Find matching outgoing transfers bu_out_transfers = self.search([ ('name', 'like', 'BU/OUT/%'), ('sale_id', '=', picking.sale_id.id), ('picking_type_code', '=', 'outgoing'), ('picking_code', '=', False), ('state', 'not in', ['done', 'cancel']) ]) # Assign sequence code to each matching transfer for transfer in bu_out_transfers: transfer.picking_code = self.env['ir.sequence'].next_by_code('stock.picking.code') def check_koli(self): for picking in self: sale_id = picking.sale_id for koli_lines in picking.scan_koli_lines: if koli_lines.koli_id.sale_order_id != sale_id: raise UserError('Koli tidak sesuai') def send_koli_to_so(self): for picking in self: if picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name: for koli_line in picking.check_koli_lines: existing_koli = self.env['sales.order.koli'].search([ ('sale_order_id', '=', picking.sale_id.id), ('picking_id', '=', picking.id), ('koli_id', '=', koli_line.id) ], limit=1) if not existing_koli: self.env['sales.order.koli'].create({ 'sale_order_id': picking.sale_id.id, 'picking_id': picking.id, 'koli_id': koli_line.id }) if picking.picking_type_code == 'outgoing' and 'BU/OUT/' in picking.name: if picking.state == 'done': for koli_line in picking.scan_koli_lines: existing_koli = self.env['sales.order.koli'].search([ ('sale_order_id', '=', picking.sale_id.id), ('koli_id', '=', koli_line.koli_id.koli_id.id) ], limit=1) existing_koli.state = 'delivered' 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 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' or self.env.context.get('active_model') == 'stock.picking.type'): if self.origin and 'Return of' in self.origin: raise UserError("Button ini hanya untuk Logistik") if not self.env.user.has_group('indoteknik_custom.group_role_it') and not self.env.user.has_group('indoteknik_custom.group_role_logistic') and self.picking_type_code == 'outgoing': 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) records = super(StockPicking, self).create(vals) # Panggil sync_sale_line setelah record dibuat # records.sync_sale_line(vals) return records def sync_sale_line(self, vals): # Pastikan kita bekerja dengan record yang sudah ada for picking in self: if picking.picking_type_code == 'internal' and 'BU/PICK/' in picking.name: for line in picking.move_ids_without_package: if line.product_id and picking.sale_id: sale_line = self.env['sale.order.line'].search([ ('product_id', '=', line.product_id.id), ('order_id', '=', picking.sale_id.id) ], limit=1) # Tambahkan limit=1 untuk efisiensi if sale_line: line.sale_line_id = sale_line.id def write(self, vals): if 'linked_manual_bu_out' in vals: for record in self: if (record.picking_type_code == 'internal' and 'BU/PICK/' in record.name): # Jika menghapus referensi (nilai di-set False/None) if record.linked_manual_bu_out and not vals['linked_manual_bu_out']: record.linked_manual_bu_out.state_packing = 'not_packing' # Jika menambahkan referensi baru elif vals['linked_manual_bu_out']: new_picking = self.env['stock.picking'].browse(vals['linked_manual_bu_out']) new_picking.state_packing = 'packing_done' self._use_faktur(vals) self.sync_sale_line(vals) for picking in self: # Periksa apakah kondisi terpenuhi saat data diubah if (vals.get('picking_type_code', picking.picking_type_code) == 'incoming' and vals.get('location_dest_id', picking.location_dest_id.id) == 58): if 'name' in vals or picking.name.startswith('BU/IN/'): name_to_modify = vals.get('name', picking.name) if name_to_modify.startswith('BU/IN/'): vals['name'] = name_to_modify.replace('BU/IN/', 'BU/INPUT/', 1) if (vals.get('picking_type_code', picking.picking_type_code) == 'internal' and vals.get('location_id', picking.location_id.id) == 58): name_to_modify = vals.get('name', picking.name) if name_to_modify.startswith('BU/INT'): new_name = name_to_modify.replace('BU/INT', 'BU/IN', 1) # Periksa apakah nama sudah ada if self.env['stock.picking'].search_count([('name', '=', new_name), ('company_id', '=', picking.company_id.id)]) > 0: new_name = f"{new_name}-DUP" vals['name'] = new_name 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() order = self.env['sale.order'].search([('name', '=', self.sale_id.name)], limit=1) response = { 'delivery_order': { 'name': self.name, 'carrier': self.carrier_id.name or '', 'service' : order.delivery_service_type 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(), 'is_biteship': True if self.biteship_id else False, 'manifests': self.get_manifests() } if self.biteship_id : histori = self.get_manifest_biteship() eta_start = order.date_order + timedelta(days=order.estimated_arrival_days_start) eta_end = order.date_order + timedelta(days=order.estimated_arrival_days) formatted_eta = f"{eta_start.strftime('%d %b')} - {eta_end.strftime('%d %b %Y')}" response['eta'] = formatted_eta response['manifests'] = histori.get("manifests", []) response['delivered'] = histori.get("delivered", False) or self.sj_return_date != False or self.driver_arrival_date != False response['status'] = self._map_status_biteship(histori.get("delivered")) return response if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0: response['delivered'] = self.sj_return_date != False or self.driver_arrival_date != False 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 get_manifest_biteship(self): api_key = _biteship_api_key headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json" } manifests = [] try: # Kirim request ke Biteship response = requests.get(_biteship_url+'/trackings/'+self.biteship_tracking_id, headers=headers, json=manifests) result = response.json() description = { 'confirmed' : 'Indoteknik telah melakukan permintaan pick-up', 'allocated' : 'Kurir akan melakukan pick-up pesanan', 'picking_up' : 'Kurir sedang dalam perjalanan menuju lokasi pick-up', 'picked' : 'Pesanan sudah di pick-up kurir '+result.get("courier", {}).get("name", ""), 'on_hold' : 'Pesanan ditahan sementara karena masalah pengiriman', 'dropping_off' : 'Kurir sudah ditugaskan dan pesanan akan segera diantar ke pembeli', 'delivered' : 'Pesanan telah sampai dan diterima oleh '+result.get("destination", {}).get("contact_name", "") } if(result.get('success') == True): history = result.get("history", []) status = result.get("status", "") for entry in reversed(history): manifests.append({ "status": re.sub(r'[^a-zA-Z0-9\s]', ' ', entry["status"]).lower().capitalize(), "datetime": self._convert_to_local_time(entry["updated_at"]), "description": description[entry["status"]], }) return { "manifests": manifests, "delivered": status } return manifests except Exception as e : _logger.error(f"Error fetching Biteship order for picking {self.id}: {str(e)}") return { 'error': str(e) } def _convert_to_local_time(self, iso_date): try: dt_with_tz = waktu.fromisoformat(iso_date) utc_dt = dt_with_tz.astimezone(pytz.utc) local_tz = pytz.timezone("Asia/Jakarta") local_dt = utc_dt.astimezone(local_tz) return local_dt.strftime("%Y-%m-%d %H:%M:%S") except Exception as e: return str(e) def _map_status_biteship(self, status): status_mapping = { "confirmed": "pending", "scheduled": "pending", "allocated": "pending", "picking_up": "pending", "picked": "shipment", "cancelled": "cancelled", "on_hold": "on_hold", "dropping_off": "shipment", "delivered": "completed" } return status_mapping.get(status, "Hubungi Admin") def generate_eta_delivery(self): current_date = datetime.datetime.now() prepare_days = 3 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') 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 class BarcodeProduct(models.Model): _name = 'barcode.product' _description = 'Barcode 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', required=True) barcode = fields.Char(string='Barcode') def check_duplicate_barcode(self): barcode_product = self.env['product.product'].search([('barcode', '=', self.barcode)]) if barcode_product: raise UserError('Barcode sudah digunakan {}'.format(barcode_product.display_name)) barcode_box = self.env['product.product'].search([('barcode_box', '=', self.barcode)]) if barcode_box: raise UserError('Barcode box sudah digunakan {}'.format(barcode_box.display_name)) @api.constrains('barcode') def send_barcode_to_product(self): for record in self: record.check_duplicate_barcode() if record.barcode and not record.product_id.barcode: record.product_id.barcode = record.barcode else: raise UserError('Barcode sudah terisi') class CheckKoli(models.Model): _name = 'check.koli' _description = 'Check Koli' _order = 'picking_id, id' _rec_name = 'koli' picking_id = fields.Many2one( 'stock.picking', string='Picking Reference', required=True, ondelete='cascade', index=True, copy=False, ) koli = fields.Char(string='Koli') reserved_id = fields.Many2one('stock.picking', string='Reserved Picking') check_koli_progress = fields.Char( string="Progress Check Koli" ) @api.constrains('koli') def _check_koli_progress(self): for check in self: if check.picking_id: all_checks = self.env['check.koli'].search([('picking_id', '=', check.picking_id.id)], order='id') if all_checks: check_index = list(all_checks).index(check) + 1 # Nomor urut check total_so_koli = len(all_checks) check.check_koli_progress = f"{check_index}/{total_so_koli}" if total_so_koli else "0/0" class ScanKoli(models.Model): _name = 'scan.koli' _description = 'Scan Koli' _order = 'picking_id, id' _rec_name = 'koli_id' picking_id = fields.Many2one( 'stock.picking', string='Picking Reference', required=True, ondelete='cascade', index=True, copy=False, ) koli_id = fields.Many2one('sales.order.koli', string='Koli') scan_koli_progress = fields.Char( string="Progress Scan Koli", compute="_compute_scan_koli_progress" ) code_koli = fields.Char(string='Code Koli') @api.onchange('code_koli') def _onchange_code_koli(self): if self.code_koli: koli = self.env['sales.order.koli'].search([('koli_id.koli', '=', self.code_koli)], limit=1) if koli: self.write({'koli_id': koli.id}) else: raise UserError('Koli tidak ditemukan') # def _compute_scan_koli_progress(self): # for scan in self: # if scan.picking_id: # all_scans = self.env['scan.koli'].search([('picking_id', '=', scan.picking_id.id)], order='id') # if all_scans: # scan_index = list(all_scans).index(scan) + 1 # Nomor urut scan # total_so_koli = scan.picking_id.total_so_koli # scan.scan_koli_progress = f"{scan_index}/{total_so_koli}" if total_so_koli else "0/0" @api.onchange('koli_id') def _onchange_koli_compare_with_konfirm_koli(self): if not self.koli_id: return if not self.picking_id.konfirm_koli_lines: raise UserError(_('Mapping Koli Harus Diisi!')) koli_picking = self.koli_id.picking_id._origin konfirm_pick_ids = [ line.pick_id._origin for line in self.picking_id.konfirm_koli_lines if line.pick_id ] if koli_picking not in konfirm_pick_ids: raise UserError(_('Koli tidak sesuai dengan mapping koli, pastikan picking terkait benar!')) @api.constrains('picking_id', 'koli_id') def _check_duplicate_koli(self): for record in self: if record.koli_id: existing_koli = self.search([ ('picking_id', '=', record.picking_id.id), ('koli_id', '=', record.koli_id.id), ('id', '!=', record.id) ]) if existing_koli: raise ValidationError(f"⚠️ Koli '{record.koli_id.display_name}' sudah discan untuk picking ini!") def unlink(self): picking_ids = set(self.mapped('koli_id.picking_id.id')) for scan in self: koli = scan.koli_id.koli_id if koli: koli.reserved_id = False for picking_id in picking_ids: remaining_scans = self.env['sales.order.koli'].search_count([ ('koli_id.picking_id', '=', picking_id) ]) delete_koli = len(self.filtered(lambda rec: rec.koli_id.picking_id.id == picking_id)) if remaining_scans == delete_koli: picking = self.env['stock.picking'].browse(picking_id) picking.linked_out_picking_id = False else: raise UserError(_("Tidak dapat menghapus scan koli, karena masih ada scan koli lain yang tersisa untuk picking ini.")) for picking_id in picking_ids: self._reset_qty_done_if_no_scan(picking_id) # self.check_koli_not_balance() return super(ScanKoli, self).unlink() @api.onchange('koli_id','scan_koli_progress') def onchange_koli_id(self): if not self.koli_id: return for scan in self: if scan.koli_id.koli_id.picking_id.group_id.id != scan.picking_id.group_id.id: scan.koli_id.koli_id.reserved_id = scan.picking_id.id.origin scan.koli_id.koli_id.picking_id.linked_out_picking_id = scan.picking_id.id.origin def _compute_scan_koli_progress(self): for scan in self: if not scan.picking_id: scan.scan_koli_progress = "0/0" continue try: all_scans = self.env['scan.koli'].search([('picking_id', '=', scan.picking_id.id)], order='id') if all_scans: scan_index = list(all_scans).index(scan) + 1 total_so_koli = scan.picking_id.total_so_koli or 0 scan.scan_koli_progress = f"{scan_index}/{total_so_koli}" else: scan.scan_koli_progress = "0/0" except Exception: # Fallback in case of any error scan.scan_koli_progress = "0/0" @api.constrains('picking_id', 'picking_id.total_so_koli') def _check_koli_validation(self): for scan in self.picking_id.scan_koli_lines: scan.koli_id.koli_id.reserved_id = scan.picking_id.id scan.koli_id.koli_id.picking_id.linked_out_picking_id = scan.picking_id.id total_scans = len(self.picking_id.scan_koli_lines) if total_scans != self.picking_id.total_so_koli: raise UserError(_("Jumlah scan koli tidak sama dengan total SO koli!")) # def check_koli_not_balance(self): # for scan in self: # total_scancs = self.env['scan.koli'].search_count([('picking_id', '=', scan.picking_id.id), ('id', '!=', scan.id)]) # if total_scancs != scan.picking_id.total_so_koli: # raise UserError(_("Jumlah scan koli tidak sama dengan total SO koli!")) @api.onchange('koli_id') def _onchange_koli_id(self): if not self.koli_id: return source_koli_so = self.picking_id.group_id.id source_koli = self.koli_id.picking_id.group_id.id if source_koli_so != source_koli: raise UserError(_('Koli tidak sesuai, pastikan picking terkait benar!')) @api.constrains('koli_id') def _send_product_from_koli_id(self): if not self.koli_id: return koli_count_by_picking = defaultdict(int) for scan in self: koli_count_by_picking[scan.koli_id.picking_id.id] += 1 for picking_id, total_koli in koli_count_by_picking.items(): picking = self.env['stock.picking'].browse(picking_id) if total_koli == picking.quantity_koli: pick_moves = self.env['stock.move.line'].search([('picking_id', '=', picking_id)]) out_moves = self.env['stock.move.line'].search([('picking_id', '=', picking.linked_out_picking_id.id)]) for pick_move in pick_moves: corresponding_out_move = out_moves.filtered(lambda m: m.product_id == pick_move.product_id) if corresponding_out_move: corresponding_out_move.qty_done += pick_move.qty_done def _reset_qty_done_if_no_scan(self, picking_id): product_bu_pick = self.env['stock.move.line'].search([('picking_id', '=', picking_id)]) for move in product_bu_pick: product_bu_out = self.env['stock.move.line'].search([('picking_id', '=', self.picking_id.id), ('product_id', '=', move.product_id.id)]) for bu_out in product_bu_out: bu_out.qty_done -= move.qty_done # if remaining_scans == 0: # picking = self.env['stock.picking'].browse(picking_id) # picking.move_line_ids_without_package.write({'qty_done': 0}) # picking.message_post(body=f"⚠ qty_done direset ke 0 untuk Picking {picking.name} karena tidak ada scan.koli yang tersisa.") # return remaining_scans class KonfirmKoli(models.Model): _name = 'konfirm.koli' _description = 'Konfirm Koli' _order = 'picking_id, id' _rec_name = 'pick_id' picking_id = fields.Many2one( 'stock.picking', string='Picking Reference', required=True, ondelete='cascade', index=True, copy=False, ) pick_id = fields.Many2one('stock.picking', string='Pick') @api.constrains('pick_id') def _check_duplicate_pick_id(self): for rec in self: exist = self.search([ ('pick_id', '=', rec.pick_id.id), ('picking_id', '=', rec.picking_id.id), ('id', '!=', rec.id), ]) if exist: raise UserError(f"⚠️ '{rec.pick_id.display_name}' sudah discan untuk picking ini!") class WarningModalWizard(models.TransientModel): _name = 'warning.modal.wizard' _description = 'Peringatan Koli Belum Diperiksa' name = fields.Char(default="⚠️ Perhatian!") message = fields.Text() picking_id = fields.Many2one('stock.picking') def action_continue(self): if self.picking_id: return self.picking_id.with_context(skip_koli_check=True).button_validate() return {'type': 'ir.actions.act_window_close'}