1# -*- encoding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4import base64
5import re
6from odoo import api, fields, models, _
7from odoo.exceptions import UserError, ValidationError
8
9FK_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']
10
11LT_HEAD_LIST = ['LT', 'NPWP', 'NAMA', 'JALAN', 'BLOK', 'NOMOR', 'RT', 'RW', 'KECAMATAN', 'KELURAHAN', 'KABUPATEN', 'PROPINSI', 'KODE_POS', 'NOMOR_TELEPON']
12
13OF_HEAD_LIST = ['OF', 'KODE_OBJEK', 'NAMA', 'HARGA_SATUAN', 'JUMLAH_BARANG', 'HARGA_TOTAL', 'DISKON', 'DPP', 'PPN', 'TARIF_PPNBM', 'PPNBM']
14
15
16def _csv_row(data, delimiter=',', quote='"'):
17    return quote + (quote + delimiter + quote).join([str(x).replace(quote, '\\' + quote) for x in data]) + quote + '\n'
18
19
20class AccountMove(models.Model):
21    _inherit = "account.move"
22
23    l10n_id_tax_number = fields.Char(string="Tax Number", copy=False)
24    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)
25    l10n_id_attachment_id = fields.Many2one('ir.attachment', readonly=True, copy=False)
26    l10n_id_csv_created = fields.Boolean('CSV Created', compute='_compute_csv_created', copy=False)
27    l10n_id_kode_transaksi = fields.Selection([
28            ('01', '01 Kepada Pihak yang Bukan Pemungut PPN (Customer Biasa)'),
29            ('02', '02 Kepada Pemungut Bendaharawan (Dinas Kepemerintahan)'),
30            ('03', '03 Kepada Pemungut Selain Bendaharawan (BUMN)'),
31            ('04', '04 DPP Nilai Lain (PPN 1%)'),
32            ('06', '06 Penyerahan Lainnya (Turis Asing)'),
33            ('07', '07 Penyerahan yang PPN-nya Tidak Dipungut (Kawasan Ekonomi Khusus/ Batam)'),
34            ('08', '08 Penyerahan yang PPN-nya Dibebaskan (Impor Barang Tertentu)'),
35            ('09', '09 Penyerahan Aktiva ( Pasal 16D UU PPN )'),
36        ], string='Kode Transaksi', help='Dua digit pertama nomor pajak',
37        readonly=True, states={'draft': [('readonly', False)]}, copy=False)
38    l10n_id_need_kode_transaksi = fields.Boolean(compute='_compute_need_kode_transaksi')
39
40    @api.onchange('partner_id')
41    def _onchange_partner_id(self):
42        self.l10n_id_kode_transaksi = self.partner_id.l10n_id_kode_transaksi
43        return super(AccountMove, self)._onchange_partner_id()
44
45    @api.onchange('l10n_id_tax_number')
46    def _onchange_l10n_id_tax_number(self):
47        for record in self:
48            if record.l10n_id_tax_number and record.move_type not in self.get_purchase_types():
49                raise UserError(_("You can only change the number manually for a Vendor Bills and Credit Notes"))
50
51    @api.depends('l10n_id_attachment_id')
52    def _compute_csv_created(self):
53        for record in self:
54            record.l10n_id_csv_created = bool(record.l10n_id_attachment_id)
55
56    @api.depends('partner_id')
57    def _compute_need_kode_transaksi(self):
58        for move in self:
59            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'
60
61    @api.constrains('l10n_id_kode_transaksi', 'line_ids')
62    def _constraint_kode_ppn(self):
63        ppn_tag = self.env.ref('l10n_id.ppn_tag')
64        for move in self.filtered(lambda m: m.l10n_id_kode_transaksi != '08'):
65            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) \
66                    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):
67                raise UserError(_('Cannot mix VAT subject and Non-VAT subject items in the same invoice with this kode transaksi.'))
68        for move in self.filtered(lambda m: m.l10n_id_kode_transaksi == '08'):
69            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):
70                raise UserError('Kode transaksi 08 is only for non VAT subject items.')
71
72    @api.constrains('l10n_id_tax_number')
73    def _constrains_l10n_id_tax_number(self):
74        for record in self.filtered('l10n_id_tax_number'):
75            if record.l10n_id_tax_number != re.sub(r'\D', '', record.l10n_id_tax_number):
76                record.l10n_id_tax_number = re.sub(r'\D', '', record.l10n_id_tax_number)
77            if len(record.l10n_id_tax_number) != 16:
78                raise UserError(_('A tax number should have 16 digits'))
79            elif record.l10n_id_tax_number[:2] not in dict(self._fields['l10n_id_kode_transaksi'].selection).keys():
80                raise UserError(_('A tax number must begin by a valid Kode Transaksi'))
81            elif record.l10n_id_tax_number[2] not in ('0', '1'):
82                raise UserError(_('The third digit of a tax number must be 0 or 1'))
83
84    def _post(self, soft=True):
85        """Set E-Faktur number after validation."""
86        for move in self:
87            if move.l10n_id_need_kode_transaksi:
88                if not move.l10n_id_kode_transaksi:
89                    raise ValidationError(_('You need to put a Kode Transaksi for this partner.'))
90                if move.l10n_id_replace_invoice_id.l10n_id_tax_number:
91                    if not move.l10n_id_replace_invoice_id.l10n_id_attachment_id:
92                        raise ValidationError(_('Replacement invoice only for invoices on which the e-Faktur is generated. '))
93                    rep_efaktur_str = move.l10n_id_replace_invoice_id.l10n_id_tax_number
94                    move.l10n_id_tax_number = '%s1%s' % (move.l10n_id_kode_transaksi, rep_efaktur_str[3:])
95                else:
96                    efaktur = self.env['l10n_id_efaktur.efaktur.range'].pop_number(move.company_id.id)
97                    if not efaktur:
98                        raise ValidationError(_('There is no Efaktur number available.  Please configure the range you get from the government in the e-Faktur menu. '))
99                    move.l10n_id_tax_number = '%s0%013d' % (str(move.l10n_id_kode_transaksi), efaktur)
100        return super()._post(soft)
101
102    def reset_efaktur(self):
103        """Reset E-Faktur, so it can be use for other invoice."""
104        for move in self:
105            if move.l10n_id_attachment_id:
106                raise UserError(_('You have already generated the tax report for this document: %s', move.name))
107            self.env['l10n_id_efaktur.efaktur.range'].push_number(move.company_id.id, move.l10n_id_tax_number[3:])
108            move.message_post(
109                body='e-Faktur Reset: %s ' % (move.l10n_id_tax_number),
110                subject="Reset Efaktur")
111            move.l10n_id_tax_number = False
112        return True
113
114    def download_csv(self):
115        action = {
116            'type': 'ir.actions.act_url',
117            '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,
118            'target': 'self'
119        }
120        return action
121
122    def download_efaktur(self):
123        """Collect the data and execute function _generate_efaktur."""
124        for record in self:
125            if record.state == 'draft':
126                raise ValidationError(_('Could not download E-faktur in draft state'))
127
128            if record.partner_id.l10n_id_pkp and not record.l10n_id_tax_number:
129                raise ValidationError(_('Connect %(move_number)s with E-faktur to download this report', move_number=record.name))
130
131        self._generate_efaktur(',')
132        return self.download_csv()
133
134    def _generate_efaktur_invoice(self, delimiter):
135        """Generate E-Faktur for customer invoice."""
136        # Invoice of Customer
137        company_id = self.company_id
138        dp_product_id = self.env['ir.config_parameter'].sudo().get_param('sale.default_deposit_product_id')
139
140        output_head = '%s%s%s' % (
141            _csv_row(FK_HEAD_LIST, delimiter),
142            _csv_row(LT_HEAD_LIST, delimiter),
143            _csv_row(OF_HEAD_LIST, delimiter),
144        )
145
146        for move in self.filtered(lambda m: m.state == 'posted'):
147            eTax = move._prepare_etax()
148
149            nik = str(move.partner_id.l10n_id_nik) if not move.partner_id.vat else ''
150
151            if move.l10n_id_replace_invoice_id:
152                number_ref = str(move.l10n_id_replace_invoice_id.name) + " replaced by " + str(move.name) + " " + nik
153            else:
154                number_ref = str(move.name) + " " + nik
155
156            street = ', '.join([x for x in (move.partner_id.street, move.partner_id.street2) if x])
157
158            invoice_npwp = '000000000000000'
159            if move.partner_id.vat and len(move.partner_id.vat) >= 12:
160                invoice_npwp = move.partner_id.vat
161            elif (not move.partner_id.vat or len(move.partner_id.vat) < 12) and move.partner_id.l10n_id_nik:
162                invoice_npwp = move.partner_id.l10n_id_nik
163            invoice_npwp = invoice_npwp.replace('.', '').replace('-', '')
164
165            # Here all fields or columns based on eTax Invoice Third Party
166            eTax['KD_JENIS_TRANSAKSI'] = move.l10n_id_tax_number[0:2] or 0
167            eTax['FG_PENGGANTI'] = move.l10n_id_tax_number[2:3] or 0
168            eTax['NOMOR_FAKTUR'] = move.l10n_id_tax_number[3:] or 0
169            eTax['MASA_PAJAK'] = move.invoice_date.month
170            eTax['TAHUN_PAJAK'] = move.invoice_date.year
171            eTax['TANGGAL_FAKTUR'] = '{0}/{1}/{2}'.format(move.invoice_date.day, move.invoice_date.month, move.invoice_date.year)
172            eTax['NPWP'] = invoice_npwp
173            eTax['NAMA'] = move.partner_id.name if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_name or move.partner_id.name
174            eTax['ALAMAT_LENGKAP'] = move.partner_id.contact_address.replace('\n', '') if eTax['NPWP'] == '000000000000000' else move.partner_id.l10n_id_tax_address or street
175            eTax['JUMLAH_DPP'] = int(round(move.amount_untaxed, 0)) # currency rounded to the unit
176            eTax['JUMLAH_PPN'] = int(round(move.amount_tax, 0))
177            eTax['ID_KETERANGAN_TAMBAHAN'] = '1' if move.l10n_id_kode_transaksi == '07' else ''
178            eTax['REFERENSI'] = number_ref
179
180            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)
181            eTax['FG_UANG_MUKA'] = 0
182            eTax['UANG_MUKA_DPP'] = int(abs(sum(lines.mapped('price_subtotal'))))
183            eTax['UANG_MUKA_PPN'] = int(abs(sum(lines.mapped(lambda l: l.price_total - l.price_subtotal))))
184
185            company_npwp = company_id.partner_id.vat or '000000000000000'
186
187            fk_values_list = ['FK'] + [eTax[f] for f in FK_HEAD_LIST[1:]]
188            eTax['JALAN'] = company_id.partner_id.l10n_id_tax_address or company_id.partner_id.street
189            eTax['NOMOR_TELEPON'] = company_id.phone or ''
190
191            lt_values_list = ['FAPR', company_npwp, company_id.name] + [eTax[f] for f in LT_HEAD_LIST[3:]]
192
193            # HOW TO ADD 2 line to 1 line for free product
194            free, sales = [], []
195
196            for line in move.line_ids.filtered(lambda l: not l.exclude_from_invoice_tab and not l.display_type):
197                # *invoice_line_unit_price is price unit use for harga_satuan's column
198                # *invoice_line_quantity is quantity use for jumlah_barang's column
199                # *invoice_line_total_price is bruto price use for harga_total's column
200                # *invoice_line_discount_m2m is discount price use for diskon's column
201                # *line.price_subtotal is subtotal price use for dpp's column
202                # *tax_line or free_tax_line is tax price use for ppn's column
203                free_tax_line = tax_line = bruto_total = total_discount = 0.0
204
205                for tax in line.tax_ids:
206                    if tax.amount > 0:
207                        tax_line += line.price_subtotal * (tax.amount / 100.0)
208
209                invoice_line_unit_price = line.price_unit
210
211                invoice_line_total_price = invoice_line_unit_price * line.quantity
212
213                line_dict = {
214                    'KODE_OBJEK': line.product_id.default_code or '',
215                    'NAMA': line.product_id.name or '',
216                    'HARGA_SATUAN': int(invoice_line_unit_price),
217                    'JUMLAH_BARANG': line.quantity,
218                    'HARGA_TOTAL': int(invoice_line_total_price),
219                    'DPP': int(line.price_subtotal),
220                    'product_id': line.product_id.id,
221                }
222
223                if line.price_subtotal < 0:
224                    for tax in line.tax_ids:
225                        free_tax_line += (line.price_subtotal * (tax.amount / 100.0)) * -1.0
226
227                    line_dict.update({
228                        'DISKON': int(invoice_line_total_price - line.price_subtotal),
229                        'PPN': int(free_tax_line),
230                    })
231                    free.append(line_dict)
232                elif line.price_subtotal != 0.0:
233                    invoice_line_discount_m2m = invoice_line_total_price - line.price_subtotal
234
235                    line_dict.update({
236                        'DISKON': int(invoice_line_discount_m2m),
237                        'PPN': int(tax_line),
238                    })
239                    sales.append(line_dict)
240
241            sub_total_before_adjustment = sub_total_ppn_before_adjustment = 0.0
242
243            # We are finding the product that has affected
244            # by free product to adjustment the calculation
245            # of discount and subtotal.
246            # - the price total of free product will be
247            # included as a discount to related of product.
248            for sale in sales:
249                for f in free:
250                    if f['product_id'] == sale['product_id']:
251                        sale['DISKON'] = sale['DISKON'] - f['DISKON'] + f['PPN']
252                        sale['DPP'] = sale['DPP'] + f['DPP']
253
254                        tax_line = 0
255
256                        for tax in line.tax_ids:
257                            if tax.amount > 0:
258                                tax_line += sale['DPP'] * (tax.amount / 100.0)
259
260                        sale['PPN'] = int(tax_line)
261
262                        free.remove(f)
263
264                sub_total_before_adjustment += sale['DPP']
265                sub_total_ppn_before_adjustment += sale['PPN']
266                bruto_total += sale['DISKON']
267                total_discount += round(sale['DISKON'], 2)
268
269            output_head += _csv_row(fk_values_list, delimiter)
270            output_head += _csv_row(lt_values_list, delimiter)
271            for sale in sales:
272                of_values_list = ['OF'] + [str(sale[f]) for f in OF_HEAD_LIST[1:-2]] + ['0', '0']
273                output_head += _csv_row(of_values_list, delimiter)
274
275        return output_head
276
277    def _prepare_etax(self):
278        # These values are never set
279        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}
280
281    def _generate_efaktur(self, delimiter):
282        if self.filtered(lambda x: not x.l10n_id_kode_transaksi):
283            raise UserError(_('Some documents don\'t have a transaction code'))
284        if self.filtered(lambda x: x.move_type != 'out_invoice'):
285            raise UserError(_('Some documents are not Customer Invoices'))
286
287        output_head = self._generate_efaktur_invoice(delimiter)
288        my_utf8 = output_head.encode("utf-8")
289        out = base64.b64encode(my_utf8)
290
291        attachment = self.env['ir.attachment'].create({
292            'datas': out,
293            'name': 'efaktur_%s.csv' % (fields.Datetime.to_string(fields.Datetime.now()).replace(" ", "_")),
294            'type': 'binary',
295        })
296
297        for record in self:
298            record.message_post(attachment_ids=[attachment.id])
299        self.l10n_id_attachment_id = attachment.id
300        return {
301            'type': 'ir.actions.client',
302            'tag': 'reload',
303        }
304