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
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, fields, api, _
from odoo.exceptions import UserError, RedirectWarning, ValidationError
from dateutil.relativedelta import relativedelta
from lxml import etree
import logging
_logger = logging.getLogger(__name__)
class AccountMove(models.Model):
_inherit = 'account.move'
@api.model
def _l10n_ar_get_document_number_parts(self, document_number, document_type_code):
# import shipments
if document_type_code in ['66', '67']:
pos = invoice_number = '0'
else:
pos, invoice_number = document_number.split('-')
return {'invoice_number': int(invoice_number), 'point_of_sale': int(pos)}
l10n_ar_afip_responsibility_type_id = fields.Many2one(
'l10n_ar.afip.responsibility.type', string='AFIP Responsibility Type', help='Defined by AFIP to'
' identify the type of responsibilities that a person or a legal entity could have and that impacts in the'
' type of operations and requirements they need.')
l10n_ar_currency_rate = fields.Float(copy=False, digits=(16, 6), readonly=True, string="Currency Rate")
# Mostly used on reports
l10n_ar_afip_concept = fields.Selection(
compute='_compute_l10n_ar_afip_concept', selection='_get_afip_invoice_concepts', string="AFIP Concept",
help="A concept is suggested regarding the type of the products on the invoice but it is allowed to force a"
" different type if required.")
l10n_ar_afip_service_start = fields.Date(string='AFIP Service Start Date', readonly=True, states={'draft': [('readonly', False)]})
l10n_ar_afip_service_end = fields.Date(string='AFIP Service End Date', readonly=True, states={'draft': [('readonly', False)]})
@api.constrains('move_type', 'journal_id')
def _check_moves_use_documents(self):
""" Do not let to create not invoices entries in journals that use documents """
not_invoices = self.filtered(lambda x: x.company_id.country_id.code == "AR" and x.journal_id.type in ['sale', 'purchase'] and x.l10n_latam_use_documents and not x.is_invoice())
if not_invoices:
raise ValidationError(_("The selected Journal can't be used in this transaction, please select one that doesn't use documents as these are just for Invoices."))
@api.constrains('move_type', 'l10n_latam_document_type_id')
def _check_invoice_type_document_type(self):
""" LATAM module define that we are not able to use debit_note or invoice document types in an invoice refunds,
However for Argentinian Document Type's 99 (internal type = invoice) we are able to used in a refund invoices.
In this method we exclude the argentinian document type 99 from the generic constraint """
ar_doctype_99 = self.filtered(
lambda x: x.country_code == 'AR' and
x.l10n_latam_document_type_id.code == '99' and
x.move_type in ['out_refund', 'in_refund'])
super(AccountMove, self - ar_doctype_99)._check_invoice_type_document_type()
def _get_afip_invoice_concepts(self):
""" Return the list of values of the selection field. """
return [('1', 'Products / Definitive export of goods'), ('2', 'Services'), ('3', 'Products and Services'),
('4', '4-Other (export)')]
@api.depends('invoice_line_ids', 'invoice_line_ids.product_id', 'invoice_line_ids.product_id.type', 'journal_id')
def _compute_l10n_ar_afip_concept(self):
recs_afip = self.filtered(lambda x: x.company_id.country_id.code == "AR" and x.l10n_latam_use_documents)
for rec in recs_afip:
rec.l10n_ar_afip_concept = rec._get_concept()
remaining = self - recs_afip
remaining.l10n_ar_afip_concept = ''
def _get_concept(self):
""" Method to get the concept of the invoice considering the type of the products on the invoice """
self.ensure_one()
invoice_lines = self.invoice_line_ids.filtered(lambda x: not x.display_type)
product_types = set([x.product_id.type for x in invoice_lines if x.product_id])
consumable = set(['consu', 'product'])
service = set(['service'])
mixed = set(['consu', 'service', 'product'])
# on expo invoice you can mix services and products
expo_invoice = self.l10n_latam_document_type_id.code in ['19', '20', '21']
# Default value "product"
afip_concept = '1'
if product_types == service:
afip_concept = '2'
elif product_types - consumable and product_types - service and not expo_invoice:
afip_concept = '3'
return afip_concept
def _get_l10n_latam_documents_domain(self):
self.ensure_one()
domain = super()._get_l10n_latam_documents_domain()
if self.journal_id.company_id.country_id.code == "AR":
letters = self.journal_id._get_journal_letter(counterpart_partner=self.partner_id.commercial_partner_id)
domain += ['|', ('l10n_ar_letter', '=', False), ('l10n_ar_letter', 'in', letters)]
codes = self.journal_id._get_journal_codes()
if codes:
domain.append(('code', 'in', codes))
if self.move_type == 'in_refund':
domain = ['|', ('code', 'in', ['99'])] + domain
return domain
def _check_argentinian_invoice_taxes(self):
# check vat on companies thats has it (Responsable inscripto)
for inv in self.filtered(lambda x: x.company_id.l10n_ar_company_requires_vat):
purchase_aliquots = 'not_zero'
# we require a single vat on each invoice line except from some purchase documents
if inv.move_type in ['in_invoice', 'in_refund'] and inv.l10n_latam_document_type_id.purchase_aliquots == 'zero':
purchase_aliquots = 'zero'
for line in inv.mapped('invoice_line_ids').filtered(lambda x: x.display_type not in ('line_section', 'line_note')):
vat_taxes = line.tax_ids.filtered(lambda x: x.tax_group_id.l10n_ar_vat_afip_code)
if len(vat_taxes) != 1:
raise UserError(_('There must be one and only one VAT tax per line. Check line "%s"', line.name))
elif purchase_aliquots == 'zero' and vat_taxes.tax_group_id.l10n_ar_vat_afip_code != '0':
raise UserError(_('On invoice id "%s" you must use VAT Not Applicable on every line.') % inv.id)
elif purchase_aliquots == 'not_zero' and vat_taxes.tax_group_id.l10n_ar_vat_afip_code == '0':
raise UserError(_('On invoice id "%s" you must use VAT taxes different than VAT Not Applicable.') % inv.id)
def _set_afip_service_dates(self):
for rec in self.filtered(lambda m: m.invoice_date and m.l10n_ar_afip_concept in ['2', '3', '4']):
if not rec.l10n_ar_afip_service_start:
rec.l10n_ar_afip_service_start = rec.invoice_date + relativedelta(day=1)
if not rec.l10n_ar_afip_service_end:
rec.l10n_ar_afip_service_end = rec.invoice_date + relativedelta(day=1, days=-1, months=+1)
@api.onchange('partner_id')
def _onchange_afip_responsibility(self):
if self.company_id.country_id.code == 'AR' and self.l10n_latam_use_documents and self.partner_id \
and not self.partner_id.l10n_ar_afip_responsibility_type_id:
return {'warning': {
'title': _('Missing Partner Configuration'),
'message': _('Please configure the AFIP Responsibility for "%s" in order to continue') % (
self.partner_id.name)}}
@api.onchange('partner_id')
def _onchange_partner_journal(self):
""" This method is used when the invoice is created from the sale or subscription """
expo_journals = ['FEERCEL', 'FEEWS', 'FEERCELP']
for rec in self.filtered(lambda x: x.company_id.country_id.code == "AR" and x.journal_id.type == 'sale'
and x.l10n_latam_use_documents and x.partner_id.l10n_ar_afip_responsibility_type_id):
res_code = rec.partner_id.l10n_ar_afip_responsibility_type_id.code
domain = [('company_id', '=', rec.company_id.id), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')]
journal = self.env['account.journal']
msg = False
if res_code in ['9', '10'] and rec.journal_id.l10n_ar_afip_pos_system not in expo_journals:
# if partner is foregin and journal is not of expo, we try to change to expo journal
journal = journal.search(domain + [('l10n_ar_afip_pos_system', 'in', expo_journals)], limit=1)
msg = _('You are trying to create an invoice for foreign partner but you don\'t have an exportation journal')
elif res_code not in ['9', '10'] and rec.journal_id.l10n_ar_afip_pos_system in expo_journals:
# if partner is NOT foregin and journal is for expo, we try to change to local journal
journal = journal.search(domain + [('l10n_ar_afip_pos_system', 'not in', expo_journals)], limit=1)
msg = _('You are trying to create an invoice for domestic partner but you don\'t have a domestic market journal')
if journal:
rec.journal_id = journal.id
elif msg:
# Throw an error to user in order to proper configure the journal for the type of operation
action = self.env.ref('account.action_account_journal_form')
raise RedirectWarning(msg, action.id, _('Go to Journals'))
def _post(self, soft=True):
ar_invoices = self.filtered(lambda x: x.company_id.country_id.code == "AR" and x.l10n_latam_use_documents)
for rec in ar_invoices:
rec.l10n_ar_afip_responsibility_type_id = rec.commercial_partner_id.l10n_ar_afip_responsibility_type_id.id
if rec.company_id.currency_id == rec.currency_id:
l10n_ar_currency_rate = 1.0
else:
l10n_ar_currency_rate = rec.currency_id._convert(
1.0, rec.company_id.currency_id, rec.company_id, rec.invoice_date or fields.Date.today(), round=False)
rec.l10n_ar_currency_rate = l10n_ar_currency_rate
# We make validations here and not with a constraint because we want validation before sending electronic
# data on l10n_ar_edi
ar_invoices._check_argentinian_invoice_taxes()
posted = super()._post(soft)
posted._set_afip_service_dates()
return posted
def _reverse_moves(self, default_values_list=None, cancel=False):
if not default_values_list:
default_values_list = [{} for move in self]
for move, default_values in zip(self, default_values_list):
default_values.update({
'l10n_ar_afip_service_start': move.l10n_ar_afip_service_start,
'l10n_ar_afip_service_end': move.l10n_ar_afip_service_end,
})
return super()._reverse_moves(default_values_list=default_values_list, cancel=cancel)
@api.onchange('l10n_latam_document_type_id', 'l10n_latam_document_number')
def _inverse_l10n_latam_document_number(self):
super()._inverse_l10n_latam_document_number()
to_review = self.filtered(
lambda x: x.journal_id.type == 'sale' and x.l10n_latam_document_type_id and x.l10n_latam_document_number and
(x.l10n_latam_manual_document_number or not x.highest_name))
for rec in to_review:
number = rec.l10n_latam_document_type_id._format_document_number(rec.l10n_latam_document_number)
current_pos = int(number.split("-")[0])
if current_pos != rec.journal_id.l10n_ar_afip_pos_number:
invoices = self.search([('journal_id', '=', rec.journal_id.id), ('posted_before', '=', True)], limit=1)
# If there is no posted before invoices the user can change the POS number (x.l10n_latam_document_number)
if (not invoices):
rec.journal_id.l10n_ar_afip_pos_number = current_pos
rec.journal_id._onchange_set_short_name()
# If not, avoid that the user change the POS number
else:
raise UserError(_('The document number can not be changed for this journal, you can only modify'
' the POS number if there is not posted (or posted before) invoices'))
def _get_formatted_sequence(self, number=0):
return "%s %05d-%08d" % (self.l10n_latam_document_type_id.doc_code_prefix,
self.journal_id.l10n_ar_afip_pos_number, number)
def _get_starting_sequence(self):
""" If use documents then will create a new starting sequence using the document type code prefix and the
journal document number with a 8 padding number """
if self.journal_id.l10n_latam_use_documents and self.env.company.country_id.code == "AR":
if self.l10n_latam_document_type_id:
return self._get_formatted_sequence()
return super()._get_starting_sequence()
def _get_last_sequence_domain(self, relaxed=False):
where_string, param = super(AccountMove, self)._get_last_sequence_domain(relaxed)
if self.company_id.country_id.code == "AR" and self.l10n_latam_use_documents:
if not self.journal_id.l10n_ar_share_sequences:
where_string += " AND l10n_latam_document_type_id = %(l10n_latam_document_type_id)s"
param['l10n_latam_document_type_id'] = self.l10n_latam_document_type_id.id or 0
elif self.journal_id.l10n_ar_share_sequences:
where_string += " AND l10n_latam_document_type_id in %(l10n_latam_document_type_ids)s"
param['l10n_latam_document_type_ids'] = tuple(self.l10n_latam_document_type_id.search(
[('l10n_ar_letter', '=', self.l10n_latam_document_type_id.l10n_ar_letter)]).ids)
return where_string, param
def _l10n_ar_get_amounts(self, company_currency=False):
""" Method used to prepare data to present amounts and taxes related amounts when creating an
electronic invoice for argentinian and the txt files for digital VAT books. Only take into account the argentinian taxes """
self.ensure_one()
amount_field = company_currency and 'balance' or 'price_subtotal'
# if we use balance we need to correct sign (on price_subtotal is positive for refunds and invoices)
sign = -1 if (company_currency and self.is_inbound()) else 1
tax_lines = self.line_ids.filtered('tax_line_id')
vat_taxes = tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_vat_afip_code)
vat_taxable = self.env['account.move.line']
for line in self.invoice_line_ids:
if any(tax.tax_group_id.l10n_ar_vat_afip_code and tax.tax_group_id.l10n_ar_vat_afip_code not in ['0', '1', '2'] for tax in line.tax_ids):
vat_taxable |= line
profits_tax_group = self.env.ref('l10n_ar.tax_group_percepcion_ganancias')
return {'vat_amount': sign * sum(vat_taxes.mapped(amount_field)),
# For invoices of letter C should not pass VAT
'vat_taxable_amount': sign * sum(vat_taxable.mapped(amount_field)) if self.l10n_latam_document_type_id.l10n_ar_letter != 'C' else self.amount_untaxed,
'vat_exempt_base_amount': sign * sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == '2')).mapped(amount_field)),
'vat_untaxed_base_amount': sign * sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == '1')).mapped(amount_field)),
# used on FE
'not_vat_taxes_amount': sign * sum((tax_lines - vat_taxes).mapped(amount_field)),
# used on BFE + TXT
'iibb_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '07').mapped(amount_field)),
'mun_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '08').mapped(amount_field)),
'intern_tax_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '04').mapped(amount_field)),
'other_taxes_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '99').mapped(amount_field)),
'profits_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id == profits_tax_group).mapped(amount_field)),
'vat_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '06').mapped(amount_field)),
'other_perc_amount': sign * sum(tax_lines.filtered(lambda r: r.tax_line_id.tax_group_id.l10n_ar_tribute_afip_code == '09' and r.tax_line_id.tax_group_id != profits_tax_group).mapped(amount_field)),
}
def _get_vat(self, company_currency=False):
""" Applies on wsfe web service and in the VAT digital books """
amount_field = company_currency and 'balance' or 'price_subtotal'
# if we use balance we need to correct sign (on price_subtotal is positive for refunds and invoices)
sign = -1 if (company_currency and self.is_inbound()) else 1
res = []
vat_taxable = self.env['account.move.line']
# get all invoice lines that are vat taxable
for line in self.line_ids:
if any(tax.tax_group_id.l10n_ar_vat_afip_code and tax.tax_group_id.l10n_ar_vat_afip_code not in ['0', '1', '2'] for tax in line.tax_line_id) and line[amount_field]:
vat_taxable |= line
for vat in vat_taxable:
base_imp = sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == vat.tax_line_id.tax_group_id.l10n_ar_vat_afip_code)).mapped(amount_field))
res += [{'Id': vat.tax_line_id.tax_group_id.l10n_ar_vat_afip_code,
'BaseImp': sign * base_imp,
'Importe': sign * vat[amount_field]}]
# Report vat 0%
vat_base_0 = sign * sum(self.invoice_line_ids.filtered(lambda x: x.tax_ids.filtered(lambda y: y.tax_group_id.l10n_ar_vat_afip_code == '3')).mapped(amount_field))
if vat_base_0:
res += [{'Id': '3', 'BaseImp': vat_base_0, 'Importe': 0.0}]
return res if res else []
def _get_name_invoice_report(self):
self.ensure_one()
if self.l10n_latam_use_documents and self.company_id.country_id.code == 'AR':
return 'l10n_ar.report_invoice_document'
return super()._get_name_invoice_report()
|