1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_sg_reports/models/iras_audit_file.py
2024-12-10 09:04:09 +07:00

404 lines
18 KiB
Python

# -*- coding: utf-8 -*-
import json
from lxml import etree
from lxml.objectify import fromstring
from odoo import fields, models, release, tools, _
from odoo.exceptions import UserError
from odoo.tools import date_utils
from odoo.tools.float_utils import float_repr
from odoo.tools.xml_utils import _check_with_xsd
IRAS_DIGITS = 2
IRAS_VERSION = 'IAFv2.0.0'
IRAS_XML_TEMPLATE = 'l10n_sg_reports.iras_audit_file_xml'
IRAS_XSD = 'l10n_sg_reports/data/iras_audit_file.xsd'
class IrasAuditFileWizard(models.TransientModel):
_name = 'l10n.sg.reports.iaf.wizard'
_description = "Singaporean IAF Report Wizard"
date_from = fields.Date(string='Start Date', required=True)
date_to = fields.Date(string='End Date', required=True)
export_type = fields.Selection([
('xml', 'XML'),
('txt', 'TXT'),
], string='Export Type', required=True, default='xml')
def generate_iras(self):
general_ledger_report = self.env.ref('account_reports.general_ledger_report')
options = general_ledger_report.get_options(previous_options={'date': {
'date_from': self.date_from,
'date_to': self.date_to
}
})
if self.export_type == 'xml':
return general_ledger_report.l10n_sg_export_iras_audit_file_xml(options)
return general_ledger_report.l10n_sg_export_iras_audit_file_txt(options)
class IrasAuditFile(models.Model):
_inherit = 'account.report'
def _l10n_sg_get_company_infos(self, date_from, date_to):
"""
Generate the informations about the company for the IRAS Audit File
"""
if not self.env.company.l10n_sg_unique_entity_number:
raise UserError(_('Your company must have a UEN.'))
if not self.env.company.vat:
raise UserError(_('Your company must have a GSTNo.'))
return {
'CompanyName': self.env.company.name,
'CompanyUEN': self.env.company.l10n_sg_unique_entity_number,
'GSTNo': self.env.company.vat,
'PeriodStart': date_from,
'PeriodEnd': date_to,
'IAFCreationDate': fields.Date.to_string(fields.Date.today()),
'ProductVersion': release.product_name + release.version,
'IAFVersion': IRAS_VERSION
}
def _l10n_sg_get_purchases_infos(self, date_from, date_to):
"""
Generate purchases informations for the IRAS Audit File
"""
purchases_lines = []
purchase_total_sgd = 0.0
gst_total_sgd = 0.0
transaction_count_total = 0
invoice_ids = self.env['account.move'].search([
('company_id', '=', self.env.company.id),
('move_type', 'in', ['in_invoice', 'in_refund']),
('state', '=', 'posted'),
('date', '>=', date_from),
('date', '<=', date_to)
])
for invoice in invoice_ids:
lines_number = 0
for lines in invoice.invoice_line_ids:
lines_number += 1
sign = -1 if invoice.move_type == 'in_refund' else 1
tax_amount = lines.price_total - lines.price_subtotal
tax_amount_company = invoice.currency_id._convert(tax_amount, invoice.company_id.currency_id, invoice.company_id, invoice.invoice_date or invoice.date)
transaction_count_total += 1
purchase_total_sgd += lines.balance
gst_total_sgd += tax_amount
if not invoice.partner_id.l10n_sg_unique_entity_number:
raise UserError(_('Your partner (%s) must have a UEN.', invoice.partner_id.name))
purchases_lines.append({
'SupplierName': (invoice.partner_id.name or '')[:100],
'SupplierUEN': (invoice.partner_id.l10n_sg_unique_entity_number or '')[:16],
'InvoiceDate': fields.Date.to_string(invoice.l10n_sg_permit_number_date if invoice.l10n_sg_permit_number and invoice.l10n_sg_permit_number_date else invoice.invoice_date),
'InvoiceNo': (invoice.name or '')[:50],
'PermitNo': invoice.l10n_sg_permit_number[:20] if invoice.l10n_sg_permit_number else False,
'LineNo': str(lines_number),
'ProductDescription': ('[' + lines.product_id.default_code + '] ' + lines.product_id.name if lines.product_id.default_code else lines.product_id.name or '')[:250],
'PurchaseValueSGD': float_repr(lines.balance, IRAS_DIGITS),
'GSTValueSGD': float_repr((lines.price_total - lines.price_subtotal) / (lines.quantity or 1), IRAS_DIGITS),
'TaxCode': (lines.tax_ids and lines.tax_ids[0].name or ' ')[:20],
'FCYCode': (invoice.currency_id.name if invoice.currency_id and invoice.currency_id.name != 'SGD' else 'XXX')[:3],
'PurchaseFCY': float_repr(lines.price_subtotal, IRAS_DIGITS) if invoice.currency_id.name != 'SGD' else '0',
'GSTFCY': float_repr(sign * tax_amount_company, IRAS_DIGITS) if invoice.currency_id.name != 'SGD' else '0'
})
return {
'lines': purchases_lines,
'PurchaseTotalSGD': float_repr(purchase_total_sgd, IRAS_DIGITS),
'GSTTotalSGD': float_repr(gst_total_sgd, IRAS_DIGITS),
'TransactionCountTotal': str(transaction_count_total)
}
def _l10n_sg_get_sales_infos(self, date_from, date_to):
"""
Generate sales informations for the IRAS Audit File
"""
supply_lines = []
supply_total_sgd = 0.0
gst_total_sgd = 0.0
transaction_count_total = 0
invoice_ids = self.env['account.move'].search([
('company_id', '=', self.env.company.id),
('move_type', 'in', ['out_invoice', 'out_refund']),
('state', '=', 'posted'),
('date', '>=', date_from),
('date', '<=', date_to)
])
for invoice in invoice_ids:
lines_number = 0
for lines in invoice.invoice_line_ids:
lines_number += 1
sign = -1 if invoice.move_type == 'out_refund' else 1
tax_amount = lines.price_total - lines.price_subtotal
tax_amount_company = invoice.currency_id._convert(tax_amount, invoice.company_id.currency_id, invoice.company_id, invoice.invoice_date or invoice.date)
transaction_count_total += 1
supply_total_sgd -= lines.balance
gst_total_sgd += tax_amount
if not invoice.partner_id.l10n_sg_unique_entity_number:
raise UserError(_('Your partner (%s) must have a UEN.', invoice.partner_id.name))
supply_lines.append({
'CustomerName': (invoice.partner_id.name or '')[:100],
'CustomerUEN': (invoice.partner_id.l10n_sg_unique_entity_number or '')[:16],
'InvoiceDate': fields.Date.to_string(invoice.invoice_date),
'InvoiceNo': (invoice.name or '')[:50],
'LineNo': str(lines_number),
'ProductDescription': ('[' + lines.product_id.default_code + '] ' + lines.product_id.name if lines.product_id.default_code else lines.product_id.name or '')[:250],
'SupplyValueSGD': float_repr(-lines.balance, IRAS_DIGITS),
'GSTValueSGD': float_repr((lines.price_total - lines.price_subtotal) / (lines.quantity or 1), IRAS_DIGITS),
'TaxCode': (lines.tax_ids and lines.tax_ids[0].name or ' ')[:20],
'Country': invoice.partner_id.commercial_partner_id.country_id.code if invoice.invoice_origin and invoice.partner_id.commercial_partner_id.country_id.code != 'SG' else False,
'FCYCode': (invoice.currency_id.name if lines.currency_id and lines.currency_id.name != 'SGD' else 'XXX')[:3],
'SupplyFCY': float_repr(lines.price_subtotal, IRAS_DIGITS) if invoice.currency_id.name != 'SGD' else '0',
'GSTFCY': float_repr(sign * tax_amount_company, IRAS_DIGITS) if invoice.currency_id.name != 'SGD' else '0'
})
return {
'lines': supply_lines,
'SupplyTotalSGD': float_repr(supply_total_sgd, IRAS_DIGITS),
'GSTTotalSGD': float_repr(gst_total_sgd, IRAS_DIGITS),
'TransactionCountTotal': str(transaction_count_total)
}
def _l10n_sg_get_gldata(self, date_from, date_to):
"""
Generate gldata for IRAS Audit File
"""
gldata_lines = []
total_debit = 0.0
total_credit = 0.0
transaction_count_total = 0
glt_currency = 'SGD'
company = self.env.company
move_line_ids = self.env['account.move.line'].search([
('company_id', '=', company.id),
('date', '>=', date_from),
('date', '<=', date_to)
])
options = self.get_options(previous_options={
'unfold_all': True,
'unfolded_lines': [],
'date': {
'mode': 'range',
'date_from': fields.Date.from_string(date_from),
'date_to': fields.Date.from_string(date_from)
}
})
general_ledger_report = self.env.ref('account_reports.general_ledger_report')
handler = self.env['account.general.ledger.report.handler']
accounts_results = handler._query_values(general_ledger_report, options)
all_accounts = self.env['account.account'].search([('company_id', '=', company.id)])
for account in all_accounts:
initial_bal = dict(accounts_results).get(account.id, {'initial_balance': {'balance': 0, 'amount_currency': 0, 'debit': 0, 'credit': 0}})['initial_balance']
gldata_lines.append({
'TransactionDate': date_from,
'AccountID': account.code,
'AccountName': account.name,
'TransactionDescription': 'OPENING BALANCE',
'Name': False,
'TransactionID': False,
'SourceDocumentID': False,
'SourceType': False,
'Debit': float_repr(initial_bal['debit'], IRAS_DIGITS),
'Credit': float_repr(initial_bal['credit'], IRAS_DIGITS),
'Balance': float_repr(initial_bal['balance'], IRAS_DIGITS)
})
balance = initial_bal['balance']
for move_line_id in move_line_ids:
if move_line_id.account_id.code == account.code:
balance = company.currency_id.round(balance + move_line_id.debit - move_line_id.credit)
total_credit += move_line_id.credit
total_debit += move_line_id.debit
transaction_count_total += 1
account_type_dict = dict(self.env['account.account']._fields['account_type']._description_selection(self.env))
# for exchange gain/loss journal items, source document should be the invoice/bill it is generated from
source_doc = move_line_id.move_id.invoice_origin
description = move_line_id.name
if move_line_id.move_id.journal_id == move_line_id.company_id.currency_exchange_journal_id:
# find all reconciled lines from exchange move, then find lines where the move_id is invoice/bill. Use payment if not found.
reconciled_lines = move_line_id.move_id.line_ids._all_reconciled_lines()
moves = reconciled_lines.move_id
invoice_move = moves.filtered(lambda x: x.is_invoice(include_receipts=True))[:1]
if invoice_move:
source_doc = invoice_move.name
else:
payment_move = moves.filtered(lambda x: x.payment_id)[:1]
source_doc = payment_move.name if payment_move else source_doc
description = move_line_id.move_id.ref or description
gldata_lines.append({
'TransactionDate': fields.Date.to_string(move_line_id.date),
'AccountID': move_line_id.account_id.code,
'AccountName': move_line_id.account_id.name,
'TransactionDescription': description,
'Name': move_line_id.partner_id.name if move_line_id.partner_id else False,
'TransactionID': move_line_id.move_id.name,
'SourceDocumentID': source_doc,
'SourceType': account_type_dict[move_line_id.account_id.account_type][:20],
'Debit': float_repr(move_line_id.debit, IRAS_DIGITS),
'Credit': float_repr(move_line_id.credit, IRAS_DIGITS),
'Balance': float_repr(balance, IRAS_DIGITS)
})
return {
'lines': gldata_lines,
'TotalDebit': float_repr(total_debit, IRAS_DIGITS),
'TotalCredit': float_repr(total_credit, IRAS_DIGITS),
'TransactionCountTotal': str(transaction_count_total),
'GLTCurrency': glt_currency
}
def _l10n_sg_get_generic_data(self, date_from, date_to):
return {
'Company': self._l10n_sg_get_company_infos(date_from, date_to),
'Purchases': self._l10n_sg_get_purchases_infos(date_from, date_to),
'Sales': self._l10n_sg_get_sales_infos(date_from, date_to),
'GlData': self._l10n_sg_get_gldata(date_from, date_to)
}
def _l10n_sg_get_xml(self, options):
"""
Generate the IRAS Audit File in xml format
"""
qweb = self.env['ir.qweb']
values = self._l10n_sg_get_generic_data(options['date']['date_from'], options['date']['date_to'])
doc = qweb._render(IRAS_XML_TEMPLATE, values=values)
with tools.file_open(IRAS_XSD, 'rb') as xsd:
_check_with_xsd(doc, xsd)
tree = fromstring(doc)
return etree.tostring(tree, pretty_print=True, xml_declaration=True, encoding='UTF-8')
def _l10n_sg_txt_create_line(self, values):
node = ''
for value in values:
node += (value if value else '') + '|'
node += '\n'
return node
def _l10n_sg_txt_company_infos(self, values):
node = 'CompInfoStart|\n'
node += self._l10n_sg_txt_create_line(values.keys())
node += self._l10n_sg_txt_create_line(values.values())
node += 'CompInfoEnd|\n\n'
return node
def _l10n_sg_txt_purchases_infos(self, values):
node = 'PurcDataStart|\n'
node += self._l10n_sg_txt_create_line([
'SupplierName',
'SupplierUEN',
'InvoiceDate',
'InvoiceNo',
'PermitNo',
'LineNo',
'ProductDescription',
'PurchaseValueSGD',
'GSTValueSGD',
'TaxCode',
'FCYCode',
'PurchaseFCY',
'GSTFCY'
])
for line in values['lines']:
node += self._l10n_sg_txt_create_line(line.values())
node += 'PurcDataEnd|' + values['PurchaseTotalSGD'] + '|' + values['GSTTotalSGD'] + '|' + values['TransactionCountTotal'] + '|\n\n'
return node
def _l10n_sg_txt_sales_infos(self, values):
node = 'SuppDataStart|\n'
node += self._l10n_sg_txt_create_line([
'CustomerName',
'CustomerUEN',
'InvoiceDate',
'InvoiceNo',
'LineNo',
'ProductDescription',
'SupplyValueSGD',
'GSTValueSGD',
'TaxCode',
'Country',
'FCYCode',
'SupplyFCY',
'GSTFCY'
])
for line in values['lines']:
node += self._l10n_sg_txt_create_line(line.values())
node += 'SuppDataEnd|' + values['SupplyTotalSGD'] + '|' + values['GSTTotalSGD'] + '|' + values['TransactionCountTotal'] + '|\n\n'
return node
def _l10n_sg_txt_gldata_infos(self, values):
node = 'GLDataStart|\n'
node += self._l10n_sg_txt_create_line([
'TransactionDate',
'AccountID',
'AccountName',
'TransactionDescription',
'Name',
'TransactionID',
'SourceDocumentID',
'SourceType',
'Debit',
'Credit',
'Balance'
])
for line in values['lines']:
node += self._l10n_sg_txt_create_line(line.values())
node += 'GLDataEnd|' + values['TotalDebit'] + '|' + values['TotalCredit'] + '|' + values['TransactionCountTotal'] + '|' + values['GLTCurrency'] + '|\n\n'
return node
def _l10n_sg_get_txt(self, options):
"""
Generate the IRAS Audit File in txt format
"""
values = self._l10n_sg_get_generic_data(options['date']['date_from'], options['date']['date_to'])
txt = self._l10n_sg_txt_company_infos(values['Company'])
txt += self._l10n_sg_txt_purchases_infos(values['Purchases'])
txt += self._l10n_sg_txt_sales_infos(values['Sales'])
txt += self._l10n_sg_txt_gldata_infos(values['GlData'])
return txt
def l10n_sg_export_iras_audit_file_xml(self, options):
"""
Print the IAF in xml format
"""
return {
'type': 'ir_actions_account_report_download',
'data': {
'options': json.dumps(options, default=date_utils.json_default),
'file_generator': 'l10n_sg_print_iras_audit_file_xml',
}
}
def l10n_sg_print_iras_audit_file_xml(self, options):
return {
'file_name': 'iras_audit_file.xml',
'file_content': self._l10n_sg_get_xml(options),
'file_type': 'xml',
}
def l10n_sg_export_iras_audit_file_txt(self, options):
"""
Print the IAF in txt format
"""
return {
'type': 'ir_actions_account_report_download',
'data': {
'options': json.dumps(options, default=date_utils.json_default),
'file_generator': 'l10n_sg_print_iras_audit_file_txt',
}
}
def l10n_sg_print_iras_audit_file_txt(self, options):
return {
'file_name': 'iras_audit_file.txt',
'file_content': self._l10n_sg_get_txt(options),
'file_type': 'txt',
}