diff --git a/__init__.py b/__init__.py index 197e0c3..9b57775 100644 --- a/__init__.py +++ b/__init__.py @@ -5,12 +5,14 @@ from trytond.pool import Pool from . import invoice from . import commission from . import payment +from . import move def register(): Pool.register( invoice.Invoice, - invoice.Move, + move.Move, + move.Line, module='account_invoice_posted2draft', type_='model') Pool.register( payment.Invoice, diff --git a/commission.py b/commission.py index b72ac34..80a6a16 100644 --- a/commission.py +++ b/commission.py @@ -4,27 +4,43 @@ from trytond.pool import Pool, PoolMeta from trytond.tools import grouped_slice -__all__ = ['Invoice'] - class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' + def get_allow_draft(self, name): + pool = Pool() + Commission = pool.get('commission') + + result = super().get_allow_draft(name) + + invoiced = Commission.search([ + ('origin.invoice', '=', self.id, 'account.invoice.line'), + ('invoice_line', '!=', None), + ]) + if invoiced: + result = False + return result + @classmethod def draft(cls, invoices): - Commission = Pool().get('commission') + pool = Pool() + Commission = pool.get('commission') + to_delete = [] for sub_invoices in grouped_slice(invoices): ids = [i.id for i in sub_invoices] - commissions = Commission.search([ + to_delete = Commission.search([ ('origin.invoice', 'in', ids, 'account.invoice.line'), + ('invoice_line', '=', None), ]) - if commissions: - commissions_origin = Commission.search([ - ('origin.id', 'in', [c.id for c in commissions], 'commission'), + if to_delete: + to_delete_origin = Commission.search([ + ('origin.id', 'in', + [x.id for x in to_delete], 'commission'), + ('invoice_line', '=', None), ]) - if commissions_origin: - commissions += commissions_origin - Commission.delete(commissions) - + if to_delete_origin: + to_delete += to_delete_origin + Commission.delete(to_delete) return super(Invoice, cls).draft(invoices) diff --git a/invoice.py b/invoice.py index e869871..581125b 100644 --- a/invoice.py +++ b/invoice.py @@ -1,8 +1,9 @@ # This file is part account_invoice_posted2draft module for Tryton. # The COPYRIGHT file at the top level of this repository contains # the full copyright notices and license terms. -from trytond.pyson import Eval from trytond.pool import Pool, PoolMeta +from trytond.model import fields +from trytond.pyson import Eval from trytond.transaction import Transaction from trytond.i18n import gettext from trytond.exceptions import UserError @@ -11,14 +12,35 @@ from trytond.exceptions import UserError class Invoice(metaclass=PoolMeta): __name__ = 'account.invoice' + allow_draft = fields.Function( + fields.Boolean("Allow Draft Invoice"), 'get_allow_draft') + @classmethod def __setup__(cls): super(Invoice, cls).__setup__() - cls._check_modify_exclude.add('move') cls._transitions |= set((('posted', 'draft'),)) - cls._buttons['draft']['invisible'] = ( - Eval('state').in_(['draft', 'paid']) | ( - (Eval('state') == 'cancelled') & Eval('cancel_move'))) + cls._buttons['draft']['invisible'] = ~Eval('allow_draft', False) + cls._buttons['draft']['depends'] += tuple(['allow_draft']) + + def get_allow_draft(self, name): + # when IN invoice is validate from scratch, the move is in 'draft' + # state, so in this case could be draft in a "normal" way + if (self.state == 'validated' and self.move + and self.move.state != 'draft'): + return False + elif self.state == 'cancelled' and self.number is not None: + return False + elif self.state in {'paid', 'draft'}: + return False + elif self.state == 'posted': + lines_to_pay = [l for l in self.lines_to_pay + if not l.reconciliation] + # Invoice already paid or partial paid, should not be possible + # to change state to draft. + if (not lines_to_pay + or self.amount_to_pay != self.total_amount): + return False + return True @classmethod def draft(cls, invoices): @@ -26,64 +48,54 @@ class Invoice(metaclass=PoolMeta): Move = pool.get('account.move') MoveLine = pool.get('account.move.line') JournalPeriod = pool.get('account.journal.period') + Warning = pool.get('res.user.warning') moves = [] - payment_lines = [] + move_lines = [] + to_draft = [] + to_save = [] for invoice in invoices: - if invoice.move: - # check period is closed - if invoice.move.period.state == 'close': - raise UserError(gettext( - 'account_invoice_posted2draft.msg_draft_closed_period', - invoice=invoice.rec_name, - period=invoice.move.period.rec_name, - )) - # check period and journal is closed - journal_periods = JournalPeriod.search([ - ('journal', '=', invoice.move.journal.id), - ('period', '=', invoice.move.period.id), - ], limit=1) - if journal_periods: - journal_period, = journal_periods - if journal_period.state == 'close': - raise UserError(gettext( - 'account_invoice_posted2draft.' - 'msg_modify_closed_journal_period', - invoice=invoice.rec_name, - journal_period=journal_period.rec_name)) - moves.append(invoice.move) - if invoice.payment_lines: - for payment_line in invoice.payment_lines: - if payment_line.move and payment_line.move.lines: - for lines in payment_line.move.lines: - payment_lines.append(lines) + if not invoice.allow_draft: + continue - if moves: - with Transaction().set_context(draft_invoices=True): - Move.write(moves, {'state': 'draft'}) - # If the payment lines dont have a reconciliation, then the field - # invoice_payment will be fill up, and when we try to draft an - # invoice it will give us an error - if payment_lines: - MoveLine.write(payment_lines, {'invoice_payment':None}) - cls.write(invoices, { - 'invoice_report_format': None, - 'invoice_report_cache': None, - }) - with Transaction().set_context(draft_invoices=True): - return super(Invoice, cls).draft(invoices) + move = invoice.move + if move: + if move.state == 'draft': + to_draft.append(invoice) + else: + to_save.append(invoice) + cancel_move = move.cancel(reversal=True) + Move.post([cancel_move]) + moves.extend((invoice.move, cancel_move)) + invoice.move = None + else: + to_draft.append(invoice) + if invoice.cancel_move: + moves.append(invoice.cancel_move) + invoice.cancel_move = None + invoice.additional_moves += tuple(moves) - @classmethod - def credit(cls, invoices, refund=False, **values): - with Transaction().set_context(cancel_from_credit=True): - return super().credit(invoices, refund, **values) + # Only make the special steps for the invoices that came from 'posted' + # state or 'validated', 'cancelled' with number, so the invoice have one + # or more move associated. + # The other possible invoices follow the standard workflow. + if to_draft: + super().draft(to_draft) + if to_save: + cls.save(to_save) + with Transaction().set_context(invoice_posted2draft=True): + super().draft(to_save) + for invoice in to_save: + to_reconcile = [] + for move in invoice.additional_moves: + for line in move.lines: + if (not line.reconciliation + and line.account == invoice.account): + to_reconcile.append(line) + if to_reconcile: + MoveLine.reconcile(to_reconcile) -class Move(metaclass=PoolMeta): - __name__ = 'account.move' - - @classmethod - def check_modify(cls, *args, **kwargs): - if Transaction().context.get('draft_invoices', False): - return - return super(Move, cls).check_modify(*args, **kwargs) + # Remove links to lines which actually do not pay the invoice + if to_save: + cls._clean_payments(to_save) diff --git a/locale/ca.po b/locale/ca.po index 7931d08..0c91f7a 100644 --- a/locale/ca.po +++ b/locale/ca.po @@ -2,22 +2,26 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" -msgctxt "model:ir.message,text:msg_cancel_invoice_with_number" -msgid "You cannot cancel invoice \"%(invoice)s\" because it already has a number." -msgstr "No podeu cancel·lar la factura \"%(invoice)s\" amb un número assignat." +msgctxt "field:account.invoice,allow_draft:" +msgid "Allow Draft Invoice" +msgstr "Permet factura esborrany" msgctxt "model:ir.message,text:msg_draft_closed_period" msgid "" -"You can not set to draft invoice \"%(invoice)s\" because period " -"\"%(period)s\" is closed." +"You cannot create an account compesantion move in the period %(period)s " +"because is closed." msgstr "" -"No es pot passar a esborrany la factura \"%(invoice)s\" perquè el període " -"\"%(period)s\" està tancat." +"No podeu crear un assentamet de compensació en el període %(period)s perquè " +"està tancat." -msgctxt "model:ir.message,text:msg_modify_closed_journal_period" +msgctxt "model:ir.message,text:msg_invoice_in_payment" msgid "" -"You can not set to draft invoice \"%(invoice)s\" on closed journal-period " -"\"%(journal_period)s\"." +"The invoice %(invoice)s could not be possible to draft, becasue it have one " +"or more move lines in payments [IDs: %(payments)s]. This means that is " +"possible that this payment will be in a payment group and this group upload " +"on a Bank." msgstr "" -"No es pot passar a esborrany la factura \"%(invoice)s\" perquè el diari-" -"període \"%(journal_period)s\". està tancat." +"La factura %(invoice)s no es pot passar a esborrany, perquè té un o més " +"apunts relacionats a pagaments [ID: %(payments)s]. Això vol dir que és " +"possible que aquest pagament estigui en una remesa bancària i aquesta estigui " +"pujada ja al Banc." diff --git a/locale/es.po b/locale/es.po index 294ed0e..f1f17f2 100644 --- a/locale/es.po +++ b/locale/es.po @@ -2,22 +2,26 @@ msgid "" msgstr "Content-Type: text/plain; charset=utf-8\n" -msgctxt "model:ir.message,text:msg_cancel_invoice_with_number" -msgid "You cannot cancel invoice \"%(invoice)s\" because it already has a number." -msgstr "No puede cancelar la factura \"%(invoice)s\" con un número asignado." +msgctxt "field:account.invoice,allow_draft:" +msgid "Allow Draft Invoice" +msgstr "Permitir factura borrador" msgctxt "model:ir.message,text:msg_draft_closed_period" msgid "" -"You can not set to draft invoice \"%(invoice)s\" because period " -"\"%(period)s\" is closed." +"You cannot create an account compesantion move in the period %(period)s " +"because is closed." msgstr "" -"No se puede pasar a borrador la factura \"%(invoice)s\" porqué el período " -"\"%(period)s\" está cerrado." +"No puedes crear un asiento de compensación en el periodo %(period)s porque " +"está cerrado." -msgctxt "model:ir.message,text:msg_modify_closed_journal_period" +msgctxt "model:ir.message,text:msg_invoice_in_payment" msgid "" -"You can not set to draft invoice \"%(invoice)s\" on closed journal-period " -"\"%(journal_period)s\"." +"The invoice %(invoice)s could not be possible to draft, becasue it have one " +"or more move lines in payments [IDs: %(payments)s]. This means that is " +"possible that this payment will be in a payment group and this group upload " +"on a Bank." msgstr "" -"No se puede pasar a borrador la factura \"%(invoice)s\" porqué el diario-" -"período \"%(journal_period)s\" está cerrado." +"La factura %(invoice)s no se puede pasar a borrador, porque tiene uno o más " +"apuntes relacionados a pagos [ID: %(payments)s]. Esto quiere decir que és " +"posible que estos pagos esten ya en una remesa bancárea y ésta esté subida " +"al Banco." diff --git a/messages.xml b/messages.xml index cb1160d..2073dbd 100644 --- a/messages.xml +++ b/messages.xml @@ -4,13 +4,10 @@ this repository contains the full copyright notices and license terms. --> - You can not set to draft invoice "%(invoice)s" because period "%(period)s" is closed. + You cannot create an account compesantion move in the period %(period)s because is closed. - - You cannot cancel invoice "%(invoice)s" because it already has a number. - - - You can not set to draft invoice "%(invoice)s" on closed journal-period "%(journal_period)s". + + The invoice %(invoice)s could not be possible to draft, becasue it have one or more move lines in payments [IDs: %(payments)s]. This means that is possible that this payment will be in a payment group and this group upload on a Bank. diff --git a/move.py b/move.py new file mode 100644 index 0000000..95008b2 --- /dev/null +++ b/move.py @@ -0,0 +1,40 @@ +# This file is part account_invoice_posted2draft module for Tryton. +# The COPYRIGHT file at the top level of this repository contains +# the full copyright notices and license terms. +from trytond.pool import Pool, PoolMeta +from trytond.transaction import Transaction + + +class Move(metaclass=PoolMeta): + __name__ = 'account.move' + + @classmethod + def check_modify(cls, *args, **kwargs): + # As now the moves related to an invoice are not delete when 'draft' + # the invoice, is needed to modify some restricted fields when the + # move is in post state + if Transaction().context.get('invoice_posted2draft', False): + return + return super().check_modify(*args, **kwargs) + + @classmethod + def delete(cls, moves): + # When invoice is set to 'draft', try to delete the move's associated + # in 'move' and 'additional_move' fields. If these moves are posted + # they cannot be deleted but keep them as history + if Transaction().context.get('invoice_posted2draft', False): + return + super().delete(moves) + + +class Line(metaclass=PoolMeta): + __name__ = 'account.move.line' + + @classmethod + def check_modify(cls, lines, modified_fields=None): + # As now the moves related to an invoice are not delete when 'draft' + # the invoice, is needed to modify some restricted fields when the + # move is in post state + if Transaction().context.get('invoice_posted2draft', False): + return + return super().check_modify(lines, modified_fields) diff --git a/payment.py b/payment.py index d15997f..4f07d90 100644 --- a/payment.py +++ b/payment.py @@ -2,8 +2,8 @@ # The COPYRIGHT file at the top level of this repository contains # the full copyright notices and license terms. from trytond.pool import Pool, PoolMeta - -__all__ = ['Invoice'] +from trytond.i18n import gettext +from trytond.exceptions import UserError class Invoice(metaclass=PoolMeta): @@ -11,18 +11,26 @@ class Invoice(metaclass=PoolMeta): @classmethod def draft(cls, invoices): - Payment = Pool().get('account.payment') + pool = Pool() + Payment = pool.get('account.payment') - lines = [] for invoice in invoices: + moves = [] if invoice.move: - lines.extend([l.id for l in invoice.move.lines]) - if lines: - payments = Payment.search([ - ('line', 'in', lines), - ('state', '=', 'failed'), - ]) - if payments: - Payment.write(payments, {'line': None}) + moves.append(invoice.move) + if invoice.additional_moves: + moves.extend(invoice.additional_moves) - return super(Invoice, cls).draft(invoices) + if moves: + lines = [l.id for m in moves for l in m.lines] + if lines: + payments = Payment.search([ + ('line', 'in', lines), + ('state', '!=', 'failed'), + ]) + if payments: + raise UserError(gettext('account_invoice_posted2draft' + '.msg_invoice_in_payment', + invoice=invoice.rec_name, + payments=", ".join([p.id for p in payments]))) + return super().draft(invoices)