diff --git a/account_payment_term_surcharge/README.rst b/account_payment_term_surcharge/README.rst new file mode 100644 index 000000000..1912efd70 --- /dev/null +++ b/account_payment_term_surcharge/README.rst @@ -0,0 +1,75 @@ +.. |company| replace:: ADHOC SA + +.. |company_logo| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-logo.png + :alt: ADHOC SA + :target: https://www.adhoc.com.ar + +.. |icon| image:: https://raw.githubusercontent.com/ingadhoc/maintainer-tools/master/resources/adhoc-icon.png + +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +=========================== +Surcharges on payment terms +=========================== + +This module lets us to defined a set of surcharge term to then +automatically create surcharge invoices via a scheduled action run every day. + +One debit note will be created for each surcharge in the invoice which has been dues that match +with the surcharge terms. + +**TODO:** + +* Agregar impuestos y cuentas analiticas a las lineas de la factura de recargo + + +Installation +============ + +To install this module, you need to: + +#. Only need to install the module + +Configuration +============= + +To configure this module, you need to: + +#. In order to used please configure the surcharge product in account settings + +Usage +===== + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: http://runbot.adhoc.com.ar/ + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues +`_. In case of trouble, please +check there if your issue has already been reported. If you spotted it first, +help us smashing it by providing a detailed and welcomed feedback. + +Credits +======= + +Images +------ + +* |company| |icon| + +Contributors +------------ + +Maintainer +---------- + +|company_logo| + +This module is maintained by the |company|. + +To contribute to this module, please visit https://www.adhoc.com.ar. diff --git a/account_payment_term_surcharge/__init__.py b/account_payment_term_surcharge/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/account_payment_term_surcharge/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/account_payment_term_surcharge/__manifest__.py b/account_payment_term_surcharge/__manifest__.py new file mode 100644 index 000000000..0d468ee56 --- /dev/null +++ b/account_payment_term_surcharge/__manifest__.py @@ -0,0 +1,42 @@ +############################################################################## +# +# Copyright (C) 2015 ADHOC SA (http://www.adhoc.com.ar) +# All Rights Reserved. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## +{ + 'name': 'Surcharges on payment terms', + 'version': "15.0.1.0.0", + 'category': 'Accounting', + 'sequence': 14, + 'summary': 'Allow to add surcharges for invoices on payment terms', + 'author': 'ADHOC SA', + 'website': 'www.adhoc.com.ar', + 'license': 'AGPL-3', + 'depends': [ + 'account', + 'account_debit_note', + ], + 'data': [ + 'views/account_payment_term_view.xml', + 'views/account_payment_term_surcharge_view.xml', + 'wizard/res_config_settings_views.xml', + 'security/ir.model.access.csv', + 'data/ir_cron_data.xml' + ], + 'installable': True, + 'application': False, +} diff --git a/account_payment_term_surcharge/data/ir_cron_data.xml b/account_payment_term_surcharge/data/ir_cron_data.xml new file mode 100644 index 000000000..1f94b70d6 --- /dev/null +++ b/account_payment_term_surcharge/data/ir_cron_data.xml @@ -0,0 +1,15 @@ + + + + + Create Surcharges Invoices + + 1 + days + -1 + + model._cron_recurring_surcharges_invoices() + code + + + diff --git a/account_payment_term_surcharge/models/__init__.py b/account_payment_term_surcharge/models/__init__.py new file mode 100644 index 000000000..e6562c4c9 --- /dev/null +++ b/account_payment_term_surcharge/models/__init__.py @@ -0,0 +1,4 @@ +from . import account_payment_term +from . import account_payment_term_surcharge +from . import account_move +from . import res_company diff --git a/account_payment_term_surcharge/models/account_move.py b/account_payment_term_surcharge/models/account_move.py new file mode 100644 index 000000000..7d6949d39 --- /dev/null +++ b/account_payment_term_surcharge/models/account_move.py @@ -0,0 +1,97 @@ +from odoo import fields, models, _ +from odoo.exceptions import UserError + +import logging +_logger = logging.getLogger(__name__) + + +class AccountMove(models.Model): + _inherit = 'account.move' + + def _get_payment_term_surcharges(self): + result = [] + for surcharge in self.invoice_payment_term_id.surcharge_ids: + result.append({'date': surcharge._calculate_date(self.invoice_date), 'surcharge': surcharge.surcharge}) + result.sort(key=lambda x: x['date']) + return result + + def _cron_recurring_surcharges_invoices(self): + _logger.info('Running Surcharges Invoices Cron Job') + self.search([ + ('invoice_payment_term_id.surcharge_ids', '!=', False), + ('state', '=', 'posted'), + ('payment_state', '=', 'not_paid')], + # buscamos facturas que tengan surcharges, esten posteadas y aun no pagadas + ).create_surcharges_invoices() + + def create_surcharges_invoices(self): + for rec in self: + _logger.info( + 'Creating Surcharges Invoices (id: %s, company: %s)', rec.id, + rec.company_id.name) + current_date = fields.Date.context_today(self) + surcharges = rec._get_payment_term_surcharges() + for surcharge in surcharges: + if surcharge.get('date') <= current_date and surcharge.get('date') not in rec.debit_note_ids.mapped('invoice_date'): + # si tiene un surcharge el dia de hoy, se evalua que no tenga notas de debito + # con fecha de hoy, en caso de que tenga, se corre el create_invoice + rec.create_surcharge_invoice(surcharge) + + def create_surcharge_invoice(self, surcharge): + self.ensure_one() + product = self.company_id.payment_term_surcharge_product_id + if not product: + raise UserError('Atención, debes configurar un producto por defecto para que aplique a la hora de crear las facturas de recargo') + debt = self.amount_residual + surcharge_percent = surcharge.get('surcharge') + to_date = surcharge.get('date') + move_debit_note_wiz = self.env['account.debit.note'].with_context(active_model="account.move", + active_ids=self.ids).create({ + 'date': to_date, + 'reason': 'Surcharge Invoice', + }) + debit_note = self.env['account.move'].browse(move_debit_note_wiz.create_debit().get('res_id')) + debit_note.narration = product.name + '.\n' + self.prepare_info(to_date, debt, surcharge.get('surcharge')) + debit_note.write({'invoice_line_ids': self._prepare_surcharge_line(product, debt, to_date, surcharge_percent)}) + if self.company_id.payment_term_surcharge_invoice_auto_post: + try: + debit_note.action_post() + except Exception as exp: + _logger.error( + "Something went wrong validating " + "surcharge invoice: {}".format(exp)) + raise exp + + def prepare_info(self, to_date, debt, surcharge): + self.ensure_one() + # Format date to customer language + lang_code = self.env.context.get('lang', self.env.user.lang) + lang = self.env['res.lang']._lang_get(lang_code) + date_format = lang.date_format + to_date_format = to_date.strftime(date_format) + res = _( + 'Deuda Vencida al %s: %s\n' + 'Tasa de interés: %s') % ( + to_date_format, debt, surcharge) + return res + + def _prepare_surcharge_line(self, product, debt, to_date, surcharge): + self.ensure_one() + partner = self.partner_id + comment = self.prepare_info(to_date, debt, surcharge) + # fpos = partner.property_account_position_id + # taxes = product.taxes_id.filtered( + # lambda r: r.company_id == self.company_id) + # tax_id = fpos.map_tax(taxes, product) + #TODO ver si se agrega el tax manualmente o no + invoice_line_vals = [(0, 0, { + "product_id": product.id, + "quantity": 1.0, + "price_unit": (surcharge / 100) * debt, + "partner_id": partner.id, + "name": product.name + '.\n' + comment, + # "analytic_account_id": self.env.context.get('analytic_id', False), + # "tax_ids": [(6, 0, tax_id.ids)] + })] + + return invoice_line_vals diff --git a/account_payment_term_surcharge/models/account_payment_term.py b/account_payment_term_surcharge/models/account_payment_term.py new file mode 100644 index 000000000..f8d214330 --- /dev/null +++ b/account_payment_term_surcharge/models/account_payment_term.py @@ -0,0 +1,11 @@ +from odoo import fields, models + + +class AccountPaymentTerm(models.Model): + _inherit = "account.payment.term" + + surcharge_ids = fields.One2many( + 'account.payment.term.surcharge', + 'payment_term_id', string='Surcharges', + copy=True, + ) diff --git a/account_payment_term_surcharge/models/account_payment_term_surcharge.py b/account_payment_term_surcharge/models/account_payment_term_surcharge.py new file mode 100644 index 000000000..3c5030592 --- /dev/null +++ b/account_payment_term_surcharge/models/account_payment_term_surcharge.py @@ -0,0 +1,62 @@ +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError +from dateutil.relativedelta import relativedelta + + +class AccountPaymentTermSurcharge(models.Model): + + _name = 'account.payment.term.surcharge' + _description = 'Payment Terms Surcharge' + _order = 'sequence, id' + + payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', required=True, index=True, ondelete='cascade') + surcharge = fields.Float(string="Surcharge [%]") + days = fields.Integer(string='Number of Days', required=True, default=0) + day_of_the_month = fields.Integer(string='Day of the month', help="Day of the month on which the invoice must come to its term. If zero or negative, this value will be ignored, and no specific day will be set. If greater than the last day of a month, this number will instead select the last day of this month.") + option = fields.Selection([ + ('day_after_invoice_date', "days after the invoice date"), + ('after_invoice_month', "days after the end of the invoice month"), + ('day_following_month', "of the following month"), + ('day_current_month', "of the current month"), + ], + default='day_after_invoice_date', required=True, string='Options' + ) + sequence = fields.Integer(default=10, help="Gives the sequence order when displaying a list of payment terms lines.") + + @api.constrains('surcharge') + def _check_percent(self): + for term_surcharge in self: + if (term_surcharge.surcharge < 0.0 or term_surcharge.surcharge > 100.0): + raise ValidationError(_('Percentages on the Payment Terms lines must be between 0 and 100.')) + + @api.constrains('days') + def _check_days(self): + for term_surcharge in self: + if term_surcharge.option in ('day_following_month', 'day_current_month') and term_surcharge.days <= 0: + raise ValidationError(_("The day of the month used for this term must be strictly positive.")) + elif term_surcharge.days < 0: + raise ValidationError(_("The number of days used for a payment term cannot be negative.")) + + @api.onchange('option') + def _onchange_option(self): + if self.option in ('day_current_month', 'day_following_month'): + self.days = 0 + + def _calculate_date(self, date_ref=None): + ''' Se retorna la fecha de un recargo segun una fecha dada, esto se hace + teniendo en cuenta la configuracion propia del recargo. ''' + date_ref = date_ref or fields.Date.today() + next_date = fields.Date.from_string(date_ref) + if self.option == 'day_after_invoice_date': + next_date += relativedelta(days=self.days) + if self.day_of_the_month > 0: + months_delta = (self.day_of_the_month < next_date.day) and 1 or 0 + next_date += relativedelta(day=self.day_of_the_month, months=months_delta) + elif self.option == 'after_invoice_month': + next_first_date = next_date + relativedelta(day=1, months=1) # Getting 1st of next month + next_date = next_first_date + relativedelta(days=self.days - 1) + elif self.option == 'day_following_month': + next_date += relativedelta(day=self.days, months=1) + elif self.option == 'day_current_month': + next_date += relativedelta(day=self.days, months=0) + return next_date diff --git a/account_payment_term_surcharge/models/res_company.py b/account_payment_term_surcharge/models/res_company.py new file mode 100644 index 000000000..8ece03b71 --- /dev/null +++ b/account_payment_term_surcharge/models/res_company.py @@ -0,0 +1,11 @@ +from odoo import api, exceptions, fields, models, _ + +class ResCompany(models.Model): + _inherit = 'res.company' + + payment_term_surcharge_product_id = fields.Many2one( + 'product.product', + 'Surcharge Product', + ) + + payment_term_surcharge_invoice_auto_post = fields.Boolean() diff --git a/account_payment_term_surcharge/security/ir.model.access.csv b/account_payment_term_surcharge/security/ir.model.access.csv new file mode 100644 index 000000000..16f5738fb --- /dev/null +++ b/account_payment_term_surcharge/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_account_payment_term_surcharge,account.payment.term.surcharge,model_account_payment_term_surcharge,account.group_account_user,1,0,0,0 +access_account_payment_term_surcharge_manager,account.payment.term.surcharge,model_account_payment_term_surcharge,account.group_account_manager,1,1,1,1 diff --git a/account_payment_term_surcharge/views/account_payment_term_surcharge_view.xml b/account_payment_term_surcharge/views/account_payment_term_surcharge_view.xml new file mode 100644 index 000000000..4d7752f9b --- /dev/null +++ b/account_payment_term_surcharge/views/account_payment_term_surcharge_view.xml @@ -0,0 +1,49 @@ + + + + + account.payment.term.surcharge.tree + account.payment.term.surcharge + + + + + + + + + + + + + account.payment.term.surcharge.form + account.payment.term.surcharge + +
+ + + + + + +

