Add support for automatic withholdings

This commit is contained in:
Adrián Bernardi 2023-09-26 21:20:02 -03:00
parent 4c97b3b965
commit 21f03eaf85
36 changed files with 1136 additions and 24 deletions

View File

@ -1,3 +1,4 @@
* Add support for automatic withholdings
* Improves voucher form
Version 6.0.0 - 2021-09-10

View File

@ -5,13 +5,29 @@
from trytond.pool import Pool
from . import account_retencion_ar
from . import account_voucher_ar
from . import party
from . import company
from . import product
def register():
Pool.register(
account_retencion_ar.TaxWithholdingType,
account_retencion_ar.TaxWithholdingTypeSequence,
account_retencion_ar.TaxWithholdingTypeScale,
account_retencion_ar.TaxWithholdingSubmitted,
account_retencion_ar.TaxWithholdingReceived,
account_voucher_ar.AccountVoucher,
account_voucher_ar.RecalculateWithholdingsStart,
party.Party,
party.PartyExemption,
party.PartyWithholdingIIBB,
company.Company,
company.CompanyWithholdingIIBB,
product.Category,
product.Product,
invoice.InvoiceLine,
module='account_retencion_ar', type_='model')
Pool.register(
account_voucher_ar.RecalculateWithholdings,
module='account_retencion_ar', type_='wizard')

View File

