from odoo import fields, models, api, _ from odoo.exceptions import AccessError, UserError, ValidationError from dateutil.relativedelta import relativedelta from datetime import datetime, timedelta import logging from pytz import timezone, utc import io import base64 from odoo.tools import lazy_property import socket import requests import json try: from odoo.tools.misc import xlsxwriter except ImportError: import xlsxwriter _logger = logging.getLogger(__name__) class PurchaseOrder(models.Model): _inherit = 'purchase.order' vcm_id = fields.Many2one('tukar.guling.po', string='Doc VCM', readonly=True, compute='_has_vcm', copy=False) order_sales_match_line = fields.One2many('purchase.order.sales.match', 'purchase_order_id', string='Sales Match Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True) sale_order_id = fields.Many2one('sale.order', string='Sale Order') procurement_status = fields.Char(string='Procurement Status', compute='get_procurement_status', readonly=True) po_status = fields.Selection([ ('terproses', 'Terproses'), ('sebagian', 'Sebagian Diproses'), ('menunggu', 'Menunggu Diproses'), ]) approval_status = fields.Selection([ ('pengajuan1', 'Approval Manager'), #siapa? darren - 11 ('pengajuan2', 'Approval Pimpinan'), #akbar - 7 temporary not used ('approved', 'Approved'), ], string='Approval Status', readonly=True, copy=False, index=True, tracking=3) approval_status_unlock = fields.Selection([ ('pengajuanFinance', 'Pengajuan Finance'), ('approvedFinance', 'Approved Finance'), ('approved', 'Approved'), ], string='Approval Status Unlock', readonly=True, copy=False, index=True, tracking=3) delivery_amount = fields.Float('Delivery Amount', compute='compute_delivery_amount') delivery_amt = fields.Float('Delivery Amt') total_margin = fields.Float( 'Margin', compute='compute_total_margin', help="Total Margin in Sales Order Header") total_percent_margin = fields.Float( 'Margin%', compute='compute_total_margin', help="Total % Margin in Sales Order Header") total_so_margin = fields.Float( 'SO Margin', compute='compute_total_margin', help="Total Margin in Sales Order Header") total_so_percent_margin = fields.Float( 'SO Margin%', compute='compute_total_margin', help="Total % Margin in Sales Order Header") amount_cashback = fields.Float('Cashback', compute = 'compute_total_margin', help = 'Total Cashback brand Altama') amount_total_without_service = fields.Float('AmtTotalWithoutService', compute='compute_amt_total_without_service') summary_qty_po = fields.Float('Total Qty', compute='_compute_summary_qty') summary_qty_receipt = fields.Float('Summary Qty Receipt', compute='_compute_summary_qty') count_line_product = fields.Float('Total Item', compute='compute_count_line_product') note_description = fields.Char(string='Noteman', help='bisa diisi sebagai informasi indent barang tertentu atau apapun') has_active_invoice = fields.Boolean(string='Has Active Invoice', compute='_compute_has_active_invoice') description = fields.Char(string='Description', help='bisa diisi sebagai informasi indent barang tertentu atau apapun') purchase_order_lines = fields.One2many('purchase.order.line', 'order_id', string='Indent', auto_join=True) responsible_ids = fields.Many2many('res.users', string='Responsibles', compute='_compute_responsibles') status_paid_cbd = fields.Boolean(string='Paid Status', tracking=3, help='Field ini diisi secara manual oleh Finance AP dan hanya untuk status PO CBD') revisi_po = fields.Boolean(string='Revisi', tracking=3) from_apo = fields.Boolean(string='From APO', tracking=3) approval_edit_line = fields.Boolean(string='Approval Edit Line', tracking=3) sale_order = fields.Char(string='Sale Order') matches_so = fields.Many2many('sale.order', string='Matches SO', compute='_compute_matches_so') is_create_uangmuka = fields.Boolean(string='Uang Muka?') move_id = fields.Many2one('account.move', string='Journal Entries Uang Muka', domain=[('move_type', '=', 'entry')], copy=False) logbook_bill_id = fields.Many2one('report.logbook.bill', string='Logbook Bill') status_printed = fields.Selection([ ('not_printed', 'Belum Print'), ('printed', 'Printed') ], string='Printed?', copy=False, tracking=True) date_done_picking = fields.Datetime(string='Date Done Picking', compute='get_date_done') bills_dp_id = fields.Many2one('account.move', string='Bills DP') bills_pelunasan_id = fields.Many2one('account.move', string='Bills Pelunasan') product_bom_id = fields.Many2one('product.product', string='Product Bom') grand_total = fields.Monetary(string='Grand Total', help='Amount total + amount delivery', compute='_compute_grand_total') total_margin_match = fields.Float(string='Total Margin Match', compute='_compute_total_margin_match') approve_by = fields.Many2one('res.users', string='Approve By') exclude_incoming = fields.Boolean(string='Exclude Incoming', default=False, help='Centang jika tidak mau masuk perhitungan Incoming Qty') not_update_purchasepricelist = fields.Boolean(string='Not Update Purchase Pricelist?') reason_unlock = fields.Char(string='Alasan unlock', tracking=3) # total_cost_service = fields.Float(string='Total Cost Service' ) # total_delivery_amt = fields.Float(string='Total Delivery Amt') total_cost_service = fields.Float(string='Total Cost Service') total_delivery_amt = fields.Float(string='Total Delivery Amt') store_name = fields.Char(string='Nama Toko') purchase_order_count = fields.Integer('Purchase Order Count', related='partner_id.purchase_order_count') is_cab_visible = fields.Boolean(string='Tampilkan Tombol CAB', compute='_compute_is_cab_visible') reason_change_date_planned = fields.Selection([ ('delay', 'Delay By Vendor'), ('urgent', 'Urgent Delivery'), ], string='Reason Change Date Planned', tracking=True) # picking_ids = fields.One2many('stock.picking', 'purchase_id', string='Pickings') bu_related_count = fields.Integer( string="BU Related Count", compute='_compute_bu_related_count' ) bills_related_count = fields.Integer( string="Bills DP & Pelunasan", compute="_compute_bills_related_count" ) manufacturing_id = fields.Many2one('mrp.production', string='Manufacturing Orders') complete_bu_in_count = fields.Integer( string="Complete BU In Count", compute='_compute_complete_bu_in_count' ) show_description = fields.Boolean( string='Show Description', default=True ) overseas_po = fields.Boolean(string='PO Luar Negeri?', tracking=3, help='Centang jika PO untuk pembelian luar negeri') order_altama_id = fields.Integer('Req Order Altama', copy=False) soo_number = fields.Char('SOO Number', copy=False) soo_price = fields.Float('SOO Price', copy=False) soo_discount = fields.Float('SOO Discount', copy=False) soo_tax = fields.Float('SOO Tax', copy=False) def _get_altama_token(self, source='auto'): ICP = self.env['ir.config_parameter'].sudo() TokenLog = self.env['token.log'].sudo() token_url = ICP.get_param('token.adempiere.altama') client_id = ICP.get_param('client.adempiere.altama') client_secret = ICP.get_param('secret_key.adempiere.altama') active_token = TokenLog.search([ ('is_active', '=', True), ('token_from', '=', 'Adempiere Altama'), ('expires_at', '>', datetime.utcnow()), ], limit=1, order='id desc') if active_token: return active_token.token headers = { "Authorization": "Basic " + base64.b64encode(f"{client_id}:{client_secret}".encode()).decode(), "Content-Type": "application/x-www-form-urlencoded", } data = {"grant_type": "client_credentials"} response = requests.post(token_url, data=data, headers=headers, timeout=15) if response.status_code == 200: result = response.json() token = result.get("access_token") expires_in = result.get("expires_in", 3600) expiry_time = datetime.utcnow() + timedelta(seconds=expires_in - 60) TokenLog.search([ ('token_from', '=', 'Adempiere Altama'), ('is_active', '=', True), ]).write({'is_active': False}) TokenLog.create({ 'token': token, 'expires_at': expiry_time, 'is_active': True, 'created_by': self.env.user.id if self.env.user else None, 'source': source, 'token_from': 'Adempiere Altama', }) return token else: raise Exception(f"Gagal ambil token: {response.status_code} - {response.text}") def action_create_order_altama(self): ICP = self.env['ir.config_parameter'].sudo() for order in self: try: token = self._get_altama_token(source='manual') url = ICP.get_param('endpoint.create.order.adempiere.altama') headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } payload = { "date_po": order.date_approve.strftime("%Y%m%d%H%M%S"), "no_po": order.name, "details": [ { "item_code": line.product_id.default_code or "", "price": line.price_unit, "qty": line.product_qty, } for line in order.order_line ], } response = requests.post(url, json=payload, headers=headers, timeout=20) try: result = response.json() except json.JSONDecodeError: raise Exception(f"Response bukan JSON valid: {response.text}") if response.status_code == 200 and result.get("code") == "00": contents = result.get("contents", {}) if isinstance(contents, dict): order.order_altama_id = contents.get("req_id") else: order.order_altama_id = contents.get("req_id") elif response.status_code == 404: raise Exception("URL endpoint gak ditemukan (404). Pastikan path-nya benar di Altama API.") elif response.status_code == 401: token = self._get_altama_token(source='auto') headers["Authorization"] = f"Bearer {token}" response = requests.post(url, json=payload, headers=headers, timeout=20) elif response.status_code not in (200, 201): raise Exception(f"Gagal kirim ke Altama: {response.status_code} - {response.text}") self.message_post(body=f"✅ PO berhasil dikirim ke Altama!\nResponse: {json.dumps(result, indent=2)}") except Exception as e: self.message_post(body=f"❌ Gagal kirim ke Altama:
{str(e)}
") def action_get_order_altama(self): ICP = self.env['ir.config_parameter'].sudo() for order in self: try: # ============================ # Get Token # ============================ token = self._get_altama_token(source='manual') url = ICP.get_param('endpoint.get.order.adempiere.altama') if not url: raise Exception("Parameter 'endpoint.adempiere.altama' belum diset di System Parameters.") headers = { "Authorization": f"Bearer {token}", "Content-Type": "application/json", } params = { "orderreq_id": order.order_altama_id or 0, } # ============================ # Request ke API # ============================ response = requests.get(url, headers=headers, params=params, timeout=20) if response.status_code == 401: token = self._get_altama_token(source='auto') headers["Authorization"] = f"Bearer {token}" response = requests.get(url, headers=headers, params=params, timeout=20) if response.status_code not in (200, 201): raise Exception(f"Gagal ambil data dari Altama: {response.status_code} - {response.text}") data = response.json() # ============================ # Extract Data # ============================ contents_root = data.get("contents", {}) contents_list = contents_root.get("contents", []) if not isinstance(contents_list, list): raise Exception("Format data contents dari Altama tidak sesuai (expected list).") order.message_post( body=f"✅ Berhasil ambil data dari Altama. Ditemukan {len(contents_list)} record." ) # ===================================================== # LOOP MAIN DATA # ===================================================== for item in contents_list: req_id = item.get("req_id") no_po = item.get("no_po") list_item_po = item.get("list_Item_po", []) list_soo = item.get("list_soo", []) # ============================ # Isi Data SOO Ke Order # ============================ soo_numbers = [s.get("no_soo") for s in list_soo if s.get("no_soo")] unique_soo = list(set(soo_numbers)) if len(unique_soo) == 1: order.soo_number = unique_soo[0] if not order.picking_ids.number_soo: # order.picking_ids[0].number_soo = unique_soo[0] for picking in order.picking_ids: picking.number_soo = unique_soo[0] elif len(unique_soo) > 1: order.soo_number = ", ".join(unique_soo) if not order.picking_ids.number_soo: # order.picking_ids[0].number_soo = ", ".join(unique_soo) for picking in order.picking_ids: picking.number_soo = ", ".join(unique_soo) else: order.soo_number = False if list_soo: first_soo = list_soo[0] order.soo_price = first_soo.get("totalprice") order.soo_discount = first_soo.get("diskon") order.soo_tax = first_soo.get("ppn") order.order_altama_id = req_id # ============================ # Update Order Lines # ============================ for item_line in list_item_po: line = order.order_line.filtered( lambda l: l.product_id.default_code == item_line.get("item_code") ) if line: line.write({ "description": item_line.get("description", ""), "altama_ordered": item_line.get("qtyordered", 0), "altama_delivered": item_line.get("qtydelivered", 0), "altama_invoiced": item_line.get("qtyinvoiced", 0), "docstatus_altama": item_line.get("docstatus", ""), }) # ===================================================== # BUILD HTML TABLES FOR CHATTER # ===================================================== # ---- SOO TABLE ---- soo_rows = "" for s in list_soo: soo_rows += f""" {s.get('no_soo')} {s.get('totalprice')} {s.get('diskon')} {s.get('ppn')} """ soo_table = f""" {soo_rows or ''}
SOO Number Total Price Diskon PPN
Tidak ada data SOO
""" # ---- ITEM PO TABLE ---- po_rows = "" for l in list_item_po: desc = l.get("description") or "" # Flag: row error kalau description tidak mulai dengan SOO/ is_error = desc and not desc.startswith("SOO/") # Style row merah row_style = "color:red; font-weight:bold;" if is_error else "" po_rows += f""" {l.get('item_code')} {desc} {l.get('qtyordered')} {l.get('qtydelivered')} {l.get('qtyinvoiced')} {l.get('docstatus')} """ po_table = f""" {po_rows or ''}
Item Code Description Ordered Delivered Invoiced Status
Tidak ada item PO
""" # ---- POST TO CHATTER ---- order.message_post( body=f""" 📦 Data SOO
{soo_table}

📦 Data Item PO
{po_table} """ ) except Exception as e: order.message_post( body=f"❌ Gagal ambil data dari Altama:
{str(e)}
" ) @staticmethod def is_local_env(): hostname = socket.gethostname().lower() keywords = ['andri', 'miqdad', 'fin', 'stephan', 'hafid', 'nathan'] return any(keyword in hostname for keyword in keywords) @api.onchange('show_description') def onchange_show_description(self): if self.show_description == True: for line in self.order_line: line.show_description = True else: for line in self.order_line: line.show_description = False def _compute_complete_bu_in_count(self): for order in self: if order.state not in ['done', 'cancel']: order.complete_bu_in_count = 1 else: relevant_pickings = order.picking_ids.filtered( lambda p: p.state != 'done' and p.state != 'cancel' and p.picking_type_code == 'incoming' and p.origin == order.name and p.name.startswith('BU/IN') ) order.complete_bu_in_count = len(relevant_pickings) def _has_vcm(self): if self.id: self.vcm_id = self.env['tukar.guling.po'].search([('origin', '=', self.name)], limit=1) @api.depends('order_line.date_planned') def _compute_date_planned(self): """ date_planned = the earliest date_planned across all order lines. """ for order in self: order.date_planned = order.date_planned @api.constrains('date_planned') def constrains_date_planned(self): for rec in self: if not self.env.user.has_group('indoteknik_custom.group_role_purchasing'): raise ValidationError("Hanya dapat diisi oleh Purchasing") base_bu = self.env['stock.picking'].search([ ('name', 'ilike', 'BU/'), ('origin', 'ilike', rec.name), ('group_id', '=', rec.group_id.id), ('state', 'not in', ['cancel','done']) ]) for bu in base_bu: bu.write({ 'scheduled_date': rec.date_planned, 'reason_change_date_planned': rec.reason_change_date_planned }) rec.sync_date_planned_to_so() def sync_date_planned_to_so(self): for line in self.order_sales_match_line: other_sales_match = self.env['purchase.order.sales.match'].search([ # ('product_id', '=', line.product_id.id), ('sale_id', '=', line.sale_id.id), # ('sale_line_id', '=', line.sale_line_id.id) ]) dates = [d for d in other_sales_match.mapped('purchase_order_id.date_planned') if d] if dates: date_planned = max(dates) line.sale_id.write({'et_products': date_planned, 'reason_change_date_planned': line.purchase_order_id.reason_change_date_planned}) @api.depends('name') def _compute_bu_related_count(self): StockPicking = self.env['stock.picking'] for order in self: if not order.name: order.bu_related_count = 0 continue # Ambil semua BU awal dari PO base_bu = StockPicking.search([ '|', '&', ('name', 'ilike', 'BU/'), ('group_id.id', '=', order.group_id.id), ('origin', '=', order.name), ]) all_bu = base_bu seen_names = set(base_bu.mapped('name')) # Loop rekursif untuk mencari seluruh return BU while True: next_bu = StockPicking.search([ ('name', 'ilike', 'BU/'), ('origin', 'in', ['Return of %s' % name for name in seen_names]) ]) next_names = set(next_bu.mapped('name')) if not next_names - seen_names: break all_bu |= next_bu seen_names |= next_names order.bu_related_count = len(all_bu) def action_view_related_bu(self): self.ensure_one() StockPicking = self.env['stock.picking'] # Step 1: cari semua BU pertama (PUT, INT) yang berasal dari PO ini base_bu = StockPicking.search([ '|', '&', ('name', 'ilike', 'BU/'), ('group_id.id', '=', self.group_id.id), ('origin', '=', self.name), ]) all_bu = base_bu seen_names = set(base_bu.mapped('name')) # Step 2: Loop rekursif cari BU dengan origin 'Return of {name}' while True: next_bu = StockPicking.search([ ('name', 'ilike', 'BU/'), ('origin', 'in', ['Return of %s' % name for name in seen_names]) ]) next_names = set(next_bu.mapped('name')) if not next_names - seen_names: break all_bu |= next_bu seen_names |= next_names return { 'name': 'Related BU (INT/PRT/PUT/VRT)', 'type': 'ir.actions.act_window', 'res_model': 'stock.picking', 'view_mode': 'tree,form', 'target': 'current', 'domain': [('id', 'in', list(all_bu.ids))], } @api.depends('move_id.state') def _compute_is_cab_visible(self): for order in self: move = order.move_id order.is_cab_visible = bool(move and move.state == 'posted') def action_view_journal_uangmuka(self): self.ensure_one() if not self.move_id: raise UserError("Journal Uang Muka belum tersedia.") return { 'type': 'ir.actions.act_window', 'name': 'Journal Entry', 'res_model': 'account.move', 'res_id': self.move_id.id, 'view_mode': 'form', 'target': 'current', } def action_view_bills(self): self.ensure_one() bill_ids = [] if self.bills_dp_id: bill_ids.append(self.bills_dp_id.id) if self.bills_pelunasan_id: bill_ids.append(self.bills_pelunasan_id.id) return { 'name': 'Bills (DP & Pelunasan)', 'type': 'ir.actions.act_window', 'res_model': 'account.move', 'view_mode': 'tree,form', 'target': 'current', 'domain': [('id', 'in', bill_ids)], } def _compute_bills_related_count(self): for order in self: count = 0 if order.bills_dp_id: count += 1 if order.bills_pelunasan_id: count += 1 order.bills_related_count = count # cek payment term def _check_payment_term(self): _logger.info("Check Payment Term Terpanggil") cbd_term = self.env['account.payment.term'].search([ ('name', 'ilike', 'Cash Before Delivery') ], limit=1) for order in self: if not order.partner_id or not order.partner_id.minimum_amount: continue if not order.order_line or order.amount_total == 0: continue if order.amount_total < order.partner_id.minimum_amount: if cbd_term and order.payment_term_id != cbd_term: order.payment_term_id = cbd_term.id self.env.user.notify_info( message="Total belanja PO belum mencapai minimum yang ditentukan vendor. " "Payment Term telah otomatis diubah menjadi Cash Before Delivery (C.B.D).", title="Payment Term Diperbarui" ) else: vendor_term = order.partner_id.property_supplier_payment_term_id if vendor_term and order.payment_term_id != vendor_term: order.payment_term_id = vendor_term.id self.env.user.notify_info( message=f"Total belanja PO telah memenuhi jumlah minimum vendor. " f"Payment Term otomatis dikembalikan ke pengaturan vendor awal: *{vendor_term.name}*.", title="Payment Term Diperbarui" ) def _check_tax_rule(self): _logger.info("Check Tax Rule Terpanggil") # Pajak 11% tax_11 = self.env['account.tax'].search([ ('type_tax_use', '=', 'purchase'), ('name', 'ilike', '11%') ], limit=1) # Pajak "No Tax" no_tax = self.env['account.tax'].search([ ('type_tax_use', '=', 'purchase'), ('name', 'ilike', 'no tax') ], limit=1) if not tax_11: raise UserError("Pajak 11% tidak ditemukan. Mohon pastikan pajak 11% tersedia.") if not no_tax: raise UserError("Pajak 'No Tax' tidak ditemukan. Harap buat tax dengan nama 'No Tax' dan tipe 'Purchase'.") for order in self: partner = order.partner_id minimum_tax = partner.minimum_amount_tax _logger.info("Partner ID: %s, Minimum Tax: %s, Untaxed Total: %s", partner.id, minimum_tax, order.amount_untaxed) if not minimum_tax or not order.order_line: continue if order.amount_total < minimum_tax: _logger.info(">>> Total di bawah minimum → apply No Tax") for line in order.order_line: line.taxes_id = [(6, 0, [no_tax.id])] if self.env.context.get('notify_tax'): self.env.user.notify_info( message="Total belanja PO belum mencapai minimum pajak vendor. " "Pajak diganti menjadi 'No Tax'.", title="Pajak Diperbarui", ) else: _logger.info(">>> Total memenuhi minimum → apply Pajak 11%") for line in order.order_line: line.taxes_id = [(6, 0, [tax_11.id])] if self.env.context.get('notify_tax'): self.env.user.notify_info( message="Total belanja sebelum pajak telah memenuhi minimum. " "Pajak 11%% diterapkan", title="Pajak Diperbarui", ) # set default no_tax pada order line # @api.onchange('order_line') # def _onchange_order_line_tax_default(self): # _logger.info("Onchange Order Line Tax Default Terpanggil") # no_tax = self.env['account.tax'].search([ # ('type_tax_use', '=', 'purchase'), # ('name', 'ilike', 'no tax') # ], limit=1) # if not no_tax: # _logger.info("No Tax tidak ditemukan") # return # for order in self: # for line in order.order_line: # if not line.taxes_id: # line.taxes_id = [(6, 0, [no_tax.id])] # _logger.info("Auto-set No tax ke baris product: %s", line.product_id.name) @api.onchange('total_cost_service') def _onchange_total_cost_service(self): for order in self: lines = order.order_line if order.total_cost_service > 0: if lines: # Hitung nilai rata-rata cost_service per_line_cost_service = order.total_cost_service / len(lines) for line in lines: line.cost_service = per_line_cost_service else: for line in lines: line.cost_service = 0 @api.onchange('total_delivery_amt') def _onchange_total_delivery_amt(self): for order in self: lines = order.order_line if order.total_delivery_amt > 0: if lines: # Hitung nilai rata-rata delivery_amt per_line_delivery_amt = order.total_delivery_amt / len(lines) for line in lines: line.delivery_amt = per_line_delivery_amt else: for line in lines: line.delivery_amt = 0 def _compute_total_margin_match(self): for purchase in self: match = self.env['purchase.order.sales.match'] result = match.read_group( [('purchase_order_id', '=', purchase.id)], ['margin_item'], [] ) purchase.total_margin_match = result[0].get('margin_item', 0.0) def _compute_grand_total(self): for order in self: if order.delivery_amt: order.grand_total = order.delivery_amt + order.amount_total else: order.grand_total = order.amount_total def create_bill_pelunasan(self): if not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa bikin bill dp') # Check for existing vendor bills with the same reference and partner existing_bill = self.env['account.move'].search([ ('ref', '=', self.name), ('partner_id', '=', self.partner_id.id), ('move_type', '=', 'in_invoice'), ('state', 'not in', ['cancel', 'posted']) ], limit=1) if existing_bill: raise UserError(_('Duplicated vendor reference detected. You probably encoded twice the same vendor bill/credit note: %s') % existing_bill.name) current_date = datetime.utcnow() data_bills = { 'partner_id': self.partner_id.id, 'partner_shipping_id': self.partner_id.id, 'ref': self.name, 'invoice_date': current_date, 'date': current_date, 'invoice_origin': self.name, 'purchase_order_id': self.id, 'move_type': 'in_invoice' } bills = self.env['account.move'].create([data_bills]) product_dp = self.env['product.product'].browse(229625) data_line_bills = [] move_line = self.env['account.move.line'].search([ ('move_id', '=', self.bills_dp_id.id), ('product_id', '=', product_dp.id), ]) bills.message_post( body=f"
" f"DP :
{move_line.price_unit}
", subtype_id=self.env.ref("mail.mt_note").id ) data_line_bills.append({ 'move_id': bills.id, 'product_id': product_dp.id, # product down payment 'name': '[IT.121456] Down Payment', # product down payment 'account_id': 669, # Uang Muka persediaan barang dagang # 'price_unit': move_line.price_unit, 'quantity': -1, 'product_uom_id': 1, 'tax_ids': [(5, 0, 0)] + [(4, tax.id) for tax in product_dp.taxes_id], }) for line in self.order_line: if line.product_id: data_line_bills.append({ 'move_id': bills.id, 'product_id': line.product_id.id, 'name': self.name + ": " + line.product_id.display_name, 'account_id': 439, # Uang Muka persediaan barang dagang 'quantity': line.product_qty, # 'price_unit': line.price_subtotal, 'product_uom_id': line.product_uom.id, 'tax_ids': [(5, 0, 0)] + [(4, tax.id) for tax in line.taxes_id], 'purchase_line_id': line.id, 'purchase_order_id': line[0].order_id.id, }) bills_line = self.env['account.move.line'].create(data_line_bills) self.bills_pelunasan_id = bills.id lognote_message = ( f"Vendor bill created from: {self.name} ({self.partner_ref})" ) bills.message_post(body=lognote_message) return { 'name': _('Account Move'), 'view_mode': 'tree,form', 'res_model': 'account.move', 'target': 'current', 'type': 'ir.actions.act_window', 'domain': [('id', '=', bills.id)] } def create_bill_dp(self): if not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa bikin bill dp') current_date = datetime.utcnow() data_bills = { 'partner_id': self.partner_id.id, 'partner_shipping_id': self.partner_id.id, 'ref': self.name, 'invoice_date': current_date, 'date': current_date, 'invoice_origin': self.name, 'purchase_order_id': self.id, 'move_type': 'in_invoice' } bills = self.env['account.move'].create([data_bills]) product_dp = self.env['product.product'].browse(229625) data_line_bills = { 'move_id': bills.id, 'product_id': product_dp.id, # product down payment 'account_id': 669, # Uang Muka persediaan barang dagang 'quantity': 1, 'product_uom_id': 1, 'tax_ids': [line[0].taxes_id.id for line in self.order_line], } bills_line = self.env['account.move.line'].create([data_line_bills]) self.bills_dp_id = bills.id move_line = bills.line_ids move_line.name = '[IT.121456] Down Payment' move_line.partner_id = self.partner_id.id # Tambahkan lognote lognote_message = ( f"Vendor bill created from: {self.name} ({self.partner_ref})" ) bills.message_post(body=lognote_message) return { 'name': _('Account Move'), 'view_mode': 'tree,form', 'res_model': 'account.move', 'target': 'current', 'type': 'ir.actions.act_window', 'domain': [('id', '=', bills.id)] } def get_date_done(self): picking = self.env['stock.picking'].search([ ('purchase_id', '=', self.id), ('state', '=', 'done') ], limit=1, order='create_date desc') self.date_done_picking = picking.date_done def _prepare_invoice(self): """Prepare the dict of values to create the new invoice for a purchase order. """ self.ensure_one() move_type = self._context.get('default_move_type', 'in_invoice') journal = self.env['account.move'].with_context(default_move_type=move_type)._get_default_journal() if not journal: raise UserError(_('Please define an accounting purchase journal for the company %s (%s).') % (self.company_id.name, self.company_id.id)) date_done = self.date_approve # day_extension = int(self.payment_term_id.line_ids.days) day_extension = int(max(self.payment_term_id.line_ids.mapped('days'), default=0)) payment_schedule = date_done + timedelta(days=day_extension) if payment_schedule.weekday() == 0: payment_schedule -= timedelta(days=4) elif payment_schedule.weekday() == 2: payment_schedule -= timedelta(days=1) elif payment_schedule.weekday() == 4: payment_schedule -= timedelta(days=1) elif payment_schedule.weekday() == 5: payment_schedule -= timedelta(days=2) elif payment_schedule.weekday() == 6: payment_schedule -= timedelta(days=3) partner_invoice_id = self.partner_id.address_get(['invoice'])['invoice'] invoice_vals = { 'ref': self.partner_ref or '', 'move_type': move_type, 'purchase_order_id': self.id, 'narration': self.notes, 'currency_id': self.currency_id.id, 'invoice_user_id': self.user_id and self.user_id.id or self.env.user.id, 'partner_id': partner_invoice_id, 'fiscal_position_id': (self.fiscal_position_id or self.fiscal_position_id.get_fiscal_position(partner_invoice_id)).id, 'payment_reference': self.partner_ref or '', 'partner_bank_id': self.partner_id.bank_ids[:1].id, 'invoice_origin': self.name, 'invoice_payment_term_id': self.payment_term_id.id, 'invoice_line_ids': [], 'company_id': self.company_id.id, 'payment_schedule': payment_schedule } receipt = self.env['stock.picking'].search([ ('purchase_id', '=', self.id), ('state', '=', 'done'), ('picking_type_code', '=', 'incoming'), ('date_done', '!=', False) ], order='date_done desc', limit=1) if receipt: invoice_vals['invoice_date'] = receipt.date_done invoice_vals['date'] = receipt.date_done return invoice_vals def _compute_matches_so(self): for po in self: matches = [] for match in po.order_sales_match_line: matches.append(match.sale_id.id) matches = list(set(matches)) po.matches_so = matches def _prepare_picking(self): if not self.group_id: self.group_id = self.group_id.create({ 'name': self.name, 'partner_id': self.partner_id.id }) if self.sale_order_id: sale_order = self.sale_order_id else: sale_order = self.sale_order if not self.partner_id.property_stock_supplier.id: raise UserError(_("You must set a Vendor Location for this partner %s", self.partner_id.name)) return { 'picking_type_id': self.picking_type_id.id, 'partner_id': self.partner_id.id, 'user_id': False, 'date': self.date_order, 'origin': self.name, 'location_dest_id': self._get_destination_location(), 'location_id': self.partner_id.property_stock_supplier.id, 'company_id': self.company_id.id, 'sale_order': sale_order } @api.model def action_multi_cancel(self): for purchase in self: purchase.update({ 'state': 'cancel', }) if purchase.state == 'cancel': purchase.update({ 'approval_status': False, }) def open_form_multi_cancel(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_po_multi_cancel') action['context'] = { 'purchase_ids': [x.id for x in self] } return action def delete_line(self): lines_to_delete = self.order_line.filtered(lambda line: line.suggest == 'masih cukup') if not lines_to_delete: raise UserError('Tidak ada item yang masih cukup') lines_to_delete.unlink() def open_form_multi_confirm_po(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_confirm') action['context'] = { 'order_ids': [x.id for x in self] } return action def open_form_multi_ask_approval_po(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_ask_approval') action['context'] = { 'po_ids': [x.id for x in self] } return action def open_form_multi_create_uang_muka(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_uangmuka') action['context'] = { 'po_ids': [x.id for x in self] } return action def open_form_multi_create_uang_muka2(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_uangmuka') action['context'] = { 'po_ids': self.id } return action def action_multi_update_paid_status(self): for purchase in self: purchase.update({ 'status_paid_cbd': True, }) def action_multi_confirm_po(self): for purchase in self: if purchase.state != 'draft': continue purchase.button_confirm() def action_multi_ask_approval_po(self): for purchase in self: if purchase.state != 'draft': continue purchase.po_approve() def open_form_multi_update_paid_status(self): action = self.env['ir.actions.act_window']._for_xml_id('indoteknik_custom.action_purchase_order_multi_update') action['context'] = { 'purchase_ids': [x.id for x in self] } return action def _compute_responsibles(self): for purchase in self: resposible_ids = [] for line in purchase.order_line: resposible_ids.append(line.product_id.x_manufacture.user_id.id) resposible_ids = list(set(resposible_ids)) purchase.responsible_ids = resposible_ids def _compute_has_active_invoice(self): for order in self: order.has_active_invoice = any(invoice.state != 'cancel' for invoice in order.invoice_ids) def add_product_to_pricelist(self): i = 0 for line in self.order_line: i += 1 utc_time = fields.Datetime.now() current_time = utc_time.astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S') # print(i, len(self.order_line)) price_unit = line.price_unit taxes = line.taxes_id # for tax in taxes: # tax_include = tax.price_include # if taxes: # if tax_include: # price_unit = price_unit # else: # price_unit = price_unit + (price_unit * 11 / 100) # else: # price_unit = price_unit + (price_unit * 11 / 100) purchase_pricelist = self.env['purchase.pricelist'].search([ ('product_id', '=', line.product_id.id), ('vendor_id', '=', line.order_id.partner_id.id) ]) if not purchase_pricelist: # Buat pricelist baru dengan context new_pricelist = self.env['purchase.pricelist'].with_context(update_by='system').create([{ 'vendor_id': line.order_id.partner_id.id, 'product_id': line.product_id.id, 'product_price': 0, 'taxes_system_id': taxes.id, 'system_price': price_unit, 'system_last_update': current_time }]) # Buat lognote untuk pricelist baru message = f""" New Purchase Pricelist Created from PO
PO: {line.order_id.name}
System Price: {price_unit:,.2f}
System Tax: {taxes.name if taxes else 'No Tax'}
System Update: {current_time}
""" new_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id) else: # Simpan nilai lama untuk logging old_values = { 'system_price': purchase_pricelist.system_price, 'taxes_system_id': purchase_pricelist.taxes_system_id, } # Update dengan context purchase_pricelist = purchase_pricelist.with_context(update_by='system') purchase_pricelist.write({ 'system_last_update': current_time, 'taxes_system_id': taxes.id, 'system_price': price_unit }) # Buat lognote jika ada perubahan changes = [] if old_values['system_price'] != price_unit: changes.append(f"
  • System Price: {old_values['system_price']:,.2f} → {price_unit:,.2f}
  • ") if old_values['taxes_system_id'] != taxes: old_tax_name = old_values['taxes_system_id'].name if old_values['taxes_system_id'] else 'No Tax' new_tax_name = taxes.name if taxes else 'No Tax' changes.append(f"
  • System Tax: {old_tax_name} → {new_tax_name}
  • ") if changes: message = f""" System Fields Updated from PO
    PO: {line.order_id.name}
    Changes: """ purchase_pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id) # def _compute_date_planned(self): # for order in self: # if order.date_approve: # leadtime = order.partner_id.leadtime # current_time = order.date_approve # delta_time = current_time + timedelta(days=leadtime) # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') # order.date_planned = delta_time # else: # order.date_planned = False def action_create_invoice(self): res = super(PurchaseOrder, self).action_create_invoice() if not self.env.user.is_accounting: raise UserError('Hanya Accounting yang bisa membuat Bill') return res 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 # _logger.info('Calculate PO Line No %s' % line.id) def calculate_po_status_beginning(self): purchases = self.env['purchase.order'].search([ ('po_status', '=', False), '|', ('state', '=', 'purchase'), ('state', '=', 'done') ]) for order in purchases: sum_qty_received = sum_qty_po = 0 for po_line in order.order_line: sum_qty_po += po_line.product_uom_qty sum_qty_received += po_line.qty_received if order.summary_qty_po == order.summary_qty_receipt: order.po_status = 'terproses' elif order.summary_qty_po > order.summary_qty_receipt > 0: order.po_status = 'sebagian' else: order.po_status = 'menunggu' _logger.info("Calculate PO Status %s" % order.id) def calculate_po_status(self): purchases = self.env['purchase.order'].search([ ('po_status', '!=', 'terproses'), # ('id', '=', 213), ]) for order in purchases: sum_qty_received = sum_qty_po = 0 have_outstanding_pick = False for pick in order.picking_ids: if pick.state == 'draft' or pick.state == 'assigned' or pick.state == 'confirmed' or pick.state == 'waiting': have_outstanding_pick = True for po_line in order.order_line: sum_qty_po += po_line.product_uom_qty sum_qty_received += po_line.qty_received if have_outstanding_pick: # if order.summary_qty_po == order.summary_qty_receipt: # order.po_status = 'terproses' if order.summary_qty_po > order.summary_qty_receipt > 0: order.po_status = 'sebagian' else: order.po_status = 'menunggu' else: order.po_status = 'terproses' _logger.info("Calculate PO Status %s" % order.id) def _compute_summary_qty(self): for order in self: sum_qty_po = sum_qty_receipt = 0 for line in order.order_line: sum_qty_po += line.product_uom_qty sum_qty_receipt += line.qty_received order.summary_qty_po = sum_qty_po order.summary_qty_receipt = sum_qty_receipt def get_procurement_status(self): for purchase_order in self: # product_uom_qty = sum_qty_received = 0 # # for order_line in purchase_order.order_line: # product_uom_qty += order_line.product_uom_qty # sum_qty_received += order_line.qty_received if purchase_order.summary_qty_po == purchase_order.summary_qty_receipt: status = 'Terproses' elif purchase_order.summary_qty_po > purchase_order.summary_qty_receipt > 0: status = 'Sebagian Diproses' else: status = 'Menunggu Diproses' purchase_order.procurement_status = status def sale_order_sync(self): if not self.sale_order_id: return purchase_orders = self.search(['&', ('sale_order_id', '=', self.sale_order_id.id), ('id', '!=', self.id)]) products_exception = [] for purchase_order in purchase_orders: for order_line in purchase_order.order_line: products_exception.append(order_line.product_id.id) self.order_line.unlink() for order_line in self.sale_order_id.order_line: if order_line.vendor_id != self.partner_id: continue if order_line.product_id.id and order_line.product_id.id not in products_exception: qty_available = order_line.product_id.qty_onhand_bandengan + order_line.product_id.qty_incoming_bandengan - order_line.product_id.outgoing_qty # suggest = 'harus beli' # if qty_available > order_line.product_qty: # suggest = 'masih cukup' values = { 'order_id': self.id, 'product_id': order_line.product_id.id, 'name': order_line.product_id.display_name, 'product_qty': order_line.product_qty, 'qty_available_store': qty_available, # 'suggest': suggest, 'so_line_id': order_line.id, 'so_id': order_line.order_id.id, } self.order_line.create(values) for order_line in self.order_line: order_line.suggest_purchasing() def compute_count_line_product(self): for order in self: count = 0 for line in order.order_line: if line.product_id.type == 'product': count += 1 if count == 0: order.count_line_product = 1 else: order.count_line_product = count def compute_delivery_amount(self): for order in self: amount = 0 for line in order.order_line: if line.product_id.type == 'service': amount += line.price_total order.delivery_amount = amount def date_deadline_ref_date_planned(self): for picking in self.picking_ids: if picking.state in ['done', 'cancel']: continue picking.scheduled_date = self.date_planned picking.date_deadline = self.date_planned def _check_qty_plafon_product(self): for line in self.order_line: if not line.product_id: continue # test = line.product_uom_qty # test2 = line.product_id.plafon_qty # test3 = test2 + line.product_uom_qty if line.product_uom_qty > line.product_id.plafon_qty + line.product_uom_qty and self.env.user.id not in [21, 7]: raise UserError('Product '+line.product_id.name+' melebihi plafon, harus Approval Rafly') def check_different_vendor_so_po(self): vendor_po = self.partner_id.id for line in self.order_line: if not line.so_line_id: continue if line.so_line_id.vendor_id.id != vendor_po: self.env.user.notify_danger( title='WARNING!!!', message="Produk "+line.product_id.name+" memiliki vendor berbeda dengan SO (Vendor PO: "+str(self.partner_id.name)+", Vendor SO: "+str(line.so_line_id.vendor_id.name)+")", sticky=True ) def _check_assets_note(self): for order in self: # Cari apakah ada line dengan produk ID 614469 ('Assets Mesin & Peralatan') asset_line = order.order_line.filtered(lambda l: l.product_id.id == 595346) if asset_line and not order.notes: raise UserError(_( "%s berisi produk 'Assets Mesin & Peralatan'. " "Harap isi Notes untuk menjelaskan kebutuhan dan divisi terkait." ) % order.name) def button_confirm(self): if self.env.user.id != 7 and not self.env.user.is_leader: # Pimpinan if '/PJ/' in self.name: price_change_detected = any(line.price_unit_before for line in self.order_line) if price_change_detected: if self.order_sales_match_line: if self.total_percent_margin <= 15.0: raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line dan Memiliki Margin <= 15%") self._check_assets_note() # self._check_payment_term() # check payment term res = super(PurchaseOrder, self).button_confirm() current_time = datetime.now() self.check_ppn_mix() self.check_different_vendor_so_po() # self.check_data_vendor() if self.amount_untaxed >= 50000000 and not self.env.user.id in (21, 7): raise UserError("Hanya Rafly Hanggara yang bisa approve") if not self.date_planned: raise UserError("Receipt Date harus diisi") if self.total_percent_margin < self.total_so_percent_margin: self.env.user.notify_danger( title='WARNING!!!', message='Beda Margin dengan Sale Order', sticky=True ) # if len(self.order_sales_match_line) == 0 and not self.env.user.has_group('indoteknik_custom.group_role_merchandiser') and not self.env.user.is_leader: # self.env.user.notify_danger( # title='WARNING!!!', # message='Tidak ada matches SO', # sticky=True # ) has_bom = self.product_bom_id.id has_manufacturing = self.manufacturing_id.id if not self.from_apo: if not self.matches_so and not self.env.user.is_purchasing_manager and not self.env.user.is_leader and not has_bom and not has_manufacturing: raise UserError("Tidak ada link dengan SO, harus di confirm oleh Purchasing Manager") send_email = False if not self.not_update_purchasepricelist: self.add_product_to_pricelist() for line in self.order_line: if line.product_id.type == 'product' and not line.product_id.categ_id: raise UserError("Product %s kategorinya kosong" % line.product_id.name) if not line.product_id.purchase_ok: raise UserError("Terdapat barang yang tidak bisa diproses") # Validasi pajak if not line.taxes_id: raise UserError("Masukkan Tax untuk produk") for tax in line.taxes_id: if tax.type_tax_use != 'purchase': raise UserError("Pastikan Tax Category nya adalah Purchase pada produk %s" % line.product_id.name) if line.price_unit != line.price_vendor and line.price_vendor != 0: self._send_po_not_sync() send_email = True break # if self.partner_id.id == 5571 and not self.revisi_po: # self.action_create_order_altama() if send_email: if self.is_local_env(): _logger.warning("📪 Local environment detected — skip sending email reminders.") return # self._send_mail() if self.revisi_po: delta_time = current_time - timedelta(days=1) delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') self.date_approve = delta_time self.approval_status = 'approved' self.po_status = 'menunggu' self.calculate_line_no() self.approve_by = self.env.user.id # override date planned added with two days # leadtime = self.partner_id.leadtime # delta_time = current_time + timedelta(days=leadtime) # delta_time = delta_time.strftime('%Y-%m-%d %H:%M:%S') # self.date_planned = delta_time self.date_deadline_ref_date_planned() self.unlink_purchasing_job_state() self._check_qty_plafon_product() if self.product_bom_id: self._remove_product_bom() # Tambahan: redirect ke BU hanya untuk single PO yang berhasil dikonfirmasi _logger.info("Jumlah PO: %s | State: %s", len(self), self.state) # if len(self) == 1: # _logger.info("Redirecting ke BU") # return self.action_view_related_bu() if self.partner_id.id == 5571 and not self.revisi_po: self.action_create_order_altama() return res def _remove_product_bom(self): pj = self.env['v.purchasing.job'].search([ ('product_id', '=', self.product_bom_id.id) ]) if pj: pj_state = self.env['purchasing.job.state'].search([ ('purchasing_job_id', '=', pj.id) ]) if pj_state: pj_state.note = 'Product BOM Sudah Di PO' pj_state.date_po = datetime.utcnow() def check_ppn_mix(self): reference_taxes = self.order_line[0].taxes_id for line in self.order_line: if line.taxes_id != reference_taxes: raise UserError(f"PPN harus sama untuk semua baris pada line {line.product_id.name}") def check_data_vendor(self): vendor = self.partner_id if not vendor.email_finance or not vendor.email_sales: raise UserError("Email Finance dan Email Sales pada vendor harus diisi") def unlink_purchasing_job_state(self): for line in self.order_line: purchasing_job_state = self.env['purchasing.job.state'].search([ ('purchasing_job_id', '=', line.product_id.id) ]) if purchasing_job_state: for purchasing_job in purchasing_job_state: purchasing_job.unlink() def _send_po_not_sync(self): # Mengirim data ke model Po Sync Price jika harga po dan purchase pricelist tidak singkron for line in self.order_line: if line.price_unit != line.price_vendor and line.price_vendor != 0: self.env['po.sync.price'].create([{ 'order_line_id': line.id, }]) # def _send_mail(self): # output = io.BytesIO() # workbook = xlsxwriter.Workbook(output, {'in_memory': True}) # worksheet = workbook.add_worksheet() # format6 = workbook.add_format({'font_size': 12, 'align': 'center', 'bg_color': '#D3D3D3', 'bold': True}) # format1 = workbook.add_format({'font_size': 11, 'align': 'center', 'valign': 'vcenter'}) # worksheet.set_column(0, 0, 10) # worksheet.set_column(1, 1, 20) # worksheet.set_column(2, 2, 20) # worksheet.set_column(3, 3, 20) # worksheet.set_column(4, 4, 15) # worksheet.set_column(5, 5, 15) # worksheet.write('A1', 'PO', format6) # worksheet.write('B1', 'SKU', format6) # worksheet.write('C1', 'Product', format6) # worksheet.write('D1', 'Brand', format6) # worksheet.write('E1', 'PO Price', format6) # worksheet.write('F1', 'Purchase Pricelist', format6) # worksheet.write('G1', 'Created On', format6) # row_number = 1 # po_sync = self.env['po.sync.price'].search([], order='create_date desc') # for po in po_sync: # worksheet.write(row_number, 0, po.order_line_id.order_id.name, format1) # worksheet.write(row_number, 1, po.order_line_id.product_id.default_code, format1) # worksheet.write(row_number, 2, po.order_line_id.product_id.name, format1) # worksheet.write(row_number, 3, po.order_line_id.product_id.x_manufacture.x_name, format1) # worksheet.write(row_number, 4, po.order_line_id.price_unit, format1) # worksheet.write(row_number, 5, po.order_line_id.price_vendor, format1) # worksheet.write(row_number, 6, po.create_date.replace(tzinfo=utc).astimezone(timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S'), format1) # row_number += 1 # workbook.close() # output.seek(0) # template = self.env.ref('indoteknik_custom.mail_template_po_sync_price') # template.attachment_ids.unlink() # attachment_vals = { # 'name': 'Purchase Order.xlsx', # 'datas': base64.b64encode(output.read()), # 'mimetype': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', # 'res_model': 'mail.template', # 'res_id': template.id, # } # attachment_id = self.env['ir.attachment'].create(attachment_vals) # template.attachment_ids = [(4, attachment_id.id)] # template.send_mail(self.id, force_send=True) def po_approve(self): greater_than_plafon, message = self._get_msg_plafon_qty() different_vendor_message = self.check_different_vendor_so() # Panggil fungsi check_different_vendor_so if self.approval_status_unlock == 'pengajuanFinance': if self.env.user.is_accounting: self.approval_status_unlock = 'approvedFinance' else: raise UserError("Bisa langsung Confirm, menunggu persetujuan Finance jika ingin unlock PO") elif self.env.user.is_leader or self.env.user.has_group('indoteknik_custom.group_role_purchasing'): raise UserError("Bisa langsung Confirm") elif self.total_percent_margin == self.total_so_percent_margin and self.matches_so and not greater_than_plafon and not different_vendor_message: raise UserError("Bisa langsung Confirm") else: reason = '' self.approval_status = 'pengajuan1' if self.amount_untaxed >= 50000000: reason = 'above 50jt, ' if self.total_percent_margin < self.total_so_percent_margin: reason += 'diff margin, ' if not self.from_apo and not self.matches_so: reason += 'not link with pj and reorder, ' if not self.matches_so: reason += 'not link with so, ' # Check Plafon Qty and Get Message every Line Product if greater_than_plafon: reason += message # Check for Different Vendor Message if different_vendor_message: reason += different_vendor_message # Post a highlighted message to lognote self.message_post( body=f"
    " f"Note (Pinned):
    {reason}
    ", subtype_id=self.env.ref("mail.mt_note").id ) def po_approve_unlock(self): if self.approval_status_unlock == 'pengajuanFinance': if self.env.user.is_accounting: self.approval_status_unlock = 'approvedFinance' else: raise UserError("Menunggu persetujuan Finance jika ingin unlock PO") elif self.approval_status_unlock == 'approvedFinance': raise UserError("PO bisa langsung di unlock") else: raise UserError("Menunggu persetujuan Finance jika ingin unlock PO") def check_different_vendor_so(self): vendor_po = self.partner_id.id message = '' for line in self.order_line: if not line.so_line_id: continue if line.so_line_id.vendor_id.id != vendor_po: product_code = line.product_id.display_name or 'Unknown' message += (f"Produk {product_code} memiliki vendor berbeda dengan SO " f"(Vendor PO: {self.partner_id.name}, Vendor SO: {line.so_line_id.vendor_id.name}), ") return message if message else None def _get_msg_plafon_qty(self): message = '' greater_than_plafon = False for line in self.order_line: if not line.product_id: continue if line.product_uom_qty > line.product_id.plafon_qty: message = (message + '\n'+line.product_id.default_code + ' melebihi plafon (' + str(line.product_id.plafon_qty) + ') vs Qty PO ('+str(line.product_uom_qty)+')' + ', ') greater_than_plafon = True return greater_than_plafon, message def re_calculate(self): if self.from_apo: self.re_calculate_from_apo() return for line in self.order_line: sale_order_line = self.env['sale.order.line'].search([ ('product_id', 'in', [line.product_id.id]), ('order_id', '=', line.order_id.sale_order_id.id) ]) for so_line in sale_order_line: so_line.purchase_price = line.price_unit def re_calculate_from_apo(self): for line in self.order_sales_match_line: order_line = self.env['purchase.order.line'].search([ ('product_id', '=', line.product_id.id), ('order_id', '=', line.purchase_order_id.id) ], limit=1) line.sale_line_id.purchase_price = order_line.price_unit def button_cancel(self): res = super(PurchaseOrder, self).button_cancel() self.approval_status = False return res def compute_total_margin(self): for rec in self: if rec.from_apo: rec.compute_total_margin_from_apo() return sum_so_margin = sum_sales_price = sum_margin = 0 for line in self.order_line: sale_order_line = line.so_line_id if not sale_order_line: sale_order_line = self.env['sale.order.line'].search([ ('product_id', '=', line.product_id.id), ('order_id', '=', line.order_id.sale_order_id.id) ], limit=1, order='price_reduce_taxexcl') sum_so_margin += sale_order_line.item_margin sales_price = sale_order_line.price_reduce_taxexcl * sale_order_line.product_uom_qty if sale_order_line.order_id.shipping_cost_covered == 'indoteknik': sales_price -= sale_order_line.delivery_amt_line if sale_order_line.order_id.fee_third_party > 0: sales_price -= sale_order_line.fee_third_party_line sum_sales_price += sales_price purchase_price = line.price_subtotal if line.ending_price > 0: if line.taxes_id.id == 22: ending_price = line.ending_price / 1.11 purchase_price = ending_price else: purchase_price = line.ending_price # purchase_price = line.price_subtotal if line.order_id.delivery_amount > 0: purchase_price += line.delivery_amt_line if line.order_id.delivery_amt > 0: purchase_price += line.order_id.delivery_amt real_item_margin = sales_price - purchase_price sum_margin += real_item_margin cashback_amount = 0 if self.partner_id.id == 5571: cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 if cashback_percent > 0: cashback_amount = purchase_price * cashback_percent purchase_price -= cashback_amount # line.amount_cashback = cashback_amount if sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0: self.total_so_margin = sum_so_margin self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 self.amount_cashback = 0 elif self.partner_id.id == 5571 and sum_so_margin != 0 and sum_sales_price != 0 and sum_margin != 0 and cashback_amount != 0: self.total_so_margin = sum_so_margin self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 self.amount_cashback = cashback_amount else: self.total_margin = 0 self.total_percent_margin = 0 self.total_so_margin = 0 self.total_so_percent_margin = 0 self.amount_cashback = 0 def compute_total_margin_from_apo(self): sum_so_margin = sum_sales_price = sum_margin = cashback_amount = 0 for line in self.order_sales_match_line: po_line = self.env['purchase.order.line'].search([ ('product_id', '=', line.product_id.id), ('order_id', '=', line.purchase_order_id.id) ], limit=1) sale_order_line = line.sale_line_id or self.env['sale.order.line'].search([ ('product_id', '=', line.product_id.id), ('order_id', '=', line.sale_id.id) ], limit=1, order='price_reduce_taxexcl') if sale_order_line and po_line: qty_so = line.qty_so or 0 qty_po = line.qty_po or 0 # Hindari division by zero so_margin = (qty_po / qty_so) * sale_order_line.item_margin if qty_so > 0 else 0 sum_so_margin += so_margin sales_price = sale_order_line.price_reduce_taxexcl * qty_po if sale_order_line.order_id.shipping_cost_covered == 'indoteknik': sales_price -= (sale_order_line.delivery_amt_line / sale_order_line.product_uom_qty) * qty_po if sale_order_line.order_id.fee_third_party > 0: sales_price -= (sale_order_line.fee_third_party_line / sale_order_line.product_uom_qty) * qty_po sum_sales_price += sales_price purchase_price = po_line.price_subtotal if po_line.ending_price > 0: if po_line.taxes_id.id == 22: purchase_price = po_line.ending_price / 1.11 else: purchase_price = po_line.ending_price if line.purchase_order_id.delivery_amount > 0: purchase_price += (po_line.delivery_amt_line / po_line.product_qty) * qty_po if line.purchase_order_id.delivery_amt > 0: purchase_price += line.purchase_order_id.delivery_amt if self.partner_id.id == 5571: cashback_percent = line.product_id.x_manufacture.cashback_percent or 0.0 if cashback_percent > 0: cashback_amount = purchase_price * cashback_percent purchase_price -= cashback_amount real_item_margin = sales_price - purchase_price sum_margin += real_item_margin self.amount_cashback = cashback_amount # Akumulasi hasil akhir if sum_sales_price != 0: self.total_so_margin = sum_so_margin self.total_so_percent_margin = round((sum_so_margin / sum_sales_price), 2) * 100 self.total_margin = sum_margin self.total_percent_margin = round((sum_margin / sum_sales_price), 2) * 100 self.amount_cashback = cashback_amount else: self.total_margin = self.total_percent_margin = 0 self.total_so_margin = self.total_so_percent_margin = 0 self.amount_cashback = 0 def compute_amt_total_without_service(self): for order in self: sum_price_total = 0 for line in order.order_line: if line.product_id.type == 'product': sum_price_total += line.price_total order.amount_total_without_service = sum_price_total def button_unlock(self): for order in self: # Check if any order line has received_qty not equal to 0 if self.env.user.is_accounting: order.state = 'purchase' order.approval_status_unlock = 'approved' break for line in order.order_line: if line.qty_received > 0: if order.approval_status_unlock == 'approvedFinance': order.approval_status_unlock = 'approved' order.state = 'purchase' break if order.approval_status_unlock == 'pengajuanFinance': raise UserError(_( "Menunggu Approve Dari Finance." )) else: return { 'type': 'ir.actions.act_window', 'name': _('Untuk mengubah PO butuh approve dari Finance. Berikan alasan anda unlock PO!'), 'res_model': 'purchase.order.unlock.wizard', 'view_mode': 'form', 'target': 'new', 'context': { 'default_purchase_order_id': order.id } } return super(PurchaseOrder, self).button_unlock() @api.model #override custom create & write for check payment term def create(self, vals): order = super().create(vals) # order.with_context(skip_check_payment=True)._check_payment_term() # order.with_context(notify_tax=True)._check_tax_rule() return order def write(self, vals): res = super().write(vals) if not self.env.context.get('skip_check_payment'): self.with_context(skip_check_payment=True)._check_payment_term() self.with_context(notify_tax=True)._check_tax_rule() # Tambahkan pemanggilan method untuk handle pricelist system update self._handle_pricelist_system_update(vals) return res def action_open_change_date_wizard(self): self.ensure_one() return { 'type': 'ir.actions.act_window', 'res_model': 'change.date.planned.wizard', 'view_mode': 'form', 'target': 'new', 'context': { 'default_purchase_id': self.id, 'default_new_date_planned': self.date_planned, } } def _handle_pricelist_system_update(self, vals): if 'order_line' in vals or any(key in vals for key in ['state', 'approval_status']): for order in self: # Hanya proses jika PO sudah approved if order.state in ['purchase', 'done'] and order.approval_status == 'approved': self._process_pricelist_update(order) def _process_pricelist_update(self, order): for line in order.order_line: pricelist = self._get_related_pricelist(line.product_id, order.partner_id) if pricelist: # Simpan nilai lama old_values = self._get_pricelist_old_values(pricelist) # Update dan cek perubahan self._update_and_log_pricelist(pricelist, line, old_values) def _get_related_pricelist(self, product_id, vendor_id): return self.env['purchase.pricelist'].search([ ('product_id', '=', product_id.id), ('vendor_id', '=', vendor_id.id) ], limit=1) def _get_pricelist_old_values(self, pricelist): return { 'system_price': pricelist.system_price, 'taxes_system_id': pricelist.taxes_system_id, 'system_last_update': pricelist.system_last_update } def _update_and_log_pricelist(self, pricelist, po_line, old_values): changes = [] current_time = fields.Datetime.now() # Cek perubahan System Price if pricelist.system_price != po_line.price_unit: if old_values['system_price'] != po_line.price_unit: changes.append(f"
  • System Price: {old_values['system_price']:,.2f} → {po_line.price_unit:,.2f}
  • ") # Cek perubahan System Tax if pricelist.taxes_system_id != po_line.taxes_id: old_tax = old_values['taxes_system_id'] old_tax_name = old_tax.name if old_tax else 'No Tax' new_tax_name = po_line.taxes_id.name if po_line.taxes_id else 'No Tax' if old_tax != po_line.taxes_id: changes.append(f"
  • System Tax: {old_tax_name} → {new_tax_name}
  • ") # Update fields jika ada perubahan if changes: pricelist.with_context(update_by='system').write({ 'system_price': po_line.price_unit, 'taxes_system_id': po_line.taxes_id.id if po_line.taxes_id else False, 'system_last_update': current_time }) # Buat lognote self._create_pricelist_lognote(pricelist, po_line, changes, current_time) def _create_pricelist_lognote(self, pricelist, po_line, changes, timestamp): message = f""" System Fields Updated from PO
    PO: {po_line.order_id.name}
    Changes: Updated By: {self.env.user.name} """ pricelist.message_post(body=message, subtype_id=self.env.ref("mail.mt_note").id) class PurchaseOrderUnlockWizard(models.TransientModel): _name = 'purchase.order.unlock.wizard' _description = 'Wizard untuk memberikan alasan unlock PO' purchase_order_id = fields.Many2one('purchase.order', string='Purchase Order', required=True) alasan = fields.Text(string='Alasan', required=True) def confirm_reject(self): order = self.purchase_order_id if order: order.write({'reason_unlock': self.alasan}) order.approval_status_unlock = 'pengajuanFinance' return {'type': 'ir.actions.act_window_close'} class ChangeDatePlannedWizard(models.TransientModel): _name = 'change.date.planned.wizard' _description = 'Change Date Planned Wizard' purchase_id = fields.Many2one('purchase.order', string="Purchase Order", required=True) new_date_planned = fields.Datetime(string="New Date Planned") # <- harus DTTM biar match old_date_planned = fields.Datetime(string="Current Planned Date", related='purchase_id.date_planned', readonly=True) reason = fields.Selection([ ('delay', 'Delay By Vendor'), ('urgent', 'Urgent Delivery'), ], string='Reason') date_changed = fields.Boolean(string="Date Changed", compute="_compute_date_changed") @api.depends('old_date_planned', 'new_date_planned') def _compute_date_changed(self): for rec in self: rec.date_changed = ( rec.old_date_planned and rec.new_date_planned and rec.old_date_planned != rec.new_date_planned ) def confirm_change(self): self.purchase_id.write({ 'date_planned': self.new_date_planned, 'reason_change_date_planned': self.reason, })