diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/l10n_id_efaktur/models/account_move.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/l10n_id_efaktur/models/account_move.py')
| -rw-r--r-- | addons/l10n_id_efaktur/models/account_move.py | 303 |
1 files changed, 303 insertions, 0 deletions
diff --git a/addons/l10n_id_efaktur/models/account_move.py b/addons/l10n_id_efaktur/models/account_move.py new file mode 100644 index 00000000..5d843b9b --- /dev/null +++ b/addons/l10n_id_efaktur/models/account_move.py @@ -0,0 +1,303 @@ +# -*- encoding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import base64 +import re +from odoo import api, fields, models, _ +from odoo.exceptions import UserError, ValidationError + +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'] + +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 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_id', '!=', False), ('state', '=', 'cancel')]", copy=False) + l10n_id_attachment_id = fields.Many2one('ir.attachment', readonly=True, copy=False) + l10n_id_csv_created = fields.Boolean('CSV Created', compute='_compute_csv_created', copy=False) + l10n_id_kode_transaksi = fields.Selection([ + ('01', '01 Kepada Pihak yang Bukan Pemungut PPN (Customer Biasa)'), + ('02', '02 Kepada Pemungut Bendaharawan (Dinas Kepemerintahan)'), + ('03', '03 Kepada Pemungut Selain Bendaharawan (BUMN)'), + ('04', '04 DPP Nilai Lain (PPN 1%)'), + ('06', '06 Penyerahan Lainnya (Turis Asing)'), + ('07', '07 Penyerahan yang PPN-nya Tidak Dipungut (Kawasan Ekonomi Khusus/ Batam)'), + ('08', '08 Penyerahan yang PPN-nya Dibebaskan (Impor Barang Tertentu)'), + ('09', '09 Penyerahan Aktiva ( Pasal 16D UU PPN )'), + ], string='Kode Transaksi', help='Dua digit pertama nomor pajak', + readonly=True, states={'draft': [('readonly', False)]}, copy=False) + l10n_id_need_kode_transaksi = fields.Boolean(compute='_compute_need_kode_transaksi') + + @api.onchange('partner_id') + def _onchange_partner_id(self): + self.l10n_id_kode_transaksi = self.partner_id.l10n_id_kode_transaksi + return super(AccountMove, self)._onchange_partner_id() + + @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('l10n_id_attachment_id') + def _compute_csv_created(self): + for record in self: + record.l10n_id_csv_created = bool(record.l10n_id_attachment_id) + + @api.depends('partner_id') + def _compute_need_kode_transaksi(self): + for move in self: + move.l10n_id_need_kode_transaksi = move.partner_id.l10n_id_pkp and not move.l10n_id_tax_number and move.move_type == 'out_invoice' and move.country_code == 'ID' + + @api.constrains('l10n_id_kode_transaksi', 'line_ids') + def _constraint_kode_ppn(self): + ppn_tag = self.env.ref('l10n_id.ppn_tag') + for move in self.filtered(lambda m: 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.exclude_from_invoice_tab is False and not line.display_type) \ + and any(ppn_tag.id not in line.tax_tag_ids.ids for line in move.line_ids if line.exclude_from_invoice_tab is False and not line.display_type): + 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_kode_transaksi == '08'): + if any(ppn_tag.id in line.tax_tag_ids.ids for line in move.line_ids if line.exclude_from_invoice_tab is False and not line.display_type): + 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 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_attachment_id: + 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: + efaktur = self.env['l10n_id_efaktur.efaktur.range'].pop_number(move.company_id.id) + if not efaktur: + raise ValidationError(_('There is no Efaktur number available. Please configure the range you get from the government in the e-Faktur menu. ')) + move.l10n_id_tax_number = '%s0%013d' % (str(move.l10n_id_kode_transaksi), efaktur) + 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_attachment_id: + 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): + action = { + 'type': 'ir.actions.act_url', + 'url': "web/content/?model=ir.attachment&id=" + str(self.l10n_id_attachment_id.id) + "&filename_field=name&field=datas&download=true&name=" + self.l10n_id_attachment_id.name, + 'target': 'self' + } + return action + + 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 record.partner_id.l10n_id_pkp and not record.l10n_id_tax_number: + raise ValidationError(_('Connect %(move_number)s with E-faktur to download this report', move_number=record.name)) + + self._generate_efaktur(',') + return self.download_csv() + + def _generate_efaktur_invoice(self, delimiter): + """Generate E-Faktur for customer invoice.""" + # Invoice of Customer + company_id = self.company_id + dp_product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id') + + output_head = '%s%s%s' % ( + _csv_row(FK_HEAD_LIST, delimiter), + _csv_row(LT_HEAD_LIST, delimiter), + _csv_row(OF_HEAD_LIST, delimiter), + ) + + for move in self.filtered(lambda m: m.state == 'posted'): + eTax = move._prepare_etax() + + nik = str(move.partner_id.l10n_id_nik) if not move.partner_id.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 + else: + number_ref = str(move.name) + " " + nik + + street = ', '.join([x for x in (move.partner_id.street, move.partner_id.street2) if x]) + + invoice_npwp = '000000000000000' + if move.partner_id.vat and len(move.partner_id.vat) >= 12: + invoice_npwp = move.partner_id.vat + elif (not move.partner_id.vat or len(move.partner_id.vat) < 12) and move.partner_id.l10n_id_nik: + invoice_npwp = move.partner_id.l10n_id_nik + invoice_npwp = invoice_npwp.replace('.', '').replace('-', '') + + # 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'] = '{0}/{1}/{2}'.format(move.invoice_date.day, move.invoice_date.month, move.invoice_date.year) + eTax['NPWP'] = invoice_npwp + eTax['NAMA'] = move.partner_id.name if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_name or move.partner_id.name + eTax['ALAMAT_LENGKAP'] = move.partner_id.contact_address.replace('\n', '') if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_address or street + eTax['JUMLAH_DPP'] = int(round(move.amount_untaxed, 0)) # currency rounded to the unit + eTax['JUMLAH_PPN'] = int(round(move.amount_tax, 0)) + eTax['ID_KETERANGAN_TAMBAHAN'] = '1' if move.l10n_id_kode_transaksi == '07' else '' + eTax['REFERENSI'] = number_ref + + lines = move.line_ids.filtered(lambda x: x.product_id.id == int(dp_product_id) and x.price_unit < 0 and not x.display_type) + eTax['FG_UANG_MUKA'] = 0 + eTax['UANG_MUKA_DPP'] = int(abs(sum(lines.mapped('price_subtotal')))) + eTax['UANG_MUKA_PPN'] = int(abs(sum(lines.mapped(lambda l: l.price_total - l.price_subtotal)))) + + company_npwp = company_id.partner_id.vat or '000000000000000' + + fk_values_list = ['FK'] + [eTax[f] for f in FK_HEAD_LIST[1:]] + eTax['JALAN'] = company_id.partner_id.l10n_id_tax_address or company_id.partner_id.street + eTax['NOMOR_TELEPON'] = company_id.phone or '' + + lt_values_list = ['FAPR', company_npwp, company_id.name] + [eTax[f] for f in LT_HEAD_LIST[3:]] + + # HOW TO ADD 2 line to 1 line for free product + free, sales = [], [] + + for line in move.line_ids.filtered(lambda l: not l.exclude_from_invoice_tab and not l.display_type): + # *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 = bruto_total = total_discount = 0.0 + + for tax in line.tax_ids: + if tax.amount > 0: + tax_line += line.price_subtotal * (tax.amount / 100.0) + + invoice_line_unit_price = line.price_unit + + invoice_line_total_price = invoice_line_unit_price * line.quantity + + line_dict = { + 'KODE_OBJEK': line.product_id.default_code or '', + 'NAMA': line.product_id.name or '', + 'HARGA_SATUAN': int(invoice_line_unit_price), + 'JUMLAH_BARANG': line.quantity, + 'HARGA_TOTAL': int(invoice_line_total_price), + 'DPP': int(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': int(invoice_line_total_price - line.price_subtotal), + 'PPN': int(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': int(invoice_line_discount_m2m), + 'PPN': int(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'] = int(tax_line) + + free.remove(f) + + sub_total_before_adjustment += sale['DPP'] + sub_total_ppn_before_adjustment += sale['PPN'] + bruto_total += sale['DISKON'] + total_discount += round(sale['DISKON'], 2) + + output_head += _csv_row(fk_values_list, delimiter) + output_head += _csv_row(lt_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 + + def _prepare_etax(self): + # These values are never set + return {'JUMLAH_PPNBM': 0, 'UANG_MUKA_PPNBM': 0, 'BLOK': '', 'NOMOR': '', 'RT': '', 'RW': '', 'KECAMATAN': '', 'KELURAHAN': '', 'KABUPATEN': '', 'PROPINSI': '', 'KODE_POS': '', 'JUMLAH_BARANG': 0, 'TARIF_PPNBM': 0, 'PPNBM': 0} + + def _generate_efaktur(self, delimiter): + if self.filtered(lambda x: not x.l10n_id_kode_transaksi): + raise UserError(_('Some documents don\'t have a transaction code')) + if self.filtered(lambda x: x.move_type != 'out_invoice'): + raise UserError(_('Some documents are not Customer Invoices')) + + output_head = self._generate_efaktur_invoice(delimiter) + my_utf8 = output_head.encode("utf-8") + out = base64.b64encode(my_utf8) + + attachment = self.env['ir.attachment'].create({ + 'datas': out, + 'name': 'efaktur_%s.csv' % (fields.Datetime.to_string(fields.Datetime.now()).replace(" ", "_")), + 'type': 'binary', + }) + + for record in self: + record.message_post(attachment_ids=[attachment.id]) + self.l10n_id_attachment_id = attachment.id + return { + 'type': 'ir.actions.client', + 'tag': 'reload', + } |
