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. -->
-
-
-
-
+
+
+
+
+
-
-
diff --git a/view/work_invoiced_progress_form.xml b/view/work_invoiced_progress_form.xml
index a063889a49..99e48cf424 100644
--- a/view/work_invoiced_progress_form.xml
+++ b/view/work_invoiced_progress_form.xml
@@ -5,8 +5,11 @@ this repository contains the full copyright notices and license terms. -->
-
-
+
+
+
+
+
diff --git a/view/work_invoiced_progress_list.xml b/view/work_invoiced_progress_list.xml
index 1b5a393c33..eefeb22106 100644
--- a/view/work_invoiced_progress_list.xml
+++ b/view/work_invoiced_progress_list.xml
@@ -3,6 +3,8 @@
this repository contains the full copyright notices and license terms. -->
-
+
+
+
diff --git a/work.xml b/work.xml
deleted file mode 100644
index fbe31bbfcc..0000000000
--- a/work.xml
+++ /dev/null
@@ -1,62 +0,0 @@
-
-
-
-
-
- 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
-
-
-
-
-
-
-
-