summaryrefslogtreecommitdiff
path: root/indoteknik_custom/models/stock_picking.py
diff options
context:
space:
mode:
Diffstat (limited to 'indoteknik_custom/models/stock_picking.py')
-rw-r--r--indoteknik_custom/models/stock_picking.py1279
1 files changed, 1139 insertions, 140 deletions
diff --git a/indoteknik_custom/models/stock_picking.py b/indoteknik_custom/models/stock_picking.py
index 36d9f63d..6a6fe352 100644
--- a/indoteknik_custom/models/stock_picking.py
+++ b/indoteknik_custom/models/stock_picking.py
@@ -1,7 +1,9 @@
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
@@ -12,11 +14,27 @@ 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'
- check_product_lines = fields.One2many('check.product', 'picking_id', string='Check Product', auto_join=True)
+ _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')
@@ -62,36 +80,47 @@ class StockPicking(models.Model):
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", )
+ sj_return_date = fields.Datetime(string="SJ Return Date", copy=False)
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")
+ ], 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")
+ ], 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)
+ ], 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([
- ('hold', 'Hold by Sales'),
+ ('wait_so_together', 'Tunggu SO Barengan'),
('not_paid', 'Customer belum bayar'),
- ('partial', 'Kirim Parsial'),
- ('indent', 'Indent'),
+ ('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")
+ 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)
@@ -112,16 +141,74 @@ class StockPicking(models.Model):
('invoiced', 'Fully Invoiced'),
('to invoice', 'To Invoice'),
('no', 'Nothing to Invoice')
- ], string='Invoice Status', related="sale_id.invoice_status")
+ ], 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")
+ 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.name or ''
+ city = record.real_shipping_id.kota_id.name 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):
@@ -133,8 +220,10 @@ class StockPicking(models.Model):
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)
+ 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)
@@ -166,6 +255,265 @@ class StockPicking(models.Model):
lalamove_image_url = fields.Char(string="Lalamove Image URL")
lalamove_image_html = fields.Html(string="Lalamove Image", compute="_compute_lalamove_image_html")
+ # KGX Section
+ kgx_pod_photo_url = fields.Char('KGX Photo URL')
+ kgx_pod_photo = fields.Html('KGX Photo', compute='_compute_kgx_image_html')
+ kgx_pod_signature = fields.Char('KGX Signature URL')
+ kgx_pod_receive_time = fields.Datetime('KGX Ata Date')
+ kgx_pod_receiver = fields.Char('KGX Receiver')
+
+ 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 _get_kgx_awb_number(self):
+ """Menggabungkan name dan origin untuk membuat AWB Number"""
+ self.ensure_one()
+ if not self.name or not self.origin:
+ return False
+ return f"{self.name} {self.origin}"
+
+ def _download_pod_photo(self, url):
+ """Mengunduh foto POD dari URL"""
+ try:
+ response = requests.get(url, timeout=10)
+ response.raise_for_status()
+ return base64.b64encode(response.content)
+ except Exception as e:
+ raise UserError(f"Gagal mengunduh foto POD: {str(e)}")
+
+ def _parse_datetime(self, dt_str):
+ """Parse datetime string dari format KGX"""
+ try:
+ from datetime import datetime
+ # Hilangkan timezone jika ada masalah parsing
+ if '+' in dt_str:
+ dt_str = dt_str.split('+')[0]
+ return datetime.strptime(dt_str, '%Y-%m-%dT%H:%M:%S')
+ except ValueError:
+ return False
+
+ def action_get_kgx_pod(self):
+ self.ensure_one()
+
+ awb_number = self._get_kgx_awb_number()
+ if not awb_number:
+ raise UserError("Nomor AWB tidak dapat dibuat, pastikan picking memiliki name dan origin")
+
+ url = "https://kgx.co.id/get_detail_awb"
+ headers = {'Content-Type': 'application/json'}
+ payload = {"params" : {'awb_number': awb_number}}
+
+ try:
+ response = requests.post(url, headers=headers, data=json.dumps(payload))
+ response.raise_for_status()
+ data = response.json()
+
+ if data.get('result', {}).get('data', []):
+ pod_data = data['result']['data'][0].get('connote_pod', {})
+ photo_url = pod_data.get('photo')
+
+ self.kgx_pod_photo_url = photo_url
+ self.kgx_pod_signature = pod_data.get('signature')
+ self.kgx_pod_receiver = pod_data.get('receiver')
+ self.kgx_pod_receive_time = self._parse_datetime(pod_data.get('timeReceive'))
+ self.driver_arrival_date = self._parse_datetime(pod_data.get('timeReceive'))
+
+ return data
+ else:
+ raise UserError(f"Tidak ditemukan data untuk AWB: {awb_number}")
+
+ except requests.exceptions.RequestException as e:
+ raise UserError(f"Gagal mengambil data POD: {str(e)}")
+
+ @api.constrains('sj_return_date')
+ def _check_sj_return_date(self):
+ for record in self:
+ if not record.driver_arrival_date:
+ if record.sj_return_date:
+ raise ValidationError(
+ _("Anda tidak dapat mengubah Tanggal Pengembalian setelah Tanggal Pengiriman!")
+ )
+
+ 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:
@@ -173,13 +521,20 @@ class StockPicking(models.Model):
else:
record.lalamove_image_html = "No image available."
+ def _compute_kgx_image_html(self):
+ for record in self:
+ if record.kgx_pod_photo_url:
+ record.kgx_pod_photo = f'<img src="{record.kgx_pod_photo_url}" width="300" height="300"/>'
+ else:
+ record.kgx_pod_photo = "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
@@ -214,7 +569,7 @@ class StockPicking(models.Model):
for stop in stops:
pod = stop.get("POD", {})
if pod.get("status") == "DELIVERED":
- image_url = pod.get("image") # Sesuaikan jika key berbeda
+ image_url = pod.get("image") # Sesuaikan jika key berbeda
self.lalamove_image_url = image_url
address = stop.get("address")
@@ -235,7 +590,6 @@ class StockPicking(models.Model):
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)
@@ -332,8 +686,9 @@ class StockPicking(models.Model):
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"
+
+ 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)])
@@ -359,17 +714,18 @@ class StockPicking(models.Model):
('order_id', '=', self.sale_id.id),
('product_id', '=', move_line.product_id.id)
], limit=1)
-
+
if order_line:
items_data_instant.append({
"name": order_line.product_id.name,
"description": order_line.name,
"value": order_line.price_unit,
- "quantity": move_line.qty_done, # Menggunakan qty_done dari move_line
+ "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,
@@ -381,7 +737,8 @@ class StockPicking(models.Model):
"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",
+ "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,
@@ -389,31 +746,57 @@ class StockPicking(models.Model):
}
# 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:
+ 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_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
+ "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(url, headers=headers, json=payload)
+ 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", "")
- if response.status_code == 201:
- return response.json()
+ 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:
- raise UserError(f"Error saat mengirim ke Biteship: {response.content}")
-
+ 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:
+ def constrains_driver_departure_date(self):
+ if not self.date_doc_kirim:
self.date_doc_kirim = self.driver_departure_date
@api.constrains('arrival_time')
@@ -441,92 +824,64 @@ class StockPicking(models.Model):
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'),
- ('name', 'ilike', 'BU/OUT/'),
+ ('picking_type_code', '=', 'internal'),
+ ('name', 'ilike', 'BU/PICK/'),
])
- count = self.search_count([
- ('state', 'not in', ['cancel', 'draft', 'done']),
- ('picking_type_code', '=', 'outgoing')
- ])
-
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/OUT/'),
- ('picking_type_code', '=', 'outgoing'),
+ ('name', 'ilike', 'BU/PICK/'),
+ ('picking_type_code', '=', 'internal'),
('state', 'not in', ['cancel', 'draft', 'done'])
])
- count = self.search_count([
- ('backorder_id', '!=', False),
- ('picking_type_code', '=', 'outgoing'),
- ('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'} },
+ 'params': {'title': title, 'message': message, 'next': {'type': 'ir.actions.act_window_close'}},
}
def _compute_shipping_status(self):
@@ -536,7 +891,7 @@ class StockPicking(models.Model):
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):
@@ -544,10 +899,10 @@ class StockPicking(models.Model):
"""
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
+ # custom here
po = self.env['purchase.order'].search([
('name', '=', self.group_id.name)
])
@@ -564,24 +919,29 @@ class StockPicking(models.Model):
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)
+ 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)))
+ 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['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.'))
+ 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'))):
+ 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()
@@ -611,7 +971,8 @@ class StockPicking(models.Model):
# 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()
+ 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)
@@ -674,7 +1035,7 @@ class StockPicking(models.Model):
# 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')
+ # raise UserError('Sudah pernah dikirim kalender')
for pick in self:
if not pick.is_internal_use:
@@ -695,23 +1056,27 @@ class StockPicking(models.Model):
if self.env.user.is_accounting:
pick.approval_return_status = 'approved'
continue
+ else:
+ pick.approval_return_status = 'pengajuan1'
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
+ 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:
+ 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
+ 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
@@ -721,7 +1086,7 @@ class StockPicking(models.Model):
raise UserError('Harus Purchasing yang Ask Return')
def calculate_line_no(self):
-
+
for picking in self:
name = picking.group_id.name
for move in picking.move_ids_without_package:
@@ -744,10 +1109,10 @@ class StockPicking(models.Model):
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
+ 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
+ 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
@@ -769,13 +1134,13 @@ class StockPicking(models.Model):
])
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
+ 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):
@@ -788,12 +1153,74 @@ class StockPicking(models.Model):
return quant.quantity
return 0
-
+
qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id)
if line.qty_done > qty_onhand:
raise UserError('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")
@@ -815,10 +1242,10 @@ class StockPicking(models.Model):
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")
@@ -841,13 +1268,130 @@ class StockPicking(models.Model):
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.calculate_line_no()
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 (self.state_reserve == 'done' and self.picking_type_code == 'internal' and 'BU/PICK/' in self.name
+ and self.linked_manual_bu_out):
+ if not self.linked_manual_bu_out.date_reserved:
+ current_datetime = datetime.datetime.utcnow()
+ self.linked_manual_bu_out.date_reserved = current_datetime
+ self.linked_manual_bu_out.message_post(
+ body=f"Date Reserved diisi secara otomatis dari validasi BU/PICK {self.name}"
+ )
+
+ 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']), ('move_type', '=', 'out_invoice')], limit=1)
+
+ if not invoice:
+ continue
+
+ if not picking.so_lama and invoice 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:
@@ -861,7 +1405,7 @@ class StockPicking(models.Model):
return quant.quantity
return 0
-
+
qty_onhand = check_qty_per_inventory(self, line.product_id, line.location_id)
if line.qty_done > qty_onhand:
raise UserError('Quantity Done melebihi Quantity Onhand')
@@ -918,32 +1462,56 @@ class StockPicking(models.Model):
return True
def action_cancel(self):
- if not self.env.user.is_logistic_approver and self.env.context.get('active_model') == 'stock.picking':
+ 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)
- if vals.get('picking_type_code') == 'incoming' and vals.get('location_dest_id') == 58:
- if 'name' in vals and vals['name'].startswith('BU/IN/'):
- vals['name'] = vals['name'].replace('BU/IN/', 'BU/INPUT/', 1)
-
- if vals.get('picking_type_code') == 'internal' and vals.get('location_id') == 58:
- if 'name' in vals and vals['name'].startswith('BU/INT'):
- new_name = vals['name'].replace('BU/INT', 'BU/IN', 1)
- # Periksa apakah nama sudah ada
- if self.env['stock.picking'].search_count([('name', '=', new_name), ('company_id', '=', vals.get('company_id'))]) > 0:
- new_name = f"{new_name}-DUP"
- vals['name'] = new_name
- return super(StockPicking, self).create(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
@@ -959,7 +1527,8 @@ class StockPicking(models.Model):
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:
+ 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)
@@ -1003,7 +1572,7 @@ class StockPicking(models.Model):
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',
@@ -1028,7 +1597,7 @@ class StockPicking(models.Model):
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:
@@ -1040,10 +1609,13 @@ class StockPicking(models.Model):
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': ''
},
@@ -1052,20 +1624,105 @@ class StockPicking(models.Model):
'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
@@ -1079,7 +1736,7 @@ class StockPicking(models.Model):
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)
@@ -1088,9 +1745,10 @@ class StockPicking(models.Model):
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'
@@ -1104,9 +1762,73 @@ class CheckProduct(models.Model):
index=True,
copy=False,
)
- product_id = fields.Many2one('product.product', string='Product', required=True)
- quantity = fields.Float(string='Quantity', default=1.0, required=True)
+ 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):
@@ -1121,7 +1843,6 @@ class CheckProduct(models.Model):
else:
record.status = 'Done'
-
def create(self, vals):
# Create the record
record = super(CheckProduct, self).create(vals)
@@ -1146,7 +1867,8 @@ class CheckProduct(models.Model):
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)
+ 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)
@@ -1199,14 +1921,14 @@ class CheckProduct(models.Model):
if not moves:
raise UserError((
- "The product '%s' tidak ada di operations. "
- ) % record.product_id.display_name)
+ "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 and line.id != record.id
+ lambda line: line.product_id == record.product_id
)
if existing_lines:
@@ -1214,22 +1936,23 @@ class CheckProduct(models.Model):
first_line = existing_lines[0]
# Calculate the total quantity after addition
- total_quantity = sum(existing_lines.mapped('quantity')) - record.quantity
+ 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))
+ "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:
+ if record.quantity == total_qty_in_moves:
raise UserError((
- "Quantity Product '%s' sudah melebihi quantity demand."
- ) % (record.product_id.display_name))
+ "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'
@@ -1246,10 +1969,286 @@ class BarcodeProduct(models.Model):
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') \ No newline at end of file
+ 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'}