Due Date Computation

+
+
+
+
+ +
+
+
+
+ + + diff --git a/account_payment_term_surcharge/views/account_payment_term_view.xml b/account_payment_term_surcharge/views/account_payment_term_view.xml new file mode 100644 index 000000000..6b87831c5 --- /dev/null +++ b/account_payment_term_surcharge/views/account_payment_term_view.xml @@ -0,0 +1,16 @@ + + + + + account.payment.term.form + account.payment.term + + + + + + + + + + diff --git a/account_payment_term_surcharge/wizard/__init__.py b/account_payment_term_surcharge/wizard/__init__.py new file mode 100644 index 000000000..0deb68c46 --- /dev/null +++ b/account_payment_term_surcharge/wizard/__init__.py @@ -0,0 +1 @@ +from . import res_config_settings diff --git a/account_payment_term_surcharge/wizard/res_config_settings.py b/account_payment_term_surcharge/wizard/res_config_settings.py new file mode 100644 index 000000000..f856fb602 --- /dev/null +++ b/account_payment_term_surcharge/wizard/res_config_settings.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + payment_term_surcharge_product_id = fields.Many2one( + 'product.product', + related='company_id.payment_term_surcharge_product_id', + string="Producto por defecto para los recargos", readonly=False + ) + + payment_term_surcharge_invoice_auto_post = fields.Boolean( + related='company_id.payment_term_surcharge_invoice_auto_post', + string= 'Validad automaticamente las facturas', readonly=False + ) diff --git a/account_payment_term_surcharge/wizard/res_config_settings_views.xml b/account_payment_term_surcharge/wizard/res_config_settings_views.xml new file mode 100644 index 000000000..11547c9b9 --- /dev/null +++ b/account_payment_term_surcharge/wizard/res_config_settings_views.xml @@ -0,0 +1,38 @@ + + + + + res.config.settings.form.inherit + res.config.settings + + +
+
+
+
+ Default payment term surcharge product + +
+ Producto por defecto para realizar recargos +
+
+ +
+
+
+
+
+ +
+
+
+
+
+ + +