from odoo import fields, models, api, _ from odoo.exceptions import AccessError, UserError, ValidationError from odoo.tools.float_utils import float_is_zero from datetime import datetime from itertools import groupby import pytz, datetime class StockPicking(models.Model): _inherit = 'stock.picking' 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='Driver Departure Date', readonly=True, copy=False ) driver_arrival_date = fields.Datetime( string='Driver Arrival Date', readonly=True, 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 ) approval_status = fields.Selection([ ('pengajuan1', 'Approval Accounting'), ('approved', 'Approved'), ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Internal Use") approval_receipt_status = fields.Selection([ ('pengajuan1', 'Approval Logistic'), ('approved', 'Approved'), ], string='Approval Receipt Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Receipt") approval_return_status = fields.Selection([ ('pengajuan1', 'Approval Finance'), ('approved', 'Approved'), ], string='Approval Return Status', readonly=True, copy=False, index=True, tracking=3, help="Approval Status untuk Return") date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ', help="Tanggal Kirim di cetakan SJ, tidak berpengaruh ke Accounting", tracking=True) note_logistic = fields.Selection([ ('hold', 'Hold by Sales'), ('not_paid', 'Customer belum bayar'), ('partial', 'Kirim Parsial'), ('not_complete', 'Belum Lengkap'), ('indent', 'Indent') ], string='Note Logistic', help='jika field ini diisi maka tidak akan dihitung ke lead time') waybill_id = fields.One2many(comodel_name='airway.bill', inverse_name='do_id', string='Airway Bill') purchase_representative_id = fields.Many2one('res.users', related='move_lines.purchase_line_id.order_id.user_id', string="Purchase Representative") carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method') shipping_status = fields.Char(string='Shipping Status', compute="_compute_shipping_status") date_reserved = fields.Datetime(string="Date Reserved", help='Tanggal ter-reserved semua barang nya') status_printed = fields.Selection([ ('not_printed', 'Belum Print'), ('printed', 'Printed') ], string='Printed?', copy=False) date_unreserve = fields.Datetime(string="Date Unreserved", copy=False, tracking=True) date_availability = fields.Datetime(string="Date Availability", copy=False, tracking=True) def do_unreserve(self): res = super(StockPicking, self).do_unreserve() current_time = datetime.datetime.utcnow() self.date_unreserve = current_time return res def _compute_shipping_status(self): for rec in self: status = 'pending' if rec.driver_departure_date and not rec.driver_arrival_date: status = 'shipment' elif rec.driver_departure_date and 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 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' else: pick.approval_return_status = 'pengajuan1' 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 button_validate(self): 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 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.group_id.sale_id: if self.group_id.sale_id.payment_link_midtrans: if self.group_id.sale_id.payment_status != 'settlement': raise UserError('Uang belum masuk (settlement), mohon konfirmasi ke sales atau finance') 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') 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() res = super(StockPicking, self).button_validate() self.calculate_line_no() return res @api.model def create(self, vals): self._use_faktur(vals) return super(StockPicking, self).create(vals) def write(self, vals): self._use_faktur(vals) return super(StockPicking, self).write(vals) def _use_faktur(self, vals): if vals.get('efaktur_id', False): self.env['vit.efaktur'].search( [ ('id', '=', vals['efaktur_id']) ], limit=1 ).is_used = True if self.efaktur_id.id != vals['efaktur_id']: self.efaktur_id.is_used = False return True def compute_delivery_status(self): for picking in self: if not picking.driver_departure_date and picking.picking_code: picking.delivery_status = "Sedang Dikemas" elif picking.driver_departure_date and not picking.driver_arrival_date: picking.delivery_status = "Dalam Perjalanan" elif picking.driver_departure_date and picking.driver_arrival_date and picking.carrier_id == 1: picking.delivery_status = "Diterima Konsumen" elif picking.driver_departure_date and picking.driver_arrival_date and picking.carrier_id != 1: picking.delivery_status = "Diterima Ekspedisi" else: picking.delivery_status = "Diterima Konsumen" def create_manifest_data(self, description, object): datetime_str = '' if isinstance(object, datetime.datetime): jakarta_timezone = pytz.timezone('Asia/Jakarta') datetime_str = object.replace(tzinfo=pytz.utc).astimezone(jakarta_timezone).strftime('%Y-%m-%d %H:%M:%S') return { 'description': description, 'datetime': datetime_str } def get_manifests(self): if self.waybill_id and len(self.waybill_id.manifest_ids) > 0: return [self.create_manifest_data(x.description, x.datetime) for x in self.waybill_id.manifest_ids] status_mapping = { 'pickup': { 'arrival': 'Sudah diambil', 'departure': 'Siap diambil', 'prepare': 'Sedang disiapkan' }, 'delivery': { 'arrival': 'Sudah sampai', 'departure': 'Sedang dikirim', 'prepare': 'Menunggu pickup', } } status_key = 'delivery' if self.carrier_id.id == 32: status_key = 'pickup' manifest_datas = [] departure_date = self.driver_departure_date arrival_date = self.driver_arrival_date status = status_mapping.get(status_key) if not status: return manifest_datas if arrival_date: manifest_datas.append(self.create_manifest_data(status['arrival'], arrival_date)) if departure_date: manifest_datas.append(self.create_manifest_data(status['departure'], departure_date)) manifest_datas.append(self.create_manifest_data(status['prepare'], self.create_date)) return manifest_datas def get_tracking_detail(self): self.ensure_one() response = { 'delivery_order': { 'name': self.name, 'carrier': self.carrier_id.name or '', 'receiver_name': '', 'receiver_city': '' }, 'delivered': False, 'status': self.shipping_status, 'waybill_number': self.delivery_tracking_no or '', 'delivery_status': None, 'eta': self.generate_eta_delivery(), 'manifests': self.get_manifests() } if not self.waybill_id or len(self.waybill_id.manifest_ids) == 0: response['delivered'] = self.driver_arrival_date != False return response response['delivery_order']['receiver_name'] = self.waybill_id.receiver_name response['delivery_order']['receiver_city'] = self.waybill_id.receiver_city response['delivery_status'] = self.waybill_id._get_history('delivery_status') response['delivered'] = self.waybill_id.delivered return response def generate_eta_delivery(self): current_date = datetime.datetime.now() prepare_days = 3 start_date = self.driver_departure_date or self.create_date ead = self.sale_id.estimated_arrival_days or 0 if not self.driver_departure_date: ead += prepare_days ead_datetime = datetime.timedelta(days=ead) fastest_eta = start_date + ead_datetime if not self.driver_departure_date and fastest_eta < current_date: fastest_eta = current_date + ead_datetime longest_days = 3 longest_eta = fastest_eta + datetime.timedelta(days=longest_days) format_time = '%d %b %Y' format_time_fastest = '%d %b' if fastest_eta.year == longest_eta.year else format_time formatted_fastest_eta = fastest_eta.strftime(format_time_fastest) formatted_longest_eta = longest_eta.strftime(format_time) return f'{formatted_fastest_eta} - {formatted_longest_eta}'