diff --git a/account_check/ANALISIS CHEQUES.ods b/account_check/ANALISIS CHEQUES.ods new file mode 100644 index 00000000..35f1ba3a Binary files /dev/null and b/account_check/ANALISIS CHEQUES.ods differ diff --git a/account_check/__init__.py b/account_check/__init__.py new file mode 100644 index 00000000..9dfc1e2a --- /dev/null +++ b/account_check/__init__.py @@ -0,0 +1,6 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import models +from . import wizard diff --git a/account_check/__manifest__.py b/account_check/__manifest__.py new file mode 100644 index 00000000..0a368506 --- /dev/null +++ b/account_check/__manifest__.py @@ -0,0 +1,36 @@ +{ + 'name': 'Account Check Management', + 'version': '12.0.1.0.0', + 'category': 'Accounting', + 'summary': 'Accounting, Payment, Check, Third, Issue', + 'website': 'www.codequarters.com', + 'author': 'CODEQUARTERS', + 'license': 'AGPL-3', + 'images': [ + ], + 'depends': [ + 'account', + 'account_payment_fix', + # TODO we should move field amount_company_currency to + # account_payment_fix so that we dont need to depend on + # account_payment_group + ], + 'data': [ + 'data/account_payment_method_data.xml', + 'data/ir_actions_server_data.xml', + 'wizard/account_check_action_wizard_view.xml', + 'wizard/print_pre_numbered_checks_view.xml', + 'views/res_config_settings_view.xml', + 'views/account_payment_view.xml', + 'views/account_check_view.xml', + 'views/account_journal_dashboard_view.xml', + 'views/account_journal_view.xml', + 'views/account_checkbook_view.xml', + 'views/account_chart_template_view.xml', + 'security/ir.model.access.csv', + 'security/account_check_security.xml', + ], + 'installable': True, + 'auto_install': False, + 'application': True, +} diff --git a/account_check/data/account_payment_method_data.xml b/account_check/data/account_payment_method_data.xml new file mode 100644 index 00000000..c205db7c --- /dev/null +++ b/account_check/data/account_payment_method_data.xml @@ -0,0 +1,26 @@ + + + + + + Received Third Check + received_third_check + inbound + + + + Delivered Third Check + delivered_third_check + outbound + + + + Issue Check + issue_check + outbound + + + + + + diff --git a/account_check/models/__init__.py b/account_check/models/__init__.py new file mode 100644 index 00000000..703c33ab --- /dev/null +++ b/account_check/models/__init__.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import account_journal +from . import account_invoice +from . import account_checkbook +from . import account_check +from . import account_payment +from . import res_company +from . import account_chart_template +from . import account_bank_statement_line +from . import res_config_settings diff --git a/account_check/models/account_bank_statement_line.py b/account_check/models/account_bank_statement_line.py new file mode 100644 index 00000000..b67f36cf --- /dev/null +++ b/account_check/models/account_bank_statement_line.py @@ -0,0 +1,59 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models, api, _ +from odoo.exceptions import ValidationError +import logging +_logger = logging.getLogger(__name__) + + +class AccountBankStatementLine(models.Model): + _inherit = "account.bank.statement.line" + + @api.multi + def button_cancel_reconciliation(self): + """ Delete operation of checks that are debited from statement + """ + for st_line in self.filtered('move_name'): + if st_line.journal_entry_ids.filtered( + lambda x: + x.payment_id.payment_reference == st_line.move_name): + check_operation = self.env['account.check.operation'].search( + [('origin', '=', + 'account.bank.statement.line,%s' % st_line.id)]) + check_operation.check_id._del_operation(st_line) + return super( + AccountBankStatementLine, self).button_cancel_reconciliation() + + def process_reconciliation( + self, counterpart_aml_dicts=None, payment_aml_rec=None, + new_aml_dicts=None): + """ + Si el move line de contrapartida es un cheque entregado, entonces + registramos el debito desde el extracto en el cheque + TODO: por ahora si se cancela la linea de extracto no borramos el + debito, habria que ver si queremos hacer eso modificando la funcion de + arriba directamente + """ + + check = False + if counterpart_aml_dicts: + for line in counterpart_aml_dicts: + move_line = line.get('move_line') + check = move_line and move_line.payment_id.check_id or False + moves = super(AccountBankStatementLine, self).process_reconciliation( + counterpart_aml_dicts=counterpart_aml_dicts, + payment_aml_rec=payment_aml_rec, new_aml_dicts=new_aml_dicts) + if check and check.state == 'handed': + if check.journal_id != self.statement_id.journal_id: + raise ValidationError(_( + 'To record the debit of a check from the statement,' + ' the check and extract journal must be the same.' ) + ) + if len(moves) != 1: + raise ValidationError(_( + 'To record the debit of a check from the extract ' + 'there should only be one counterpart line.')) + check._add_operation('debited', self, date=self.date) + return moves diff --git a/account_check/models/account_chart_template.py b/account_check/models/account_chart_template.py new file mode 100644 index 00000000..23fe72ce --- /dev/null +++ b/account_check/models/account_chart_template.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models, api, fields +import logging +_logger = logging.getLogger(__name__) + + +class AccountChartTemplate(models.Model): + _inherit = 'account.chart.template' + + rejected_check_account_id = fields.Many2one( + 'account.account.template', + 'Rejected Check Account', + help='Rejection Checks account, for eg. "Rejected Checks"', + # domain=[('type', 'in', ['other'])], + ) + deferred_check_account_id = fields.Many2one( + 'account.account.template', + 'Deferred Check Account', + help='Deferred Checks account, for eg. "Deferred Checks"', + # domain=[('type', 'in', ['other'])], + ) + holding_check_account_id = fields.Many2one( + 'account.account.template', + 'Holding Check Account', + help='Holding Checks account for third checks, ' + 'for eg. "Holding Checks"', + # domain=[('type', 'in', ['other'])], + ) + + @api.multi + def _load_template( + self, company, code_digits=None, + account_ref=None, taxes_ref=None): + account_ref, taxes_ref = super( + AccountChartTemplate, self)._load_template( + company, + code_digits=code_digits, + account_ref=account_ref, + taxes_ref=taxes_ref) + for field in [ + 'rejected_check_account_id', + 'deferred_check_account_id', + 'holding_check_account_id']: + account_field = self[field] + # TODO we should send it in the context and overwrite with + # lower hierichy values + if account_field: + company[field] = account_ref[account_field.id] + return account_ref, taxes_ref + + @api.multi + def _create_bank_journals(self, company, acc_template_ref): + """ + Bank - Cash journals are created with this method + Inherit this function in order to add checks to cash and bank + journals. This is because usually will be installed before chart loaded + and they will be disable by default + """ + + res = super( + AccountChartTemplate, self)._create_bank_journals( + company, acc_template_ref) + + # creamos diario para cheques de terceros + received_third_check = self.env.ref( + 'account_check.account_payment_method_received_third_check') + delivered_third_check = self.env.ref( + 'account_check.account_payment_method_delivered_third_check') + self.env['account.journal'].create({ + 'name': 'Cheques de Terceros', + 'type': 'cash', + 'company_id': company.id, + 'inbound_payment_method_ids': [ + (4, received_third_check.id, None)], + 'outbound_payment_method_ids': [ + (4, delivered_third_check.id, None)], + }) + + self.env['account.journal'].with_context( + force_company_id=company.id)._enable_issue_check_on_bank_journals() + return res diff --git a/account_check/models/account_check.py b/account_check/models/account_check.py new file mode 100644 index 00000000..d60136b7 --- /dev/null +++ b/account_check/models/account_check.py @@ -0,0 +1,831 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import fields, models, _, api +from odoo.exceptions import UserError, ValidationError +import logging +_logger = logging.getLogger(__name__) + + +class AccountCheckOperation(models.Model): + + _name = 'account.check.operation' + _description = 'account.check.operation' + _rec_name = 'operation' + _order = 'date asc, id asc' + # _order = 'create_date desc' + + # al final usamos solo date y no datetime porque el otro dato ya lo tenemos + # en create_date. ademas el orden es una mezcla de la fecha el id + # y entonces la fecha la solemos computar con el payment date para que + # sea igual a la fecha contable (payment date va al asiento) + # date = fields.Datetime( + date = fields.Date( + default=fields.Date.context_today, + # default=lambda self: fields.Datetime.now(), + required=True, + index=True, + ) + check_id = fields.Many2one( + 'account.check', + 'Check', + required=True, + ondelete='cascade', + auto_join=True, + index=True, + ) + operation = fields.Selection([ + # from payments + ('holding', 'Received'), + ('deposited', 'Deposited'), + ('sold', 'Sold'), + ('delivered', 'Delivered'), + # usado para hacer transferencias internas, es lo mismo que delivered + # (endosado) pero no queremos confundir con terminos, a la larga lo + # volvemos a poner en holding + ('transfered', 'Transfered'), + ('handed', 'Handed'), + ('withdrawed', 'Withdrawn'), + # from checks + ('reclaimed', 'Claimed'), + ('rejected', 'Rejected'), + ('debited', 'Debited'), + ('returned', 'Returned'), + # al final no vamos a implemnetar esto ya que habria que hacer muchas + # cosas hasta actualizar el asiento, mejor se vuelve atras y se + # vuelve a generar deuda y listo, igualmente lo dejamos por si se + # quiere usar de manera manual + ('changed', 'Changed'), + ('cancel', 'Cancelled'), + ], + required=True, + index=True, + string='Operation', + ) + origin_name = fields.Char( + compute='_compute_origin_name' + ) + origin = fields.Reference( + string='Origin Document', + selection='_reference_models') + partner_id = fields.Many2one( + 'res.partner', + string='Partner', + ) + notes = fields.Text( + string='Operation Note' + ) + + @api.multi + def unlink(self): + for rec in self: + if rec.origin: + raise ValidationError(_( + 'You can not delete a check operation that has an origin.' + '\nYou can delete the origin reference and unlink after.')) + return super(AccountCheckOperation, self).unlink() + + @api.multi + @api.depends('origin') + def _compute_origin_name(self): + """ + We add this computed method because an error on tree view displaying + reference field when destiny record is deleted. + As said in this post (last answer) we should use name_get instead of + display_name + https://www.odoo.com/es_ES/forum/ayuda-1/question/ + how-to-override-name-get-method-in-new-api-61228 + """ + for rec in self: + try: + if rec.origin: + _id, name = rec.origin.name_get()[0] + origin_name = name + # origin_name = rec.origin.display_name + else: + origin_name = False + except Exception as e: + _logger.exception( + "Compute origin on checks exception: %s" % e) + # if we can get origin we clean it + rec.write({'origin': False}) + origin_name = False + rec.origin_name = origin_name + + @api.model + def _reference_models(self): + return [ + ('account.payment', 'Payment'), + ('account.check', 'Check'), + ('account.invoice', 'Invoice'), + ('account.move', 'Journal Entry'), + ('account.move.line', 'Journal Item'), + ('account.bank.statement.line', 'Statement Line'), + ] + + +class AccountCheck(models.Model): + + _name = 'account.check' + _description = 'Account Check' + _order = "id desc" + _inherit = ['mail.thread', 'mail.activity.mixin'] + + operation_ids = fields.One2many( + 'account.check.operation', + 'check_id', + auto_join=True, + ) + name = fields.Char( + required=True, + readonly=True, + copy=False, + states={'draft': [('readonly', False)]}, + index=True, + ) + number = fields.Integer( + required=True, + readonly=True, + states={'draft': [('readonly', False)]}, + copy=False, + index=True, + ) + checkbook_id = fields.Many2one( + 'account.checkbook', + 'Checkbook', + readonly=True, + states={'draft': [('readonly', False)]}, + auto_join=True, + index=True, + ) + issue_check_subtype = fields.Selection( + related='checkbook_id.issue_check_subtype', + ) + type = fields.Selection( + [('issue_check', 'Issue Check'), ('third_check', 'Third Check'), + ('issue_promissory_note', 'Issue Promissory Note'), ('deposit_promissory_note', 'Deposit Promissory Note')], + readonly=True, + index=True, + ) + partner_id = fields.Many2one( + related='operation_ids.partner_id', + store=True, + index=True, + string='Last operation partner', + ) + first_partner_id = fields.Many2one( + 'res.partner', + compute='_compute_first_partner', + string='First operation partner', + readonly=True, + store=True, + ) + state = fields.Selection([ + ('draft', 'Draft'), + ('holding', 'Holding'), + ('deposited', 'Deposited'), + ('sold', 'Sold'), + ('delivered', 'Delivered'), + ('transfered', 'Transfered'), + ('reclaimed', 'Reclaimed'), + ('withdrawed', 'Withdrawed'), + ('handed', 'Handed'), + ('rejected', 'Rejected'), + ('debited', 'Debited'), + ('returned', 'Returned'), + ('changed', 'Changed'), + ('cancel', 'Cancelled'), + ], + required=True, + default='draft', + copy=False, + compute='_compute_state', + store=True, + index=True, + string="Status" + ) + issue_date = fields.Date( + 'Issue Date', + required=True, + readonly=True, + states={'draft': [('readonly', False)]}, + default=fields.Date.context_today, + ) + owner_vat = fields.Char( + 'Owner Vat', + readonly=True, + states={'draft': [('readonly', False)]} + ) + owner_name = fields.Char( + 'Owner Name', + readonly=True, + states={'draft': [('readonly', False)]} + ) + bank_id = fields.Many2one( + 'res.bank', 'Bank', + readonly=True, + states={'draft': [('readonly', False)]} + ) + amount = fields.Monetary( + currency_field='currency_id', + readonly=True, + states={'draft': [('readonly', False)]} + ) + amount_company_currency = fields.Monetary( + currency_field='company_currency_id', + readonly=True, + states={'draft': [('readonly', False)]}, + ) + currency_id = fields.Many2one( + 'res.currency', + readonly=True, + states={'draft': [('readonly', False)]}, + default=lambda self: self.env.user.company_id.currency_id.id, + required=True, + ) + payment_date = fields.Date( + readonly=True, + states={'draft': [('readonly', False)]}, + index=True, + ) + journal_id = fields.Many2one( + 'account.journal', + string='Journal', + required=True, + domain=[('type', 'in', ['cash', 'bank'])], + readonly=True, + states={'draft': [('readonly', False)]}, + index=True, + ) + company_id = fields.Many2one( + related='journal_id.company_id', + store=True, + ) + company_currency_id = fields.Many2one( + related='company_id.currency_id', + string='Company currency', + ) + + @api.depends('operation_ids.partner_id') + def _compute_first_partner(self): + for rec in self: + rec.first_partner_id = rec.operation_ids and rec.operation_ids[0].partner_id or False + + @api.multi + def onchange(self, values, field_name, field_onchange): + """ + Con esto arreglamos el borrador del origin de una operacíón de deposito + (al menos depositos de v8 migrados), habría que ver si pasa en otros + casos y hay algo más que arreglar + # TODO si no pasa en v11 borrarlo + """ + 'operation_ids.origin' in field_onchange and field_onchange.pop( + 'operation_ids.origin') + return super(AccountCheck, self).onchange( + values, field_name, field_onchange) + + @api.multi + @api.constrains('issue_date', 'payment_date') + @api.onchange('issue_date', 'payment_date') + def onchange_date(self): + for rec in self: + if ( + rec.issue_date and rec.payment_date and + rec.issue_date > rec.payment_date): + raise UserError( + _('Check Payment Date must be greater than Issue Date')) + + @api.multi + @api.constrains( + 'type', + 'number', + ) + def issue_number_interval(self): + for rec in self: + # if not range, then we dont check it + if rec.type == 'issue_check' and rec.checkbook_id.range_to: + if rec.number > rec.checkbook_id.range_to: + raise UserError(_( + "Check number (%s) can't be greater than %s on " + "checkbook %s (%s)") % ( + rec.number, + rec.checkbook_id.range_to, + rec.checkbook_id.name, + rec.checkbook_id.id, + )) + elif rec.number == rec.checkbook_id.range_to: + rec.checkbook_id.state = 'used' + return False + + @api.multi + @api.constrains( + 'type', + 'owner_name', + 'bank_id', + ) + def _check_unique(self): + for rec in self: + if rec.type == 'issue_check': + same_checks = self.search([ + ('checkbook_id', '=', rec.checkbook_id.id), + ('type', '=', rec.type), + ('number', '=', rec.number), + ]) + same_checks -= self + if same_checks: + raise ValidationError(_( + 'Check Number (%s) must be unique per Checkbook!\n' + '* Check ids: %s') % ( + rec.name, same_checks.ids)) + elif self.type == 'third_check': + # agregamos condicion de company ya que un cheque de terceros + # se puede pasar entre distintas cias + same_checks = self.search([ + ('company_id', '=', rec.company_id.id), + ('bank_id', '=', rec.bank_id.id), + ('owner_name', '=', rec.owner_name), + ('type', '=', rec.type), + ('number', '=', rec.number), + ]) + same_checks -= self + if same_checks: + raise ValidationError(_( + 'Check Number (%s) must be unique per Owner and Bank!' + '\n* Check ids: %s') % ( + rec.name, same_checks.ids)) + return True + + @api.multi + def _del_operation(self, origin): + """ + We check that the operation that is being cancel is the last operation + done (same as check state) + """ + for rec in self: + if not rec.operation_ids or rec.operation_ids[-1].origin != origin: + raise ValidationError(_( + 'You can not cancel this operation because this is not ' + 'the last operation over the check.\nCheck (id): %s (%s)' + ) % (rec.name, rec.id)) + rec.operation_ids[-1].origin = False + rec.operation_ids[-1].unlink() + + @api.multi + def _add_operation( + self, operation, origin, partner=None, date=False): + for rec in self: + rec._check_state_change(operation) + # agregamos validacion de fechas + date = date or fields.Datetime.now() + if rec.operation_ids and rec.operation_ids[-1].date > date: + raise ValidationError(_( + 'The date of a new check operation can not be minor than ' + 'last operation date.\n' + '* Check Id: %s\n' + '* Check Number: %s\n' + '* Operation: %s\n' + '* Operation Date: %s\n' + '* Last Operation Date: %s') % ( + rec.id, rec.name, operation, date, + rec.operation_ids[-1].date)) + vals = { + 'operation': operation, + 'date': date, + 'check_id': rec.id, + 'origin': '%s,%i' % (origin._name, origin.id), + 'partner_id': partner and partner.id or False, + } + rec.operation_ids.create(vals) + + @api.multi + @api.depends( + 'operation_ids', + 'operation_ids.operation', + 'operation_ids.date', + ) + def _compute_state(self): + for rec in self: + if rec.operation_ids: + operation = rec.operation_ids[-1].operation + rec.state = operation + else: + rec.state = 'draft' + + @api.multi + def _check_state_change(self, operation): + """ + We only check state change from _add_operation because we want to + leave the user the possibility of making anything from interface. + Necesitamos este chequeo para evitar, por ejemplo, que un cheque se + agregue dos veces en un pago y luego al confirmar se entregue dos veces + On operation_from_state_map dictionary: + * key is 'to state' + * value is 'from states' + """ + self.ensure_one() + # if we do it from _add_operation only, not from a contraint of before + # computing the value, we can just read it + old_state = self.state + operation_from_state_map = { + # 'draft': [False], + 'holding': [ + 'draft', 'deposited', 'sold', 'delivered', 'transfered'], + 'delivered': ['holding'], + 'deposited': ['holding', 'rejected'], + 'sold': ['holding'], + 'handed': ['draft'], + 'transfered': ['holding'], + 'withdrawed': ['draft'], + 'rejected': ['delivered', 'deposited', 'sold', 'handed'], + 'debited': ['handed'], + 'returned': ['handed', 'holding'], + 'changed': ['handed', 'holding'], + 'cancel': ['draft'], + 'reclaimed': ['rejected'], + } + from_states = operation_from_state_map.get(operation) + if not from_states: + raise ValidationError(_( + 'Operation %s not implemented for checks!') % operation) + if old_state not in from_states: + raise ValidationError(_( + 'You can not "%s" a check from state "%s"!\n' + 'Check nbr (id): %s (%s)') % ( + self.operation_ids._fields['operation'].convert_to_export( + operation, self), + self._fields['state'].convert_to_export(old_state, self), + self.name, + self.id)) + + @api.multi + def unlink(self): + for rec in self: + if rec.state not in ('draft', 'cancel'): + raise ValidationError(_( + 'The Check must be in draft state for unlink !')) + return super(AccountCheck, self).unlink() + +# checks operations from checks + + @api.multi + def bank_debit(self): + self.ensure_one() + if self.state in ['handed']: + payment_values = self.get_payment_values(self.journal_id) + payment = self.env['account.payment'].with_context( + default_name=_('Check "%s" debit') % (self.name), + force_account_id=self.company_id._get_check_account( + 'deferred').id, + ).create(payment_values) + self.post_payment_check(payment) + self.handed_reconcile(payment.move_line_ids.mapped('move_id')) + self._add_operation('debited', payment, date=payment.payment_date) + + @api.model + def post_payment_check(self, payment): + """ No usamos post() porque no puede obtener secuencia, hacemos + parecido a los statements donde odoo ya lo genera posteado + """ + # payment.post() + move = payment._create_payment_entry(payment.amount) + payment.write({'state': 'posted', 'move_name': move.name}) + + @api.multi + def handed_reconcile(self, move): + """ + Funcion que por ahora solo intenta conciliar cheques propios entregados + cuando se hace un debito o cuando el proveedor lo rechaza + """ + + self.ensure_one() + debit_account = self.company_id._get_check_account('deferred') + + # conciliamos + if debit_account.reconcile: + operation = self._get_operation('handed') + if operation.origin._name == 'account.payment': + move_lines = operation.origin.move_line_ids + elif operation.origin._name == 'account.move': + move_lines = operation.origin.line_ids + move_lines |= move.line_ids + move_lines = move_lines.filtered( + lambda x: x.account_id == debit_account) + if len(move_lines) != 2: + raise ValidationError(_( + 'We have found more or less than two journal items to ' + 'reconcile with check debit.\n' + '*Journal items: %s') % move_lines.ids) + move_lines.reconcile() + + @api.model + def get_third_check_account(self): + """ + For third checks, if we use a journal only for third checks, we use + accounts on journal, if not we use company account + # TODO la idea es depreciar esto y que si se usa cheques de terceros + se use la misma cuenta que la del diario y no la cuenta configurada en + la cia, lo dejamos por ahora por nosotros y 4 clientes que estan asi + (cro, ncool, bog). + Esto era cuando permitíamos o usabamos diario de efectivo con cash y + cheques + """ + # self.ensure_one() + # desde los pagos, pueden venir mas de un cheque pero para que + # funcione bien, todos los cheques deberian usar la misma cuenta, + # hacemos esa verificación + account = self.env['account.account'] + for rec in self: + credit_account = rec.journal_id.default_credit_account_id + debit_account = rec.journal_id.default_debit_account_id + inbound_methods = rec.journal_id['inbound_payment_method_ids'] + outbound_methods = rec.journal_id['outbound_payment_method_ids'] + # si hay cuenta en diario y son iguales, y si los metodos de pago + # y cobro son solamente uno, usamos el del diario, si no, usamos el + # de la compañía + if credit_account and credit_account == debit_account and len( + inbound_methods) == 1 and len(outbound_methods) == 1: + account |= credit_account + else: + account |= rec.company_id._get_check_account('holding') + if len(account) != 1: + raise ValidationError(_('Error not specified')) + return account + + @api.model + def _get_checks_to_date_on_state(self, state, date, force_domain=None): + """ + Devuelve el listado de cheques que a la fecha definida se encontraban + en el estadao definido. + Esta función no la usamos en este módulo pero si en otros que lo + extienden + La funcion devuelve un listado de las operaciones a traves de las + cuales se puede acceder al cheque, devolvemos las operaciones porque + dan información util de fecha, partner y demas + """ + # buscamos operaciones anteriores a la fecha que definan este estado + if not force_domain: + force_domain = [] + + operations = self.operation_ids.search([ + ('date', '<=', date), + ('operation', '=', state)] + force_domain) + + for operation in operations: + # buscamos si hay alguna otra operacion posterior para el cheque + newer_op = operation.search([ + ('date', '<=', date), + ('id', '>', operation.id), + ('check_id', '=', operation.check_id.id), + ]) + # si hay una operacion posterior borramos la op del cheque porque + # hubo otra operación antes de la fecha + if newer_op: + operations -= operation + return operations + + @api.multi + def _get_operation(self, operation, partner_required=False): + self.ensure_one() + op = self.operation_ids.search([ + ('check_id', '=', self.id), ('operation', '=', operation)], + limit=1) + if partner_required: + if not op.partner_id: + raise ValidationError(_( + 'The %s (id %s) operation has no partner linked.' + 'You will need to do it manually.') % (operation, op.id)) + return op + + @api.multi + def claim(self): + self.ensure_one() + if self.state in ['rejected'] and self.type == 'third_check': + # anulamos la operación en la que lo recibimos + return self.action_create_debit_note( + 'reclaimed', 'customer', self.first_partner_id, + self.company_id._get_check_account('rejected')) + + @api.multi + def customer_return(self): + self.ensure_one() + if self.state in ['holding'] and self.type == 'third_check': + return self.action_create_debit_note( + 'returned', 'customer', self.first_partner_id, + False) + #self.get_third_check_account()) + + @api.model + def get_payment_values(self, journal): + """ return dictionary with the values to create the reject check + payment record. + We create an outbound payment instead of a transfer because: + 1. It is easier to inherit + 2. Outbound payment withot partner type and partner is not seen by user + and we don't want to confuse them with this payments + """ + action_date = self._context.get('action_date', fields.Date.today()) + return { + 'amount': self.amount, + 'currency_id': self.currency_id.id, + 'journal_id': journal.id, + 'payment_date': action_date, + 'payment_type': 'outbound', + 'payment_method_id': journal._default_outbound_payment_methods().id, + # 'check_ids': [(4, self.id, False)], + } + + @api.constrains('currency_id', 'amount', 'amount_company_currency') + def _check_amounts(self): + for rec in self.filtered( + lambda x: not x.amount or not x.amount_company_currency): + if rec.currency_id != rec.company_currency_id: + raise ValidationError(_( + 'If you create a check with different currency thant the ' + 'company currency, you must provide "Amount" and "Amount ' + 'Company Currency"')) + elif not rec.amount: + if not rec.amount_company_currency: + raise ValidationError(_( + 'No puede crear un cheque sin importe')) + rec.amount = rec.amount_company_currency + elif not rec.amount_company_currency: + rec.amount_company_currency = rec.amount + + @api.multi + def reject(self): + self.ensure_one() + if self.state in ['deposited', 'sold']: + operation = self._get_operation(self.state) + if operation.origin._name == 'account.payment': + journal = operation.origin.destination_journal_id + # for compatibility with migration from v8 + elif operation.origin._name == 'account.move': + journal = operation.origin.journal_id + else: + raise ValidationError(_( + 'The deposit operation is not linked to a payment.' + 'If you want to reject you need to do it manually.')) + payment_vals = self.get_payment_values(journal) + payment = self.env['account.payment'].with_context( + default_name=_('Check "%s" rejection') % (self.name), + force_account_id=self.company_id._get_check_account( + 'rejected').id, + ).create(payment_vals) + self.post_payment_check(payment) + self._add_operation('rejected', payment, date=payment.payment_date) + elif self.state == 'delivered': + operation = self._get_operation(self.state, True) + return self.action_create_debit_note( + 'rejected', 'supplier', operation.partner_id, + self.company_id._get_check_account('rejected')) + elif self.state == 'handed': + operation = self._get_operation(self.state, True) + return self.action_create_debit_note( + 'rejected', 'supplier', operation.partner_id, + self.company_id._get_check_account('deferred')) + + @api.multi + def action_create_debit_note(self, operation, partner_type, partner, account): + self.ensure_one() +# action_date = self._context.get('action_date') +# +# if partner_type == 'supplier': +# invoice_type = 'in_invoice' +# journal_type = 'purchase' +# view_id = self.env.ref('account.invoice_supplier_form').id +# else: +# invoice_type = 'out_invoice' +# journal_type = 'sale' +# view_id = self.env.ref('account.invoice_form').id +# +# journal = self.env['account.journal'].search([ +# ('company_id', '=', self.company_id.id), +# ('type', '=', journal_type), +# ], limit=1) +# +# # si pedimos rejected o reclamo, devolvemos mensaje de rechazo y cuenta +# # de rechazo +# if operation in ['rejected', 'reclaimed']: +# name = 'Rechazo cheque "%s"' % (self.name) +# # si pedimos la de holding es una devolucion +# elif operation == 'returned': +# name = 'Devolución cheque "%s"' % (self.name) +# else: +# raise ValidationError(_( +# 'Debit note for operation %s not implemented!' % ( +# operation))) +# +# inv_line_vals = { +# # 'product_id': self.product_id.id, +# 'name': name, +# 'account_id': account.id, +# 'price_unit': self.amount, +# # 'invoice_id': invoice.id, +# } +# +# inv_vals = { +# # this is the reference that goes on account.move.line of debt line +# # 'name': name, +# # this is the reference that goes on account.move +# 'rejected_check_id': self.id, +# 'reference': name, +# 'date_invoice': action_date, +# 'origin': _('Check nbr (id): %s (%s)') % (self.name, self.id), +# 'journal_id': journal.id, +# # this is done on muticompany fix +# # 'company_id': journal.company_id.id, +# 'partner_id': partner.id, +# 'type': invoice_type, +# 'invoice_line_ids': [(0, 0, inv_line_vals)], +# } +# if self.currency_id: +# inv_vals['currency_id'] = self.currency_id.id +# # we send internal_type for compatibility with account_document +# invoice = self.env['account.invoice'].with_context( +# internal_type='debit_note').create(inv_vals) +# self._add_operation(operation, invoice, partner, date=action_date) +# +# return { +# 'name': name, +# 'view_type': 'form', +# 'view_mode': 'form', +# 'res_model': 'account.invoice', +# 'view_id': view_id, +# 'res_id': invoice.id, +# 'type': 'ir.actions.act_window', +# } + + action_date = self._context.get('action_date') + journal = self._context.get('journal_id') + debit_account = self._context.get('debit_account_id') + credit_account = self._context.get('credit_account_id') + + if operation in ['rejected', 'reclaimed'] and self.type == 'third_check': + name = 'Rejected check "%s"' % (self.name) + elif operation == 'returned' and self.type == 'third_check': + name = 'Returned check "%s"' % (self.name) +# elif operation in ['rejected', 'reclaimed'] and self.type == 'deposit_promissory_note': +# name = 'Rejected promissory note "%s"' % (self.name) +# elif operation == 'returned' and self.type == 'third_promissory_note': +# name = 'Returned promissory note "%s"' % (self.name) + else: + raise ValidationError(_( + 'Debit note for operation %s not implemented!' % ( + operation))) + + vals = self.prepare_new_operation_move_values(debit_account, credit_account, name) + vals['journal_id'] = journal.id or False + vals['date'] = action_date + move = self.env['account.move'].create(vals) + move.post() + self._add_operation(operation, move, partner, date=action_date) + + return True + + + @api.multi + def prepare_new_operation_move_values(self, debit_account, credit_account, name, partner=False): + self.ensure_one() + ref = name + amount = self.amount + debit, credit, amount_currency, currency_id = self.env['account.move.line']._compute_amount_fields( + amount, self.currency_id, self.company_currency_id) + if self.company_currency_id != self.currency_id: + currency_id = self.currency_id.id + else: + currency_id = False + amount_currency = False + debit_line_vals = { + 'name': name, + 'debit': debit, + 'credit': credit, + 'partner_id': partner and partner.id or False, + 'account_id': debit_account.id, + 'amount_currency': amount_currency, + 'currency_id': currency_id, + 'ref': ref, + } + credit_line_vals = debit_line_vals.copy() + credit_line_vals['debit'] = debit_line_vals['credit'] + credit_line_vals['credit'] = debit_line_vals['debit'] + credit_line_vals['account_id'] = credit_account.id + credit_line_vals['amount_currency'] = -1 * debit_line_vals['amount_currency'] + + move_vals = { + 'ref': name, + 'line_ids': [ + (0, False, debit_line_vals), + (0, False, credit_line_vals)], + } + return move_vals + + + + + + + + + diff --git a/account_check/models/account_checkbook.py b/account_check/models/account_checkbook.py new file mode 100644 index 00000000..2206e9b1 --- /dev/null +++ b/account_check/models/account_checkbook.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import fields, models, api, _ +import logging +from odoo.exceptions import ValidationError +_logger = logging.getLogger(__name__) + + +class AccountCheckbook(models.Model): + + _name = 'account.checkbook' + _description = 'Account Checkbook' + + name = fields.Char( + compute='_compute_name', + ) + sequence_id = fields.Many2one( + 'ir.sequence', + 'Sequence', + copy=False, + domain=[('code', '=', 'issue_check')], + help="Checks numbering sequence.", + context={'default_code': 'issue_check'}, + ) + next_number = fields.Integer( + 'Next Number', + # usamos compute y no related para poder usar sudo cuando se setea + # secuencia sin necesidad de dar permiso en ir.sequence + compute='_compute_next_number', + inverse='_inverse_next_number', + ) + issue_check_subtype = fields.Selection( + [('deferred', 'Deferred'), ('currents', 'Currents')], + string='Issue Check Subtype', + required=True, + default='deferred', + help='* Con cheques corrientes el asiento generado por el pago ' + 'descontará directamente de la cuenta de banco y además la fecha de ' + 'pago no es obligatoria.\n' + '* Con cheques diferidos el asiento generado por el pago se hará ' + 'contra la cuenta definida para tal fin en la compañía, luego será ' + 'necesario el asiento de débito que se puede generar desde el extracto' + ' o desde el cheque.', + ) + journal_id = fields.Many2one( + 'account.journal', 'Journal', + help='Journal where it is going to be used', + readonly=True, + required=True, + domain=[('type', '=', 'bank')], + ondelete='cascade', + context={'default_type': 'bank'}, + states={'draft': [('readonly', False)]}, + auto_join=True, + ) + range_to = fields.Integer( + 'To Number', + # readonly=True, + # states={'draft': [('readonly', False)]}, + help='If you set a number here, this checkbook will be automatically' + ' set as used when this number is raised.' + ) + issue_check_ids = fields.One2many( + 'account.check', + 'checkbook_id', + string='Issue Checks', + readonly=True, + ) + state = fields.Selection( + [('draft', 'Draft'), ('active', 'In Use'), ('used', 'Used')], + string='Status', + # readonly=True, + default='draft', + copy=False, + ) + # TODO depreciar esta funcionalidad que no estamos usando + block_manual_number = fields.Boolean( + default=True, + string='Block manual number?', + # readonly=True, + # states={'draft': [('readonly', False)]}, + help='Block user to enter manually another number than the suggested' + ) + numerate_on_printing = fields.Boolean( + default=False, + string='Numerate on printing?', + # readonly=True, + # states={'draft': [('readonly', False)]}, + help='No number will be assigne while creating payment, number will be' + 'assigned after printing check.' + ) + report_template = fields.Many2one( + 'ir.actions.report', + 'Report', + domain="[('model', '=', 'account.payment')]", + context="{'default_model': 'account.payment'}", + help='Report to use when printing checks. If not report selected, ' + 'report with name "check_report" will be used', + ) + + @api.multi + @api.depends('sequence_id.number_next_actual') + def _compute_next_number(self): + for rec in self: + rec.next_number = rec.sequence_id.number_next_actual + + @api.multi + def _inverse_next_number(self): + for rec in self.filtered('sequence_id'): + rec.sequence_id.sudo().number_next_actual = rec.next_number + + @api.model + def create(self, vals): + rec = super(AccountCheckbook, self).create(vals) + if not rec.sequence_id: + rec._create_sequence(vals.get('next_number', 0)) + return rec + + @api.multi + def _create_sequence(self, next_number): + """ Create a check sequence for the checkbook """ + for rec in self: + rec.sequence_id = rec.env['ir.sequence'].sudo().create({ + 'name': '%s - %s' % (rec.journal_id.name, rec.name), + 'implementation': 'no_gap', + 'padding': 8, + 'number_increment': 1, + 'code': 'issue_check', + # si no lo pasamos, en la creacion se setea 1 + 'number_next_actual': next_number, + 'company_id': rec.journal_id.company_id.id, + }) + + @api.multi + def _compute_name(self): + for rec in self: + if rec.issue_check_subtype == 'deferred': + name = _('Deferred Checks') + else: + name = _('Currents Checks') + if rec.range_to: + name += _(' up to %s') % rec.range_to + rec.name = name + + @api.multi + def unlink(self): + if self.mapped('issue_check_ids'): + raise ValidationError( + _('You can drop a checkbook if it has been used on checks!')) + return super(AccountCheckbook, self).unlink() diff --git a/account_check/models/account_invoice.py b/account_check/models/account_invoice.py new file mode 100644 index 00000000..9426c3bf --- /dev/null +++ b/account_check/models/account_invoice.py @@ -0,0 +1,49 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models, fields, api + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + # we add this field so that when invoice is validated we can reconcile + # move lines between check and invoice lines + # igual se setea para todos los rechazos, tal vez mas adelante lo usamos + # para otra cosa + rejected_check_id = fields.Many2one( + 'account.check', + 'Rejected Check', + ) + + @api.multi + def action_cancel(self): + """ + Si al cancelar la factura la misma estaba vinculada a un rechazo + intentamos romper la conciliacion del rechazo + """ + for rec in self.filtered(lambda x: x.rejected_check_id): + check = rec.rejected_check_id + deferred_account = check.company_id._get_check_account('deferred') + if ( + check.state == 'rejected' and + check.type == 'issue_check' and + deferred_account.reconcile): + deferred_account_line = rec.move_id.line_ids.filtered( + lambda x: x.account_id == deferred_account) + deferred_account_line.remove_move_reconcile() + return super(AccountInvoice, self).action_cancel() + + @api.multi + def action_move_create(self): + """ + Si al validar la factura, la misma tiene un cheque de rechazo asociado + intentamos concilarlo + """ + res = super(AccountInvoice, self).action_move_create() + for rec in self.filtered(lambda x: x.rejected_check_id): + check = rec.rejected_check_id + if check.state == 'rejected' and check.type == 'issue_check': + rec.rejected_check_id.handed_reconcile(rec.move_id) + return res diff --git a/account_check/models/account_journal.py b/account_check/models/account_journal.py new file mode 100644 index 00000000..63c299ca --- /dev/null +++ b/account_check/models/account_journal.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import models, fields, api, _ +from odoo.tools.misc import formatLang +from ast import literal_eval + + +class AccountJournal(models.Model): + _inherit = 'account.journal' + + checkbook_ids = fields.One2many( + 'account.checkbook', + 'journal_id', + 'Checkbooks', + auto_join=True, + ) + + @api.model + def create(self, vals): + rec = super(AccountJournal, self).create(vals) + issue_checks = self.env.ref( + 'account_check.account_payment_method_issue_check') + if (issue_checks in rec.outbound_payment_method_ids and + not rec.checkbook_ids): + rec._create_checkbook() + return rec + + @api.multi + def _create_checkbook(self): + """ Create a check sequence for the journal """ + for rec in self: + checkbook = rec.checkbook_ids.create({ + 'journal_id': rec.id, + }) + checkbook.state = 'active' + + @api.model + def _enable_issue_check_on_bank_journals(self): + """ Enables issue checks payment method + Called upon module installation via data file. + """ + issue_checks = self.env.ref( + 'account_check.account_payment_method_issue_check') + domain = [('type', '=', 'bank')] + force_company_id = self._context.get('force_company_id') + if force_company_id: + domain += [('company_id', '=', force_company_id)] + bank_journals = self.search(domain) + for bank_journal in bank_journals: + if not bank_journal.checkbook_ids: + bank_journal._create_checkbook() + bank_journal.write({ + 'outbound_payment_method_ids': [(4, issue_checks.id, None)], + }) + +############### +# For dashboard +############### + + @api.multi + def get_journal_dashboard_datas(self): + domain_holding_third_checks = [ + ('type', '=', 'third_check'), + ('journal_id', '=', self.id), + ('state', '=', 'holding') + ] + domain_handed_issue_checks = [ + ('type', '=', 'issue_check'), + ('journal_id', '=', self.id), + ('state', '=', 'handed') + ] + handed_checks = self.env['account.check'].search( + domain_handed_issue_checks) + holding_checks = self.env['account.check'].search( + domain_holding_third_checks) + + num_checks_to_numerate = False + if self.env['ir.actions.report'].search( + [('report_name', '=', 'check_report')]): + num_checks_to_numerate = self.env['account.payment'].search_count([ + ('journal_id', '=', self.id), + ('payment_method_id.code', '=', 'issue_check'), + ('state', '=', 'draft'), + ('check_name', '=', False), + ]) + return dict( + super(AccountJournal, self).get_journal_dashboard_datas(), + num_checks_to_numerate=num_checks_to_numerate, + num_holding_third_checks=len(holding_checks), + show_third_checks=( + 'received_third_check' in + self.inbound_payment_method_ids.mapped('code')), + show_issue_checks=( + 'issue_check' in + self.outbound_payment_method_ids.mapped('code')), + num_handed_issue_checks=len(handed_checks), + handed_amount=formatLang( + self.env, sum(handed_checks.mapped('amount_company_currency')), + currency_obj=self.company_id.currency_id), + holding_amount=formatLang( + self.env, sum(holding_checks.mapped( + 'amount_company_currency')), + currency_obj=self.company_id.currency_id), + ) + + @api.multi + def open_action_checks(self): + check_type = self.env.context.get('check_type', False) + if check_type == 'third_check': + action_name = 'account_check.action_third_check' + elif check_type == 'issue_check': + action_name = 'account_check.action_issue_check' + else: + return False + actions = self.env.ref(action_name) + action_read = actions.read()[0] + context = literal_eval(action_read['context']) + context['search_default_journal_id'] = self.id + action_read['context'] = context + return action_read + + @api.multi + def action_checks_to_numerate(self): + return { + 'name': _('Checks to Print and Numerate'), + 'type': 'ir.actions.act_window', + 'view_mode': 'list,form,graph', + 'res_model': 'account.payment', + 'context': dict( + self.env.context, + search_default_checks_to_numerate=1, + search_default_journal_id=self.id, + journal_id=self.id, + default_journal_id=self.id, + default_payment_type='outbound', + default_payment_method_id=self.env.ref( + 'account_check.account_payment_method_issue_check').id, + ), + } diff --git a/account_check/models/account_payment.py b/account_check/models/account_payment.py new file mode 100644 index 00000000..ef508014 --- /dev/null +++ b/account_check/models/account_payment.py @@ -0,0 +1,627 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import fields, models, _, api +from odoo.exceptions import UserError, ValidationError +import logging +# import odoo.addons.decimal_precision as dp +_logger = logging.getLogger(__name__) + + +class AccountPayment(models.Model): + + _inherit = 'account.payment' + + check_ids = fields.Many2many( + 'account.check', + string='Checks', + copy=False, + readonly=True, + states={'draft': [('readonly', False)]}, + auto_join=True, + ) + # we add this field for better usability on issue checks and received + # checks. We keep m2m field for backward compatibility where we allow to + # use more than one check per payment + check_id = fields.Many2one( + 'account.check', + compute='_compute_check', + string='Check', + ) + check_deposit_type = fields.Selection( + [('consolidated', 'Consolidated'), + ('detailed', 'Detailed')], + default='detailed', + help="This option is relevant if you use bank statements. Detailed is" + " used when the bank credits one by one the checks, consolidated is" + " for when the bank credits all the checks in a single movement", + ) + + @api.multi + @api.depends('check_ids') + def _compute_check(self): + for rec in self: + # we only show checks for issue checks or received thid checks + # if len of checks is 1 + if rec.payment_method_code in ( + 'received_third_check', + 'issue_check',) and len(rec.check_ids) == 1: + rec.check_id = rec.check_ids[0].id + else: + rec.check_id = False + +# check fields, just to make it easy to load checks without need to create +# them by a m2o record + check_name = fields.Char( + 'Check Name', + readonly=True, + copy=False, + states={'draft': [('readonly', False)]}, + ) + check_number = fields.Integer( + 'Check Number', + readonly=True, + states={'draft': [('readonly', False)]}, + copy=False, + ) + check_issue_date = fields.Date( + 'Check Issue Date', + readonly=True, + copy=False, + states={'draft': [('readonly', False)]}, + default=fields.Date.context_today, + ) + check_payment_date = fields.Date( + 'Check Payment Date', + readonly=True, + help="Only if this check is post dated", + states={'draft': [('readonly', False)]}, + ) + checkbook_id = fields.Many2one( + 'account.checkbook', + 'Checkbook', + readonly=True, + states={'draft': [('readonly', False)]}, + auto_join=True, + ) + check_subtype = fields.Selection( + related='checkbook_id.issue_check_subtype', + ) + check_bank_id = fields.Many2one( + 'res.bank', + 'Check Bank', + readonly=True, + copy=False, + states={'draft': [('readonly', False)]}, + auto_join=True, + ) + check_owner_vat = fields.Char( + 'Check Owner Vat', + readonly=True, + copy=False, + states={'draft': [('readonly', False)]} + ) + check_owner_name = fields.Char( + 'Check Owner Name', + readonly=True, + copy=False, + states={'draft': [('readonly', False)]} + ) + # this fields is to help with code and view + check_type = fields.Char( + compute='_compute_check_type', + ) + checkbook_numerate_on_printing = fields.Boolean( + related='checkbook_id.numerate_on_printing', + ) + # TODO borrar, esto estaria depreciado + # checkbook_block_manual_number = fields.Boolean( + # related='checkbook_id.block_manual_number', + # readonly=True, + # ) + # check_number_readonly = fields.Integer( + # related='check_number', + # readonly=True, + # ) + + @api.multi + @api.depends('payment_method_code') + def _compute_check_type(self): + for rec in self: + if rec.payment_method_code == 'issue_check': + rec.check_type = 'issue_check' + elif rec.payment_method_code in [ + 'received_third_check', + 'delivered_third_check']: + rec.check_type = 'third_check' + else: + rec.check_type = False + + @api.multi + def _compute_payment_method_description(self): + check_payments = self.filtered( + lambda x: x.payment_method_code in + ['issue_check', 'received_third_check', 'delivered_third_check']) + for rec in check_payments: + if rec.check_ids: + checks_desc = ', '.join(rec.check_ids.mapped('name')) + else: + checks_desc = rec.check_name + name = "%s: %s" % (rec.payment_method_id.display_name, checks_desc) + rec.payment_method_description = name + return super( + AccountPayment, + (self - check_payments))._compute_payment_method_description() + +# on change methods + + @api.constrains('check_ids') + @api.onchange('check_ids', 'payment_method_code') + def onchange_checks(self): + for rec in self: + # we only overwrite if payment method is delivered + if rec.payment_method_code == 'delivered_third_check': + rec.amount = sum(rec.check_ids.mapped('amount')) + currency = rec.check_ids.mapped('currency_id') + + if len(currency) > 1: + raise ValidationError(_( + 'You are trying to deposit checks of difference' + ' currencies, this functionality is not supported')) + elif len(currency) == 1: + rec.currency_id = currency.id + + # si es una entrega de cheques de terceros y es en otra moneda + # a la de la cia, forzamos el importe en moneda de cia de los + # cheques originales + # escribimos force_amount_company_currency directamente en vez + # de amount_company_currency por lo explicado en + # _inverse_amount_company_currency + if rec.currency_id != rec.company_id.currency_id: + rec.force_amount_company_currency = sum( + rec.check_ids.mapped('amount_company_currency')) + + @api.multi + @api.onchange('check_number') + def change_check_number(self): + # TODO make default padding a parameter + def _get_name_from_number(number): + padding = 8 + if len(str(number)) > padding: + padding = len(str(number)) + return ('%%0%sd' % padding % number) + + for rec in self: + if rec.payment_method_code in ['received_third_check']: + if not rec.check_number: + check_name = False + else: + check_name = _get_name_from_number(rec.check_number) + rec.check_name = check_name + elif rec.payment_method_code in ['issue_check']: + sequence = rec.checkbook_id.sequence_id + if not rec.check_number: + check_name = False + elif sequence: + if rec.check_number != sequence.number_next_actual: + # write with sudo for access rights over sequence + sequence.sudo().write( + {'number_next_actual': rec.check_number}) + check_name = rec.checkbook_id.sequence_id.next_by_id() + else: + # in sipreco, for eg, no sequence on checkbooks + check_name = _get_name_from_number(rec.check_number) + rec.check_name = check_name + + @api.onchange('check_issue_date', 'check_payment_date') + def onchange_date(self): + if ( + self.check_issue_date and self.check_payment_date and + self.check_issue_date > self.check_payment_date): + self.check_payment_date = False + raise UserError( + _('Check Payment Date must be greater than Issue Date')) + + @api.onchange('check_owner_vat') + def onchange_check_owner_vat(self): + """ + We suggest owner name from owner vat + """ + # if not self.check_owner_name: + self.check_owner_name = self.search( + [('check_owner_vat', '=', self.check_owner_vat)], + limit=1).check_owner_name + + @api.onchange('partner_id', 'payment_method_code') + def onchange_partner_check(self): + commercial_partner = self.partner_id.commercial_partner_id + if self.payment_method_code == 'received_third_check': + self.check_bank_id = ( + commercial_partner.bank_ids and + commercial_partner.bank_ids[0].bank_id or False) + # en realidad se termina pisando con onchange_check_owner_vat + # entonces llevamos nombre solo si ya existe la priemr vez + # TODO ver si lo mejoramos o borramos esto directamente + # self.check_owner_name = commercial_partner.name + vat_field = 'vat' + # to avoid needed of another module, we add this check to see + # if l10n_ar cuit field is available + if 'cuit' in commercial_partner._fields: + vat_field = 'cuit' + self.check_owner_vat = commercial_partner[vat_field] + elif self.payment_method_code == 'issue_check': + self.check_bank_id = self.journal_id.bank_id + self.check_owner_name = False + self.check_owner_vat = False + # no hace falta else porque no se usa en otros casos + + @api.onchange('payment_method_code') + def _onchange_payment_method_code(self): + if self.payment_method_code == 'issue_check': + checkbook = self.env['account.checkbook'].search([ + ('state', '=', 'active'), + ('journal_id', '=', self.journal_id.id)], + limit=1) + self.checkbook_id = checkbook + elif self.checkbook_id: + # TODO ver si interesa implementar volver atras numeracion + self.checkbook_id = False + # si cambiamos metodo de pago queremos refrescar listado de cheques + # seleccionados + self.check_ids = False + + @api.onchange('checkbook_id') + def onchange_checkbook(self): + if self.checkbook_id and not self.checkbook_id.numerate_on_printing: + self.check_number = self.checkbook_id.next_number + else: + self.check_number = False + +# post methods + @api.multi + def cancel(self): + for rec in self: + # solo cancelar operaciones si estaba postead, por ej para comp. + # con pagos confirmados, se cancelan pero no hay que deshacer nada + # de asientos ni cheques + if rec.state in ['confirmed', 'posted']: + rec.do_checks_operations(cancel=True) + res = super(AccountPayment, self).cancel() + return res + + @api.multi + def create_check(self, check_type, operation, bank): + self.ensure_one() + + check_vals = { + 'bank_id': bank.id, + 'owner_name': self.check_owner_name, + 'owner_vat': self.check_owner_vat, + 'number': self.check_number, + 'name': self.check_name, + 'checkbook_id': self.checkbook_id.id, + 'issue_date': self.check_issue_date, + 'type': self.check_type, + 'journal_id': self.journal_id.id, + 'amount': self.amount, + 'payment_date': self.check_payment_date, + 'currency_id': self.currency_id.id, + } + + check = self.env['account.check'].create(check_vals) + self.check_ids = [(4, check.id, False)] + check._add_operation( + operation, self, self.partner_id, date=self.payment_date) + return check + + @api.multi + def do_checks_operations(self, vals=None, cancel=False): + self.ensure_one() + rec = self + if not rec.check_type: + return vals + if ( + rec.payment_method_code == 'received_third_check' and + rec.payment_type == 'inbound' + # el chequeo de partner type no seria necesario + # un proveedor nos podria devolver plata con un cheque + # and rec.partner_type == 'customer' + ): + operation = 'holding' + if cancel: + _logger.info('Cancelled Received Check') + rec.check_ids._del_operation(self) + rec.check_ids.unlink() + return None + + _logger.info('Receive Check') + check = self.create_check( + 'third_check', operation, self.check_bank_id) + vals['date_maturity'] = self.check_payment_date + vals['account_id'] = check.get_third_check_account().id + vals['name'] = _('Receive check %s') % check.name + elif ( + rec.payment_method_code == 'delivered_third_check' and + rec.payment_type == 'transfer'): + # si el cheque es entregado en una transferencia tenemos tres + # opciones + # TODO we should make this method selectable for transfers + inbound_method = ( + rec.destination_journal_id.inbound_payment_method_ids) + # si un solo inbound method y es received third check + # entonces consideramos que se esta moviendo el cheque de un diario + # al otro + if len(inbound_method) == 1 and ( + inbound_method.code == 'received_third_check'): + if cancel: + _logger.info('Cancelled Transfered Check') + for check in rec.check_ids: + check._del_operation(self) + check._del_operation(self) + receive_op = check._get_operation('holding') + if receive_op.origin._name == 'account.payment': + check.journal_id = receive_op.origin.journal_id.id + return None + + _logger.info('Transfered Check') + # get the account before changing the journal on the check + vals['account_id'] = rec.check_ids.get_third_check_account().id + rec.check_ids._add_operation( + 'transfered', rec, False, date=rec.payment_date) + rec.check_ids._add_operation( + 'holding', rec, False, date=rec.payment_date) + rec.check_ids.write({ + 'journal_id': rec.destination_journal_id.id}) + vals['name'] = _('Transfer checks %s') % ', '.join( + rec.check_ids.mapped('name')) + elif rec.destination_journal_id.type == 'cash': + if cancel: + _logger.info('Cancelled Sold Check') + rec.check_ids._del_operation(self) + return None + + _logger.info('Sold Check') + rec.check_ids._add_operation( + 'sold', rec, False, date=rec.payment_date) + vals['account_id'] = rec.check_ids.get_third_check_account().id + vals['name'] = _('Sell check %s') % ', '.join( + rec.check_ids.mapped('name')) + # bank + else: + if cancel: + _logger.info('Cancelled Deposited Check') + rec.check_ids._del_operation(self) + return None + + _logger.info('Deposited Check') + rec.check_ids._add_operation( + 'deposited', rec, False, date=rec.payment_date) + vals['account_id'] = rec.check_ids.get_third_check_account().id + vals['name'] = _('Deposit checks %s') % ', '.join( + rec.check_ids.mapped('name')) + elif ( + rec.payment_method_code == 'delivered_third_check' and + rec.payment_type == 'outbound' + # el chequeo del partner type no es necesario + # podriamos entregarlo a un cliente + # and rec.partner_type == 'supplier' + ): + if cancel: + _logger.info('Cancelled Delivered Check') + rec.check_ids._del_operation(self) + return None + + _logger.info('Delivered Check') + rec.check_ids._add_operation( + 'delivered', rec, rec.partner_id, date=rec.payment_date) + vals['account_id'] = rec.check_ids.get_third_check_account().id + vals['name'] = _('Deliver checks %s') % ', '.join( + rec.check_ids.mapped('name')) + elif ( + rec.payment_method_code == 'issue_check' and + rec.payment_type == 'outbound' + # el chequeo del partner type no es necesario + # podriamos entregarlo a un cliente + # and rec.partner_type == 'supplier' + ): + if cancel: + _logger.info('Cancelled Hand/debit Check') + rec.check_ids._del_operation(self) + rec.check_ids.unlink() + return None + + _logger.info('Hand/debit Check') + # if check is deferred, hand it and later debit it change account + # if check is current, debit it directly + # operation = 'debited' + # al final por ahora depreciamos esto ya que deberiamos adaptar + # rechazos y demas, deferred solamente sin fecha pero con cuenta + # puente + # if self.check_subtype == 'deferred': + vals['account_id'] = self.company_id._get_check_account( + 'deferred').id + operation = 'handed' + check = self.create_check( + 'issue_check', operation, self.check_bank_id) + vals['date_maturity'] = self.check_payment_date + vals['name'] = _('Hand check %s') % check.name + elif ( + rec.payment_method_code == 'issue_check' and + rec.payment_type == 'transfer' and + rec.destination_journal_id.type == 'cash'): + if cancel: + _logger.info('Cancelled Withdrawn Check') + rec.check_ids._del_operation(self) + rec.check_ids.unlink() + return None + + _logger.info('Withdrawn Check') + self.create_check('issue_check', 'withdrawed', self.check_bank_id) + vals['name'] = _('Withdraw with checks %s') % ', '.join( + rec.check_ids.mapped('name')) + vals['date_maturity'] = self.check_payment_date + # if check is deferred, change account + # si retiramos por caja directamente lo sacamos de banco + # if self.check_subtype == 'deferred': + # vals['account_id'] = self.company_id._get_check_account( + # 'deferred').id + else: + raise UserError(_( + 'This operatios is not implemented for checks:\n' + '* Payment type: %s\n' + '* Partner type: %s\n' + '* Payment method: %s\n' + '* Destination journal: %s\n' % ( + rec.payment_type, + rec.partner_type, + rec.payment_method_code, + rec.destination_journal_id.type))) + return vals + + @api.multi + def post(self): + for rec in self: + if rec.check_ids and not rec.currency_id.is_zero( + sum(rec.check_ids.mapped('amount')) - rec.amount): + raise UserError(_( + 'The total of the payment does not match the total of the selected checks. ' + 'Please try deleting or re-adding a check.')) + if rec.payment_method_code == 'issue_check' and (not rec.check_number or not rec.check_name): + raise UserError(_('Please be sure that check number or name is filled!')) + + res = super(AccountPayment, self).post() + return res + + def _get_liquidity_move_line_vals(self, amount): + vals = super(AccountPayment, self)._get_liquidity_move_line_vals( + amount) + vals = self.do_checks_operations(vals=vals) + return vals + + @api.multi + def do_print_checks(self): + # si cambiamos nombre de check_report tener en cuenta en sipreco + checkbook = self.mapped('checkbook_id') + # si todos los cheques son de la misma chequera entonces buscamos + # reporte específico para esa chequera + report_name = len(checkbook) == 1 and \ + checkbook.report_template.report_name \ + or 'check_report' + check_report = self.env['ir.actions.report'].search( + [('report_name', '=', report_name)], limit=1).report_action(self) + # ya el buscar el reporte da el error solo + # if not check_report: + # raise UserError(_( + # "There is no check report configured.\nMake sure to configure " + # "a check report named 'account_check_report'.")) + return check_report + + @api.multi + def print_checks(self): + if len(self.mapped('checkbook_id')) != 1: + raise UserError(_( + "In order to print multiple checks at once, they must belong " + "to the same checkbook.")) + # por ahora preferimos no postearlos + # self.filtered(lambda r: r.state == 'draft').post() + + # si numerar al imprimir entonces llamamos al wizard + if self[0].checkbook_id.numerate_on_printing: + if all([not x.check_name for x in self]): + next_check_number = self[0].checkbook_id.next_number + return { + 'name': _('Print Pre-numbered Checks'), + 'type': 'ir.actions.act_window', + 'res_model': 'print.prenumbered.checks', + 'view_type': 'form', + 'view_mode': 'form', + 'target': 'new', + 'context': { + 'payment_ids': self.ids, + 'default_next_check_number': next_check_number, + } + } + # si ya están enumerados mandamos a imprimir directamente + elif all([x.check_name for x in self]): + return self.do_print_checks() + else: + raise UserError(_( + 'Está queriendo imprimir y enumerar cheques que ya han ' + 'sido numerados. Seleccione solo cheques numerados o solo' + ' cheques sin número.')) + else: + return self.do_print_checks() + + def _get_counterpart_move_line_vals(self, invoice=False): + vals = super(AccountPayment, self)._get_counterpart_move_line_vals( + invoice=invoice) + force_account_id = self._context.get('force_account_id') + if force_account_id: + vals['account_id'] = force_account_id + return vals + + @api.multi + def _split_aml_line_per_check(self, move): + """ Take an account mvoe, find the move lines related to check and + split them one per earch check related to the payment + """ + self.ensure_one() + res = self.env['account.move.line'] + move.button_cancel() + checks = self.check_ids + aml = move.line_ids.with_context(check_move_validity=False).filtered( + lambda x: x.name != self.name) + if len(aml) > 1: + raise UserError( + _('Seems like this move has been already splited')) + elif len(aml) == 0: + raise UserError( + _('There is not move lines to split')) + + amount_field = 'credit' if aml.credit else 'debit' + new_name = _('Deposit check %s') if aml.credit else \ + aml.name + _(' check %s') + + # if the move line has currency then we are delivering checks on a + # different currency than company one + currency = aml.currency_id + currency_sign = amount_field == 'debit' and 1.0 or -1.0 + aml.write({ + 'name': new_name % checks[0].name, + amount_field: checks[0].amount_company_currency, + 'amount_currency': currency and currency_sign * checks[0].amount, + }) + res |= aml + checks -= checks[0] + for check in checks: + res |= aml.copy({ + 'name': new_name % check.name, + amount_field: check.amount_company_currency, + 'payment_id': self.id, + 'amount_currency': currency and currency_sign * check.amount, + }) + move.post() + return res + + @api.multi + def _create_payment_entry(self, amount): + move = super(AccountPayment, self)._create_payment_entry(amount) + if self.filtered( + lambda x: x.payment_type == 'transfer' and + x.payment_method_code == 'delivered_third_check' and + x.check_deposit_type == 'detailed'): + self._split_aml_line_per_check(move) + return move + + @api.multi + def _create_transfer_entry(self, amount): + transfer_debit_aml = super( + AccountPayment, self)._create_transfer_entry(amount) + if self.filtered( + lambda x: x.payment_type == 'transfer' and + x.payment_method_code == 'delivered_third_check' and + x.check_deposit_type == 'detailed'): + self._split_aml_line_per_check(transfer_debit_aml.move_id) + return transfer_debit_aml diff --git a/account_check/models/res_company.py b/account_check/models/res_company.py new file mode 100644 index 00000000..f9007103 --- /dev/null +++ b/account_check/models/res_company.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import fields, models, api, _ +from odoo.exceptions import UserError +import logging +_logger = logging.getLogger(__name__) + + +class ResCompany(models.Model): + _inherit = 'res.company' + + rejected_check_account_id = fields.Many2one( + 'account.account', + 'Rejected Checks Account', + help='Rejection Checks account, for eg. "Rejected Checks"', + ) + deferred_check_account_id = fields.Many2one( + 'account.account', + 'Deferred Checks Account', + help='Deferred Checks account, for eg. "Deferred Checks"', + ) + holding_check_account_id = fields.Many2one( + 'account.account', + 'Holding Checks Account', + help='Holding Checks account for third checks, ' + 'for eg. "Holding Checks"', + ) + + @api.multi + def _get_check_account(self, check_type): + self.ensure_one() + if check_type == 'holding': + account = self.holding_check_account_id + elif check_type == 'rejected': + account = self.rejected_check_account_id + elif check_type == 'deferred': + account = self.deferred_check_account_id + else: + raise UserError(_("Check type %s not implemented!") % check_type) + if not account: + raise UserError(_( + 'No checks %s account defined for company %s' + ) % (check_type, self.name)) + return account diff --git a/account_check/models/res_config_settings.py b/account_check/models/res_config_settings.py new file mode 100644 index 00000000..08de3cbb --- /dev/null +++ b/account_check/models/res_config_settings.py @@ -0,0 +1,19 @@ +from odoo import fields, models +# from odoo.exceptions import UserError + + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + rejected_check_account_id = fields.Many2one( + related='company_id.rejected_check_account_id', + readonly=False, + ) + deferred_check_account_id = fields.Many2one( + related='company_id.deferred_check_account_id', + readonly=False, + ) + holding_check_account_id = fields.Many2one( + related='company_id.holding_check_account_id', + readonly=False, + ) diff --git a/account_check/security/account_check_security.xml b/account_check/security/account_check_security.xml new file mode 100644 index 00000000..4754e95d --- /dev/null +++ b/account_check/security/account_check_security.xml @@ -0,0 +1,18 @@ + + + + + Check Multi-Company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Check Operation Multi-Company + + + ['|',('check_id.company_id','=',False),('check_id.company_id','child_of',[user.company_id.id])] + + + diff --git a/account_check/security/ir.model.access.csv b/account_check/security/ir.model.access.csv new file mode 100644 index 00000000..d87cc722 --- /dev/null +++ b/account_check/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +account_check_access_full,account_check_access_full,model_account_check,account.group_account_invoice,1,1,1,1 +account_checkbook_access_full,account_checkbook_access_full,model_account_checkbook,account.group_account_invoice,1,1,1,1 +account_check_access_global,account_check_access_global,model_account_check,,1,0,0,0 +account_checkbook_access_global,account_checkbook_access_global,model_account_checkbook,,1,0,0,0 +account_check_operation_access_global,account_check_operation_access_global,model_account_check_operation,,1,0,0,0 +account_check_operation_access_full,account_check_operation_access_full,model_account_check_operation,account.group_account_invoice,1,1,1,1 diff --git a/account_check/views/account_chart_template_view.xml b/account_check/views/account_chart_template_view.xml new file mode 100644 index 00000000..b9a76a15 --- /dev/null +++ b/account_check/views/account_chart_template_view.xml @@ -0,0 +1,15 @@ + + + + account.chart.template.form + account.chart.template + + + + + + + + + + diff --git a/account_check/views/account_check_view.xml b/account_check/views/account_check_view.xml new file mode 100644 index 00000000..2c89ed4e --- /dev/null +++ b/account_check/views/account_check_view.xml @@ -0,0 +1,266 @@ + + + + + + account.check.tree + account.check + 100 + + + + + + + + + + + + + + + + + + + + + + + account.check.create.tree + account.check + + + + + true + + + + + + account.check.form + account.check + +
+ + + + + +
+ +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ +
+
+ + + account.check.create.form + account.check + + + +
+ true +
+ + + + + + + 0 + {'readonly':[('id','=',False)]} + +
+
+ + + check.search + account.check + + + + + + + + + + + + + + + + + + + + + + + + + + + + + account.check.calendar + account.check + + + + + + + + + account.check.graph + account.check + + + + + + + + + + account.check.calendar + account.check + + + + + + + + + + + + + Third Checks + account.check + form + tree,form,calendar,graph,pivot + [('type','=','third_check')] + {'default_type':'third_check'} + + + + + + + Issue Checks + account.check + form + tree,form,calendar,graph,pivot + [('type','=','issue_check')] + {'default_type':'issue_check'} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/account_check/views/account_checkbook_view.xml b/account_check/views/account_checkbook_view.xml new file mode 100644 index 00000000..84ca92c6 --- /dev/null +++ b/account_check/views/account_checkbook_view.xml @@ -0,0 +1,44 @@ + + + + + account.checkbook.tree + account.checkbook + + + + + + + + + + + + + + + account.checkbook.form + account.checkbook + +
+
+ +
+ + + + + + + + + + + + +
+
+
+ +
diff --git a/account_check/views/account_journal_dashboard_view.xml b/account_check/views/account_journal_dashboard_view.xml new file mode 100644 index 00000000..aef48ba9 --- /dev/null +++ b/account_check/views/account_journal_dashboard_view.xml @@ -0,0 +1,49 @@ + + + + account.journal.dashboard.kanban.inherited + account.journal + + + + + + + + + diff --git a/account_check/views/account_journal_view.xml b/account_check/views/account_journal_view.xml new file mode 100644 index 00000000..2155da05 --- /dev/null +++ b/account_check/views/account_journal_view.xml @@ -0,0 +1,14 @@ + + + + account_check.account.journal.form + account.journal + + + + + + + diff --git a/account_check/views/account_payment_view.xml b/account_check/views/account_payment_view.xml new file mode 100644 index 00000000..a75a8b75 --- /dev/null +++ b/account_check/views/account_payment_view.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + account.payment.form.inherited + account.payment + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {'readonly': ['|',('state','!=','draft'),('payment_method_code', '=', 'delivered_third_check')]} + + + {'readonly': ['|',('state','!=','draft'),('payment_method_code', '=', 'delivered_third_check')]} + + + + + + +
+ + + + account.payment.check.search + account.payment + + + + + + + + + + + +
diff --git a/account_check/views/res_config_settings_view.xml b/account_check/views/res_config_settings_view.xml new file mode 100644 index 00000000..23e9ad01 --- /dev/null +++ b/account_check/views/res_config_settings_view.xml @@ -0,0 +1,27 @@ + + + + account.config.settings.inherit + res.config.settings + + + +

Checks

+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/account_check/views/res_partner_views.xml b/account_check/views/res_partner_views.xml new file mode 100644 index 00000000..976d5823 --- /dev/null +++ b/account_check/views/res_partner_views.xml @@ -0,0 +1,30 @@ + + + + + Check List + ir.actions.act_window + account.check + tree,form + [('partner_id','=',active_id)] + {'search_default_state':'holding'} + current + + + + res.partner.form.inherit.local + res.partner + + + + + + + + + + + diff --git a/account_check/wizard/__init__.py b/account_check/wizard/__init__.py new file mode 100644 index 00000000..e4159c68 --- /dev/null +++ b/account_check/wizard/__init__.py @@ -0,0 +1,6 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from . import account_check_action_wizard +from . import print_pre_numbered_checks diff --git a/account_check/wizard/account_check_action_wizard.py b/account_check/wizard/account_check_action_wizard.py new file mode 100644 index 00000000..72d151b2 --- /dev/null +++ b/account_check/wizard/account_check_action_wizard.py @@ -0,0 +1,56 @@ +############################################################################## +# For copyright and license notices, see __manifest__.py file in module root +# directory +############################################################################## +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class AccountCheckActionWizard(models.TransientModel): + _name = 'account.check.action.wizard' + _description = 'Account Check Action Wizard' + + date = fields.Date( + default=fields.Date.context_today, + required=True, + ) + action_type = fields.Char( + 'Action type passed on the context', + required=True, + ) + journal_id = fields.Many2one( + 'account.journal', string='Journal' + ) + debit_account_id = fields.Many2one( + 'account.account', string='Debit Account' + ) + credit_account_id = fields.Many2one( + 'account.account', string='Credit Account' + ) + + @api.onchange('journal_id') + def onchange_journal_id(self): + if self.journal_id: + self.debit_account_id = self.journal_id.default_debit_account_id + self.credit_account_id = self.journal_id.default_credit_account_id + + @api.multi + def action_confirm(self): + self.ensure_one() + if self.action_type not in [ + 'claim', 'bank_debit', 'reject', 'customer_return']: + raise ValidationError(_( + 'Action %s not supported on checks') % self.action_type) + checks = self.env['account.check'].browse( + self._context.get('active_ids')) + for check in checks: + res = getattr( + check.with_context(action_date=self.date, + journal_id=self.journal_id, + debit_account_id=self.debit_account_id, + credit_account_id=self.credit_account_id + ), self.action_type)() + if len(checks) == 1: + return res + else: + return True diff --git a/account_check/wizard/account_check_action_wizard_view.xml b/account_check/wizard/account_check_action_wizard_view.xml new file mode 100644 index 00000000..cfa879b2 --- /dev/null +++ b/account_check/wizard/account_check_action_wizard_view.xml @@ -0,0 +1,34 @@ + + + + + account.check.action.wizard.form + account.check.action.wizard + +
+ + + + + + + + +
+
+ +
+
+ + + Check Action + account.check.action.wizard + form + form + new + + +
diff --git a/account_check/wizard/print_pre_numbered_checks.py b/account_check/wizard/print_pre_numbered_checks.py new file mode 100644 index 00000000..2ff4daea --- /dev/null +++ b/account_check/wizard/print_pre_numbered_checks.py @@ -0,0 +1,20 @@ + +from odoo import api, fields, models + + +class PrintPreNumberedChecks(models.TransientModel): + _name = 'print.prenumbered.checks' + _description = 'Print Pre-numbered Checks' + + next_check_number = fields.Integer('Next Check Number', required=True) + + @api.multi + def print_checks(self): + check_number = self.next_check_number + payments = self.env['account.payment'].browse( + self.env.context['payment_ids']) + for payment in payments: + payment.check_number = check_number + check_number += 1 + payment.change_check_number() + return payments.do_print_checks() diff --git a/account_check/wizard/print_pre_numbered_checks_view.xml b/account_check/wizard/print_pre_numbered_checks_view.xml new file mode 100644 index 00000000..176417ec --- /dev/null +++ b/account_check/wizard/print_pre_numbered_checks_view.xml @@ -0,0 +1,23 @@ + + + + + Print Pre-numbered Checks + print.prenumbered.checks + +
+

Please enter the number of the first pre-printed check that you are about to print on.

+

This will allow to save on payments the number of the corresponding check.

+ + + + +
+
+
+ +
diff --git a/account_payment_fix/README.rst b/account_payment_fix/README.rst new file mode 100644 index 00000000..e7b40d62 --- /dev/null +++ b/account_payment_fix/README.rst @@ -0,0 +1,69 @@ +.. |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 + +====================================== +Account Payment Fixes and Improvements +====================================== + +Several modification, fixes or improvements to payments: + +* Fix domains for payment method, journal and partner on payment view so that is not loosed when you enter an already created payment. +* It also fix available payment methods when you change several times the journal +* It also restrict destination journal selection if available inbound methods +* We also recreate the menu "Bank and Cash" +* Allow to make payments of child companies + +Installation +============ + +To install this module, you need to: + +#. Just install it + +Configuration +============= + +To configure this module, you need to: + +#. No configuration required + +.. 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_fix/__init__.py b/account_payment_fix/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/account_payment_fix/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/account_payment_fix/__manifest__.py b/account_payment_fix/__manifest__.py new file mode 100644 index 00000000..851d78b8 --- /dev/null +++ b/account_payment_fix/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'website': 'https://www.codequarters.com', + 'license': 'AGPL-3', + 'category': 'Accounting & Finance', + 'data': [ + 'views/account_payment_view.xml', + ], + 'demo': [], + 'depends': [ + 'account', + ], + 'installable': True, + 'name': 'Account Payment Fix', + 'test': [], + 'version': '12.0.1.1.0', +} diff --git a/account_payment_fix/models/__init__.py b/account_payment_fix/models/__init__.py new file mode 100644 index 00000000..9cc484bd --- /dev/null +++ b/account_payment_fix/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +from . import account_payment +from . import account_invoice diff --git a/account_payment_fix/models/account_invoice.py b/account_payment_fix/models/account_invoice.py new file mode 100644 index 00000000..9838591c --- /dev/null +++ b/account_payment_fix/models/account_invoice.py @@ -0,0 +1,24 @@ +# © 2016 ADHOC SA +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, api + + +class AccountInvoice(models.Model): + _inherit = "account.invoice" + + @api.multi + def register_payment( + self, payment_line, + writeoff_acc_id=False, writeoff_journal_id=False): + """ + Con esto arreglamos que los pagos puedan pagar contra una cuenta + no conciliable, arreglamos porque odoo manda a conciliar por mas + que no haya facturas y da error, entonces si no hay facturas + que no intente conciliar nada (lo usamos en sipreco esto por ej) + """ + if not self: + return True + return super(AccountInvoice, self).register_payment( + payment_line, writeoff_acc_id=writeoff_acc_id, + writeoff_journal_id=writeoff_journal_id) diff --git a/account_payment_fix/models/account_payment.py b/account_payment_fix/models/account_payment.py new file mode 100644 index 00000000..0f8ce2a7 --- /dev/null +++ b/account_payment_fix/models/account_payment.py @@ -0,0 +1,207 @@ +from odoo import fields, models, api +# from odoo.exceptions import ValidationError +import logging +_logger = logging.getLogger(__name__) + + +class AccountPayment(models.Model): + _inherit = "account.payment" + + name = fields.Char(readonly=False) + state = fields.Selection(track_visibility='always') + amount = fields.Monetary(track_visibility='always') + partner_id = fields.Many2one(track_visibility='always') + journal_id = fields.Many2one(track_visibility='always') + destination_journal_id = fields.Many2one(track_visibility='always') + currency_id = fields.Many2one(track_visibility='always') + # campo a ser extendido y mostrar un nombre detemrinado en las lineas de + # pago de un payment group o donde se desee (por ej. con cheque, retención, + # etc) + payment_method_description = fields.Char( + compute='_compute_payment_method_description', + string='Payment Method', + ) + + @api.multi + def _compute_payment_method_description(self): + for rec in self: + rec.payment_method_description = rec.payment_method_id.display_name + + # nuevo campo funcion para definir dominio de los metodos + payment_method_ids = fields.Many2many( + 'account.payment.method', + compute='_compute_payment_methods', + string='Available payment methods', + ) + journal_ids = fields.Many2many( + 'account.journal', + compute='_compute_journals' + ) + # journal_at_least_type = fields.Char( + # compute='_compute_journal_at_least_type' + # ) + destination_journal_ids = fields.Many2many( + 'account.journal', + compute='_compute_destination_journals' + ) + + @api.multi + @api.depends( + # 'payment_type', + 'journal_id', + ) + def _compute_destination_journals(self): + for rec in self: + domain = [ + ('type', 'in', ('bank', 'cash')), + # al final pensamos mejor no agregar esta restricción, por ej, + # para poder transferir a tarjeta a pagar. Esto solo se usa + # en transferencias + # ('at_least_one_inbound', '=', True), + ('company_id', '=', rec.journal_id.company_id.id), + ('id', '!=', rec.journal_id.id), + ] + rec.destination_journal_ids = rec.journal_ids.search(domain) + + # @api.multi + # @api.depends( + # 'payment_type', + # ) + # def _compute_journal_at_least_type(self): + # for rec in self: + # if rec.payment_type == 'inbound': + # journal_at_least_type = 'at_least_one_inbound' + # else: + # journal_at_least_type = 'at_least_one_outbound' + # rec.journal_at_least_type = journal_at_least_type + + @api.multi + def get_journals_domain(self): + """ + We get domain here so it can be inherited + """ + self.ensure_one() + domain = [('type', 'in', ('bank', 'cash'))] + if self.payment_type == 'inbound': + domain.append(('at_least_one_inbound', '=', True)) + # Al final dejamos que para transferencias se pueda elegir + # cualquier sin importar si tiene outbound o no + # else: + elif self.payment_type == 'outbound': + domain.append(('at_least_one_outbound', '=', True)) + return domain + + @api.multi + @api.depends( + 'payment_type', + ) + def _compute_journals(self): + for rec in self: + rec.journal_ids = rec.journal_ids.search(rec.get_journals_domain()) + + @api.multi + @api.depends( + 'journal_id.outbound_payment_method_ids', + 'journal_id.inbound_payment_method_ids', + 'payment_type', + ) + def _compute_payment_methods(self): + for rec in self: + if rec.payment_type in ('outbound', 'transfer'): + methods = rec.journal_id.outbound_payment_method_ids + else: + methods = rec.journal_id.inbound_payment_method_ids + rec.payment_method_ids = methods + + @api.onchange('payment_type') + def _onchange_payment_type(self): + """ + Sobre escribimos y desactivamos la parte del dominio de la funcion + original ya que se pierde si se vuelve a entrar + """ + if not self.invoice_ids: + # Set default partner type for the payment type + if self.payment_type == 'inbound': + self.partner_type = 'customer' + elif self.payment_type == 'outbound': + self.partner_type = 'supplier' + else: + self.partner_type = False + # limpiamos journal ya que podria no estar disponible para la nueva + # operacion y ademas para que se limpien los payment methods + self.journal_id = False + # # Set payment method domain + # res = self._onchange_journal() + # if not res.get('domain', {}): + # res['domain'] = {} + # res['domain']['journal_id'] = self.payment_type == 'inbound' and [ + # ('at_least_one_inbound', '=', True)] or [ + # ('at_least_one_outbound', '=', True)] + # res['domain']['journal_id'].append(('type', 'in', ('bank', 'cash'))) + # return res + + # @api.onchange('partner_type') + def _onchange_partner_type(self): + """ + Agregasmos dominio en vista ya que se pierde si se vuelve a entrar + Anulamos funcion original porque no haria falta + """ + return True + + @api.onchange('journal_id') + def _onchange_journal(self): + """ + Sobre escribimos y desactivamos la parte del dominio de la funcion + original ya que se pierde si se vuelve a entrar + """ + if self.journal_id: + self.currency_id = ( + self.journal_id.currency_id or self.company_id.currency_id) + # Set default payment method + # (we consider the first to be the default one) + payment_methods = ( + self.payment_type == 'inbound' and + self.journal_id.inbound_payment_method_ids or + self.journal_id.outbound_payment_method_ids) + # si es una transferencia y no hay payment method de origen, + # forzamos manual + if not payment_methods and self.payment_type == 'transfer': + payment_methods = self.env.ref( + 'account.account_payment_method_manual_out') + self.payment_method_id = ( + payment_methods and payment_methods[0] or False) + # si se eligió de origen el mismo diario de destino, lo resetiamos + if self.journal_id == self.destination_journal_id: + self.destination_journal_id = False + # # Set payment method domain + # # (restrict to methods enabled for the journal and to selected + # # payment type) + # payment_type = self.payment_type in ( + # 'outbound', 'transfer') and 'outbound' or 'inbound' + # return { + # 'domain': { + # 'payment_method_id': [ + # ('payment_type', '=', payment_type), + # ('id', 'in', payment_methods.ids)]}} + # return {} + + @api.multi + @api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id') + def _compute_destination_account_id(self): + """ + We send force_company on context so payments can be created from parent + companies. We try to send force_company on self but it doesnt works, it + only works sending it on partner + """ + res = super(AccountPayment, self)._compute_destination_account_id() + for rec in self.filtered( + lambda x: not x.invoice_ids and x.payment_type != 'transfer'): + partner = self.partner_id.with_context( + force_company=self.company_id.id) + if self.partner_type == 'customer': + self.destination_account_id = ( + partner.property_account_receivable_id.id) + else: + self.destination_account_id = ( + partner.property_account_payable_id.id) + return res diff --git a/account_payment_fix/views/account_payment_view.xml b/account_payment_fix/views/account_payment_view.xml new file mode 100644 index 00000000..880ff403 --- /dev/null +++ b/account_payment_fix/views/account_payment_view.xml @@ -0,0 +1,51 @@ + + + + + account.payment.form + account.payment + + +
+ + + + + + + [('id', 'in', journal_ids)] + {'no_create': True} + + + [('id', 'in', destination_journal_ids)] + {'no_create': True} + + + [('id', 'in', payment_method_ids)] + {'no_create': True} + + + + + + +
+ + + + + + payment.tree.inherit + account.payment + + + + 1 + + + + + + + +