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 _logger = logging.getLogger(__name__) class SaleOrder(models.Model): _inherit = "sale.order" fullfillment_line = fields.One2many('sales.order.fullfillment', 'sales_order_id', string='Fullfillment') 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") 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('Delivery Amt') shipping_cost_covered = fields.Selection([ ('indoteknik', 'Indoteknik'), ('customer', 'Customer') ], string='Shipping Covered by', help='Siapa yang menanggung biaya ekspedisi?', copy=False) shipping_paid_by = fields.Selection([ ('indoteknik', 'Indoteknik'), ('customer', 'Customer') ], string='Shipping Paid by', help='Siapa yang talangin dulu Biaya ekspedisi-nya?', copy=False) 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, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Dipakai untuk alamat tempel") real_invoice_id = fields.Many2one( 'res.partner', string='Delivery Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)], 'sale': [('readonly', False)]}, domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]", help="Dipakai untuk alamat tempel") fee_third_party = fields.Float('Fee Pihak Ketiga') 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", tracking=True) 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') due_id = fields.Many2one('due.extension', string="Due Extension", readonly=True, tracking=True) customer_type = fields.Selection([ ('pkp', 'PKP'), ('nonpkp', 'Non PKP') ], required=True) sppkp = fields.Char(string="SPPKP", required=True) npwp = fields.Char(string="NPWP", required=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') source_id = fields.Many2one('utm.source', 'Source', domain="[('id', 'in', [32, 59, 60, 61])]", required=True) estimated_arrival_days = fields.Integer('Estimated Arrival Days', 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 = fields.Datetime(string='ETA Date', copy=False, compute='_compute_eta_date') web_approval = fields.Selection([ ('company', 'Company'), ('cust_manager', 'Customer Manager'), ('cust_director', 'Customer Director') ], string='Web Approval', copy=False) compute_fullfillment = fields.Boolean(string='Compute Fullfillment', compute="_compute_fullfillment") def _compute_fullfillment(self): for rec in self: for fullfillment in rec.fullfillment_line: fullfillment.unlink() for line in rec.order_line: line._compute_reserved_from() rec.compute_fullfillment = True def _compute_eta_date(self): max_leadtime = 0 for line in self.order_line: leadtime = line.vendor_id.leadtime max_leadtime = max(max_leadtime, leadtime) for rec in self: if rec.date_order and rec.state not in ['cancel', 'draft']: eta_date = datetime.now() + timedelta(days=max_leadtime) rec.eta_date = eta_date else: rec.eta_date = False 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') 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: 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 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, } 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) @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): res = super(SaleOrder, self).write(vals) if 'carrier_id' in vals: for picking in self.picking_ids: if picking.state == 'assigned': picking.carrier_id = self.carrier_id return res 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_shipping_id') def onchange_partner_shipping(self): self.real_shipping_id = self.partner_shipping_id self.real_invoice_id = self.partner_invoice_id @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 @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: #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_approve(self): self.check_due() self._validate_order() for order in self: order.order_line.validate_line() 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: 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 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') raise UserError("Bisa langsung Confirm") def action_confirm(self): for order in self: 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 != 'cust_director': raise UserError("This order not yet approved by customer director") 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.order_line.get_reserved_from() res = super(SaleOrder, self).action_confirm() return res def action_cancel(self): # TODO stephan prevent cancel if have invoice, do, and po if self._name != 'sale.order': return super(SaleOrder, self).action_cancel() if self.have_outstanding_invoice: raise UserError("Invoice harus di Cancel dahulu") elif self.have_outstanding_picking: raise UserError("DO harus di Cancel dahulu") 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 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 <= 22 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 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) 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 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 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 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 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'}, } }