diff --git a/modules/project_invoice/invoice.py b/modules/project_invoice/invoice.py index 0fc5b547fe..769adb99b9 100644 --- a/modules/project_invoice/invoice.py +++ b/modules/project_invoice/invoice.py @@ -1,13 +1,114 @@ # This file is part of Tryton. The COPYRIGHT file at the top level of # this repository contains the full copyright notices and license terms. + +import datetime as dt + +from sql.aggregate import Sum + +from trytond.i18n import gettext +from trytond.model import fields +from trytond.modules.account_invoice.exceptions import ( + InvoiceLineValidationError) from trytond.pool import Pool, PoolMeta -from trytond.tools import grouped_slice +from trytond.tools import grouped_slice, reduce_ids from trytond.transaction import Transaction class InvoiceLine(metaclass=PoolMeta): __name__ = 'account.invoice.line' + project_invoice_works = fields.One2Many( + 'project.work', 'invoice_line', + "Project Invoice Works", readonly=True) + project_invoice_progresses = fields.One2Many( + 'project.work.invoiced_progress', 'invoice_line', + "Project Invoice Progresses", readonly=True) + project_invoice_timesheet_duration = fields.Function( + fields.TimeDelta("Project Invoice Timesheet Duration"), + 'get_project_invoice_timesheet_duration') + + @classmethod + def check_validate_project_invoice_quantity(cls, lines, field_names): + pool = Pool() + Lang = pool.get('ir.lang') + if field_names and not (field_names & { + 'quantity', 'project_invoice_works'}): + return + for line in lines: + project_invoice_quantity = line.project_invoice_quantity + if project_invoice_quantity is None: + continue + if line.unit: + project_invoice_quantity = line.unit.round( + project_invoice_quantity) + if line.quantity != project_invoice_quantity: + lang = Lang.get() + if line.unit: + quantity = lang.format_number_symbol( + project_invoice_quantity, line.unit) + else: + quantity = lang.format_number(project_invoice_quantity) + raise InvoiceLineValidationError(gettext( + 'project_invoice.msg_project_invoice_line_quantity', + invoice_line=line.rec_name, + quantity=quantity, + )) + + @property + def project_invoice_quantity(self): + quantity = None + for work in self.project_invoice_works: + if quantity is None: + quantity = 0 + if work.price_list_hour: + quantity += work.effort_hours + else: + quantity += 1 + for progress in self.project_invoice_progresses: + if quantity is None: + quantity = 0 + work = progress.work + if work.price_list_hour: + quantity += progress.progress * work.effort_hours + else: + quantity += progress.progress + if self.project_invoice_timesheet_duration is not None: + if quantity is None: + quantity = 0 + quantity += ( + self.project_invoice_timesheet_duration.total_seconds() + / 60 / 60) + return quantity + + @classmethod + def get_project_invoice_timesheet_duration(cls, lines, name): + pool = Pool() + TimesheetLine = pool.get('timesheet.line') + cursor = Transaction().connection.cursor() + ts_line = TimesheetLine.__table__() + + durations = dict.fromkeys(map(int, lines)) + query = ts_line.select( + ts_line.invoice_line, Sum(ts_line.duration), + group_by=ts_line.invoice_line) + for sub_lines in grouped_slice(lines): + query.where = reduce_ids( + ts_line.invoice_line, map(int, sub_lines)) + cursor.execute(*query) + + for line_id, duration in cursor: + # SQLite uses float for SUM + if (duration is not None + and not isinstance(duration, dt.timedelta)): + duration = dt.timedelta(seconds=duration) + durations[line_id] = duration + return durations + + @classmethod + def validate_fields(cls, lines, field_names): + super().validate_fields(lines, field_names) + cls.check_validate_project_invoice_quantity(lines, field_names) + @classmethod def delete(cls, lines): pool = Pool() diff --git a/modules/project_invoice/message.xml b/modules/project_invoice/message.xml index 524ee4be69..14f7feef9c 100644 --- a/modules/project_invoice/message.xml +++ b/modules/project_invoice/message.xml @@ -30,5 +30,8 @@ this repository contains the full copyright notices and license terms. --> You cannot delete work "%(work)s" because it has been invoiced. + + The quantity of project invoice line "%(invoice_line)s" must be "%(quantity)s" + diff --git a/modules/project_invoice/tests/scenario_project_invoice_effort.rst b/modules/project_invoice/tests/scenario_project_invoice_effort.rst index c0fd8c8860..ddad0e454d 100644 --- a/modules/project_invoice/tests/scenario_project_invoice_effort.rst +++ b/modules/project_invoice/tests/scenario_project_invoice_effort.rst @@ -192,3 +192,16 @@ Invoice again project:: Decimal('0.00') >>> project.invoiced_amount Decimal('170.00') + +Try to change invoice line quantity:: + + >>> set_user(1) + >>> ProjectWork = Model.get('project.work') + >>> task = ProjectWork(task.id) + >>> task.invoice_line.quantity = 1 + >>> task.invoice_line.save() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvoiceLineValidationError: ... + >>> task.invoice_line.quantity = 5 + >>> task.invoice_line.save() diff --git a/modules/project_invoice/tests/scenario_project_invoice_progress.rst b/modules/project_invoice/tests/scenario_project_invoice_progress.rst index f4182b9a56..019877a1d3 100644 --- a/modules/project_invoice/tests/scenario_project_invoice_progress.rst +++ b/modules/project_invoice/tests/scenario_project_invoice_progress.rst @@ -194,3 +194,17 @@ Invoice again project:: Decimal('0.00') >>> project.invoiced_amount Decimal('136.00') + +Try to change invoice line quantity:: + + >>> set_user(1) + >>> ProjectWork = Model.get('project.work') + >>> task = ProjectWork(task.id) + >>> invoice_line = task.invoiced_progress[0].invoice_line + >>> invoice_line.quantity = 2. + >>> invoice_line.save() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvoiceLineValidationError: ... + >>> invoice_line.quantity = 2.5 + >>> invoice_line.save() diff --git a/modules/project_invoice/tests/scenario_project_invoice_timesheet.rst b/modules/project_invoice/tests/scenario_project_invoice_timesheet.rst index 4dab1b1101..82557002be 100644 --- a/modules/project_invoice/tests/scenario_project_invoice_timesheet.rst +++ b/modules/project_invoice/tests/scenario_project_invoice_timesheet.rst @@ -225,3 +225,16 @@ Invoice again project:: >>> _, _, invoice = Invoice.find([], order=[('id', 'ASC')]) >>> invoice.total_amount Decimal('80.00') + +Try to change invoice line quantity:: + + >>> set_user(1) + >>> TimesheetLine = Model.get('timesheet.line') + >>> line = TimesheetLine(line.id) + >>> line.invoice_line.quantity = 5 + >>> line.invoice_line.save() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvoiceLineValidationError: ... + >>> line.invoice_line.quantity = 4 + >>> line.invoice_line.save()