from re import search from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError from datetime import datetime, timedelta, timezone, time import logging, random, string, requests, math, json, re, qrcode, base64 from io import BytesIO from collections import defaultdict import pytz _logger = logging.getLogger(__name__) class CancelReasonOrder(models.TransientModel): _name = 'cancel.reason.order' _description = 'Wizard for Cancel Reason order' request_id = fields.Many2one('sale.order', string='Request') reason_cancel = fields.Selection([ ('harga_terlalu_mahal', 'Harga barang terlalu mahal'), ('harga_web_tidak_valid', 'Harga web tidak valid'), ('stok_kosong', 'Stock kosong'), ('tidak_mau_indent', 'Customer tidak mau indent'), ('batal_rencana_pembelian', 'Customer membatalkan rencana pembelian'), ('vendor_tidak_support_demo', 'Vendor tidak support demo/trial product'), ('product_knowledge_kurang', 'Product knowledge kurang baik'), ('barang_tidak_sesuai', 'Barang tidak sesuai/tepat'), ('tidak_sepakat_pembayaran', 'Tidak menemukan kesepakatan untuk pembayaran'), ('dokumen_tidak_support', 'Indoteknik tidak bisa support document yang dibutuhkan (Ex: TKDN, COO, SNI)'), ('ganti_quotation', 'Ganti Quotation'), ('testing_internal', 'Testing Internal'), ('revisi_data', 'Revisi Data'), ], string='Reason for Cancel', required=True, copy=False, index=True, tracking=3) attachment_bukti = fields.Many2many( 'ir.attachment', string="Attachment Bukti", readonly=False, tracking=3, required=True ) nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) def confirm_reject(self): order = self.request_id if order: order.write({'reason_cancel': self.reason_cancel}) if not self.attachment_bukti: raise UserError('Attachment bukti wajib disertakan') order.write({'attachment_bukti': self.attachment_bukti}) order.message_post(body='Attachment Bukti Cancel', attachment_ids=[self.attachment_bukti.id]) if self.reason_cancel == 'ganti_quotation': if self.nomor_so_pengganti: order.write({'nomor_so_pengganti': self.nomor_so_pengganti}) else: raise UserError('Nomor SO pengganti wajib disertakan') order.confirm_cancel_order() return {'type': 'ir.actions.act_window_close'} class ShippingOption(models.Model): _name = "shipping.option" _description = "Shipping Option" name = fields.Char(string="Option Name", required=True) price = fields.Float(string="Price", required=True) provider = fields.Char(string="Provider") etd = fields.Char(string="Estimated Delivery Time") sale_order_id = fields.Many2one('sale.order', string="Sale Order", ondelete="cascade") class SaleOrder(models.Model): _inherit = "sale.order" ongkir_ke_xpdc = fields.Float(string='Ongkir ke Ekspedisi', help='Biaya ongkir ekspedisi', copy=False, index=True, tracking=3) metode_kirim_ke_xpdc = fields.Selection([ ('indoteknik_deliv', 'Indoteknik Delivery'), ('lalamove', 'Lalamove'), ('grab', 'Grab'), ('gojek', 'Gojek'), ('deliveree', 'Deliveree'), ('other', 'Other'), ], string='Metode Kirim Ke Ekspedisi', copy=False, index=True, tracking=3) koli_lines = fields.One2many('sales.order.koli', 'sale_order_id', string='Sales Order Koli', auto_join=True) fulfillment_line_v2 = fields.One2many('sales.order.fulfillment.v2', 'sale_order_id', string='Fullfillment2') fullfillment_line = fields.One2many('sales.order.fullfillment', 'sales_order_id', string='Fullfillment') reject_line = fields.One2many('sales.order.reject', 'sale_order_id', string='Reject Lines') order_sales_match_line = fields.One2many('sales.order.purchase.match', 'sales_order_id', string='Purchase Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True) total_margin = fields.Float('Total Margin', compute='_compute_total_margin', help="Total Margin in Sales Order Header") total_percent_margin = fields.Float('Total Percent Margin', compute='_compute_total_percent_margin', help="Total % Margin in Sales Order Header") total_margin_excl_third_party = fields.Float('Before Margin', help="Before Margin in Sales Order Header") approval_status = fields.Selection([ ('pengajuan1', 'Approval Manager'), ('pengajuan2', 'Approval Pimpinan'), ('approved', 'Approved'), ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3) carrier_id = fields.Many2one('delivery.carrier', string='Shipping Method', tracking=3) have_visit_service = fields.Boolean(string='Have Visit Service', compute='_have_visit_service', help='To compute is customer get visit service') delivery_amt = fields.Float(string='Delivery Amt', copy=False) shipping_cost_covered = fields.Selection([ ('indoteknik', 'Indoteknik'), ('customer', 'Customer') ], string='Shipping Covered by', help='Siapa yang menanggung biaya ekspedisi?', copy=False, tracking=3) shipping_paid_by = fields.Selection([ ('indoteknik', 'Indoteknik'), ('customer', 'Customer') ], string='Shipping Paid by', help='Siapa yang talangin dulu Biaya ekspedisi-nya?', copy=False, tracking=3) sales_tax_id = fields.Many2one('account.tax', string='Tax', domain=['|', ('active', '=', False), ('active', '=', True)]) have_outstanding_invoice = fields.Boolean('Have Outstanding Invoice', compute='_have_outstanding_invoice') have_outstanding_picking = fields.Boolean('Have Outstanding Picking', compute='_have_outstanding_picking') have_outstanding_po = fields.Boolean('Have Outstanding PO', compute='_have_outstanding_po') purchase_ids = fields.Many2many('purchase.order', string='Purchases', compute='_get_purchases') real_shipping_id = fields.Many2one( 'res.partner', string='Real Delivery Address', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Dipakai untuk alamat tempel", tracking=True) real_invoice_id = fields.Many2one( 'res.partner', string='Delivery Invoice Address', required=True, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Dipakai untuk alamat tempel", tracking=True) fee_third_party = fields.Float('Fee Pihak Ketiga') biaya_lain_lain = fields.Float('Biaya Lain Lain') so_status = fields.Selection([ ('terproses', 'Terproses'), ('sebagian', 'Sebagian Diproses'), ('menunggu', 'Menunggu Diproses'), ], copy=False) partner_purchase_order_name = fields.Char(string='Nama PO Customer', copy=False, help="Nama purchase order customer, diisi oleh customer melalui website.", tracking=3) partner_purchase_order_description = fields.Text(string='Keterangan PO Customer', copy=False, help="Keterangan purchase order customer, diisi oleh customer melalui website.", tracking=3) partner_purchase_order_file = fields.Binary(string='File PO Customer', copy=False, help="File purchase order customer, diisi oleh customer melalui website.") payment_status = fields.Selection([ ('pending', 'Pending'), ('capture', 'Capture'), ('settlement', 'Settlement'), ('deny', 'Deny'), ('cancel', 'Cancel'), ('expire', 'Expire'), ('failure', 'Failure'), ('refund', 'Refund'), ('chargeback', 'Chargeback'), ('partial_refund', 'Partial Refund'), ('partial_chargeback', 'Partial Chargeback'), ('authorize', 'Authorize'), ], tracking=True, string='Payment Status', help='Payment Gateway Status / Midtrans / Web, https://docs.midtrans.com/en/after-payment/status-cycle') date_doc_kirim = fields.Datetime(string='Tanggal Kirim di SJ', help="Tanggal Kirim di cetakan SJ yang terakhir, tidak berpengaruh ke Accounting") payment_type = fields.Char(string='Payment Type', help='Jenis pembayaran dengan Midtrans') gross_amount = fields.Float(string='Gross Amount', help='Jumlah pembayaran yang dilakukan dengan Midtrans') notification = fields.Char(string='Notification', help='Dapat membantu error dari approval') delivery_service_type = fields.Char(string='Delivery Service Type', help='data dari rajaongkir') grand_total = fields.Monetary(string='Grand Total', help='Amount total + amount delivery', compute='_compute_grand_total') payment_link_midtrans = fields.Char(string='Payment Link', help='Url payment yg digenerate oleh midtrans, harap diserahkan ke customer agar dapat dilakukan pembayaran secara mandiri') payment_qr_code = fields.Binary("Payment QR Code") due_id = fields.Many2one('due.extension', string="Due Extension", readonly=True, tracking=True) vendor_approval_id = fields.Many2many('vendor.approval', string="Vendor Approval", readonly=True, tracking=True, copy=False) customer_type = fields.Selection([ ('pkp', 'PKP'), ('nonpkp', 'Non PKP') ], required=True) sppkp = fields.Char(string="SPPKP", required=True, tracking=True) npwp = fields.Char(string="NPWP", required=True, tracking=True) purchase_total = fields.Monetary(string='Purchase Total', compute='_compute_purchase_total') voucher_id = fields.Many2one(comodel_name='voucher', string='Voucher', copy=False) applied_voucher_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False) amount_voucher_disc = fields.Float(string='Voucher Discount') applied_voucher_shipping_id = fields.Many2one(comodel_name='voucher', string='Applied Voucher', copy=False) amount_voucher_shipping_disc = fields.Float(string='Voucher Discount') source_id = fields.Many2one('utm.source', 'Source', domain="[('id', 'in', [32, 59, 60, 61])]", required=True) estimated_arrival_days = fields.Integer('Estimated Arrival To', default=0) estimated_arrival_days_start = fields.Integer('Estimated Arrival From', default=0) email = fields.Char(string='Email') picking_iu_id = fields.Many2one('stock.picking', 'Picking IU') helper_by_id = fields.Many2one('res.users', 'Helper By') eta_date_start = fields.Datetime(string='ETA Date start', copy=False, compute='_compute_eta_date') eta_date = fields.Datetime(string='ETA Date end', copy=False, compute='_compute_eta_date') flash_sale = fields.Boolean(string='Flash Sale', help='Data dari web') is_continue_transaction = fields.Boolean(string='Button Transaction', help='Data dari web') web_approval = fields.Selection([ ('company', 'Company'), ('cust_manager', 'Customer Manager'), ('cust_director', 'Customer Director'), ('cust_procurement', 'Customer Procurement') ], string='Web Approval', copy=False) compute_fullfillment = fields.Boolean(string='Compute Fullfillment', compute="_compute_fullfillment") vendor_approval = fields.Boolean(string='Vendor Approval') note_ekspedisi = fields.Char(string="Note Ekspedisi") date_kirim_ril = fields.Datetime(string='Tanggal Kirim SJ', compute='_compute_date_kirim', copy=False) date_status_done = fields.Datetime(string='Date Done DO', compute='_compute_date_kirim', copy=False) date_driver_arrival = fields.Datetime(string='Arrival Date', compute='_compute_date_kirim', copy=False) date_driver_departure = fields.Datetime(string='Departure Date', compute='_compute_date_kirim', copy=False) note_website = fields.Char(string="Note Website") use_button = fields.Boolean(string='Using Calculate Selling Price', copy=False) unreserve_id = fields.Many2one('stock.picking', 'Unreserve Picking') voucher_shipping_id = fields.Many2one(comodel_name='voucher', string='Voucher Shipping', copy=False) margin_after_delivery_purchase = fields.Float(string='Margin After Delivery Purchase', compute='_compute_margin_after_delivery_purchase') percent_margin_after_delivery_purchase = fields.Float(string='% Margin After Delivery Purchase', compute='_compute_margin_after_delivery_purchase') purchase_delivery_amt = fields.Float(string='Purchase Delivery Amount', compute='_compute_purchase_delivery_amount') type_promotion = fields.Char(string='Type Promotion', compute='_compute_type_promotion') partner_invoice_id = fields.Many2one( 'res.partner', string='Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True, # Menambahkan tracking=True ) partner_shipping_id = fields.Many2one( 'res.partner', string='Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Terms', check_company=True, # Unrequired company domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", tracking=True) total_weight = fields.Float(string='Total Weight', compute='_compute_total_weight') pareto_status = fields.Selection([ ('PR', 'Pareto Repeating'), ('PPR', 'Potensi Pareto Repeating'), ('PNR', 'Pareto Non Repeating'), ('NP', 'Non Pareto') ]) # estimated_ready_ship_date = fields.Datetime( # string='ET Ready to Ship compute', # compute='_compute_etrts_date' # ) expected_ready_to_ship = fields.Datetime( string='ET Ready to Ship', copy=False ) shipping_method_picking = fields.Char(string='Shipping Method Picking', compute='_compute_shipping_method_picking') reason_cancel = fields.Selection([ ('harga_terlalu_mahal', 'Harga barang terlalu mahal'), ('harga_web_tidak_valid', 'Harga web tidak valid'), ('stok_kosong', 'Stock kosong'), ('tidak_mau_indent', 'Customer tidak mau indent'), ('batal_rencana_pembelian', 'Customer membatalkan rencana pembelian'), ('vendor_tidak_support_demo', 'Vendor tidak support demo/trial product'), ('product_knowledge_kurang', 'Product knowledge kurang baik'), ('barang_tidak_sesuai', 'Barang tidak sesuai/tepat'), ('tidak_sepakat_pembayaran', 'Tidak menemukan kesepakatan untuk pembayaran'), ('dokumen_tidak_support', 'Indoteknik tidak bisa support document yang dibutuhkan (Ex: TKDN, COO, SNI)'), ('ganti_quotation', 'Ganti Quotation'), ('testing_internal', 'Testing Internal'), ('revisi_data', 'Revisi Data'), ], string='Reason for Cancel', copy=False, index=True, tracking=3) attachment_bukti = fields.Many2one( 'ir.attachment', string="Attachment Bukti Cancel", readonly=False, ) nomor_so_pengganti = fields.Char(string='Nomor SO Pengganti', copy=False, tracking=3) shipping_option_id = fields.Many2one("shipping.option", string="Selected Shipping Option", domain="['|', ('sale_order_id', '=', False), ('sale_order_id', '=', id)]") select_shipping_option = fields.Selection([ ('biteship', 'Biteship'), ('custom', 'Custom'), ], string='Select Shipping Option', help="Select shipping option for delivery", Tracking=True) @api.onchange('shipping_option_id') def _onchange_shipping_option_id(self): if self.shipping_option_id: self.delivery_amt = self.shipping_option_id.price def _get_biteship_courier_codes(self): return [ 'gojek','grab','deliveree','lalamove','jne','tiki','ninja','lion','rara','sicepat','jnt','pos','idexpress','rpx','wahana','jdl','pos','anteraja','sap','paxel','borzo' ] @api.onchange('select_shipping_option') def _onchange_select_shipping_option(self): self.shipping_option_id = False self.carrier_id = False self.delivery_amt = 0 # Dapatkan semua ID carrier untuk Biteship biteship_carrier_ids = [] # Gunakan SQL langsung untuk menghindari masalah ORM self.env.cr.execute(""" SELECT delivery_carrier_id FROM rajaongkir_kurir WHERE name IN %s """, (tuple(self._get_biteship_courier_codes()),)) # Ambil ID numerik hasil query biteship_carrier_ids = [row[0] for row in self.env.cr.fetchall() if row[0]] if self.select_shipping_option == 'biteship': domain = [('id', 'in', biteship_carrier_ids)] if biteship_carrier_ids else [] else: # 'custom' domain = [('id', 'not in', biteship_carrier_ids)] if biteship_carrier_ids else [] return {'domain': {'carrier_id': domain}} @api.constrains('fee_third_party', 'delivery_amt', 'biaya_lain_lain') def _check_total_margin_excl_third_party(self): for rec in self: if rec.fee_third_party == 0 and rec.total_margin_excl_third_party != rec.total_percent_margin: # Gunakan direct SQL atau flag context untuk menghindari rekursi self.env.cr.execute(""" UPDATE sale_order SET total_margin_excl_third_party = %s WHERE id = %s """, (rec.total_percent_margin, rec.id)) self.invalidate_cache() @api.constrains('shipping_option_id') def _check_shipping_option(self): for rec in self: if rec.shipping_option_id: rec.delivery_amt = rec.shipping_option_id.price def _compute_shipping_method_picking(self): for order in self: if order.picking_ids: carrier_names = order.picking_ids.mapped('carrier_id.name') order.shipping_method_picking = ', '.join(filter(None, carrier_names)) else: order.shipping_method_picking = False @api.onchange('payment_status') def _is_continue_transaction(self): if not self.is_continue_transaction: if self.payment_status == 'settlement': self.is_continue_transaction = True else: self.is_continue_transaction = False def _compute_total_weight(self): total_weight = 0 missing_weight_products = [] for line in self.order_line: if line.weight > 0: total_weight += line.weight * line.product_uom_qty self.total_weight = total_weight def action_indoteknik_estimate_shipping(self): if not self.real_shipping_id.kota_id.is_jabodetabek: raise UserError('Estimasi ongkir hanya bisa dilakukan di kota Jabodetabek') total_weight = 0 missing_weight_products = [] for line in self.order_line: if line.weight > 0: total_weight += line.weight * line.product_uom_qty line.product_id.weight = line.weight else: missing_weight_products.append(line.product_id.name) if missing_weight_products: product_names = '
'.join(missing_weight_products) self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}") if total_weight == 0: raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") if total_weight < 10: total_weight = 10 self.delivery_amt = total_weight * 3000 shipping_option = self.env["shipping.option"].create({ "name": "Indoteknik Delivery", "price": self.delivery_amt, "provider": "Indoteknik", "etd": "1-2 Hari", "sale_order_id": self.id, }) self.shipping_option_id = shipping_option.id self.message_post( body=( f"Estimasi pengiriman Indoteknik berhasil:
" f"Layanan: {shipping_option.name}
" f"ETD: {shipping_option.etd}
" f"Biaya: Rp {shipping_option.price:,}
" f"Provider: {shipping_option.provider}" ), message_type="comment", ) def action_estimate_shipping(self): # if self.carrier_id.id in [1, 151]: # self.action_indoteknik_estimate_shipping() # return if self.select_shipping_option == 'biteship': return self.action_estimate_shipping_biteship() elif self.carrier_id.id in [1, 151]: # ID untuk Indoteknik Delivery return self.action_indoteknik_estimate_shipping() else: total_weight = 0 missing_weight_products = [] for line in self.order_line: if line.weight > 0: total_weight += line.weight * line.product_uom_qty line.product_id.weight = line.weight else: missing_weight_products.append(line.product_id.name) if missing_weight_products: product_names = '
'.join(missing_weight_products) self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}") if total_weight == 0: raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") destination_subsdistrict_id = self.real_shipping_id.kecamatan_id.rajaongkir_id if not destination_subsdistrict_id: raise UserError("Gagal mendapatkan ID kota tujuan.") result = self._call_rajaongkir_api(total_weight, destination_subsdistrict_id) if result: shipping_options = [] for courier in result['rajaongkir']['results']: for cost_detail in courier['costs']: service = cost_detail['service'] description = cost_detail['description'] etd = cost_detail['cost'][0]['etd'] value = cost_detail['cost'][0]['value'] shipping_options.append((service, description, etd, value, courier['code'])) self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() _logger.info(f"Shipping options: {shipping_options}") for service, description, etd, value, provider in shipping_options: self.env["shipping.option"].create({ "name": service, "price": value, "provider": provider, "etd": etd, "sale_order_id": self.id, }) self.shipping_option_id = self.env["shipping.option"].search([('sale_order_id', '=', self.id)], limit=1).id _logger.info(f"Shipping option SO ID: {self.shipping_option_id}") self.message_post( body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
" f"{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment" ) # self.message_post(body=f"Estimasi Ongkos Kirim: Rp{self.delivery_amt}
Detail Lain:
{'
'.join([f'Service: {s[0]}, Description: {s[1]}, ETD: {s[2]} hari, Cost: Rp {s[3]}' for s in shipping_options])}", message_type="comment") else: raise UserError("Gagal mendapatkan estimasi ongkir.") def _validate_for_shipping_estimate(self): # Cek berat produk total_weight = 0 missing_weight_products = [] for line in self.order_line: if line.weight > 0: total_weight += line.weight * line.product_uom_qty line.product_id.weight = line.weight else: missing_weight_products.append(line.product_id.name) if missing_weight_products: product_names = '
'.join(missing_weight_products) self.message_post(body=f"Produk berikut tidak memiliki berat:
{product_names}") if total_weight == 0: raise UserError("Tidak dapat mengestimasi ongkir tanpa berat yang valid.") # Validasi alamat pengiriman if not self.real_shipping_id: raise UserError("Alamat pengiriman (Real Delivery Address) harus diisi.") if not self.real_shipping_id.kota_id: raise UserError("Kota pada alamat pengiriman harus diisi.") if not self.real_shipping_id.zip: raise UserError("Kode pos pada alamat pengiriman harus diisi.") if not self.real_shipping_id.state_id: raise UserError("Provinsi pada alamat pengiriman harus diisi.") return total_weight def action_estimate_shipping_biteship(self): total_weight = self._validate_for_shipping_estimate() # Konversi berat ke gram untuk Biteship weight_gram = int(total_weight * 1000) if weight_gram < 100: weight_gram = 100 # Minimum weight untuk Biteship # Persiapkan data item items = [{ "name": "Paket Pesanan", "description": f"Sale Order {self.name}", "value": int(self.amount_untaxed), "weight": weight_gram, "quantity": 1, "height": 10, "width": 10, "length": 10 }] # Coba dapatkan data koordinat dari alamat pengiriman shipping_address = self.real_shipping_id _logger.info(f"Shipping Address: {shipping_address}") # Data asal (tetap gudang Bandengan) origin_data = { "origin_latitude": -6.3031123, "origin_longitude": 106.7794934, } # Prioritaskan penggunaan koordinat jika tersedia destination_data = {} use_coordinate = False # Cek apakah latitude dan longitude tersedia dan valid if hasattr(shipping_address, 'latitude') and hasattr(shipping_address, 'longtitude'): if shipping_address.latitude and shipping_address.longtitude: try: # Validasi format koordinat lat = float(shipping_address.latitude) lng = float(shipping_address.longtitude) destination_data = { "destination_latitude": lat, "destination_longitude": lng } use_coordinate = True _logger.info(f"Using coordinates: lat={lat}, lng={lng}") except (ValueError, TypeError): _logger.warning(f"Invalid coordinates, falling back to postal code") use_coordinate = False # Jika koordinat tidak tersedia atau tidak valid, gunakan kode pos if not use_coordinate: if shipping_address.zip: origin_data = {"origin_postal_code": 14440} # Reset origin untuk mode kode pos destination_data = { "destination_postal_code": shipping_address.zip } _logger.info(f"Using postal code: {shipping_address.zip}") else: raise UserError("Tidak dapat mengestimasikan ongkir: Kode pos tujuan tidak tersedia.") # Siapkan daftar kurir couriers = ','.join(self._get_biteship_courier_codes()) # Panggil API Biteship dengan format yang benar api_mode = "koordinat" if use_coordinate else "kode_pos" _logger.info(f"Calling Biteship API with mode: {api_mode}") result = self._call_biteship_api(origin_data, destination_data, items, couriers) if not result: raise UserError("Gagal mendapatkan estimasi ongkir dari Biteship.") # Hapus shipping_option lama self.env["shipping.option"].search([('sale_order_id', '=', self.id)]).unlink() # Proses hasil API shipping_options = [] shipping_services = result.get('pricing', []) _logger.info(f"Ditemukan {len(shipping_services)} layanan pengiriman") for service in shipping_services: courier_code = service.get('courier_code', '').lower() courier_name = service.get('courier_name', '') service_name = service.get('courier_service_name', '') price = service.get('price', 0) _logger.info(f"Layanan: {courier_name} - {service_name}, Harga: {price}") # Lewati layanan dengan harga 0 if not price: _logger.warning(f"Melewati layanan dengan harga 0: {courier_name} - {service_name}") continue # Format estimasi waktu duration = service.get('duration', '') shipment_range = service.get('shipment_duration_range', '') shipment_unit = service.get('shipment_duration_unit', 'days') # Gunakan duration jika tersedia, jika tidak, buat dari range if duration: etd = duration elif shipment_range: etd = f"{shipment_range} {shipment_unit}" else: etd = "1-3 days" # Default fallback # Buat shipping option try: shipping_option = self.env["shipping.option"].create({ "name": f"{courier_name} - {service_name}", "price": price, "provider": courier_code, "etd": etd, "sale_order_id": self.id, }) shipping_options.append(shipping_option) _logger.info(f"Berhasil membuat opsi pengiriman: {courier_name} - {service_name}") except Exception as e: _logger.error(f"Gagal membuat opsi pengiriman: {str(e)}") # Jika tidak ada opsi pengiriman if not shipping_options: raise UserError(f"Tidak ada layanan pengiriman ditemukan untuk kode pos {destination_data}. Mohon periksa kembali kode pos atau gunakan metode pengiriman lain.") # Set opsi pertama sebagai default self.shipping_option_id = shipping_options[0].id self.delivery_amt = shipping_options[0].price # Format pesan untuk log yang lebih informatif if use_coordinate: origin_info = f"Koordinat ({origin_data.get('origin_latitude')}, {origin_data.get('origin_longitude')})" destination_info = f"Koordinat ({destination_data.get('destination_latitude')}, {destination_data.get('destination_longitude')})" else: origin_info = f"Kode Pos {origin_data.get('origin_postal_code')}" destination_info = f"Kode Pos {destination_data.get('destination_postal_code')}" # Format daftar opsi pengiriman option_list = '
'.join([ f"{opt.name}: Rp {opt.price:,.0f} ({opt.etd})" for opt in shipping_options ]) # Log hasil estimasi dengan format yang lebih baik self.message_post( body=f"Estimasi Ongkir Biteship ({origin_info} → {destination_info}):
{option_list}", message_type="comment" ) # Simpan informasi untuk note ekspedisi selected_option = shipping_options[0] # Opsi pertama dipilih sebagai default self.note_ekspedisi = f"Pengiriman: {selected_option.name} - Rp {selected_option.price:,.0f} ({selected_option.etd}) [via {api_mode}]" return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Estimasi Ongkir Berhasil', 'message': f'Mendapatkan {len(shipping_options)} opsi pengiriman menggunakan {api_mode}', 'type': 'success', 'sticky': False, } } def _call_biteship_api(self, origin_data, destination_data, items, couriers=None): url = 'https://api.biteship.com/v1/rates/couriers' api_key = 'biteship_test.eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSW5kb3Rla25payIsInVzZXJJZCI6IjY3MTViYTJkYzVkMjdkMDAxMjRjODk2MiIsImlhdCI6MTcyOTQ5ODAwMX0.L6C73couP4-cgVEfhKI2g7eMCMo3YOFSRZhS-KSuHNA' headers = { 'Authorization': api_key, 'Content-Type': 'application/json' } if not couriers: couriers = ','.join(self._get_biteship_courier_codes()) # Persiapkan payload dengan menggabungkan origin, destination, dan items payload = { **origin_data, **destination_data, "couriers": couriers, "items": items } api_mode = "koordinat" if "destination_latitude" in destination_data else "kode_pos" try: _logger.info(f"Calling Biteship API with mode: {api_mode}") _logger.info(f"Payload: {payload}") response = requests.post(url, headers=headers, json=payload, timeout=30) _logger.info(f"Biteship API Status Code: {response.status_code}") if response.status_code != 200: _logger.error(f"Biteship API Error Response: {response.text}") if response.status_code == 200: result = response.json() result['api_mode'] = api_mode # Tambahkan info mode API return result else: error_msg = response.text _logger.error(f"Error calling Biteship API: {response.status_code} - {error_msg}") return False except Exception as e: _logger.error(f"Exception calling Biteship API: {str(e)}") return False def _call_rajaongkir_api(self, total_weight, destination_subsdistrict_id): url = 'https://pro.rajaongkir.com/api/cost' headers = { 'key': '9b1310f644056d84d60b0af6bb21611a', } courier = self.carrier_id.name.lower() data = { 'origin': 2127, 'originType': 'subdistrict', 'destination': int(destination_subsdistrict_id), 'destinationType': 'subdistrict', 'weight': int(total_weight * 1000), 'courier': courier, } response = requests.post(url, headers=headers, data=data) if response.status_code == 200: return response.json() return None def _normalize_city_name(self, city_name): city_name = city_name.lower() if city_name.startswith('kabupaten'): city_name = city_name.replace('kabupaten', '').strip() elif city_name.startswith('kota'): city_name = city_name.replace('kota', '').strip() city_name = " ".join(city_name.split()) return city_name def _get_city_id_by_name(self, city_name): url = 'https://pro.rajaongkir.com/api/city' headers = { 'key': '9b1310f644056d84d60b0af6bb21611a', } normalized_city_name = self._normalize_city_name(city_name) response = requests.get(url, headers=headers) if response.status_code == 200: city_data = response.json() for city in city_data['rajaongkir']['results']: if city['city_name'].lower() == normalized_city_name: return city['city_id'] return None def _get_subdistrict_id_by_name(self, city_id, subdistrict_name): url = f'https://pro.rajaongkir.com/api/subdistrict?city={city_id}' headers = { 'key': '9b1310f644056d84d60b0af6bb21611a', } response = requests.get(url, headers=headers) if response.status_code == 200: subdistrict_data = response.json() for subdistrict in subdistrict_data['rajaongkir']['results']: subsdistrict_1 = subdistrict['subdistrict_name'].lower() subsdistrict_2 = subdistrict_name.lower() if subsdistrict_1 == subsdistrict_2: return subdistrict['subdistrict_id'] return None def _compute_type_promotion(self): for rec in self: promotion_types = [] for promotion in rec.order_promotion_ids: for line_program in promotion.program_line_id: if line_program.promotion_type: promotion_types.append(dict(line_program._fields['promotion_type'].selection).get(line_program.promotion_type)) rec.type_promotion = ', '.join(sorted(set(promotion_types))) def _compute_purchase_delivery_amount(self): for order in self: match = self.env['purchase.order.sales.match'] result2 = match.search([ ('sale_id.id', '=', order.id) ]) delivery_amt = 0 for res in result2: delivery_amt = res.delivery_amt order.purchase_delivery_amt = delivery_amt def _compute_margin_after_delivery_purchase(self): for order in self: order.margin_after_delivery_purchase = order.total_margin - order.purchase_delivery_amt if order.amount_untaxed == 0: order.percent_margin_after_delivery_purchase = 0 continue if order.shipping_cost_covered == 'indoteknik': delivery_amt = order.delivery_amt else: delivery_amt = 0 order.percent_margin_after_delivery_purchase = round((order.margin_after_delivery_purchase / (order.amount_untaxed-delivery_amt-order.fee_third_party-order.biaya_lain_lain)) * 100, 2) def _compute_date_kirim(self): for rec in self: picking = self.env['stock.picking'].search([('sale_id', '=', rec.id), ('state', 'not in', ['cancel']), ('name', 'not ilike', 'BU/PICK/%')], order='date_doc_kirim desc', limit=1) rec.date_kirim_ril = picking.date_doc_kirim rec.date_status_done = picking.date_done rec.date_driver_arrival = picking.driver_arrival_date rec.date_driver_departure = picking.driver_departure_date def open_form_multi_create_uang_muka(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_sale_order_multi_uangmuka') action['context'] = { 'so_ids': [x.id for x in self] } return action def _compute_fullfillment(self): for rec in self: # rec.fullfillment_line.unlink() # # for line in rec.order_line: # line._compute_reserved_from() rec.compute_fullfillment = True @api.depends('date_order', 'estimated_arrival_days', 'state', 'estimated_arrival_days_start') def _compute_eta_date(self): current_date = datetime.now() for rec in self: if rec.date_order and rec.state not in ['cancel'] and rec.estimated_arrival_days and rec.estimated_arrival_days_start: rec.eta_date = current_date + timedelta(days=rec.estimated_arrival_days) rec.eta_date_start = current_date + timedelta(days=rec.estimated_arrival_days_start) else: rec.eta_date = False rec.eta_date_start = False def get_days_until_next_business_day(self, start_date=None, *args, **kwargs): jakarta = pytz.timezone("Asia/Jakarta") now = datetime.now(jakarta) if start_date is None: start_date = now if start_date.tzinfo is None: start_date = jakarta.localize(start_date) holiday = self.env['hr.public.holiday'] batas_waktu = datetime.strptime("15:00", "%H:%M").time() current_day = start_date.date() offset = 0 is3pm = False # Step 1: Lewat jam 15 → Tambah 1 hari if start_date.time() > batas_waktu: is3pm = True offset += 1 # Step 2: Hitung hari libur selama offset itu i = 0 total_days = 0 while i < offset: current_day += timedelta(days=1) total_days += 1 is_weekend = current_day.weekday() >= 5 is_holiday = holiday.search([("start_date", "=", current_day)]) if not is_weekend and not is_holiday: i += 1 # hanya hitung hari kerja # Step 3: Tambah 1 hari masa persiapan gudang i = 0 while i < 1: current_day += timedelta(days=1) total_days += 1 is_weekend = current_day.weekday() >= 5 is_holiday = holiday.search([("start_date", "=", current_day)]) if not is_weekend and not is_holiday: i += 1 # Step 4: Kalau current_day ternyata weekend/libur, cari hari kerja berikutnya while True: is_weekend = current_day.weekday() >= 5 is_holiday = holiday.search([("start_date", "=", current_day)]) if is_weekend or is_holiday: current_day += timedelta(days=1) total_days += 1 else: break offset = (current_day - start_date.date()).days return offset, is3pm def calculate_sla_by_vendor(self, products): product_ids = products.mapped('product_id.id') # Kumpulkan semua ID produk include_instant = True # Default True, tetapi bisa menjadi False # Cek apakah SEMUA produk memiliki qty_free_bandengan >= qty_needed all_fast_products = all(product.product_id.qty_free_bandengan >= product.product_uom_qty for product in products) if all_fast_products: return {'slatime': 0, 'include_instant': include_instant} # Cari semua vendor pemenang untuk produk yang diberikan vendors = self.env['purchase.pricelist'].search([ ('product_id', 'in', product_ids), ('is_winner', '=', True) ]) max_slatime = 1 for vendor in vendors: vendor_sla = self.env['vendor.sla'].search([('id_vendor', '=', vendor.vendor_id.id)], limit=1) slatime = 15 if vendor_sla: if vendor_sla.unit == 'hari': vendor_duration = vendor_sla.duration * 24 * 60 include_instant = False else : vendor_duration = vendor_sla.duration * 60 include_instant = True estimation_sla = (1 * 24 * 60) + vendor_duration estimation_sla_days = estimation_sla / (24 * 60) slatime = math.ceil(estimation_sla_days) max_slatime = max(max_slatime, slatime) return {'slatime': max_slatime, 'include_instant': include_instant} def _calculate_etrts_date(self): for rec in self: if not rec.date_order: rec.expected_ready_to_ship = False return jakarta = pytz.timezone("Asia/Jakarta") current_date = datetime.now(jakarta) max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) offset , is3pm = self.get_days_until_next_business_day(current_date) sum_days = max_slatime + offset sum_days -= 1 if not rec.estimated_arrival_days: rec.estimated_arrival_days = sum_days eta_date = current_date + timedelta(days=sum_days) if is3pm: eta_date = datetime.combine(eta_date, time(10, 0)) # jam 10:00 eta_date = jakarta.localize(eta_date).astimezone(timezone.utc) # ubah ke UTC eta_date = eta_date.astimezone(timezone.utc).replace(tzinfo=None) rec.commitment_date = eta_date rec.expected_ready_to_ship = eta_date @api.depends("order_line.product_id", "date_order") def _compute_etrts_date(self): #Function to calculate Estimated Ready To Ship Date self._calculate_etrts_date() def _validate_expected_ready_ship_date(self): for rec in self: if rec.expected_ready_to_ship and rec.commitment_date: current_date = datetime.now() # Hanya membandingkan tanggal saja, tanpa jam expected_date = rec.expected_ready_to_ship.date() max_slatime = 1 # Default SLA jika tidak ada slatime = self.calculate_sla_by_vendor(rec.order_line) max_slatime = max(max_slatime, slatime['slatime']) offset , is3pm = self.get_days_until_next_business_day(current_date) sum_days = max_slatime + offset sum_days -= 1 eta_minimum = current_date + timedelta(days=sum_days) if expected_date < eta_minimum.date(): rec.expected_ready_to_ship = eta_minimum raise ValidationError( "Tanggal 'Expected Ready to Ship' tidak boleh lebih kecil dari {}. Mohon pilih tanggal minimal {}." .format(eta_minimum.strftime('%d-%m-%Y'), eta_minimum.strftime('%d-%m-%Y')) ) else: rec.commitment_date = rec.expected_ready_to_ship @api.onchange('expected_ready_to_ship') #Hangle Onchange form Expected Ready to Ship def _onchange_expected_ready_ship_date(self): self._validate_expected_ready_ship_date() def _set_etrts_date(self): for order in self: if order.state in ('done', 'cancel', 'sale'): raise UserError(_("You cannot change the Estimated Ready To Ship Date on a done, sale or cancelled order.")) # order.move_lines.write({'estimated_ready_ship_date': order.estimated_ready_ship_date}) def _prepare_invoice(self): """ Prepare the dict of values to create the new invoice for a sales order. This method may be overridden to implement custom invoice generation (making sure to call super() to establish a clean extension chain). """ self.ensure_one() journal = self.env['account.move'].with_context(default_move_type='out_invoice')._get_default_journal() if not journal: raise UserError(_('Please define an accounting sales journal for the company %s (%s).') % (self.company_id.name, self.company_id.id)) parent_id = self.partner_id.parent_id parent_id = parent_id if parent_id else self.partner_id invoice_vals = { 'ref': self.client_order_ref or '', 'move_type': 'out_invoice', 'narration': self.note, 'currency_id': self.pricelist_id.currency_id.id, 'campaign_id': self.campaign_id.id, 'medium_id': self.medium_id.id, 'source_id': self.source_id.id, 'user_id': self.user_id.id, 'sale_id': self.id, 'invoice_user_id': self.user_id.id, 'team_id': self.team_id.id, 'partner_id': parent_id.id, 'partner_shipping_id': parent_id.id, 'real_invoice_id': self.real_invoice_id.id, 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(self.partner_invoice_id.id)).id, 'partner_bank_id': self.company_id.partner_id.bank_ids[:1].id, 'journal_id': journal.id, # company comes from the journal 'invoice_origin': self.name, 'invoice_payment_term_id': self.payment_term_id.id, 'payment_reference': self.reference, 'transaction_ids': [(6, 0, self.transaction_ids.ids)], 'invoice_line_ids': [], 'company_id': self.company_id.id, } return invoice_vals @api.constrains('email') def _validate_email(self): rule_regex = self.env['ir.config_parameter'].sudo().get_param('sale.order.validate_email') or '' pattern = rf'^{rule_regex}$' if self.email and not re.match(pattern, self.email): raise UserError('Email yang anda input kurang valid') # @api.constrains('delivery_amt', 'carrier_id', 'shipping_cost_covered') def _validate_delivery_amt(self): is_indoteknik = self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik' is_active_id = not self.env.context.get('active_id', []) if is_indoteknik and is_active_id: if self.delivery_amt == 0: if self.carrier_id.id == 1: raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum diisi.') else: raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum diisi.') if self.delivery_amt < 100: if self.carrier_id.id == 1: raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum memenuhi tarif minimum.') else: raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum memenuhi tarif minimum.') # if self.delivery_amt < 5000: # if (self.carrier_id.id == 1 or self.shipping_cost_covered == 'indoteknik') and not self.env.context.get('active_id', []): # if self.carrier_id.id == 1: # raise UserError('Untuk Kurir Indoteknik Delivery, estimasi ongkos kirim belum memenuhi jumlah minimum.') # else: # raise UserError('Untuk Shipping Covered Indoteknik, estimasi ongkos kirim belum memenuhi jumlah minimum.') def override_allow_create_invoice(self): if not self.env.user.is_accounting: raise UserError('Hanya Finance Accounting yang dapat klik tombol ini') for term in self.payment_term_id.line_ids: if term.days > 0: raise UserError('Hanya dapat digunakan pada Cash Before Delivery') for line in self.order_line: line.qty_to_invoice = line.product_uom_qty # def _get_pickings(self): # state = ['assigned'] # for order in self: # pickings = self.env['stock.picking'].search([ # ('sale_id.id', '=', order.id), # ('state', 'in', state) # ]) # order.picking_ids = pickings @api.model def action_multi_update_state(self): for sale in self: for picking_ids in sale.picking_ids: if not picking_ids.state == 'cancel': raise UserError('DO harus cancel terlebih dahulu') sale.update({ 'state': 'cancel', }) if sale.state == 'cancel': sale.update({ 'approval_status': False, }) def open_form_multi_update_status(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_sale_orders_multi_update') action['context'] = { 'sale_ids': [x.id for x in self] } return action def open_form_multi_update_state(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_quotation_so_multi_update') action['context'] = { 'quotation_ids': [x.id for x in self] } return action def action_multi_update_invoice_status(self): for sale in self: sale.update({ 'invoice_status': 'invoiced', }) def _compute_purchase_total(self): for order in self: total = 0 for line in order.order_line: total += line.vendor_subtotal order.purchase_total = total def check_data_real_delivery_address(self): real_delivery_address = self.real_shipping_id if not real_delivery_address.state_id: raise UserError('State Real Delivery Address harus diisi') if not real_delivery_address.zip: raise UserError('Zip code Real Delivery Address harus diisi') if not real_delivery_address.mobile: raise UserError('Mobile Real Delivery Address harus diisi') if not real_delivery_address.phone: raise UserError('Phone Real Delivery Address harus diisi') if not real_delivery_address.kecamatan_id: raise UserError('Kecamatan Real Delivery Address harus diisi') # if not real_delivery_address.kelurahan_id: # raise UserError('Kelurahan Real Delivery Address harus diisi') def generate_payment_link_midtrans_sales_order(self): # midtrans_url = 'https://app.sandbox.midtrans.com/snap/v1/transactions' # dev - sandbox # midtrans_auth = 'Basic U0ItTWlkLXNlcnZlci1uLVY3ZDJjMlpCMFNWRUQyOU95Q1dWWXA6' # dev - sandbox midtrans_url = 'https://app.midtrans.com/snap/v1/transactions' # production midtrans_auth = 'Basic TWlkLXNlcnZlci1SbGMxZ2gzWGpSVW5scl9JblZzTV9OTnU6' # production so_number = self.name so_number = so_number.replace('/', '-') so_grandtotal = math.floor(self.grand_total) headers = { 'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': midtrans_auth, } check_url = f'https://api.midtrans.com/v2/{so_number}/status' check_response = requests.get(check_url, headers=headers) if check_response.status_code == 200: status_response = check_response.json() if status_response.get('transaction_status') == 'expire' or status_response.get('transaction_status') == 'cancel': so_number = so_number + '-cpl' json_data = { 'transaction_details': { 'order_id': so_number, 'gross_amount': so_grandtotal, }, 'credit_card': { 'secure': True, }, } response = requests.post(midtrans_url, headers=headers, json=json_data).json() lookup_json = json.dumps(response, indent=4, sort_keys=True) redirect_url = json.loads(lookup_json)['redirect_url'] self.payment_link_midtrans = str(redirect_url) if 'redirect_url' in response: qr = qrcode.QRCode( version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4, ) qr.add_data(redirect_url) qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") buffer = BytesIO() img.save(buffer, format="PNG") qr_code_img = base64.b64encode(buffer.getvalue()).decode() self.payment_qr_code = qr_code_img @api.model def _generate_so_access_token(self, limit=50): orders = self.search([('access_token', '=', False)], limit=limit) for order in orders: token_source = string.ascii_letters + string.digits order.access_token = ''.join(random.choice(token_source) for i in range(20)) def calculate_line_no(self): line_no = 0 for line in self.order_line: if line.product_id.type == 'product': line_no += 1 line.line_no = line_no def write(self, vals): if 'carrier_id' in vals: for picking in self.picking_ids: if picking.state == 'assigned': picking.carrier_id = self.carrier_id def calculate_so_status(self): so_state = ['sale'] sales = self.search([ ('state', 'in', so_state), ('so_status', '!=', 'terproses'), ]) for sale in sales: picking_states = ['draft', 'assigned', 'confirmed', 'waiting'] have_outstanding_pick = any(x.state in picking_states for x in sale.picking_ids) sum_qty_so = sum(so_line.product_uom_qty for so_line in sale.order_line) sum_qty_ship = sum(so_line.qty_delivered for so_line in sale.order_line) if sum_qty_so > sum_qty_ship > 0: sale.so_status = 'sebagian' elif not have_outstanding_pick: sale.so_status = 'terproses' else: sale.so_status = 'menunggu' for picking in sale.picking_ids: sum_qty_pick = sum(move_line.product_uom_qty for move_line in picking.move_ids_without_package) sum_qty_reserved = sum(move_line.product_uom_qty for move_line in picking.move_line_ids_without_package) if picking.state == 'done': continue elif sum_qty_pick == sum_qty_reserved and not picking.date_reserved:# baru ke reserved current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') picking.date_reserved = current_time elif sum_qty_pick == sum_qty_reserved:# sudah ada data reserved picking.date_reserved = picking.date_reserved else: picking.date_reserved = '' _logger.info('Calculate SO Status %s' % sale.id) # def _search_picking_ids(self, operator, value): # if operator == 'in' and value: # self.env.cr.execute(""" # SELECT array_agg(so.sale_id) # FROM stock_picking so # WHERE # so.sale_id is not null and so.id = ANY(%s) # """, (list(value),)) # so_ids = self.env.cr.fetchone()[0] or [] # return [('id', 'in', so_ids)] # elif operator == '=' and not value: # order_ids = self._search([ # ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')) # ]) # return [('id', 'not in', order_ids)] # return ['&', ('order_line.invoice_lines.move_id.move_type', 'in', ('out_invoice', 'out_refund')), ('order_line.invoice_lines.move_id', operator, value)] @api.onchange('partner_id') def onchange_partner_contact(self): parent_id = self.partner_id.parent_id parent_id = parent_id if parent_id else self.partner_id self.npwp = parent_id.npwp self.sppkp = parent_id.sppkp self.customer_type = parent_id.customer_type self.email = parent_id.email self.pareto_status = parent_id.pareto_status self.user_id = parent_id.user_id @api.onchange('partner_id') def onchange_partner_id(self): # INHERIT result = super(SaleOrder, self).onchange_partner_id() parent_id = self.partner_id.parent_id parent_id = parent_id if parent_id else self.partner_id self.partner_invoice_id = parent_id return result def _get_purchases(self): po_state = ['done', 'draft', 'purchase'] for order in self: purchases = self.env['purchase.order'].search([ ('sale_order_id', '=', order.id), ('state', 'in', po_state) ]) order.purchase_ids = purchases def _have_outstanding_invoice(self): invoice_state = ['posted', 'draft'] for order in self: order.have_outstanding_invoice = any(inv.state in invoice_state for inv in order.invoice_ids) def _have_outstanding_picking(self): picking_state = ['done', 'confirmed', 'draft'] for order in self: order.have_outstanding_picking = any(pick.state in picking_state for pick in order.picking_ids) def _have_outstanding_po(self): po_state = ['done', 'draft', 'purchase'] for order in self: order.have_outstanding_po = any(po.state in po_state for po in order.purchase_ids) def _have_visit_service(self): minimum_amount = 20000000 for order in self: order.have_visit_service = self.amount_total > minimum_amount def _get_helper_ids(self): helper_ids_str = self.env['ir.config_parameter'].sudo().get_param('sale.order.user_helper_ids') return helper_ids_str.split(', ') def write(self, values): helper_ids = self._get_helper_ids() if str(self.env.user.id) in helper_ids: values['helper_by_id'] = self.env.user.id return super(SaleOrder, self).write(values) def check_due(self): """To show the due amount and warning stage""" for order in self: partner = order.partner_id.parent_id or order.partner_id if partner and partner.active_limit and partner.enable_credit_limit: order.has_due = partner.due_amount > 0 if order.outstanding_amount >= partner.warning_stage and partner.warning_stage != 0: order.is_warning = True else: order.has_due = False order.is_warning = False def _validate_order(self): if self.payment_term_id.id == 31 and self.total_percent_margin < 25: raise UserError("Jika ingin menggunakan Tempo 90 Hari maka margin harus di atas 25%") if self.warehouse_id.id != 8 and self.warehouse_id.id != 10: #GD Bandengan raise UserError('Gudang harus Bandengan') if self.state not in ['draft', 'sent']: raise UserError("Status harus draft atau sent") self._validate_npwp() def _validate_npwp(self): num_digits = sum(c.isdigit() for c in self.npwp) if num_digits < 10: raise UserError("NPWP harus memiliki minimal 10 digit") # pattern = r'^\d{10,}$' # return re.match(pattern, self.npwp) is not None def sale_order_check_approve(self): self.check_due() self._validate_order() for order in self: order.order_line.validate_line() term_days = 0 for term_line in order.payment_term_id.line_ids: term_days += term_line.days partner = order.partner_id.parent_id or order.partner_id if not partner.property_payment_term_id: raise UserError("Payment Term pada Master Data Customer harus diisi") if not partner.active_limit and term_days > 0: raise UserError("Credit Limit pada Master Data Customer harus diisi") if order.payment_term_id != partner.property_payment_term_id: raise UserError("Payment Term berbeda pada Master Data Customer") if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp: raise UserError("NPWP berbeda pada Master Data Customer") if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.sppkp != partner.sppkp: raise UserError("SPPKP berbeda pada Master Data Customer") if not order.client_order_ref and order.create_date > datetime(2024, 6, 27): raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.user_id.active: raise UserError("Salesperson sudah tidak aktif, mohon diisi yang benar pada data SO dan Contact") def check_product_bom(self): for order in self: for line in order.order_line: if 'bom-it' in line.name.lower() or 'bom' in line.product_id.default_code.lower() if line.product_id.default_code else False: search_bom = self.env['mrp.production'].search([('product_id', '=', line.product_id.id)],order='name desc') if search_bom: confirmed_bom = search_bom.filtered(lambda x: x.state == 'confirmed') if not confirmed_bom: raise UserError("Product BOM belum dikonfirmasi di Manufacturing Orders. Silakan hubungi MD.") else: raise UserError("Product BOM tidak di temukan di manufacturing orders, silahkan hubungi MD") def check_duplicate_product(self): for order in self: for line in order.order_line: search_product = self.env['sale.order.line'].search([('product_id', '=', line.product_id.id), ('order_id', '=', order.id)]) if len(search_product) > 1: raise UserError("Terdapat DUPLIKASI data pada Product {}".format(line.product_id.display_name)) def sale_order_approve(self): self.check_duplicate_product() self.check_product_bom() self.check_credit_limit() self.check_limit_so_to_invoice() if self.validate_different_vendor() and not self.vendor_approval: return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor') self.check_due() self._validate_order() for order in self: order.order_line.validate_line() order.check_data_real_delivery_address() order._validate_order() order.sale_order_check_approve() main_parent = order.partner_id.get_main_parent() SYSTEM_UID = 25 FROM_WEBSITE = order.create_uid.id == SYSTEM_UID if FROM_WEBSITE and main_parent.use_so_approval and order.web_approval not in ['cust_procurement','cust_director']: raise UserError("This order not yet approved by customer procurement or director") if not order.client_order_ref and order.create_date > datetime(2024, 6, 27): raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.commitment_date and order.create_date > datetime(2024, 9, 12): raise UserError("Expected Delivery Date kosong, wajib diisi") if not order.real_shipping_id: UserError('Real Delivery Address harus di isi') if order.validate_partner_invoice_due(): return self._create_notification_action('Notification','Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') term_days = 0 for term_line in order.payment_term_id.line_ids: term_days += term_line.days partner = order.partner_id.parent_id or order.partner_id if not partner.property_payment_term_id: raise UserError("Payment Term pada Master Data Customer harus diisi") if not partner.active_limit and term_days > 0: raise UserError("Credit Limit pada Master Data Customer harus diisi") if order.payment_term_id != partner.property_payment_term_id: raise UserError("Payment Term berbeda pada Master Data Customer") if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.npwp != partner.npwp: raise UserError("NPWP berbeda pada Master Data Customer") if (partner.customer_type == 'pkp' or order.customer_type == 'pkp') and order.sppkp != partner.sppkp: raise UserError("SPPKP berbeda pada Master Data Customer") if not order.client_order_ref and order.create_date > datetime(2024, 6, 27): raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.user_id.active: raise UserError("Salesperson sudah tidak aktif, mohon diisi yang benar pada data SO dan Contact") if order.validate_partner_invoice_due(): return self._create_notification_action('Notification', 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') elif order._requires_approval_margin_manager(): self.check_product_bom() self.check_credit_limit() self.check_limit_so_to_invoice() order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') raise UserError("Bisa langsung Confirm") def send_notif_to_salesperson(self, cancel=False): if not cancel: grouping_so = self.search([ ('partner_id.parent_id.id', '=', self.partner_id.parent_id.id), ('partner_id.site_id.id', '=', self.partner_id.site_id.id), ]) else: grouping_so = self.search([ ('partner_id.parent_id.id', '=', self.partner_id.parent_id.id), ('partner_id.site_id.id', '=', self.partner_id.site_id.id), ('id', '!=', self.id), ]) # Kelompokkan data berdasarkan salesperson salesperson_data = {} for rec in grouping_so: if rec.user_id.id not in salesperson_data: salesperson_data[rec.user_id.id] = {'name': rec.user_id.name, 'orders': [], 'total_amount': 0, 'sum_total_amount': 0, 'business_partner': '', 'site': ''} # Menetapkan nilai awal untuk 'site' if rec.picking_ids: if not any(picking.state in ['assigned', 'confirmed', 'waiting'] for picking in rec.picking_ids): continue if all(picking.state == 'done' for picking in rec.picking_ids): continue if all(picking.state == 'cancel' for picking in rec.picking_ids): continue if not rec.partner_id.main_parent_id.use_so_approval: continue order_total_amount = rec.amount_total # Mengakses langsung rec.amount_total salesperson_data[rec.user_id.id]['orders'].append({ 'order_name': rec.name, 'parent_name': rec.partner_id.name, 'site_name': rec.partner_id.site_id.name, 'total_amount': rec.amount_total, }) salesperson_data[rec.user_id.id]['sum_total_amount'] += order_total_amount salesperson_data[rec.user_id.id]['business_partner'] = grouping_so[0].partner_id.main_parent_id.name salesperson_data[rec.user_id.id]['site'] = grouping_so[0].partner_id.site_id.name # Menambahkan nilai hanya jika ada # Kirim email untuk setiap salesperson for salesperson_id, data in salesperson_data.items(): if data['orders']: # Buat isi tabel untuk email table_content = '' for order_data in data['orders']: table_content += f""" {order_data['order_name']} {order_data['parent_name']} {order_data['site_name']} {order_data['total_amount']} """ # Dapatkan email salesperson salesperson_email = self.env['res.users'].browse(salesperson_id).partner_id.email # Kirim email hanya jika ada data yang dikumpulkan template = self.env.ref('indoteknik_custom.mail_template_sale_order_notification_to_salesperson') email_body = template.body_html.replace('${table_content}', table_content) email_body = email_body.replace('${salesperson_name}', data['name']) email_body = email_body.replace('${sum_total_amount}', str(data['sum_total_amount'])) email_body = email_body.replace('${site}', str(data['site'])) email_body = email_body.replace('${business_partner}', str(data['business_partner'])) # Kirim email self.env['mail.mail'].create({ 'subject': 'Notification: Sale Orders', 'body_html': email_body, 'email_to': salesperson_email, }).send() def check_credit_limit(self): for rec in self: outstanding_amount = rec.outstanding_amount check_credit_limit = False ###### block_stage = 0 if rec.partner_id.parent_id: if rec.partner_id.parent_id.active_limit and rec.partner_id.parent_id.enable_credit_limit: check_credit_limit = True else: if rec.partner_id.active_limit and rec.partner_id.enable_credit_limit: check_credit_limit = True term_days = 0 for term_line in rec.payment_term_id.line_ids: term_days += term_line.days if term_days == 0: check_credit_limit = False if check_credit_limit: if rec.partner_id.parent_id: block_stage = rec.partner_id.parent_id.blocking_stage or 0 else: block_stage = rec.partner_id.blocking_stage or 0 if (outstanding_amount + rec.amount_total) >= block_stage: if block_stage != 0: remaining_credit_limit = block_stage - outstanding_amount raise UserError(_("%s is in Blocking Stage, Remaining credit limit is %s, from %s and outstanding %s") % (rec.partner_id.name, remaining_credit_limit, block_stage, outstanding_amount)) def check_limit_so_to_invoice(self): for rec in self: # Ambil jumlah outstanding_amount dan rec.amount_total sebagai current_total outstanding_amount = rec.outstanding_amount current_total = rec.amount_total + outstanding_amount # Ambil blocking stage dari partner block_stage = rec.partner_id.parent_id.blocking_stage if rec.partner_id.parent_id else rec.partner_id.blocking_stage or 0 is_cbd = rec.partner_id.parent_id.property_payment_term_id.id == 26 if rec.partner_id.parent_id else rec.partner_id.property_payment_term_id.id == 26 or False # Ambil jumlah nilai dari SO yang invoice_status masih 'to invoice' so_to_invoice = 0 for sale in rec.partner_id.sale_order_ids: if sale.invoice_status == 'to invoice': so_to_invoice = so_to_invoice + sale.amount_total # Hitung remaining credit limit remaining_credit_limit = block_stage - current_total - so_to_invoice # Validasi limit if remaining_credit_limit <= 0 and block_stage > 0 and not is_cbd: raise UserError(_("The credit limit for %s will exceed the Blocking Stage if the Sale Order is confirmed. The remaining credit limit is %s, from %s and the outstanding amount is %s.") % (rec.partner_id.name, block_stage - current_total, block_stage, outstanding_amount)) def validate_different_vendor(self): if self.vendor_approval_id.filtered(lambda v: v.state == 'draft'): draft_names = ", ".join(self.vendor_approval_id.filtered(lambda v: v.state == 'draft').mapped('number')) raise UserError(f"SO ini sedang dalam review Vendor Approval: {draft_names}") if self.vendor_approval_id and all(v.state != 'draft' for v in self.vendor_approval_id): return False different_vendor = self.order_line.filtered( lambda l: l.vendor_id and l.vendor_md_id and l.vendor_id.id != l.vendor_md_id.id ) if different_vendor: vendor_approvals = [] for line in different_vendor: vendor_approval = self.env['vendor.approval'].create({ 'order_id': self.id, 'order_line_id': line.id, 'create_date_so': self.create_date, 'partner_id': self.partner_id.id, 'state': 'draft', 'product_id': line.product_id.id, 'product_uom_qty': line.product_uom_qty, 'vendor_id': line.vendor_id.id, 'vendor_md_id': line.vendor_md_id.id, 'purchase_price': line.purchase_price, 'purchase_price_md': line.purchase_price_md, 'sales_price': line.price_unit, 'margin_before': line.margin_md, 'margin_after': line.item_percent_margin, 'purchase_tax_id': line.purchase_tax_id.id, 'sales_tax_id': line.tax_id[0].id if line.tax_id else False, 'percent_margin_difference': ( (line.price_unit - line.purchase_price_md) / line.purchase_price_md if line.purchase_price_md else False ), }) vendor_approvals.append(vendor_approval.id) self.vendor_approval_id = [(4, vid) for vid in vendor_approvals] return True else: return False def action_confirm(self): for order in self: order.check_duplicate_product() order.check_product_bom() order.check_credit_limit() order.check_limit_so_to_invoice() if self.validate_different_vendor() and not self.vendor_approval: return self._create_notification_action('Notification', 'Terdapat Vendor yang berbeda dengan MD Vendor') order.check_data_real_delivery_address() order.sale_order_check_approve() order._validate_order() order.order_line.validate_line() main_parent = order.partner_id.get_main_parent() SYSTEM_UID = 25 FROM_WEBSITE = order.create_uid.id == SYSTEM_UID if FROM_WEBSITE and main_parent.use_so_approval and order.web_approval not in ['cust_procurement', 'cust_director']: raise UserError("This order not yet approved by customer procurement or director") if not order.client_order_ref and order.create_date > datetime(2024, 6, 27): raise UserError("Customer Reference kosong, di isi dengan NO PO jika PO tidak ada mohon ditulis Tanpa PO") if not order.commitment_date and order.create_date > datetime(2024, 9, 12): raise UserError("Expected Delivery Date kosong, wajib diisi") if not order.real_shipping_id: UserError('Real Delivery Address harus di isi') if order.validate_partner_invoice_due(): return self._create_notification_action('Notification', 'Terdapat invoice yang telah melewati batas waktu, mohon perbarui pada dokumen Due Extension') if order._requires_approval_margin_leader(): order.approval_status = 'pengajuan2' return self._create_approval_notification('Pimpinan') elif order._requires_approval_margin_manager(): order.approval_status = 'pengajuan1' return self._create_approval_notification('Sales Manager') order.approval_status = 'approved' order._set_sppkp_npwp_contact() order.calculate_line_no() order.send_notif_to_salesperson() # order._compute_etrts_date() # order.order_line.get_reserved_from() res = super(SaleOrder, self).action_confirm() for order in self: note = [] for line in order.order_line: if line.display_type == 'line_note': note.append(line.name) if order.picking_ids: # Sort picking_ids by creation date to get the most recent one latest_picking = order.picking_ids.sorted(key=lambda p: p.create_date, reverse=True)[0] latest_picking.notee = '\n'.join(note) return res def action_cancel(self): # TODO stephan prevent cancel if have invoice, do, and po main_parent = self.partner_id.get_main_parent() if self._name != 'sale.order': return super(SaleOrder, self).action_cancel() if self.have_outstanding_invoice: raise UserError("Invoice harus di Cancel dahulu") disallow_states = ['draft', 'waiting', 'confirmed', 'assigned'] for picking in self.picking_ids: if picking.state in disallow_states: raise UserError("DO yang draft, waiting, confirmed, atau assigned harus di-cancel oleh Logistik") for line in self.order_line: if line.qty_delivered > 0: raise UserError("DO yang done harus di-Return oleh Logistik") if not self.web_approval: self.web_approval = 'company' # elif self.have_outstanding_po: # raise UserError("PO harus di Cancel dahulu") self.approval_status = False self.due_id = False if main_parent.use_so_approval: self.send_notif_to_salesperson(cancel=True) for order in self: if order.amount_total > 30000000: return { 'type': 'ir.actions.act_window', 'name': _('Cancel Reason'), 'res_model': 'cancel.reason.order', 'view_mode': 'form', 'target': 'new', 'context': {'default_request_id': self.id}, } return super(SaleOrder, self).action_cancel() def confirm_cancel_order(self): """Fungsi ini akan dipanggil oleh wizard setelah alasan pembatalan dipilih""" if self.state != 'cancel': self.state = 'cancel' return super(SaleOrder, self).action_cancel() def validate_partner_invoice_due(self): parent_id = self.partner_id.parent_id.id parent_id = parent_id if parent_id else self.partner_id.id if self.due_id and self.due_id.is_approve == False: raise UserError('Document Over Due Yang Anda Buat Belum Di Approve') query = [ ('partner_id', '=', parent_id), ('state', '=', 'posted'), ('move_type', '=', 'out_invoice'), ('amount_residual_signed', '>', 0) ] invoices = self.env['account.move'].search(query, order='invoice_date') if invoices: if not self.env.user.is_leader and not self.env.user.is_sales_manager: due_extension = self.env['due.extension'].create([{ 'partner_id': parent_id, 'day_extension': '3', 'order_id': self.id, }]) due_extension.generate_due_line() self.due_id = due_extension.id if len(self.due_id.due_line) > 0: return True else: due_extension.unlink() return False def _requires_approval_margin_leader(self): return self.total_percent_margin < 15 and not self.env.user.is_leader def _requires_approval_margin_manager(self): return self.total_percent_margin >= 15 and not self.env.user.is_leader and not self.env.user.is_sales_manager def _create_approval_notification(self, approval_role): title = 'Warning' message = f'SO butuh approval {approval_role}' 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 _set_sppkp_npwp_contact(self): partner = self.partner_id.parent_id or self.partner_id if not partner.sppkp: partner.sppkp = self.sppkp if not partner.npwp: partner.npwp = self.npwp if not partner.email: partner.email = self.email if not partner.customer_type: partner.customer_type = self.customer_type if not partner.user_id: partner.user_id = self.user_id.id # if not partner.sppkp or not partner.npwp or not partner.email or partner.customer_type: # partner.customer_type = self.customer_type # partner.npwp = self.npwp # partner.sppkp = self.sppkp # partner.email = self.email def _compute_total_margin(self): for order in self: total_margin = sum(line.item_margin for line in order.order_line if line.product_id) if order.ongkir_ke_xpdc: total_margin -= order.ongkir_ke_xpdc order.total_margin = total_margin def _compute_total_percent_margin(self): for order in self: if order.amount_untaxed == 0: order.total_percent_margin = 0 continue if order.shipping_cost_covered == 'indoteknik': delivery_amt = order.delivery_amt else: delivery_amt = 0 # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-delivery_amt-order.fee_third_party)) * 100, 2) order.total_percent_margin = round((order.total_margin / (order.amount_untaxed-order.fee_third_party-order.biaya_lain_lain)) * 100, 2) # order.total_percent_margin = round((order.total_margin / (order.amount_untaxed)) * 100, 2) @api.onchange('sales_tax_id') def onchange_sales_tax_id(self): for line in self.order_line: line.product_id_change() def _compute_grand_total(self): for order in self: if order.shipping_cost_covered == 'customer': order.grand_total = order.delivery_amt + order.amount_total else: order.grand_total = order.amount_total def action_apply_voucher(self): for line in self.order_line: if line.order_promotion_id: raise UserError('Voucher tidak dapat digabung dengan promotion program') voucher = self.voucher_id if voucher.limit > 0 and voucher.count_order >= voucher.limit: raise UserError('Voucher tidak dapat digunakan karena sudah habis digunakan') partner_voucher_orders = [] for order in voucher.order_ids: if order.partner_id.id == self.partner_id.id: partner_voucher_orders.append(order) if voucher.limit_user > 0 and len(partner_voucher_orders) >= voucher.limit_user: raise UserError('Voucher tidak dapat digunakan karena Customer ini sudah menghabiskan kuota voucher') if self.pricelist_id.id in [x.id for x in voucher.excl_pricelist_ids]: raise UserError('Voucher tidak dapat digunakan karena pricelist ini tidak berlaku pada voucher') self.apply_voucher() def action_apply_voucher_shipping(self): for line in self.order_line: if line.order_promotion_id: raise UserError('Voucher tidak dapat digabung dengan promotion program') voucher = self.voucher_shipping_id if voucher.limit > 0 and voucher.count_order >= voucher.limit: raise UserError('Voucher tidak dapat digunakan karena sudah habis digunakan') partner_voucher_orders = [] for order in voucher.order_ids: if order.partner_id.id == self.partner_id.id: partner_voucher_orders.append(order) if voucher.limit_user > 0 and len(partner_voucher_orders) >= voucher.limit_user: raise UserError('Voucher tidak dapat digunakan karena Customer ini sudah menghabiskan kuota voucher') if self.pricelist_id.id in [x.id for x in voucher.excl_pricelist_ids]: raise UserError('Voucher tidak dapat digunakan karena pricelist ini tidak berlaku pada voucher') self.apply_voucher_shipping() def apply_voucher(self): order_line = [] for line in self.order_line: order_line.append({ 'product_id': line.product_id, 'price': line.price_unit, 'discount': line.discount, 'qty': line.product_uom_qty, 'subtotal': line.price_subtotal }) voucher = self.voucher_id.apply(order_line) for line in self.order_line: line.initial_discount = line.discount voucher_type = voucher['type'] used_total = voucher['total'][voucher_type] used_discount = voucher['discount'][voucher_type] manufacture_id = line.product_id.x_manufacture.id if voucher_type == 'brand': used_total = used_total.get(manufacture_id) used_discount = used_discount.get(manufacture_id) if not used_total or not used_discount: continue line_contribution = line.price_subtotal / used_total line_voucher = used_discount * line_contribution line_voucher_item = line_voucher / line.product_uom_qty line_price_unit = line.price_unit / 1.11 if any(tax.id == 23 for tax in line.tax_id) else line.price_unit line_discount_item = line_price_unit * line.discount / 100 + line_voucher_item line_voucher_item = line_discount_item / line_price_unit * 100 line.amount_voucher_disc = line_voucher line.discount = line_voucher_item self.amount_voucher_disc = voucher['discount']['all'] self.applied_voucher_id = self.voucher_id def apply_voucher_shipping(self): for order in self: delivery_amt = order.delivery_amt voucher = order.voucher_shipping_id if voucher: max_discount_amount = voucher.discount_amount voucher_type = voucher.discount_type if voucher_type == 'fixed_price': discount = max_discount_amount elif voucher_type == 'percentage': discount = delivery_amt * (max_discount_amount / 100) delivery_amt -= discount delivery_amt = max(delivery_amt, 0) order.delivery_amt = delivery_amt order.amount_voucher_shipping_disc = discount order.applied_voucher_shipping_id = order.voucher_id.id def cancel_voucher(self): self.applied_voucher_id = False self.amount_voucher_disc = 0 for line in self.order_line: line.amount_voucher_disc = 0 line.discount = line.initial_discount line.initial_discount = False def cancel_voucher_shipping(self): self.delivery_amt + self.amount_voucher_shipping_disc self.applied_voucher_shipping_id = False self.amount_voucher_shipping_disc = 0 def action_web_approve(self): if self.env.uid != self.partner_id.user_id.id: raise UserError('You are not authorized to approve this order. Only %s can approve this order.' % self.partner_id.user_id.name) self.web_approval = 'company' template = self.env.ref('indoteknik_custom.mail_template_sale_order_web_approve_notification') template.send_mail(self.id, force_send=True) return { 'type': 'ir.actions.client', 'tag': 'display_notification', 'params': { 'title': 'Notification', 'message': 'Berhasil approve web order', 'next': {'type': 'ir.actions.act_window_close'}, } } def calculate_selling_price(self): # ongkos kirim, biaya pihak ketiga calculate @stephan # TODO voucher @stephan # vendor hilangin child di field SO Line @stephan # button pindahin @stephan # last so 1 tahun ke belakang @stephan # pastikan harga beli 1 tahun ke belakang jg # harga yg didapat dari semua kumpulan parent parner dan child nya # counter di klik berapa banyak @stephan for order_line in self.order_line: if not order_line.product_id: continue current_time = datetime.now() delta_time = current_time - timedelta(days=365) delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') # Initialize partners list with parent_id or partner_id partners = [] parent_id = self.partner_id.parent_id or self.partner_id # Add all child_ids and the parent itself to partners as IDs partners.extend(parent_id.child_ids.ids) partners.append(parent_id.id) rec_purchase_price, rec_taxes_id, rec_vendor_id = order_line._get_purchase_price(order_line.product_id) state = ['sale', 'done'] last_so = self.env['sale.order.line'].search([ # ('order_id.partner_id.id', '=', order_line.order_id.partner_id.id), ('order_id.partner_id', 'in', partners), ('product_id.id', '=', order_line.product_id.id), ('order_id.state', 'in', state), ('id', '!=', order_line.id), ('order_id.date_order', '>=', delta_time) ], limit=1, order='create_date desc') if last_so and rec_vendor_id != last_so.vendor_id.id: last_so = self.env['sale.order.line'].search([ # ('order_id.partner_id.id', '=', order_line.order_id.partner_id.id), ('order_id.partner_id', 'in', partners), ('product_id.id', '=', order_line.product_id.id), ('order_id.state', 'in', state), ('vendor_id', '=', rec_vendor_id), ('id', '!=', order_line.id), ('order_id.date_order', '>=', delta_time) ], limit=1, order='create_date desc') if last_so and rec_purchase_price != last_so.purchase_price: rec_taxes = self.env['account.tax'].search([('id', '=', rec_taxes_id)], limit=1) if rec_taxes.price_include: selling_price = (rec_purchase_price / 1.11) / (1 - (last_so.item_percent_margin_without_deduction / 100)) else: selling_price = rec_purchase_price / (1 - (last_so.item_percent_margin_without_deduction / 100)) tax_id = last_so.tax_id for tax in tax_id: if tax.price_include: selling_price = selling_price + (selling_price*11/100) else: selling_price = selling_price discount = 0 elif last_so: selling_price = last_so.price_unit tax_id = last_so.tax_id discount = last_so.discount else: selling_price = order_line.price_unit tax_id = order_line.tax_id discount = order_line.discount elif last_so and rec_vendor_id == order_line.vendor_id.id and rec_purchase_price != last_so.purchase_price: rec_taxes = self.env['account.tax'].search([('id', '=', rec_taxes_id)], limit=1) if rec_taxes.price_include: selling_price = (rec_purchase_price / 1.11) / (1 - (last_so.item_percent_margin_without_deduction / 100)) else: selling_price = rec_purchase_price / (1 - (last_so.item_percent_margin_without_deduction / 100)) tax_id = last_so.tax_id for tax in tax_id: if tax.price_include: selling_price = selling_price + (selling_price*11/100) else: selling_price = selling_price discount = 0 elif last_so: selling_price = last_so.price_unit tax_id = last_so.tax_id discount = last_so.discount else: selling_price = order_line.price_unit tax_id = order_line.tax_id discount = order_line.discount order_line.price_unit = selling_price order_line.tax_id = tax_id order_line.discount = discount order_line.order_id.use_button = True @api.model def create(self, vals): # Ensure partner details are updated when a sale order is created order = super(SaleOrder, self).create(vals) order._compute_etrts_date() order._validate_expected_ready_ship_date() order._validate_delivery_amt() # order._check_total_margin_excl_third_party() # order._update_partner_details() return order # def write(self, vals): # Call the super method to handle the write operation # res = super(SaleOrder, self).write(vals) # self._compute_etrts_date() # Check if the update is coming from a save operation # if any(field in vals for field in ['sppkp', 'npwp', 'email', 'customer_type']): # self._update_partner_details() # return res def _update_partner_details(self): for order in self: partner = order.partner_id.parent_id or order.partner_id if partner: # Update partner details partner.sppkp = order.sppkp partner.npwp = order.npwp partner.email = order.email partner.customer_type = order.customer_type # Save changes to the partner record partner.write({ 'sppkp': partner.sppkp, 'npwp': partner.npwp, 'email': partner.email, 'customer_type': partner.customer_type, }) def write(self, vals): for order in self: if order.state in ['sale', 'cancel']: if 'order_line' in vals: new_lines = vals.get('order_line', []) for command in new_lines: if command[0] == 0: # A new line is being added raise UserError( "SO tidak dapat ditambahkan produk baru karena SO sudah menjadi sale order.") res = super(SaleOrder, self).write(vals) # self._check_total_margin_excl_third_party() if any(fields in vals for fields in ['delivery_amt', 'carrier_id', 'shipping_cost_covered']): self._validate_delivery_amt() if any(field in vals for field in ["order_line", "client_order_ref"]): self._calculate_etrts_date() return res