1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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',
}
|