from re import search from odoo import fields, models, api, _ from odoo.exceptions import UserError, ValidationError from datetime import datetime, timedelta import logging, random, string, requests, math, json, re, qrcode, base64 from io import BytesIO from collections import defaultdict _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)]") @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 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 _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().date() 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): today = start_date or datetime.today().date() offset = 0 # Counter jumlah hari yang ditambahkan holiday = self.env['hr.public.holiday'] while True : today += timedelta(days=1) offset += 1 if today.weekday() >= 5: continue is_holiday = holiday.search([("start_date", "=", today)]) if is_holiday: continue break return offset 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': 1, '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 current_date = datetime.now().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']) sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1 if not rec.estimated_arrival_days: rec.estimated_arrival_days = sum_days eta_date = current_date + timedelta(days=sum_days) 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().date() # 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']) sum_days = max_slatime + self.get_days_until_next_business_day(current_date) - 1 eta_minimum = current_date + timedelta(days=sum_days) if expected_date < eta_minimum: 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