account_retencion_ar/account_voucher_ar.py

645 lines
25 KiB
Python

# This file is part of the account_retencion_ar module for Tryton.
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from decimal import Decimal
from dateutil.relativedelta import relativedelta
from trytond.model import Workflow, ModelView, fields
from trytond.wizard import Wizard, StateView, StateTransition, Button
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, Or, And
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
class AccountVoucher(metaclass=PoolMeta):
__name__ = 'account.voucher'
retenciones_efectuadas = fields.One2Many('account.retencion.efectuada',
'voucher', 'Tax Withholding Submitted',
states={
'invisible': Eval('voucher_type') != 'payment',
'readonly': Or(
Eval('state') == 'posted',
Eval('currency_code') != 'ARS'),
})
retenciones_soportadas = fields.One2Many('account.retencion.soportada',
'voucher', 'Tax Withholding Received',
states={
'invisible': Eval('voucher_type') != 'receipt',
'readonly': Or(
Eval('state') == 'posted',
Eval('currency_code') != 'ARS'),
})
@classmethod
def __setup__(cls):
super().__setup__()
calculated_state = ('calculated', 'Calculated')
if calculated_state not in cls.state.selection:
cls.state.selection.append(calculated_state)
cls._transitions |= set((
('draft', 'calculated'),
('calculated', 'draft'),
('calculated', 'posted'),
))
cls._buttons.update({
'calculate': {
'invisible': Or(
Eval('voucher_type') != 'payment',
Eval('state') != 'draft'),
'depends': ['voucher_type', 'state'],
},
'recalculate': {
'invisible': Or(
Eval('voucher_type') != 'payment',
Eval('state') != 'calculated',
Eval('amount_to_pay', 0) > Eval('amount', 0)),
'depends': ['voucher_type', 'state', 'amount_to_pay',
'amount'],
},
'draft': {
'invisible': Eval('state') != 'calculated',
'depends': ['state'],
},
'post': {
'invisible': Or(
And(Eval('voucher_type') == 'payment',
Eval('state') != 'calculated'),
And(Eval('voucher_type') == 'receipt',
Eval('state') != 'draft')),
'depends': ['voucher_type', 'state'],
},
'cancel': {
'invisible': Eval('state') != 'posted',
'depends': ['state'],
},
})
@fields.depends('retenciones_efectuadas', 'retenciones_soportadas')
def on_change_with_amount(self, name=None):
amount = super().on_change_with_amount(name)
if self.retenciones_efectuadas:
for retencion in self.retenciones_efectuadas:
if retencion.amount:
amount += retencion.amount
if self.retenciones_soportadas:
for retencion in self.retenciones_soportadas:
if retencion.amount:
amount += retencion.amount
return amount
@classmethod
@ModelView.button
@Workflow.transition('draft')
def draft(cls, vouchers):
for voucher in vouchers:
voucher.delete_withholding()
def delete_withholding(self):
pool = Pool()
TaxWithholdingSubmitted = pool.get('account.retencion.efectuada')
if self.retenciones_efectuadas:
with Transaction().set_context(delete_calculated=True):
TaxWithholdingSubmitted.delete(self.retenciones_efectuadas)
@classmethod
@ModelView.button
@Workflow.transition('calculated')
def calculate(cls, vouchers):
for voucher in vouchers:
voucher.calculate_withholdings()
@classmethod
@ModelView.button_action(
'account_retencion_ar.wizard_recalculate_withholdings')
def recalculate(cls, vouchers):
pass
def calculate_withholdings(self, context={}):
if self.company.ganancias_agente_retencion:
self._calculate_withholding_ganancias(context)
if self.company.iibb_agente_retencion:
self._calculate_withholding_iibb(context)
def _calculate_withholding_ganancias(self, context={}):
pool = Pool()
TaxWithholdingSubmitted = pool.get('account.retencion.efectuada')
quantize = Decimal(10) ** -Decimal(2)
withholding_data = self._get_withholding_data_ganancias(context)
for data in withholding_data.values():
withholding_type = data['tax']
payment_amount = data['payment_amount']
accumulated_amount = data['accumulated_amount'] + payment_amount
minimum_non_taxable_amount = (
withholding_type.minimum_non_taxable_amount or Decimal(0))
if accumulated_amount < minimum_non_taxable_amount:
continue
taxable_amount = accumulated_amount - minimum_non_taxable_amount
rate = data['rate']
if not rate:
continue
scale_non_taxable_amount = data['scale_non_taxable_amount']
computed_amount = ((taxable_amount - scale_non_taxable_amount) *
rate / Decimal(100))
computed_amount = computed_amount.quantize(quantize)
scale_fixed_amount = data['scale_fixed_amount']
computed_amount += scale_fixed_amount
minimum_withholdable_amount = (
withholding_type.minimum_withholdable_amount or Decimal(0))
if computed_amount < minimum_withholdable_amount:
continue
accumulated_withheld = data['accumulated_withheld']
amount = computed_amount - accumulated_withheld
withholding = TaxWithholdingSubmitted()
withholding.tax = withholding_type
withholding.voucher = self
withholding.party = self.party
withholding.date = self.date
withholding.payment_amount = payment_amount
withholding.accumulated_amount = accumulated_amount
withholding.minimum_non_taxable_amount = minimum_non_taxable_amount
withholding.scale_non_taxable_amount = scale_non_taxable_amount
withholding.taxable_amount = taxable_amount
withholding.rate = rate
withholding.scale_fixed_amount = scale_fixed_amount
withholding.computed_amount = computed_amount
withholding.minimum_withholdable_amount = (
minimum_withholdable_amount)
withholding.accumulated_withheld = accumulated_withheld
withholding.amount = amount
withholding.save()
def _get_withholding_data_ganancias(self, context={}):
pool = Pool()
Invoice = pool.get('account.invoice')
AccountVoucher = pool.get('account.voucher')
TaxWithholdingSubmitted = pool.get('account.retencion.efectuada')
# Verify conditions
if not self.party.ganancias_condition:
raise UserError(gettext(
'account_retencion_ar.msg_party_ganancias_condition'))
if self.party.ganancias_condition == 'ex':
return {}
if self.party.iva_condition not in ['responsable_inscripto', 'exento']:
return {}
quantize = Decimal(10) ** -Decimal(2)
res = {}
default_regimen = self.party.ganancias_regimen
if not default_regimen:
default_regimen = self.company.ganancias_regimen_retencion
if not default_regimen:
return {}
# Payment Amount
vat_rate = context.get('vat_rate', Decimal(0.21))
if context:
amount = context.get('amount', Decimal(0))
amount_option = context.get('amount_option', 'add')
if amount_option == 'add':
tax = default_regimen
if tax.id not in res:
res[tax.id] = {
'tax': tax,
'payment_amount': Decimal(0),
'accumulated_amount': Decimal(0),
'accumulated_withheld': Decimal(0),
}
payment_amount = amount / (1 + vat_rate)
res[tax.id]['payment_amount'] += (
payment_amount.quantize(quantize))
else: # amount_option == 'included'
pass
else:
for line in self.lines:
origin = str(line.move_line.move_origin)
if origin[:origin.find(',')] != 'account.invoice':
continue
if not line.amount:
continue
invoice = Invoice(line.move_line.move_origin.id)
payment_rate = Decimal(line.amount / invoice.total_amount)
for invoice_line in invoice.lines:
if invoice_line.type != 'line':
continue
tax = invoice_line.ganancias_regimen or default_regimen
if tax.id not in res:
res[tax.id] = {
'tax': tax,
'payment_amount': Decimal(0),
'accumulated_amount': Decimal(0),
'accumulated_withheld': Decimal(0),
}
payment_amount = invoice_line.amount * payment_rate
res[tax.id]['payment_amount'] += (
payment_amount.quantize(quantize))
# Verify exemptions
taxes = [x for x in res.keys()]
for exemption in self.party.exemptions:
for tax_id in taxes:
reference = 'account.retencion,%s' % str(tax_id)
if (str(exemption.tax) == reference and
exemption.end_date >= self.date):
del res[tax_id]
# Accumulated Amount
period_first_date = self.date + relativedelta(day=1)
period_last_date = self.date + relativedelta(day=31)
vouchers = AccountVoucher.search([
('party', '=', self.party),
('date', '>=', period_first_date),
('date', '<=', period_last_date),
('state', '=', 'posted'),
])
for voucher in vouchers:
for line in voucher.lines:
origin = str(line.move_line.move_origin)
if origin[:origin.find(',')] != 'account.invoice':
continue
if not line.amount:
continue
invoice = Invoice(line.move_line.move_origin.id)
payment_rate = Decimal(line.amount / invoice.total_amount)
for invoice_line in invoice.lines:
if invoice_line.type != 'line':
continue
tax = invoice_line.ganancias_regimen or default_regimen
if tax.id not in res:
continue
accumulated_amount = invoice_line.amount * payment_rate
res[tax.id]['accumulated_amount'] += (
accumulated_amount.quantize(quantize))
if voucher.amount > voucher.amount_to_pay:
difference = ((voucher.amount - voucher.amount_to_pay) /
(1 + vat_rate))
if default_regimen.id in res:
res[default_regimen.id]['accumulated_amount'] += (
difference.quantize(quantize))
# Accumulated Withheld
for tax_id in res.keys():
withholdings = TaxWithholdingSubmitted.search([
('tax', '=', tax_id),
('party', '=', self.party),
('date', '>=', period_first_date),
('date', '<=', period_last_date),
('state', '=', 'issued'),
])
for withholding in withholdings:
res[tax_id]['accumulated_withheld'] += withholding.amount
# Rate and extra data
for tax_id, tax in res.items():
tax.update(self._get_withholding_extra_data_ganancias(tax))
return res
def _get_withholding_extra_data_ganancias(self, tax_data):
res = {
'rate': Decimal(0),
'scale_non_taxable_amount': Decimal(0),
'scale_fixed_amount': Decimal(0),
}
regimen = tax_data['tax']
if regimen.scales:
taxable_amount = (tax_data['payment_amount'] +
tax_data['accumulated_amount'] -
regimen.minimum_non_taxable_amount)
for scale in regimen.scales:
if (taxable_amount >= scale.start_amount and
taxable_amount <= scale.end_amount):
res['rate'] = scale.rate
res['scale_non_taxable_amount'] = (
scale.minimum_non_taxable_amount)
res['scale_fixed_amount'] = (
scale.fixed_withholdable_amount)
return res
if (self.party.ganancias_condition == 'in' or
self.party.company_type == 's_de_h'):
res['rate'] = regimen.rate_registered
else:
res['rate'] = regimen.rate_non_registered
return res
def _calculate_withholding_iibb(self, context={}):
pool = Pool()
TaxWithholdingSubmitted = pool.get('account.retencion.efectuada')
quantize = Decimal(10) ** -Decimal(2)
withholding_data = self._get_withholding_data_iibb(context)
for data in withholding_data.values():
withholding_type = data['tax']
payment_amount = data['payment_amount']
accumulated_amount = data['accumulated_amount'] + payment_amount
minimum_non_taxable_amount = (
withholding_type.minimum_non_taxable_amount or Decimal(0))
if accumulated_amount < minimum_non_taxable_amount:
continue
taxable_amount = accumulated_amount - minimum_non_taxable_amount
rate = data['rate']
if not rate:
continue
computed_amount = taxable_amount * rate / Decimal(100)
computed_amount = computed_amount.quantize(quantize)
minimum_withholdable_amount = (
withholding_type.minimum_withholdable_amount or Decimal(0))
if computed_amount < minimum_withholdable_amount:
continue
accumulated_withheld = data['accumulated_withheld']
amount = computed_amount - accumulated_withheld
withholding = TaxWithholdingSubmitted()
withholding.tax = withholding_type
withholding.voucher = self
withholding.party = self.party
withholding.date = self.date
withholding.payment_amount = payment_amount
withholding.accumulated_amount = accumulated_amount
withholding.minimum_non_taxable_amount = minimum_non_taxable_amount
withholding.taxable_amount = taxable_amount
withholding.rate = rate
withholding.computed_amount = computed_amount
withholding.minimum_withholdable_amount = (
minimum_withholdable_amount)
withholding.accumulated_withheld = accumulated_withheld
withholding.amount = amount
withholding.save()
def _get_withholding_data_iibb(self, context={}):
pool = Pool()
Invoice = pool.get('account.invoice')
# Verify conditions
if not self.party.iibb_condition:
raise UserError(gettext(
'account_retencion_ar.msg_party_iibb_condition'))
if self.party.iibb_condition in ['ex', 'rs', 'na', 'cs']:
return {}
if self.party.iva_condition not in ['responsable_inscripto', 'exento']:
return {}
company_address = self.company.party.address_get('invoice')
if not company_address or not company_address.subdivision:
raise UserError(gettext(
'account_retencion_ar.msg_company_subdivision'))
company_subdivision = company_address.subdivision
quantize = Decimal(10) ** -Decimal(2)
res = {}
vat_rate = context.get('vat_rate', Decimal(0.21))
if context:
amount = context.get('amount', Decimal(0))
amount_option = context.get('amount_option', 'add')
for tax in self.company.iibb_regimenes_retencion:
ok = False
if tax.subdivision == company_subdivision:
ok = True
elif self.party.iibb_condition == 'cm':
for x in self.party.iibb_regimenes:
if x.regimen_retencion == tax:
ok = True
if not ok:
continue
# Payment Amount
if context:
if amount_option == 'add':
if tax.id not in res:
res[tax.id] = {
'tax': tax,
'payment_amount': Decimal(0),
'accumulated_amount': Decimal(0),
'accumulated_withheld': Decimal(0),
}
payment_amount = amount / (1 + vat_rate)
res[tax.id]['payment_amount'] += (
payment_amount.quantize(quantize))
else: # amount_option == 'included'
pass
else:
for line in self.lines:
origin = str(line.move_line.move_origin)
if origin[:origin.find(',')] != 'account.invoice':
continue
if not line.amount:
continue
if tax.id not in res:
res[tax.id] = {
'tax': tax,
'payment_amount': Decimal(0),
'accumulated_amount': Decimal(0),
'accumulated_withheld': Decimal(0),
}
invoice = Invoice(line.move_line.move_origin.id)
if line.amount == invoice.total_amount:
payment_amount = invoice.untaxed_amount
else:
payment_amount = (line.amount *
invoice.untaxed_amount / invoice.total_amount)
res[tax.id]['payment_amount'] += payment_amount.quantize(
quantize)
# Verify exemptions
taxes = [x for x in res.keys()]
for exemption in self.party.exemptions:
for tax_id in taxes:
reference = 'account.retencion,%s' % str(tax_id)
if (str(exemption.tax) == reference and
exemption.end_date >= self.date):
del res[tax_id]
# Rate and extra data
for tax_id, tax in res.items():
tax.update(self._get_withholding_extra_data_iibb(tax))
return res
def _get_withholding_extra_data_iibb(self, tax_data):
res = {
'rate': Decimal(0),
}
regimen = tax_data['tax']
for x in self.party.iibb_regimenes:
if x.regimen_retencion == regimen and x.rate_retencion:
res['rate'] = x.rate_retencion
return res
if self.party.iibb_condition in ['in', 'cm']:
res['rate'] = regimen.rate_registered
else:
res['rate'] = regimen.rate_non_registered
return res
def prepare_move_lines(self):
pool = Pool()
Period = pool.get('account.period')
move_lines = super().prepare_move_lines()
if self.voucher_type == 'receipt':
if self.retenciones_soportadas:
for retencion in self.retenciones_soportadas:
move_lines.append({
'debit': retencion.amount,
'credit': Decimal('0.0'),
'account': (retencion.tax.account.id if retencion.tax
else None),
'move': self.move.id,
'journal': self.journal.id,
'period': Period.find(self.company.id, date=self.date),
})
if self.voucher_type == 'payment':
if self.retenciones_efectuadas:
for retencion in self.retenciones_efectuadas:
move_lines.append({
'debit': Decimal('0.0'),
'credit': retencion.amount,
'account': (retencion.tax.account.id if retencion.tax
else None),
'move': self.move.id,
'journal': self.journal.id,
'period': Period.find(self.company.id, date=self.date),
})
return move_lines
@classmethod
@ModelView.button
def post(cls, vouchers):
pool = Pool()
TaxWithholdingReceived = pool.get('account.retencion.soportada')
TaxWithholdingSubmitted = pool.get('account.retencion.efectuada')
super().post(vouchers)
for voucher in vouchers:
if voucher.retenciones_soportadas:
TaxWithholdingReceived.write(list(
voucher.retenciones_soportadas), {
'party': voucher.party.id,
'state': 'held',
})
if voucher.retenciones_efectuadas:
for retencion in voucher.retenciones_efectuadas:
if not retencion.tax.sequence:
raise UserError(gettext(
'account_retencion_ar.msg_missing_retencion_seq'))
TaxWithholdingSubmitted.write([retencion], {
'party': voucher.party.id,
'name': retencion.tax.sequence.get(),
'state': 'issued',
})
@classmethod
@ModelView.button
def cancel(cls, vouchers):
pool = Pool()
TaxWithholdingReceived = pool.get('account.retencion.soportada')
TaxWithholdingSubmitted = pool.get('account.retencion.efectuada')
super().cancel(vouchers)
for voucher in vouchers:
if voucher.retenciones_soportadas:
TaxWithholdingReceived.write(list(
voucher.retenciones_soportadas), {
'party': None,
'state': 'cancelled',
})
if voucher.retenciones_efectuadas:
TaxWithholdingSubmitted.write(list(
voucher.retenciones_efectuadas), {
'party': None,
'state': 'cancelled',
})
class RecalculateWithholdingsStart(ModelView):
'Recalculate withholdings'
__name__ = 'account.voucher.recalculate_withholdings.start'
amount = fields.Numeric('Payment Amount', digits=(16, 2), required=True)
amount_option = fields.Selection([
('add', 'Add withholdings to the amount'),
#('included', 'Withholdings included in the amount'),
], 'Option', required=True, sort=False)
class RecalculateWithholdings(Wizard):
'Recalculate withholdings'
__name__ = 'account.voucher.recalculate_withholdings'
start = StateView(
'account.voucher.recalculate_withholdings.start',
'account_retencion_ar.recalculate_withholdings_start_view', [
Button('Cancelar', 'end', 'tryton-cancel'),
Button('Recalculate', 'recalculate', 'tryton-ok', default=True),
])
recalculate = StateTransition()
def default_start(self, fields):
AccountVoucher = Pool().get('account.voucher')
voucher = AccountVoucher(Transaction().context['active_id'])
res = {
'amount': Decimal(0),
'amount_option': 'add',
}
if voucher:
res['amount'] = voucher.amount
return res
def transition_recalculate(self):
AccountVoucher = Pool().get('account.voucher')
voucher = AccountVoucher(Transaction().context['active_id'])
voucher.delete_withholding()
voucher.calculate_withholdings(context={
'amount': self.start.amount,
'vat_rate': Decimal(0.21),
'amount_option': self.start.amount_option,
})
return 'end'
def end(self):
return 'reload'