@ -2,10 +2,11 @@
# The COPYRIGHT file at the top level of this repository contains
# the full copyright notices and license terms.
from decimal import Decimal
from sql import Null
from trytond import backend
from trytond.model import ModelView, ModelSQL, fields
from trytond.pool import Pool
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval, Bool, Not, Id
from trytond.transaction import Transaction
from trytond.exceptions import UserError
@ -24,6 +25,7 @@ class TaxWithholdingType(ModelSQL, ModelView, CompanyMultiValueMixin):
('efectuada', 'Submitted'),
('soportada', 'Received'),
], 'Type', required=True)
tax = fields.Selection('get_tax', 'Tax', required=True, sort=False)
account = fields.Many2One('account.account', 'Account', required=True,
domain=[
('type', '!=', None),
@ -40,6 +42,38 @@ class TaxWithholdingType(ModelSQL, ModelView, CompanyMultiValueMixin):
states={'invisible': Eval('type') != 'efectuada'}))
sequences = fields.One2Many('account.retencion.sequence',
'retencion', 'Sequences')
regime_code = fields.Char('Regime code')
regime_name = fields.Char('Regime name')
subdivision = fields.Many2One('country.subdivision', 'Subdivision',
domain=[('country.code', '=', 'AR')],
states={'invisible': Eval('tax') != 'iibb'})
minimum_non_taxable_amount = fields.Numeric('Minimum Non-Taxable Amount',
digits=(16, 2))
rate_registered = fields.Numeric('% Withholding to Registered',
digits=(14, 10))
rate_non_registered = fields.Numeric('% Withholding to Non-Registered',
digits=(14, 10))
minimum_withholdable_amount = fields.Numeric('Minimum Amount to Withhold',
digits=(16, 2))
scales = fields.One2Many('account.retencion.scale', 'retencion', 'Scales')
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('type', 'ASC'))
cls._order.insert(1, ('name', 'ASC'))
cls._order.insert(2, ('regime_code', 'ASC'))
@classmethod
def get_tax(cls):
selection = [
('iva', 'IVA'),
('gana', 'Ganancias'),
('suss', 'SUSS'),
('iibb', 'Ingresos Brutos'),
('otro', 'Otro'),
]
return selection
@classmethod
def multivalue_model(cls, field):
@ -48,6 +82,18 @@ class TaxWithholdingType(ModelSQL, ModelView, CompanyMultiValueMixin):
return pool.get('account.retencion.sequence')
return super().multivalue_model(field)
def get_rec_name(self, name):
if self.regime_name:
return '%s - %s' % (self.name, self.regime_name)
return self.name
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('//group[@id="calculation"]', 'states',
{'invisible': Eval('type') != 'efectuada'}),
]
class TaxWithholdingTypeSequence(ModelSQL, CompanyValueMixin):
'Tax Withholding Type Sequence'
@ -80,6 +126,26 @@ class TaxWithholdingTypeSequence(ModelSQL, CompanyValueMixin):
parent='retencion', fields=fields)
class TaxWithholdingTypeScale(ModelSQL, ModelView):
'Tax Withholding Type Scale'
__name__ = 'account.retencion.scale'
retencion = fields.Many2One('account.retencion', 'Tax Withholding Type',
ondelete='CASCADE')
start_amount = fields.Numeric('Amount from', digits=(16, 2))
end_amount = fields.Numeric('Amount up to', digits=(16, 2))
fixed_withholdable_amount = fields.Numeric('Fixed Amount to Withhold',
digits=(16, 2))
rate = fields.Numeric('% Withholding', digits=(14, 10))
minimum_non_taxable_amount = fields.Numeric('Non-Taxable Base',
digits=(16, 2))
@classmethod
def __setup__(cls):
super().__setup__()
cls._order.insert(0, ('start_amount', 'ASC'))
class TaxWithholdingSubmitted(ModelSQL, ModelView):
'Tax Withholding Submitted'
__name__ = 'account.retencion.efectuada'
@ -95,7 +161,10 @@ class TaxWithholdingSubmitted(ModelSQL, ModelView):
'on_change_with_name_required')
tax = fields.Many2One('account.retencion', 'Type',
domain=[('type', '=', 'efectuada')], states=_states)
aliquot = fields.Float('Aliquot')
regime_code = fields.Function(fields.Char('Regime code'),
'get_tax_field')
regime_name = fields.Function(fields.Char('Regime name'),
'get_tax_field')
date = fields.Date('Date', required=True, states=_states)
voucher = fields.Many2One('account.voucher', 'Voucher', readonly=True)
party = fields.Many2One('party.party', 'Party', states=_states)
@ -104,6 +173,26 @@ class TaxWithholdingSubmitted(ModelSQL, ModelView):
('issued', 'Issued'),
('cancelled', 'Cancelled'),
], 'State', readonly=True)
payment_amount = fields.Numeric('Payment Amount',
digits=(16, 2), readonly=True)
accumulated_amount = fields.Numeric('Accumulated Amount',
digits=(16, 2), readonly=True)
minimum_non_taxable_amount = fields.Numeric('Minimum Non-Taxable Amount',
digits=(16, 2), readonly=True)
scale_non_taxable_amount = fields.Numeric('Non-Taxable Base (Scale)',
digits=(16, 2), readonly=True)
taxable_amount = fields.Numeric('Taxable Amount',
digits=(16, 2), readonly=True)
rate = fields.Numeric('% Withholding',
digits=(14, 10), readonly=True)
scale_fixed_amount = fields.Numeric('Fixed Amount to Withhold (Scale)',
digits=(16, 2), readonly=True)
computed_amount = fields.Numeric('Computed Amount',
digits=(16, 2), readonly=True)
minimum_withholdable_amount = fields.Numeric('Minimum Amount to Withhold',
digits=(16, 2), readonly=True)
accumulated_withheld = fields.Numeric('Accumulated Withheld',
digits=(16, 2), readonly=True)
amount = fields.Numeric('Amount', digits=(16, 2), required=True,
states=_states)
@ -113,10 +202,19 @@ class TaxWithholdingSubmitted(ModelSQL, ModelView):
def __register__(cls, module_name):
cursor = Transaction().connection.cursor()
sql_table = cls.__table__()
table_h = cls.__table_handler__(module_name)
aliquot_exist = table_h.column_exist('aliquot')
super().__register__(module_name)
cursor.execute(*sql_table.update(
[sql_table.state], ['cancelled'],
where=sql_table.state == 'canceled'))
if aliquot_exist:
cursor.execute(*sql_table.update(
[sql_table.rate], [sql_table.aliquot.cast('NUMERIC')],
where=sql_table.aliquot != Null))
table_h.drop_column('aliquot')
@staticmethod
def default_state():
@ -144,11 +242,13 @@ class TaxWithholdingSubmitted(ModelSQL, ModelView):
@classmethod
def check_delete(cls, retenciones):
if Transaction().context.get('delete_calculated', False):
return
for retencion in retenciones:
if retencion.voucher:
raise UserError(gettext(
'account_retencion_ar.msg_not_delete',
retention=retencion.name))
retencion=retencion.name))
@classmethod
def copy(cls, retenciones, default=None):
@ -160,6 +260,27 @@ class TaxWithholdingSubmitted(ModelSQL, ModelView):
current_default['voucher'] = None
return super().copy(retenciones, default=current_default)
@classmethod
def get_tax_field(cls, retenciones, names):
result = {}
for name in names:
result[name] = {}
if cls._fields[name]._type == 'many2one':
for r in retenciones:
field = getattr(r.tax, name, None)
result[name][r.id] = field.id if field else None
elif cls._fields[name]._type == 'boolean':
for r in retenciones:
result[name][r.id] = getattr(r.tax, name, False)
else:
for r in retenciones:
result[name][r.id] = getattr(r.tax, name, None)
return result
@classmethod
def search_tax_field(cls, name, clause):
return [('tax.' + name,) + tuple(clause[1:])]
class TaxWithholdingReceived(ModelSQL, ModelView):
'Tax Withholding Received'
@ -216,7 +337,7 @@ class TaxWithholdingReceived(ModelSQL, ModelView):
if retencion.voucher:
raise UserError(gettext(
'account_retencion_ar.msg_not_delete',
retention=retencion.name))
retencion=retencion.name))
@classmethod
def copy(cls, retenciones, default=None):

