From 5b65c27c3ec6fcd676d4586feee4519f4efc7e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 6 Mar 2020 23:34:09 +0100 Subject: [PATCH] Remove duration fields and base invoicing on quantity and price issue7776 review64441002 --- CHANGELOG | 3 + __init__.py | 8 +- work.py => project.py | 607 +++++++++--------- project.xml | 56 ++ tests/scenario_project_invoice_effort.rst | 36 +- ...cenario_project_invoice_multiple_party.rst | 22 +- tests/scenario_project_invoice_progress.rst | 36 +- tests/scenario_project_invoice_timesheet.rst | 28 +- tryton.cfg | 1 - view/work_form.xml | 12 +- view/work_invoiced_progress_form.xml | 7 +- view/work_invoiced_progress_list.xml | 4 +- work.xml | 62 -- 13 files changed, 427 insertions(+), 455 deletions(-) rename work.py => project.py (66%) delete mode 100644 work.xml diff --git a/CHANGELOG b/CHANGELOG index 6673ab0678..a67058f7a4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,6 @@ +* Base invoicing on quantity and price +* Remove duration fields + Version 5.4.0 - 2019-11-04 * Bug fixes (see mercurial logs for details) * Add invoice method to project list view diff --git a/__init__.py b/__init__.py index b5f908a873..2f983f3dfb 100644 --- a/__init__.py +++ b/__init__.py @@ -2,18 +2,18 @@ # this repository contains the full copyright notices and license terms. from trytond.pool import Pool -from . import work +from . import project from . import timesheet from . import invoice def register(): Pool.register( - work.Work, - work.WorkInvoicedProgress, + project.Work, + project.WorkInvoicedProgress, timesheet.Line, invoice.InvoiceLine, module='project_invoice', type_='model') Pool.register( - work.OpenInvoice, + project.OpenInvoice, module='project_invoice', type_='wizard') diff --git a/work.py b/project.py similarity index 66% rename from work.py rename to project.py index 0e0ffa4b12..c083e4f4ba 100644 --- a/work.py +++ b/project.py @@ -9,6 +9,7 @@ import datetime from sql import Null from sql.aggregate import Sum +from sql.functions import Extract from sql.operators import Concat from trytond.i18n import gettext @@ -22,158 +23,25 @@ from trytond.tools import reduce_ids, grouped_slice from .exceptions import InvoicingError -INVOICE_METHODS = [ - ('manual', 'Manual'), - ('effort', 'On Effort'), - ('progress', 'On Progress'), - ('timesheet', 'On Timesheet'), - ] - -class Work(metaclass=PoolMeta): - __name__ = 'project.work' - project_invoice_method = fields.Selection(INVOICE_METHODS, - 'Invoice Method', - states={ - 'readonly': Bool(Eval('invoiced_duration')), - 'required': Eval('type') == 'project', - 'invisible': Eval('type') != 'project', - }, - depends=['invoiced_duration', 'type']) - invoice_method = fields.Function(fields.Selection(INVOICE_METHODS, - 'Invoice Method'), 'on_change_with_invoice_method') - invoiced_duration = fields.Function(fields.TimeDelta('Invoiced Duration', - 'company_work_time', - states={ - 'invisible': Eval('invoice_method') == 'manual', - }, - depends=['invoice_method']), 'get_total') - duration_to_invoice = fields.Function(fields.TimeDelta( - 'Duration to Invoice', 'company_work_time', - states={ - 'invisible': Eval('invoice_method') == 'manual', - }, - depends=['invoice_method']), 'get_total') - invoiced_amount = fields.Function(fields.Numeric('Invoiced Amount', - digits=(16, Eval('currency_digits', 2)), - states={ - 'invisible': Eval('invoice_method') == 'manual', - }, - depends=['currency_digits', 'invoice_method']), - 'get_total') - invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', - readonly=True) - invoiced_progress = fields.One2Many('project.work.invoiced_progress', - 'work', 'Invoiced Progress', readonly=True) +class Effort: @classmethod def __setup__(cls): - super(Work, cls).__setup__() - cls._buttons.update({ - 'invoice': { - 'invisible': ((Eval('type') != 'project') - | (Eval('project_invoice_method', 'manual') - == 'manual')), - 'readonly': ~Eval('duration_to_invoice'), - 'depends': ['type', 'project_invoice_method', - 'duration_to_invoice'] - }, - }) - - @staticmethod - def default_project_invoice_method(): - return 'manual' + super().__setup__() + cls.project_invoice_method.selection.append( + ('effort', "On Effort")) @classmethod - def copy(cls, records, default=None): - if default is None: - default = {} - else: - default = default.copy() - default.setdefault('invoice_line', None) - return super(Work, cls).copy(records, default=default) - - @fields.depends('type', 'project_invoice_method', - 'parent', '_parent_parent.invoice_method') - def on_change_with_invoice_method(self, name=None): - if self.type == 'project': - return self.project_invoice_method - elif self.parent: - return self.parent.invoice_method - else: - return 'manual' - - @staticmethod - def default_invoiced_duration(): - return datetime.timedelta() - - @staticmethod - def _get_invoiced_duration_manual(works): - return {} - - @staticmethod - def _get_invoiced_duration_effort(works): - return dict((w.id, w.effort_duration) for w in works - if w.invoice_line and w.effort_duration) - - @staticmethod - def _get_invoiced_duration_progress(works): - durations = {} + def _get_quantity_to_invoice_effort(cls, works): + quantities = {} for work in works: - durations[work.id] = sum((p.effort_duration - for p in work.invoiced_progress if p.effort_duration), - datetime.timedelta()) - return durations + if work.progress == 1 and not work.invoice_line: + quantities[work.id] = work.effort_hours + return quantities @classmethod - def _get_invoiced_duration_timesheet(cls, works): - return cls._get_duration_timesheet(works, True) - - @staticmethod - def default_duration_to_invoice(): - return datetime.timedelta() - - @staticmethod - def _get_duration_to_invoice_manual(works): - return {} - - @staticmethod - def _get_duration_to_invoice_effort(works): - return dict((w.id, w.effort_duration) for w in works - if w.progress == 1 and not w.invoice_line and w.effort_duration) - - @staticmethod - def _get_duration_to_invoice_progress(works): - durations = {} - for work in works: - if work.progress is None or work.effort_duration is None: - continue - effort_to_invoice = datetime.timedelta( - hours=work.effort_hours * work.progress) - effort_invoiced = sum( - (p.effort_duration - for p in work.invoiced_progress), - datetime.timedelta()) - if effort_to_invoice > effort_invoiced: - durations[work.id] = effort_to_invoice - effort_invoiced - else: - durations[work.id] = datetime.timedelta() - return durations - - @classmethod - def _get_duration_to_invoice_timesheet(cls, works): - return cls._get_duration_timesheet(works, False) - - @staticmethod - def default_invoiced_amount(): - return Decimal(0) - - @staticmethod - def _get_invoiced_amount_manual(works): - return {} - - @staticmethod - def _get_invoiced_amount_effort(works): + def _get_invoiced_amount_effort(cls, works): pool = Pool() InvoiceLine = pool.get('account.invoice.line') Currency = pool.get('currency.currency') @@ -197,6 +65,61 @@ class Work(metaclass=PoolMeta): amounts[work.id] = Decimal(0) return amounts + def get_origins_to_invoice(self): + try: + origins = super().get_origins_to_invoice() + except AttributeError: + origins = [] + if self.invoice_method == 'effort': + origins.append(self) + return origins + + +class Progress: + + invoiced_progress = fields.One2Many('project.work.invoiced_progress', + 'work', 'Invoiced Progress', readonly=True) + + @classmethod + def __setup__(cls): + super().__setup__() + cls.project_invoice_method.selection.append( + ('progress', 'On Progress')) + + @classmethod + def _get_quantity_to_invoice_progress(cls, works): + pool = Pool() + Progress = pool.get('project.work.invoiced_progress') + + cursor = Transaction().connection.cursor() + table = cls.__table__() + progress = Progress.__table__() + + invoiced_progress = {} + quantities = {} + for sub_works in grouped_slice(works): + sub_works = list(sub_works) + where = reduce_ids(table.id, [x.id for x in sub_works]) + cursor.execute(*table.join(progress, + condition=progress.work == table.id + ).select(table.id, Sum(progress.progress), + where=where, + group_by=table.id)) + invoiced_progress.update(dict(cursor.fetchall())) + + for work in sub_works: + delta = ( + (work.progress or 0) + - invoiced_progress.get(work.id, 0.0)) + if delta > 0: + quantities[work.id] = delta * work.effort_hours + return quantities + + @property + def progress_to_invoice(self): + if self.quantity_to_invoice: + return self.quantity_to_invoice / self.effort_hours + @classmethod def _get_invoiced_amount_progress(cls, works): pool = Pool() @@ -213,26 +136,22 @@ class Work(metaclass=PoolMeta): amounts = defaultdict(Decimal) work2currency = {} - work_ids = [w.id for w in works] - for sub_ids in grouped_slice(work_ids): + ids2work = dict((w.id, w) for w in works) + for sub_ids in grouped_slice(ids2work.keys()): where = reduce_ids(table.id, sub_ids) cursor.execute(*table.join(progress, condition=progress.work == table.id ).join(invoice_line, condition=progress.invoice_line == invoice_line.id ).select(table.id, - Sum(progress.effort_duration * invoice_line.unit_price), + Sum(progress.progress * invoice_line.unit_price), where=where, group_by=table.id)) for work_id, amount in cursor.fetchall(): - if isinstance(amount, datetime.timedelta): - amount = amount.total_seconds() - # Amount computed in second instead of hours - if amount is not None: - amount /= 60 * 60 - else: - amount = 0 - amounts[work_id] = amount + if not isinstance(amount, Decimal): + amount = Decimal(str(amount)) + amounts[work_id] = ( + amount * Decimal(str(ids2work[work_id].effort_hours))) cursor.execute(*table.join(company, condition=table.company == company.id @@ -248,6 +167,57 @@ class Work(metaclass=PoolMeta): amounts[work.id] = currency.round(Decimal(amounts[work.id])) return amounts + def get_origins_to_invoice(self): + pool = Pool() + InvoicedProgress = pool.get('project.work.invoiced_progress') + try: + origins = super().get_origins_to_invoice() + except AttributeError: + origins = [] + if self.invoice_method == 'progress': + invoiced_progress = InvoicedProgress( + work=self, progress=self.progress_to_invoice) + origins.append(invoiced_progress) + return origins + + +class Timesheet: + + @classmethod + def __setup__(cls): + super().__setup__() + cls.project_invoice_method.selection.append( + ('timesheet', 'On Timesheet')) + + @classmethod + def _get_quantity_to_invoice_timesheet(cls, works): + pool = Pool() + TimesheetLine = pool.get('timesheet.line') + cursor = Transaction().connection.cursor() + line = TimesheetLine.__table__() + + durations = defaultdict(datetime.timedelta) + twork2work = {tw.id: w.id for w in works for tw in w.timesheet_works} + for sub_ids in grouped_slice(twork2work.keys()): + red_sql = reduce_ids(line.work, sub_ids) + cursor.execute(*line.select(line.work, Sum(line.duration), + where=red_sql & (line.invoice_line == Null), + group_by=line.work)) + for twork_id, duration in cursor.fetchall(): + if duration: + # SQLite uses float for SUM + if not isinstance(duration, datetime.timedelta): + duration = datetime.timedelta(seconds=duration) + durations[twork2work[twork_id]] += duration + + quantities = {} + for work in works: + duration = durations[work.id] + if work.list_price: + hours = duration.total_seconds() / 60 / 60 + quantities[work.id] = hours + return quantities + @classmethod def _get_invoiced_amount_timesheet(cls, works): pool = Pool() @@ -301,52 +271,131 @@ class Work(metaclass=PoolMeta): amounts[work.id] = currency.round(Decimal(str(amount))) return amounts - @staticmethod - def _get_duration_timesheet(works, invoiced): - pool = Pool() - TimesheetLine = pool.get('timesheet.line') - cursor = Transaction().connection.cursor() - line = TimesheetLine.__table__() + def get_origins_to_invoice(self): + try: + origins = super().get_origins_to_invoice() + except AttributeError: + origins = [] + if self.invoice_method == 'timesheet': + origins.extend( + l for tw in self.timesheet_works + for l in tw.timesheet_lines + if not l.invoice_line) + return origins - durations = defaultdict(datetime.timedelta) - twork2work = {tw.id: w.id for w in works for tw in w.timesheet_works} - for sub_ids in grouped_slice(twork2work.keys()): - red_sql = reduce_ids(line.work, sub_ids) - if invoiced: - where = line.invoice_line != Null - else: - where = line.invoice_line == Null - cursor.execute(*line.select(line.work, Sum(line.duration), - where=red_sql & where, - group_by=line.work)) - for twork_id, duration in cursor.fetchall(): - if duration: - # SQLite uses float for SUM - if not isinstance(duration, datetime.timedelta): - duration = datetime.timedelta(seconds=duration) - durations[twork2work[twork_id]] += duration - return durations + +class Work(Effort, Progress, Timesheet, metaclass=PoolMeta): + __name__ = 'project.work' + project_invoice_method = fields.Selection([ + ('manual', "Manual"), + ], "Invoice Method", + states={ + 'readonly': Bool(Eval('invoiced_amount')), + 'required': Eval('type') == 'project', + 'invisible': Eval('type') != 'project', + }, + depends=['invoiced_amount', 'type']) + invoice_method = fields.Function(fields.Selection( + 'get_invoice_methods', "Invoice Method"), + 'on_change_with_invoice_method') + quantity_to_invoice = fields.Function( + fields.Float("Quantity to Invoice"), '_get_invoice_values') + amount_to_invoice = fields.Function(fields.Numeric("Amount to Invoice", + digits=(16, Eval('currency_digits', 2)), + states={ + 'invisible': Eval('invoice_method') == 'manual', + }, + depends=['currency_digits', 'invoice_method']), + 'get_total') + invoiced_amount = fields.Function(fields.Numeric('Invoiced Amount', + digits=(16, Eval('currency_digits', 2)), + states={ + 'invisible': Eval('invoice_method') == 'manual', + }, + depends=['currency_digits', 'invoice_method']), + 'get_total') + invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', + readonly=True) + + @classmethod + def __setup__(cls): + super(Work, cls).__setup__() + cls._buttons.update({ + 'invoice': { + 'invisible': ((Eval('type') != 'project') + | (Eval('project_invoice_method', 'manual') + == 'manual')), + 'readonly': ~Eval('amount_to_invoice'), + 'depends': [ + 'type', 'project_invoice_method', 'amount_to_invoice'], + }, + }) + + @staticmethod + def default_project_invoice_method(): + return 'manual' + + @classmethod + def copy(cls, records, default=None): + if default is None: + default = {} + else: + default = default.copy() + default.setdefault('invoice_line', None) + return super(Work, cls).copy(records, default=default) + + @classmethod + def get_invoice_methods(cls): + field = 'project_invoice_method' + return cls.fields_get(field)[field]['selection'] + + @fields.depends('type', 'project_invoice_method', + 'parent', '_parent_parent.invoice_method') + def on_change_with_invoice_method(self, name=None): + if self.type == 'project': + return self.project_invoice_method + elif self.parent: + return self.parent.invoice_method + else: + return 'manual' + + @classmethod + def default_quantity_to_invoice(cls): + return 0 + + @classmethod + def _get_quantity_to_invoice_manual(cls, works): + return {} + + @classmethod + def _get_amount_to_invoice(cls, works): + amounts = {} + for work in works: + amounts[work.id] = work.company.currency.round( + (work.invoice_unit_price or 0) + * Decimal(str(work.quantity_to_invoice))) + return amounts + + @classmethod + def default_invoiced_amount(cls): + return Decimal(0) + + @classmethod + def _get_invoiced_amount_manual(cls, works): + return {} @classmethod def _get_invoice_values(cls, works, name): default = getattr(cls, 'default_%s' % name) - durations = dict.fromkeys((w.id for w in works), default()) + amounts = dict.fromkeys((w.id for w in works), default()) method2works = defaultdict(list) for work in works: method2works[work.invoice_method].append(work) for method, m_works in method2works.items(): method = getattr(cls, '_get_%s_%s' % (name, method)) # Re-browse for cache alignment - durations.update(method(cls.browse(m_works))) - return durations - - @classmethod - def _get_invoiced_duration(cls, works): - return cls._get_invoice_values(works, 'invoiced_duration') - - @classmethod - def _get_duration_to_invoice(cls, works): - return cls._get_invoice_values(works, 'duration_to_invoice') + amounts.update(method(cls.browse(m_works))) + return amounts @classmethod def _get_invoiced_amount(cls, works): @@ -362,7 +411,8 @@ class Work(metaclass=PoolMeta): uninvoiced = works[:] while uninvoiced: work = uninvoiced.pop(0) - invoice_lines, uninvoiced_children = work._get_lines_to_invoice() + invoice_lines, uninvoiced_children = ( + work._get_all_lines_to_invoice()) uninvoiced.extend(uninvoiced_children) if not invoice_lines: continue @@ -376,10 +426,10 @@ class Work(metaclass=PoolMeta): invoice_line = work._get_invoice_line(key, invoice, lines) invoice_line.invoice = invoice.id invoice_line.save() - origins = {} + origins = defaultdict(list) for line in lines: - origin = line['origin'] - origins.setdefault(origin.__class__, []).append(origin) + for origin in line['origins']: + origins[origin.__class__].append(origin) # TODO: remove when _check_access ignores record rule with Transaction().set_user(0): for klass, records in origins.items(): @@ -470,139 +520,96 @@ class Work(metaclass=PoolMeta): invoice_line.taxes = taxes return invoice_line - def _get_lines_to_invoice_manual(self): - return [] - - def _get_lines_to_invoice_effort(self): - pool = Pool() - ModelData = pool.get('ir.model.data') - Uom = pool.get('product.uom') - - hour = Uom(ModelData.get_id('product', 'uom_hour')) - - if (not self.invoice_line - and self.effort_hours - and self.progress == 1): - if not self.product: - raise InvoicingError( - gettext('project_invoice.msg_missing_product', - work=self.rec_name)) - elif self.list_price is None: - raise InvoicingError( - gettext('project_invoice.msg_missing_list_price', - work=self.rec_name)) - return [{ - 'product': self.product, - 'quantity': self.effort_hours, - 'unit': hour, - 'unit_price': self.list_price, - 'origin': self, - 'description': self.name, - }] - return [] - - def _get_lines_to_invoice_progress(self): - pool = Pool() - InvoicedProgress = pool.get('project.work.invoiced_progress') - ModelData = pool.get('ir.model.data') - Uom = pool.get('product.uom') - - hour = Uom(ModelData.get_id('product', 'uom_hour')) - - if self.progress is None or self.effort_duration is None: - return [] - - invoiced_progress = sum(x.effort_hours for x in self.invoiced_progress) - quantity = self.effort_hours * self.progress - invoiced_progress - if self.product: - quantity = Uom.compute_qty( - hour, quantity, self.product.default_uom) - if quantity > 0: - if not self.product: - raise InvoicingError( - gettext('project_invoice.msg_missing_product', - work=self.rec_name)) - elif self.list_price is None: - raise InvoicingError( - gettext('project_invoice.msg_missing_list_price', - work=self.rec_name)) - invoiced_progress = InvoicedProgress(work=self, - effort_duration=datetime.timedelta(hours=quantity)) - return [{ - 'product': self.product, - 'quantity': quantity, - 'unit': hour, - 'unit_price': self.list_price, - 'origin': invoiced_progress, - 'description': self.name, - }] - return [] - - def _get_lines_to_invoice_timesheet(self): - pool = Pool() - ModelData = pool.get('ir.model.data') - Uom = pool.get('product.uom') - - hour = Uom(ModelData.get_id('product', 'uom_hour')) - if (self.timesheet_works - and any(tw.timesheet_lines for tw in self.timesheet_works)): - if not self.product: - raise InvoicingError( - gettext('project_invoice.msg_missing_product', - work=self.rec_name)) - elif self.list_price is None: - raise InvoicingError( - gettext('project_invoice.msg_missing_list_price', - work=self.rec_name)) - return [{ - 'product': self.product, - 'quantity': l.hours, - 'unit': hour, - 'unit_price': self.list_price, - 'origin': l, - 'description': self.name, - } - for tw in self.timesheet_works - for l in tw.timesheet_lines - if not l.invoice_line] - return [] - def _test_group_invoice(self): return (self.company, self.party) - def _get_lines_to_invoice(self, test=None): + def _get_all_lines_to_invoice(self, test=None): "Return lines for work and children" lines = [] if test is None: test = self._test_group_invoice() uninvoiced_children = [] - lines += getattr(self, '_get_lines_to_invoice_%s' % - self.invoice_method)() + lines += self._get_lines_to_invoice() for children in self.children: if children.type == 'project': if test != children._test_group_invoice(): uninvoiced_children.append(children) continue - child_lines, uninvoiced = children._get_lines_to_invoice(test=test) + child_lines, uninvoiced = children._get_all_lines_to_invoice( + test=test) lines.extend(child_lines) uninvoiced_children.extend(uninvoiced) return lines, uninvoiced_children + def _get_lines_to_invoice(self): + if self.quantity_to_invoice: + if not self.product: + raise InvoicingError( + gettext('project_invoice.msg_missing_product', + work=self.rec_name)) + elif self.invoice_unit_price is None: + raise InvoicingError( + gettext('project_invoice.msg_missing_list_price', + work=self.rec_name)) + return [{ + 'product': self.product, + 'quantity': self.quantity_to_invoice, + 'unit': self.unit_to_invoice, + 'unit_price': self.invoice_unit_price, + 'origins': self.get_origins_to_invoice(), + 'description': self.name, + }] + return [] + + @property + def invoice_unit_price(self): + return self.list_price + + @property + def unit_to_invoice(self): + pool = Pool() + ModelData = pool.get('ir.model.data') + Uom = pool.get('product.uom') + return Uom(ModelData.get_id('product', 'uom_hour')) + + def get_origins_to_invoice(self): + return super().get_origins_to_invoice() + class WorkInvoicedProgress(ModelView, ModelSQL): 'Work Invoiced Progress' __name__ = 'project.work.invoiced_progress' work = fields.Many2One('project.work', 'Work', ondelete='RESTRICT', select=True) - effort_duration = fields.TimeDelta('Effort', 'company_work_time') + progress = fields.Float('Progress', required=True, + domain=[ + ('progress', '>=', 0), + ]) invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line', ondelete='CASCADE') - @property - def effort_hours(self): - if not self.effort_duration: - return 0 - return self.effort_duration.total_seconds() / 60 / 60 + @classmethod + def __register__(cls, module_name): + cursor = Transaction().connection.cursor() + table = cls.__table_handler__(module_name) + sql_table = cls.__table__() + pool = Pool() + Work = pool.get('project.work') + work = Work.__table__() + + created_progress = not table.column_exist('progress') + effort_exist = table.column_exist('effort_duration') + + super().__register__(module_name) + + # Migration from 5.0: Effort renamed into to progress + if created_progress and effort_exist: + # Don't use UPDATE FROM because SQLite does not support it. + value = work.select( + (Extract('EPOCH', sql_table.effort_duration) + / Extract('EPOCH', work.effort_duration)), + where=work.id == sql_table.work) + cursor.execute(*sql_table.update([sql_table.progress], [value])) class OpenInvoice(Wizard): diff --git a/project.xml b/project.xml index 0cc0a97308..713afe6c91 100644 --- a/project.xml +++ b/project.xml @@ -10,5 +10,61 @@ this repository contains the full copyright notices and license terms. --> + + + project.work + + work_list + + + project.work + + work_form + + + + invoice + Invoice + + + + + + + + + project.work.invoiced_progress + form + work_invoiced_progress_form + + + project.work.invoiced_progress + tree + work_invoiced_progress_list + + + + + + + + + + + Invoices + project.open_invoice + project.work + + + form_relate + project.work,-1 + + + + + + diff --git a/tests/scenario_project_invoice_effort.rst b/tests/scenario_project_invoice_effort.rst index 1239ab52e1..f1fb8891c8 100644 --- a/tests/scenario_project_invoice_effort.rst +++ b/tests/scenario_project_invoice_effort.rst @@ -125,39 +125,33 @@ Create a Project:: >>> project.save() >>> task, task_no_effort = project.children -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration - datetime.timedelta(0) - >>> project.duration_to_invoice - datetime.timedelta(0) >>> project.invoiced_amount Decimal('0') + >>> project.amount_to_invoice + Decimal('0.00') Do 1 task:: >>> task.progress = 1 >>> task.save() -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration - datetime.timedelta(0) - >>> project.duration_to_invoice == datetime.timedelta(0, 18000) - True >>> project.invoiced_amount Decimal('0') + >>> project.amount_to_invoice + Decimal('100.00') Invoice project:: >>> set_user(project_invoice_user) >>> project.click('invoice') - >>> project.invoiced_duration == datetime.timedelta(0, 18000) - True - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('100.00') @@ -169,13 +163,11 @@ Do project:: >>> project.progress = 1 >>> project.save() -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration == datetime.timedelta(0, 18000) - True - >>> project.duration_to_invoice == datetime.timedelta(0, 3600) - True + >>> project.amount_to_invoice + Decimal('20.00') >>> project.invoiced_amount Decimal('100.00') @@ -183,9 +175,7 @@ Invoice again project:: >>> set_user(project_invoice_user) >>> project.click('invoice') - >>> project.invoiced_duration == datetime.timedelta(0, 21600) - True - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('120.00') diff --git a/tests/scenario_project_invoice_multiple_party.rst b/tests/scenario_project_invoice_multiple_party.rst index 6d0bcd6eb0..d30101bee3 100644 --- a/tests/scenario_project_invoice_multiple_party.rst +++ b/tests/scenario_project_invoice_multiple_party.rst @@ -83,13 +83,11 @@ Create a Project with multiple customers:: >>> project.save() >>> subproject, = project.children -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration - datetime.timedelta(0) - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('0') @@ -100,23 +98,19 @@ Do project and subproject:: >>> project.progress = 1 >>> project.save() -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration - datetime.timedelta(0) - >>> project.duration_to_invoice == datetime.timedelta(0, 21600) - True + >>> project.amount_to_invoice + Decimal('120.00') >>> project.invoiced_amount Decimal('0') Invoice project:: >>> project.click('invoice') - >>> project.invoiced_duration == datetime.timedelta(0, 21600) - True - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('120.00') diff --git a/tests/scenario_project_invoice_progress.rst b/tests/scenario_project_invoice_progress.rst index bd837ade89..4be7e14b2d 100644 --- a/tests/scenario_project_invoice_progress.rst +++ b/tests/scenario_project_invoice_progress.rst @@ -123,13 +123,11 @@ Create a Project:: >>> project.save() >>> task, = project.children -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration - datetime.timedelta(0) - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('0.00') @@ -138,13 +136,11 @@ Do 50% of task:: >>> task.progress = 0.5 >>> task.save() -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration - datetime.timedelta(0) - >>> project.duration_to_invoice == datetime.timedelta(0, 9000) - True + >>> project.amount_to_invoice + Decimal('50.00') >>> project.invoiced_amount Decimal('0.00') @@ -152,10 +148,8 @@ Invoice project:: >>> set_user(project_invoice_user) >>> project.click('invoice') - >>> project.invoiced_duration == datetime.timedelta(0, 9000) - True - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('50.00') @@ -167,13 +161,11 @@ Do 75% of task and 80% of project:: >>> project.progress = 0.80 >>> project.save() -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration == datetime.timedelta(0, 9000) - True - >>> project.duration_to_invoice == datetime.timedelta(0, 7380) - True + >>> project.amount_to_invoice + Decimal('41.00') >>> project.invoiced_amount Decimal('50.00') @@ -181,9 +173,7 @@ Invoice again project:: >>> set_user(project_invoice_user) >>> project.click('invoice') - >>> project.invoiced_duration == datetime.timedelta(0, 16380) - True - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('91.00') diff --git a/tests/scenario_project_invoice_timesheet.rst b/tests/scenario_project_invoice_timesheet.rst index de783cef97..f8cf755e61 100644 --- a/tests/scenario_project_invoice_timesheet.rst +++ b/tests/scenario_project_invoice_timesheet.rst @@ -143,13 +143,11 @@ Create timesheets:: >>> line.work, = project.timesheet_works >>> line.save() -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration - datetime.timedelta(0) - >>> project.duration_to_invoice == datetime.timedelta(0, 18000) - True + >>> project.amount_to_invoice + Decimal('100.00') >>> project.invoiced_amount Decimal('0.00') @@ -157,10 +155,8 @@ Invoice project:: >>> set_user(project_invoice_user) >>> project.click('invoice') - >>> project.invoiced_duration == datetime.timedelta(0, 18000) - True - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('100.00') @@ -174,13 +170,11 @@ Create more timesheets:: >>> line.work, = task.timesheet_works >>> line.save() -Check project duration:: +Check project amounts:: >>> project.reload() - >>> project.invoiced_duration == datetime.timedelta(0, 18000) - True - >>> project.duration_to_invoice == datetime.timedelta(0, 14400) - True + >>> project.amount_to_invoice + Decimal('80.00') >>> project.invoiced_amount Decimal('100.00') @@ -188,9 +182,7 @@ Invoice again project:: >>> set_user(project_invoice_user) >>> project.click('invoice') - >>> project.invoiced_duration == datetime.timedelta(0, 32400) - True - >>> project.duration_to_invoice - datetime.timedelta(0) + >>> project.amount_to_invoice + Decimal('0.00') >>> project.invoiced_amount Decimal('180.00') diff --git a/tryton.cfg b/tryton.cfg index 6818ad377b..b63779f9cf 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -10,6 +10,5 @@ depends: product xml: project.xml - work.xml timesheet.xml message.xml diff --git a/view/work_form.xml b/view/work_form.xml index e2a6cb66cc..18ea2f3dc8 100644 --- a/view/work_form.xml +++ b/view/work_form.xml @@ -8,15 +8,13 @@ this repository contains the full copyright notices and license terms. -->