# -*- 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', }