View File

@ -55,6 +55,19 @@
<menuitem name="Tax Withholdings" parent="account.menu_account"
id="menu_retenciones" sequence="22"/>
<!-- Tax Withholding Type Scales -->
<record model="ir.ui.view" id="account_retencion_scale_form">
<field name="model">account.retencion.scale</field>
<field name="type">form</field>
<field name="name">account_retencion_scale_form</field>
</record>
<record model="ir.ui.view" id="account_retencion_scale_tree">
<field name="model">account.retencion.scale</field>
<field name="type">tree</field>
<field name="name">account_retencion_scale_tree</field>
</record>
<!-- Tax Withholdings Submitted -->
<record model="ir.ui.view" id="account_retencion_efectuada_form">

View File

@ -2,10 +2,13 @@
# 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 ModelView, fields
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
from trytond.pyson import Eval, Or, And
from trytond.transaction import Transaction
from trytond.exceptions import UserError
from trytond.i18n import gettext
@ -30,6 +33,48 @@ class AccountVoucher(metaclass=PoolMeta):
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': And(
Eval('state') != 'calculated',
Eval('amount_to_pay', 0) > Eval('amount', 0)),
'depends': ['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)
@ -43,9 +88,421 @@ class AccountVoucher(metaclass=PoolMeta):
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 {}
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':
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 {}
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()
Period = Pool().get('account.period')
if self.voucher_type == 'receipt':
if self.retenciones_soportadas:
for retencion in self.retenciones_soportadas:
@ -124,3 +581,49 @@ class AccountVoucher(metaclass=PoolMeta):
'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)
@staticmethod
def default_amount_option():
return 'add'
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 transition_recalculate(self):
AccountVoucher = Pool().get('account.voucher')
voucher = AccountVoucher(Transaction().context['active_id'])
if not voucher:
return {}
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'

View File

@ -2,6 +2,8 @@
<tryton>
<data>
<!-- Account Voucher -->
<record model="ir.ui.view" id="view_voucher_retencion_form">
<field name="model">account.voucher</field>
<field name="inherit" ref="account_voucher_ar.account_voucher_form"/>
@ -20,5 +22,50 @@
<field name="name">retenciones_soportadas_tree</field>
</record>
<record model="ir.model.button" id="voucher_calculate_button">
<field name="name">calculate</field>
<field name="string">Calculate withholdings</field>
<field name="model" search="[('model', '=', 'account.voucher')]"/>
</record>
<record model="ir.model.button-res.group"
id="voucher_calculate_button_group_account">
<field name="button" ref="voucher_calculate_button"/>
<field name="group" ref="account.group_account"/>
</record>
<record model="ir.model.button" id="voucher_draft_button">
<field name="name">draft</field>
<field name="string">Draft</field>
<field name="model" search="[('model', '=', 'account.voucher')]"/>
</record>
<record model="ir.model.button-res.group"
id="voucher_draft_button_group_account">
<field name="button" ref="voucher_draft_button"/>
<field name="group" ref="account.group_account"/>
</record>
<record model="ir.model.button" id="voucher_recalculate_button">
<field name="name">recalculate</field>
<field name="string">Recalculate withholdings</field>
<field name="model" search="[('model', '=', 'account.voucher')]"/>
</record>
<record model="ir.model.button-res.group"
id="voucher_recalculate_button_group_account">
<field name="button" ref="voucher_recalculate_button"/>
<field name="group" ref="account.group_account"/>
</record>
<!-- Wizard: Recalculate withholdings -->
<record model="ir.ui.view" id="recalculate_withholdings_start_view">
<field name="model">account.voucher.recalculate_withholdings.start</field>
<field name="type">form</field>
<field name="name">recalculate_withholdings_start_form</field>
</record>
<record model="ir.action.wizard" id="wizard_recalculate_withholdings">
<field name="name">Recalculate withholdings</field>
<field name="wiz_name">account.voucher.recalculate_withholdings</field>
<field name="model">account.voucher</field>
</record>
</data>
</tryton>

32
company.py Normal file
View File

@ -0,0 +1,32 @@
# 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 trytond.model import ModelSQL, fields
from trytond.pool import PoolMeta
from trytond.pyson import Eval
class Company(metaclass=PoolMeta):
__name__ = 'company.company'
ganancias_agente_retencion = fields.Boolean(
'Agente de Retención de Impuesto a las Ganancias')
ganancias_regimen_retencion = fields.Many2One('account.retencion',
'Régimen de Ganancias',
domain=[('type', '=', 'efectuada'), ('tax', '=', 'gana')])
iibb_agente_retencion = fields.Boolean(
'Agente de Retención de Impuesto a los Ingresos Brutos')
iibb_regimenes_retencion = fields.Many2Many('company.retencion.iibb',
'company', 'regimen', 'Jurisdicciones de Ingresos Brutos',
domain=[('type', '=', 'efectuada'), ('tax', '=', 'iibb')])
class CompanyWithholdingIIBB(ModelSQL):
'Régimen de Ingresos Brutos de Empresa'
__name__ = 'company.retencion.iibb'
company = fields.Many2One('company.company', 'Company',
ondelete='CASCADE', required=True)
regimen = fields.Many2One('account.retencion', 'Régimen',
ondelete='CASCADE', required=True)

12
company.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0"?>
<tryton>
<data>
<record model="ir.ui.view" id="company_view_form">
<field name="model">company.company</field>
<field name="inherit" ref="company.company_view_form"/>
<field name="name">company_form</field>
</record>
</data>
</tryton>

33
invoice.py Normal file
View File

@ -0,0 +1,33 @@
# 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 trytond.model import fields
from trytond.pool import PoolMeta
from trytond.pyson import Eval
from trytond.exceptions import UserError
from trytond.i18n import gettext
class InvoiceLine(metaclass=PoolMeta):
__name__ = 'account.invoice.line'
ganancias_regimen = fields.Many2One('account.retencion',
'Régimen Ganancias',
domain=[('type', '=', 'efectuada'), ('tax', '=', 'gana')],
states={'invisible': Eval('invoice_type') != 'in'})
@fields.depends('product', 'invoice', '_parent_invoice.type',
'invoice_type')
def on_change_product(self):
super().on_change_product()
if not self.product:
return
if self.invoice and self.invoice.type:
type_ = self.invoice.type
else:
type_ = self.invoice_type
if type_ != 'in':
return
self.ganancias_regimen = self.product.ganancias_regimen_used

14
invoice.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- Invoice Line -->
<record model="ir.ui.view" id="invoice_line_view_form">
<field name="model">account.invoice.line</field>
<field name="inherit" ref="account_invoice.invoice_line_view_form"/>
<field name="name">invoice_line_form</field>
</record>
</data>
</tryton>

View File

@ -2,10 +2,31 @@
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_not_delete">
<field name="text">You cannot delete retention "%(retention)s" because it is associated to a voucher</field>
<field name="text">You cannot delete Tax Withholding "%(retencion)s" because it is associated with a Voucher</field>
</record>
<record model="ir.message" id="msg_missing_retencion_seq">
<field name="text">You must set a sequence to Retencion efecutada.</field>
<field name="text">You must define the Sequence for the Tax Withholding</field>
</record>
<record model="ir.message" id="msg_print_not_issued">
<field name="text">You cannot print Tax Withholding "%(retencion)s" because it is not issued</field>
</record>
<record model="ir.message" id="msg_party_iva_condition">
<field name="text">El Tercero no tiene definida su Condición ante IVA</field>
</record>
<record model="ir.message" id="msg_party_iibb_condition">
<field name="text">El Tercero no tiene definida su Condición ante Ingresos Brutos</field>
</record>
<record model="ir.message" id="msg_party_ganancias_condition">
<field name="text">El Tercero no tiene definida su Condición ante Ganancias</field>
</record>
<record model="ir.message" id="msg_party_subdivision">
<field name="text">El Tercero no tiene definida una Provincia/Jurisdicción</field>
</record>
<record model="ir.message" id="msg_party_iibb_regimenes">
<field name="text">The Party does not have any Regime defined for Ingresos Brutos</field>
</record>
<record model="ir.message" id="msg_company_subdivision">
<field name="text">La empresa no tiene definida una Provincia/Jurisdicción</field>
</record>
</data>
</tryton>

61
party.py Normal file
View File

@ -0,0 +1,61 @@
# 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 trytond.model import ModelSQL, ModelView, fields
from trytond.pool import PoolMeta
class Party(metaclass=PoolMeta):
__name__ = 'party.party'
exemptions = fields.One2Many('party.exemption',
'party', 'Exenciones de Retención y Percepción')
ganancias_regimen = fields.Many2One('account.retencion',
'Régimen de Ganancias',
domain=[('type', '=', 'efectuada'), ('tax', '=', 'gana')])
iibb_regimenes = fields.One2Many('party.retencion.iibb',
'party', 'Jurisdicciones de Ingresos Brutos')
class PartyExemption(ModelSQL, ModelView):
'Exención de Retención/Percepción de Tercero'
__name__ = 'party.exemption'
party = fields.Many2One('party.party', 'Party',
ondelete='CASCADE', required=True)
tax = fields.Reference('Type', [
('account.retencion', 'Retención'),
('account.tax', 'Percepción'),
],
required=True,
domain={
'account.retencion': [
('type', '=', 'efectuada')
],
'account.tax': [
('group.afip_kind', 'in',
['nacional', 'provincial', 'municipal']),
('group.kind', '=', 'sale'),
],
})
end_date = fields.Date('Valid until', required=True)
class PartyWithholdingIIBB(ModelSQL, ModelView):
'Régimen de Ingresos Brutos de Tercero'
__name__ = 'party.retencion.iibb'
party = fields.Many2One('party.party', 'Party',
ondelete='CASCADE', required=True)
regimen_retencion = fields.Many2One('account.retencion',
'Retención', ondelete='CASCADE',
domain=[('type', '=', 'efectuada'), ('tax', '=', 'iibb')])
rate_retencion = fields.Numeric('% Retención', digits=(14, 10))
regimen_percepcion = fields.Many2One('account.tax',
'Percepción', ondelete='CASCADE',
domain=[
('group.afip_kind', '=', 'provincial'),
('group.kind', '=', 'sale'),
])
rate_percepcion = fields.Numeric('% Percepción', digits=(14, 10))

40
party.xml Normal file
View File

@ -0,0 +1,40 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- Party -->
<record model="ir.ui.view" id="party_view_form">
<field name="model">party.party</field>
<field name="inherit" ref="party.party_view_form"/>
<field name="name">party_form</field>
</record>
<!-- Exención de Retención/Percepción de Tercero -->
<record model="ir.ui.view" id="party_exemption_view_form">
<field name="model">party.exemption</field>
<field name="type">form</field>
<field name="name">party_exemption_form</field>
</record>
<record model="ir.ui.view" id="party_exemption_view_tree">
<field name="model">party.exemption</field>
<field name="type">tree</field>
<field name="name">party_exemption_tree</field>
</record>
<!-- Régimen de Ingresos Brutos de Tercero -->
<record model="ir.ui.view" id="party_retencion_iibb_view_form">
<field name="model">party.retencion.iibb</field>
<field name="type">form</field>
<field name="name">party_retencion_iibb_form</field>
</record>
<record model="ir.ui.view" id="party_retencion_iibb_view_tree">
<field name="model">party.retencion.iibb</field>
<field name="type">tree</field>
<field name="name">party_retencion_iibb_tree</field>
</record>
</data>
</tryton>

27
product.py Normal file
View File

@ -0,0 +1,27 @@
# 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 trytond.model import fields
from trytond.pool import PoolMeta
class Category(metaclass=PoolMeta):
__name__ = 'product.category'
ganancias_regimen = fields.Many2One('account.retencion',
'Régimen de Ganancias',
domain=[('type', '=', 'efectuada'), ('tax', '=', 'gana')])
class Product(metaclass=PoolMeta):
__name__ = 'product.product'
ganancias_regimen_used = fields.Function(fields.Many2One(
'account.retencion', 'Régimen Ganancias'),
'get_ganancias_regimen_used')
def get_ganancias_regimen_used(self, name):
if self.account_category:
if self.account_category.ganancias_regimen:
return self.account_category.ganancias_regimen.id

14
product.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<tryton>
<data>
<!-- Product Category -->
<record model="ir.ui.view" id="category_view_form">
<field name="model">product.category</field>
<field name="inherit" ref="product.category_view_form"/>
<field name="name">category_form</field>
</record>
</data>
</tryton>

View File

@ -5,4 +5,7 @@ depends:
xml:
account_retencion_ar.xml
account_voucher_ar.xml
party.xml
company.xml
product.xml
message.xml

View File

@ -1,17 +1,41 @@
<?xml version="1.0"?>
<form>
<label name="tax"/>
<field name="tax" widget="selection"/>
<label name="name"/>
<field name="name"/>
<label name="amount"/>
<field name="amount"/>
<label name="aliquot"/>
<field name="aliquot"/>
<label name="date"/>
<field name="date"/>
<label name="tax"/>
<field name="tax" widget="selection"/>
<label name="party"/>
<field name="party"/>
<label name="state"/>
<field name="state"/>
<group id="withholding" colspan="4">
<label name="regime_name"/>
<field name="regime_name"/>
<label name="voucher"/>
<field name="voucher"/>
<label name="payment_amount"/>
<field name="payment_amount"/>
<label name="accumulated_amount"/>
<field name="accumulated_amount"/>
<label name="minimum_non_taxable_amount"/>
<field name="minimum_non_taxable_amount"/>
<label name="taxable_amount"/>
<field name="taxable_amount"/>
<label name="scale_non_taxable_amount"/>
<field name="scale_non_taxable_amount"/>
<label name="rate"/>
<field name="rate"/>
<label name="scale_fixed_amount"/>
<field name="scale_fixed_amount"/>
<label name="computed_amount"/>
<field name="computed_amount"/>
<label name="minimum_withholdable_amount"/>
<field name="minimum_withholdable_amount"/>
<label name="accumulated_withheld"/>
<field name="accumulated_withheld"/>
</group>
</form>

View File

@ -1,10 +1,9 @@
<?xml version="1.0"?>
<tree>
<field name="tax"/>
<field name="name"/>
<field name="amount"/>
<field name="aliquot"/>
<field name="date"/>
<field name="tax"/>
<field name="party"/>
<field name="state"/>
</tree>

View File

@ -2,10 +2,29 @@
<form>
<label name="name"/>
<field name="name"/>
<label name="account"/>
<field name="account"/>
<label name="tax"/>
<field name="tax"/>
<label name="type"/>
<field name="type"/>
<label name="account"/>
<field name="account"/>
<label name="sequence"/>
<field name="sequence"/>
<label name="subdivision"/>
<field name="subdivision"/>
<label name="regime_code"/>
<field name="regime_code"/>
<label name="regime_name"/>
<field name="regime_name"/>
<group id="calculation" colspan="4" string="Automatic Calculation">
<label name="minimum_non_taxable_amount"/>
<field name="minimum_non_taxable_amount"/>
<label name="minimum_withholdable_amount"/>
<field name="minimum_withholdable_amount"/>
<label name="rate_registered"/>
<field name="rate_registered"/>
<label name="rate_non_registered"/>
<field name="rate_non_registered"/>
<field name="scales" colspan="4"/>
</group>
</form>

View File

@ -0,0 +1,13 @@
<?xml version="1.0"?>
<form>
<label name="start_amount"/>
<field name="start_amount"/>
<label name="end_amount"/>
<field name="end_amount"/>
<label name="fixed_withholdable_amount"/>
<field name="fixed_withholdable_amount"/>
<label name="rate"/>
<field name="rate"/>
<label name="minimum_non_taxable_amount"/>
<field name="minimum_non_taxable_amount"/>
</form>

View File

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<tree>
<field name="start_amount"/>
<field name="end_amount"/>
<field name="fixed_withholdable_amount"/>
<field name="rate"/>
<field name="minimum_non_taxable_amount"/>
</tree>

View File

@ -1,13 +1,13 @@
<?xml version="1.0"?>
<form>
<label name="tax"/>
<field name="tax" widget="selection"/>
<label name="name"/>
<field name="name"/>
<label name="amount"/>
<field name="amount"/>
<label name="date"/>
<field name="date"/>
<label name="tax"/>
<field name="tax" widget="selection"/>
<label name="party"/>
<field name="party"/>
<label name="state"/>

View File

@ -1,9 +1,9 @@
<?xml version="1.0"?>
<tree>
<field name="tax"/>
<field name="name"/>
<field name="amount"/>
<field name="date"/>
<field name="tax"/>
<field name="party"/>
<field name="state"/>
</tree>

View File

@ -1,7 +1,8 @@
<?xml version="1.0"?>
<tree>
<field name="name"/>
<field name="account"/>
<field name="tax"/>
<field name="type"/>
<field name="sequence"/>
<field name="account"/>
<field name="regime_name"/>
</tree>

10
view/category_form.xml Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<data>
<xpath
expr="/form/notebook/page[@id='accounting']/field[@name='supplier_taxes']"
position="after">
<separator id="withholding" string="Withholding" colspan="4"/>
<label name="ganancias_regimen"/>
<field name="ganancias_regimen"/>
</xpath>
</data>

14
view/company_form.xml Normal file
View File

@ -0,0 +1,14 @@
<?xml version="1.0"?>
<data>
<xpath expr="/form/notebook/page[@id='afip']" position="after">
<page id="taxes" string="Withholding and Perception of Taxes">
<label name="ganancias_agente_retencion"/>
<field name="ganancias_agente_retencion"/>
<label name="ganancias_regimen_retencion"/>
<field name="ganancias_regimen_retencion"/>
<label name="iibb_agente_retencion"/>
<field name="iibb_agente_retencion"/>
<field name="iibb_regimenes_retencion" colspan="4"/>
</page>
</xpath>
</data>

View File

@ -0,0 +1,9 @@
<?xml version="1.0"?>
<data>
<xpath
expr="/form/notebook/page[@id='general']/group[@id='taxes_deductible_rate']"
position="after">
<label name="ganancias_regimen"/>
<field name="ganancias_regimen"/>
</xpath>
</data>

View File

@ -0,0 +1,7 @@
<?xml version="1.0"?>
<form>
<label name="tax"/>
<field name="tax"/>
<label name="end_date"/>
<field name="end_date"/>
</form>

View File

@ -0,0 +1,5 @@
<?xml version="1.0"?>
<tree>
<field name="tax"/>
<field name="end_date"/>
</tree>

15
view/party_form.xml Normal file
View File

@ -0,0 +1,15 @@
<?xml version="1.0"?>
<data>
<xpath expr="/form/notebook/page[@id='empresa']" position="after">
<page id="taxes" string="Withholding and Perception of Taxes">
<label name="ganancias_condition"/>
<field name="ganancias_condition"/>
<label name="ganancias_regimen"/>
<field name="ganancias_regimen"/>
<label name="iibb_condition"/>
<field name="iibb_condition"/>
<field name="iibb_regimenes" colspan="4"/>
<field name="exemptions" colspan="4"/>
</page>
</xpath>
</data>

View File

@ -0,0 +1,11 @@
<?xml version="1.0"?>
<form>
<label name="regimen_retencion"/>
<field name="regimen_retencion"/>
<label name="rate_retencion"/>
<field name="rate_retencion"/>
<label name="regimen_percepcion"/>
<field name="regimen_percepcion"/>
<label name="rate_percepcion"/>
<field name="rate_percepcion"/>
</form>

View File

@ -0,0 +1,7 @@
<?xml version="1.0"?>
<tree>
<field name="regimen_retencion"/>
<field name="rate_retencion"/>
<field name="regimen_percepcion"/>
<field name="rate_percepcion"/>
</tree>

View File

@ -0,0 +1,7 @@
<?xml version="1.0"?>
<form>
<label name="amount"/>
<field name="amount"/>
<label name="amount_option"/>
<field name="amount_option"/>
</form>

View File

@ -1,6 +1,6 @@
<?xml version="1.0"?>
<tree>
<field name="name"/>
<field name="tax"/>
<field name="name"/>
<field name="amount"/>
</tree>

View File

@ -1,6 +1,6 @@
<?xml version="1.0"?>
<tree>
<field name="name"/>
<field name="tax"/>
<field name="name"/>
<field name="amount"/>
</tree>

View File

@ -6,4 +6,9 @@
<field name="retenciones_soportadas" colspan="4"
view_ids="account_retencion_ar.retenciones_soportadas_view_tree"/>
</xpath>
<xpath expr="/form/group[@id='buttons']/button[@name='post']" position="before">
<button name="draft"/>
<button name="calculate"/>
<button name="recalculate"/>
</xpath>
</data>