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
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_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
)
@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)
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:
{"".join(changes)}
- System Update: {current_time}
"""
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:
low_margin_lines = self.order_sales_match_line.filtered(
lambda match: match.so_header_margin <= 15.0
)
price_change_detected = any(line.price_unit_before for line in self.order_line)
if low_margin_lines and price_change_detected:
# raise UserError("Matches SO terdapat item dengan header margin SO <= 15%. Approval Pimpinan diperlukan.")
raise UserError("Approval Pimpinan diperlukan jika terdapat perubahan Unit Price pada PO Line yang Matches SO item memiliki header margin SO <= 15%")
# else:
# is_po_manual = '/A/' not in self.name and '/MO/' not in self.name
# if is_po_manual:
# if not self.order_sales_match_line:
# raise UserError("Tidak ada matches SO, Approval Pimpinan diperlukan.")
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 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()
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
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
else:
self.total_margin = 0
self.total_percent_margin = 0
self.total_so_margin = 0
self.total_so_percent_margin = 0
def compute_total_margin_from_apo(self):
sum_so_margin = sum_sales_price = sum_margin = 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
real_item_margin = sales_price - purchase_price
sum_margin += real_item_margin
# 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
else:
self.total_margin = self.total_percent_margin = 0
self.total_so_margin = self.total_so_percent_margin = 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:
{"".join(changes)}
- System Update: {timestamp}
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,
})