# -*- encoding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import re from odoo import api, fields, models, _ from odoo.exceptions import UserError, ValidationError, RedirectWarning from odoo.tools import float_round, float_repr class AccountMove(models.Model): _inherit = "account.move" l10n_id_tax_number = fields.Char(string="Tax Number", copy=False) l10n_id_replace_invoice_id = fields.Many2one('account.move', string="Replace Invoice", domain="['|', '&', '&', ('state', '=', 'posted'), ('partner_id', '=', partner_id), ('reversal_move_ids', '!=', False), ('state', '=', 'cancel')]", copy=False, index='btree_not_null') l10n_id_efaktur_document = fields.Many2one('l10n_id_efaktur.document', readonly=True, copy=False, string="e-Faktur Document") l10n_id_kode_transaksi = fields.Selection([ ('01', '01 To the Parties that is not VAT Collector (Regular Customers)'), ('02', '02 To the Treasurer'), ('03', '03 To other VAT Collectors other than the Treasurer'), ('04', '04 Other Value of VAT Imposition Base'), ('05', '05 Specified Amount (Article 9A Paragraph (1) VAT Law)'), ('06', '06 Other Deliveries'), ('07', '07 Deliveries that the VAT is not Collected'), ('08', '08 Deliveries that the VAT is Exempted'), ('09', '09 Deliveries of Assets (Article 16D of VAT Law)'), ], string='Tax Transaction Code', help='The first 2 digits of tax number', readonly=False, copy=False, compute="_compute_kode_transaksi", store=True) l10n_id_efaktur_range = fields.Many2one("l10n_id_efaktur.efaktur.range", string="E-faktur Range", copy=False, domain="[('company_id', '=', company_id), ('available', '>', 0)]") l10n_id_need_kode_transaksi = fields.Boolean(compute='_compute_need_kode_transaksi') l10n_id_available_range_count = fields.Integer(compute="_compute_available_range_count", compute_sudo=True) l10n_id_show_kode_transaksi = fields.Boolean(compute='_compute_show_kode_transaksi') @api.depends('company_id') def _compute_available_range_count(self): # Only invoices under Indonesian company needs computation for l10n_id_available_range_count id_moves = self.filtered(lambda x: x.country_code == 'ID') other_moves = self - id_moves if other_moves: other_moves.l10n_id_available_range_count = 0 if id_moves: range_count_per_company = dict( self.env['l10n_id_efaktur.efaktur.range']._read_group( [('available', '>', 0), ('company_id', 'in', id_moves.company_id.ids)], ['company_id'], ['__count'] ) ) for company_id, moves in id_moves.grouped('company_id').items(): moves.l10n_id_available_range_count = range_count_per_company.get(company_id, 0) @api.onchange('l10n_id_tax_number') def _onchange_l10n_id_tax_number(self): for record in self: if record.l10n_id_tax_number and record.move_type not in self.get_purchase_types(): raise UserError(_("You can only change the number manually for a Vendor Bills and Credit Notes")) @api.depends('partner_id') def _compute_kode_transaksi(self): for move in self: move.l10n_id_kode_transaksi = move.partner_id.commercial_partner_id.l10n_id_kode_transaksi @api.depends('commercial_partner_id', 'invoice_line_ids.tax_ids') def _compute_need_kode_transaksi(self): for move in self: # If there are no taxes at all on every line (0% taxes counts as having a tax) then we don't need a kode transaksi move.l10n_id_need_kode_transaksi = ( move.commercial_partner_id.l10n_id_pkp and not move.l10n_id_tax_number and move.move_type == 'out_invoice' and move.country_code == 'ID' and move.invoice_line_ids.tax_ids ) @api.depends('commercial_partner_id') def _compute_show_kode_transaksi(self): for move in self: move.l10n_id_show_kode_transaksi = ( move.commercial_partner_id.l10n_id_pkp and move.move_type == 'out_invoice' and move.country_code == 'ID' ) @api.constrains('l10n_id_kode_transaksi', 'line_ids', 'partner_id') def _constraint_kode_ppn(self): ppn_tag = self.env.ref('l10n_id.ppn_tag') for move in self.filtered(lambda m: m.l10n_id_need_kode_transaksi and m.l10n_id_kode_transaksi != '08'): if any(ppn_tag.id in line.tax_tag_ids.ids for line in move.line_ids if line.display_type == 'product') \ and any(ppn_tag.id not in line.tax_tag_ids.ids for line in move.line_ids if line.display_type == 'product'): raise UserError(_('Cannot mix VAT subject and Non-VAT subject items in the same invoice with this kode transaksi.')) for move in self.filtered(lambda m: m.l10n_id_need_kode_transaksi and m.l10n_id_kode_transaksi == '08'): if any(ppn_tag.id in line.tax_tag_ids.ids for line in move.line_ids if line.display_type == 'product'): raise UserError('Kode transaksi 08 is only for non VAT subject items.') @api.constrains('l10n_id_tax_number') def _constrains_l10n_id_tax_number(self): for record in self.filtered('l10n_id_tax_number'): if record.l10n_id_tax_number != re.sub(r'\D', '', record.l10n_id_tax_number): record.l10n_id_tax_number = re.sub(r'\D', '', record.l10n_id_tax_number) if len(record.l10n_id_tax_number) != 16: raise UserError(_('A tax number should have 16 digits')) elif record.l10n_id_tax_number[:2] not in dict(self._fields['l10n_id_kode_transaksi'].selection).keys(): raise UserError(_('A tax number must begin by a valid Kode Transaksi')) elif record.l10n_id_tax_number[2] not in ('0', '1'): raise UserError(_('The third digit of a tax number must be 0 or 1')) def _post(self, soft=True): """Set E-Faktur number after validation.""" for move in self: if move.l10n_id_need_kode_transaksi: # If the code was set on the partner after the invoice was created, we set it on the move at this step so that it triggers the constrains if needed. if not move.l10n_id_kode_transaksi and move.commercial_partner_id.l10n_id_kode_transaksi: move.l10n_id_kode_transaksi = move.commercial_partner_id.l10n_id_kode_transaksi if not move.l10n_id_kode_transaksi: raise ValidationError(_('You need to put a Kode Transaksi for this partner.')) if move.l10n_id_replace_invoice_id.l10n_id_tax_number: if not move.l10n_id_replace_invoice_id.l10n_id_efaktur_document: raise ValidationError(_('Replacement invoice only for invoices on which the e-Faktur is generated. ')) rep_efaktur_str = move.l10n_id_replace_invoice_id.l10n_id_tax_number move.l10n_id_tax_number = '%s1%s' % (move.l10n_id_kode_transaksi, rep_efaktur_str[3:]) else: # Auto-select the smallest range available if not move.l10n_id_efaktur_range: move.l10n_id_efaktur_range = self.env['l10n_id_efaktur.efaktur.range'].search([('company_id', '=', move.company_id.id), ('available', '>', 0)], order="min ASC", limit=1) if not move.l10n_id_efaktur_range: raise ValidationError(_('There is no Efaktur range available. Please configure the range you get from the government in the e-Faktur Ranges menu. ')) efaktur_num = move.l10n_id_efaktur_range.pop_number() move.l10n_id_tax_number = '%s0%013d' % (str(move.l10n_id_kode_transaksi), efaktur_num) return super()._post(soft) def reset_efaktur(self): """Reset E-Faktur, so it can be use for other invoice.""" for move in self: if move.l10n_id_efaktur_document: raise UserError(_('You have already generated the tax report for this document: %s', move.name)) self.env['l10n_id_efaktur.efaktur.range'].push_number(move.company_id.id, move.l10n_id_tax_number[3:]) move.message_post( body='e-Faktur Reset: %s ' % (move.l10n_id_tax_number), subject="Reset Efaktur") move.l10n_id_tax_number = False return True def download_csv(self): return self.l10n_id_efaktur_document.action_download() def download_efaktur(self): """Collect the data and execute function _generate_efaktur.""" for record in self: if record.state == 'draft': raise ValidationError(_('Could not download E-faktur in draft state')) if not record.country_code == 'ID': raise ValidationError(_("E-faktur is only available on invoices under Indonesian companies")) if not record.partner_id.commercial_partner_id.l10n_id_pkp: raise ValidationError(_("E-faktur is only available for taxable customers")) if not record.move_type == 'out_invoice': raise ValidationError(_("E-faktur is only available for invoices")) if not record.line_ids.tax_ids: raise ValidationError(_('E-faktur is not available for invoices without any taxes.')) if not record.l10n_id_tax_number: raise ValidationError(_("Please reset %(move_number)s to draft and post it again to generate the eTax number", move_number=record.name)) # Should prevent users from generating e-Faktur document on invoices across multi-company. # Allowing it will cause issues on the invoice/eFaktur document record rule if len(self.company_id) > 1: raise UserError(_("You are not allowed to generate e-Faktur document from invoices coming from different companies")) # All invoices in self have no documents; we can create a new one for them. # Or all invoices in self have a document, but it's the same one. Special use case but we allow downloading it. if not self.l10n_id_efaktur_document: self.l10n_id_efaktur_document = self.env['l10n_id_efaktur.document'].create({ 'invoice_ids': self.ids, 'company_id': self.company_id.id, }) self.l10n_id_efaktur_document.action_regenerate() # If there is more than one document, or all invoices for a document were not selected, the resulting file could cause mistakes; # They could get a file with additional invoices for example. In this case, we redirect them to the document view to make it clearer. elif len(self.l10n_id_efaktur_document) > 1 or set(self.l10n_id_efaktur_document.invoice_ids.ids) != set(self.ids): action_error = { 'name': _('Document Mismatch'), 'view_mode': 'list', 'res_model': 'l10n_id_efaktur.document', 'type': 'ir.actions.act_window', 'views': [[False, 'list'], [False, 'form']], 'domain': [('id', 'in', self.l10n_id_efaktur_document.ids)], } msg = _("The selected invoices are partially part of one or more e-faktur documents.\n" "Please download them from the e-faktur documents directly.") raise RedirectWarning(msg, action_error, _("Display Related Documents")) return self.download_csv() def _prepare_etax(self): # These values are never set return {'JUMLAH_PPNBM': 0, 'UANG_MUKA_PPNBM': 0, 'JUMLAH_BARANG': 0, 'TARIF_PPNBM': 0, 'PPNBM': 0} def button_draft(self): # EXTENDS 'account' # When resetting to draft, we want the invoice to be removed from the document they are linked to. # That document will be regenerated when downloading later on with only the remaining invoices. invoices_with_document = self.filtered(lambda i: i.l10n_id_efaktur_document) if invoices_with_document: invoices_document = invoices_with_document.l10n_id_efaktur_document invoices_document.attachment_id.unlink() invoices_with_document.l10n_id_efaktur_document = False empty_documents = invoices_document.filtered(lambda d: not d.invoice_ids) # We would like to keep them in case the documents in the chatter are important. # Users can always delete them manually as needed. if empty_documents: empty_documents.active = False body = _("This invoice has been unlinked from the e-faktur document %(document_link)s following the reset to draft.", document_link=invoices_document._get_html_link(title=f"{invoices_document.id}")) invoices_with_document._message_log_batch(bodies={inv.id: body for inv in invoices_with_document}) return super().button_draft()