Odoo18-Base/addons/l10n_id_efaktur/models/efaktur_document.py
2025-01-06 10:57:38 +07:00

300 lines
14 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError, RedirectWarning
from odoo.tools import float_repr, float_round
FK_HEAD_LIST = ['FK', 'KD_JENIS_TRANSAKSI', 'FG_PENGGANTI', 'NOMOR_FAKTUR', 'MASA_PAJAK', 'TAHUN_PAJAK', 'TANGGAL_FAKTUR', 'NPWP', 'NAMA', 'ALAMAT_LENGKAP', 'JUMLAH_DPP', 'JUMLAH_PPN', 'JUMLAH_PPNBM', 'ID_KETERANGAN_TAMBAHAN', 'FG_UANG_MUKA', 'UANG_MUKA_DPP', 'UANG_MUKA_PPN', 'UANG_MUKA_PPNBM', 'REFERENSI', 'KODE_DOKUMEN_PENDUKUNG']
LT_HEAD_LIST = ['LT', 'NPWP', 'NAMA', 'JALAN', 'BLOK', 'NOMOR', 'RT', 'RW', 'KECAMATAN', 'KELURAHAN', 'KABUPATEN', 'PROPINSI', 'KODE_POS', 'NOMOR_TELEPON']
OF_HEAD_LIST = ['OF', 'KODE_OBJEK', 'NAMA', 'HARGA_SATUAN', 'JUMLAH_BARANG', 'HARGA_TOTAL', 'DISKON', 'DPP', 'PPN', 'TARIF_PPNBM', 'PPNBM']
def _csv_row(data, delimiter=',', quote='"'):
return quote + (quote + delimiter + quote).join([str(x).replace(quote, '\\' + quote) for x in data]) + quote + '\n'
class EfakturDocument(models.Model):
_name = "l10n_id_efaktur.document"
_description = "E-faktur Document"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char(
compute='_compute_name',
store=True,
readonly=False,
required=True,
precompute=True,
)
company_id = fields.Many2one('res.company', required=True, readonly=True, default=lambda self: self.env.company)
active = fields.Boolean(
string="Active",
default=True,
)
invoice_ids = fields.One2many(
comodel_name="account.move",
inverse_name="l10n_id_efaktur_document",
domain="[('move_type', 'in', ['out_invoice', 'out_refund']), ('company_id', '=', company_id), ('l10n_id_efaktur_document', '=', False), ('l10n_id_tax_number', '!=', False), ('state', '=', 'posted')]",
tracking=True,
)
attachment_id = fields.Many2one(comodel_name="ir.attachment", readonly=True)
def action_download(self):
""" Download the e-faktur related attachment """
for document in self:
if not document.attachment_id:
document._generate_csv()
return {
'type': 'ir.actions.act_url',
'url': f'/l10n_id_efaktur/download_attachments/{",".join(map(str, self.attachment_id.ids))}',
}
def action_regenerate(self):
""" Regenerate the e-faktur csv file, based on the invoice in the document.
All new file generation will log a copy of the attachment to keep track of past generations.
"""
self._generate_csv()
def _generate_csv(self, delimiter=','):
self.ensure_one()
if self.invoice_ids.filtered(lambda x: not x.l10n_id_kode_transaksi):
raise UserError(_("Some documents don't have a transaction code"))
if self.invoice_ids.filtered(lambda x: x.move_type != 'out_invoice'):
raise UserError(_("Some documents are not Customer Invoices"))
output_head = self._generate_efaktur_invoice(delimiter)
raw_data = output_head.encode("utf-8")
# create/update attachment and link it to efaktur document if new
if not self.attachment_id:
attachment = self.env['ir.attachment'].create({
'raw': raw_data,
'name': 'efaktur_%s.csv' % (fields.Datetime.to_string(fields.Datetime.now()).replace(" ", "_")),
'type': 'binary',
'res_model': 'l10n_id_efaktur.document',
'res_id': self.id,
})
self.attachment_id = attachment.id
else:
attachment = self.attachment_id
self.attachment_id.write({
'raw': raw_data,
'name': 'efaktur_%s.csv' % (fields.Datetime.to_string(fields.Datetime.now()).replace(" ", "_")),
})
self.message_post(
body=_("The e-Faktur report has been generated"),
attachments=[(attachment.name, attachment.raw)]
)
def _generate_efaktur_invoice(self, delimiter=','):
"""Generate E-Faktur for customer invoice."""
# Invoice of Customer
output_head = '%s%s%s' % (
_csv_row(FK_HEAD_LIST, delimiter),
_csv_row(LT_HEAD_LIST, delimiter),
_csv_row(OF_HEAD_LIST, delimiter),
)
idr = self.env.ref('base.IDR')
for move in self.invoice_ids.filtered(lambda m: m.state == 'posted'):
eTax = move._prepare_etax()
commercial_partner = move.partner_id.commercial_partner_id
nik = str(commercial_partner.l10n_id_nik) if not commercial_partner.vat else ''
if move.l10n_id_replace_invoice_id:
number_ref = str(move.l10n_id_replace_invoice_id.name) + " replaced by " + str(move.name) + " " + nik
elif nik:
number_ref = str(move.name) + " " + nik
else:
number_ref = str(move.name)
invoice_npwp = ''
if commercial_partner.vat and len(commercial_partner.vat) >= 15:
invoice_npwp = commercial_partner.vat
elif commercial_partner.l10n_id_nik:
invoice_npwp = commercial_partner.l10n_id_nik
if not invoice_npwp:
action_error = {
'view_mode': 'form',
'res_model': 'res.partner',
'type': 'ir.actions.act_window',
'res_id': commercial_partner.id,
'views': [[self.env.ref('base.view_partner_form').id, 'form']],
}
msg = _("Please make sure that you've input the appropriate NPWP or NIK for the following customer")
raise RedirectWarning(msg, action_error, _("Edit Customer Information"))
invoice_npwp = invoice_npwp.replace('.', '').replace('-', '')
etax_name = commercial_partner.name
if invoice_npwp[:15] == '000000000000000' and commercial_partner.l10n_id_nik:
etax_name = "%s#NIK#NAMA#%s" % (commercial_partner.l10n_id_nik, etax_name)
# Here all fields or columns based on eTax Invoice Third Party
eTax['KD_JENIS_TRANSAKSI'] = move.l10n_id_tax_number[0:2] or 0
eTax['FG_PENGGANTI'] = move.l10n_id_tax_number[2:3] or 0
eTax['NOMOR_FAKTUR'] = move.l10n_id_tax_number[3:] or 0
eTax['MASA_PAJAK'] = move.invoice_date.month
eTax['TAHUN_PAJAK'] = move.invoice_date.year
eTax['TANGGAL_FAKTUR'] = move.invoice_date.strftime("%-d/%-m/%Y")
eTax['NPWP'] = invoice_npwp
eTax['NAMA'] = etax_name
eTax['ALAMAT_LENGKAP'] = move.partner_id._display_address(without_company=True).replace('\n', ' ').replace(' ', ' ').strip()
eTax['JUMLAH_DPP'] = int(float_round(move.amount_untaxed, 0)) # currency rounded to the unit
eTax['JUMLAH_PPN'] = int(float_round(move.amount_tax, 0, rounding_method="DOWN")) # tax amount ALWAYS rounded down
eTax['ID_KETERANGAN_TAMBAHAN'] = '1' if move.l10n_id_kode_transaksi == '07' else ''
eTax['REFERENSI'] = number_ref
eTax['KODE_DOKUMEN_PENDUKUNG'] = '0'
lines = move.line_ids.filtered(lambda x: x.move_id._is_downpayment() and x.price_unit < 0 and x.display_type == 'product')
eTax['FG_UANG_MUKA'] = 0
eTax['UANG_MUKA_DPP'] = float_repr(abs(sum(lines.mapped(lambda l: float_round(l.price_subtotal, 0)))), 0)
eTax['UANG_MUKA_PPN'] = float_repr(abs(sum(lines.mapped(lambda l: float_round(l.price_total - l.price_subtotal, 0)))), 0)
fk_values_list = ['FK'] + [eTax[f] for f in FK_HEAD_LIST[1:]]
# HOW TO ADD 2 line to 1 line for free product
free, sales = [], []
for line in move.line_ids.filtered(lambda l: l.display_type == 'product'):
# *invoice_line_unit_price is price unit use for harga_satuan's column
# *invoice_line_quantity is quantity use for jumlah_barang's column
# *invoice_line_total_price is bruto price use for harga_total's column
# *invoice_line_discount_m2m is discount price use for diskon's column
# *line.price_subtotal is subtotal price use for dpp's column
# *tax_line or free_tax_line is tax price use for ppn's column
free_tax_line = tax_line = 0.0
for tax in line.tax_ids:
if tax.amount > 0:
tax_line += line.price_subtotal * (tax.amount / 100.0)
discount = 1 - (line.discount / 100)
# guarantees price to be tax-excluded
invoice_line_total_price = line.price_subtotal / discount if discount else 0
invoice_line_unit_price = invoice_line_total_price / line.quantity if line.quantity else 0
line_dict = {
'KODE_OBJEK': line.product_id.default_code or '',
'NAMA': line.product_id.name or '',
'HARGA_SATUAN': float_repr(idr.round(invoice_line_unit_price), idr.decimal_places),
'JUMLAH_BARANG': line.quantity,
'HARGA_TOTAL': idr.round(invoice_line_total_price),
'DPP': line.price_subtotal,
'product_id': line.product_id.id,
}
if line.price_subtotal < 0:
for tax in line.tax_ids:
free_tax_line += (line.price_subtotal * (tax.amount / 100.0)) * -1.0
line_dict.update({
'DISKON': float_round(invoice_line_total_price - line.price_subtotal, 0),
'PPN': free_tax_line,
})
free.append(line_dict)
elif line.price_subtotal != 0.0:
invoice_line_discount_m2m = invoice_line_total_price - line.price_subtotal
line_dict.update({
'DISKON': float_round(invoice_line_discount_m2m, 0),
'PPN': tax_line,
})
sales.append(line_dict)
sub_total_before_adjustment = sub_total_ppn_before_adjustment = 0.0
# We are finding the product that has affected
# by free product to adjustment the calculation
# of discount and subtotal.
# - the price total of free product will be
# included as a discount to related of product.
for sale in sales:
for f in free:
if f['product_id'] == sale['product_id']:
sale['DISKON'] = sale['DISKON'] - f['DISKON'] + f['PPN']
sale['DPP'] = sale['DPP'] + f['DPP']
tax_line = 0
for tax in line.tax_ids:
if tax.amount > 0:
tax_line += sale['DPP'] * (tax.amount / 100.0)
sale['PPN'] = tax_line
free.remove(f)
sub_total_before_adjustment += sale['DPP']
sub_total_ppn_before_adjustment += sale['PPN']
sale.update({
# Use the db currency rounding to float_round the DPP/PPN.
# As we will correct them we need them to be close to the final result.
'DPP': idr.round(sale['DPP']),
'PPN': idr.round(sale['PPN']),
'DISKON': float_repr(sale['DISKON'], 0),
})
# The total of the base (DPP) and taxes (PPN) must be a integer, equal to the JUMLAH_DPP and JUMLAH_PPN
# To do so, we adjust the first line in order to achieve the correct total
if sales:
diff_dpp = idr.round(eTax['JUMLAH_DPP'] - sum(sale['DPP'] for sale in sales))
total_sales_ppn = idr.round(eTax['JUMLAH_PPN'] - sum(sale['PPN'] for sale in sales))
# We will add the differences to the first line for which adding the difference will not result in a negative value.
for sale in sales:
if sale['DPP'] + diff_dpp >= 0 and sale['PPN'] + total_sales_ppn >= 0:
sale['HARGA_TOTAL'] += diff_dpp
sale['DPP'] += diff_dpp
diff_dpp = 0
sale['PPN'] += total_sales_ppn
total_sales_ppn = 0
break
# We couldn't adjust everything in a single line as their values is too low.
# So we will instead slit the adjustment in multiple lines.
if diff_dpp or total_sales_ppn:
for sale in sales:
# DPP
sale_dpp = sale['DPP']
sale["DPP"] = max(0, sale["DPP"] + diff_dpp)
diff_dpp -= (sale["DPP"] - sale_dpp)
sale['HARGA_TOTAL'] = sale["DPP"]
# PPN
sale_ppn = sale['PPN']
sale["PPN"] = max(0, sale["PPN"] + total_sales_ppn)
total_sales_ppn -= (sale["PPN"] - sale_ppn)
# Values now being corrected, we can format them for the CSV
for sale in sales:
sale.update({
'HARGA_TOTAL': float_repr(sale['HARGA_TOTAL'], idr.decimal_places),
'DPP': float_repr(sale['DPP'], idr.decimal_places),
'PPN': float_repr(sale['PPN'], idr.decimal_places),
})
output_head += _csv_row(fk_values_list, delimiter)
for sale in sales:
of_values_list = ['OF'] + [str(sale[f]) for f in OF_HEAD_LIST[1:-2]] + ['0', '0']
output_head += _csv_row(of_values_list, delimiter)
return output_head
@api.depends('invoice_ids')
def _compute_name(self):
""" First compute will be done at creation, from a selection of invoice(s).
We still want to allow to rename the document to another name if it makes sense.
"""
for doc in self:
sorted_invoices = doc.invoice_ids.sorted('name')
name = []
if sorted_invoices:
name.append(sorted_invoices[0].name)
if len(sorted_invoices) > 1:
name.append(sorted_invoices[-1].name)
doc.name = "%s - Efaktur (%s)" % (fields.Date.context_today(doc).strftime("%Y%m%d"), "....".join(name))