Remove duration fields and base invoicing on quantity and price

issue7776
review64441002
This commit is contained in:
Cédric Krier committed 2020-03-06 23:34:09 +01:00
1 parent 9175442ef3
commit 5b65c27c3e
13 files changed
+427 -455

No files matched your search

+3
View File
@@ -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
+4 -4
View File
@@ -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')
+307 -300
View File
@@ -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):
+56
View File
@@ -10,5 +10,61 @@ this repository contains the full copyright notices and license terms. -->
<field name="user" ref="res.user_admin"/>
<field name="group" ref="group_project_invoice"/>
</record>
<record model="ir.ui.view" id="work_view_list">
<field name="model">project.work</field>
<field name="inherit" ref="project.work_view_list"/>
<field name="name">work_list</field>
</record>
<record model="ir.ui.view" id="work_view_form">
<field name="model">project.work</field>
<field name="inherit" ref="project.work_view_form"/>
<field name="name">work_form</field>
</record>
<record model="ir.model.button" id="work_invoice_button">
<field name="name">invoice</field>
<field name="string">Invoice</field>
<field name="model" search="[('model', '=', 'project.work')]"/>
</record>
<record model="ir.model.button-res.group"
id="work_invoice_button_group_project_invoice">
<field name="button" ref="work_invoice_button"/>
<field name="group" ref="group_project_invoice"/>
</record>
<record model="ir.ui.view" id="work_invoiced_progress_view_form">
<field name="model">project.work.invoiced_progress</field>
<field name="type">form</field>
<field name="name">work_invoiced_progress_form</field>
</record>
<record model="ir.ui.view" id="work_invoiced_progress_view_list">
<field name="model">project.work.invoiced_progress</field>
<field name="type">tree</field>
<field name="name">work_invoiced_progress_list</field>
</record>
<record model="ir.model.access" id="access_work_invoiced_progress">
<field name="model"
search="[('model', '=', 'project.work.invoiced_progress')]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.action.wizard" id="open_invoice">
<field name="name">Invoices</field>
<field name="wiz_name">project.open_invoice</field>
<field name="model">project.work</field>
</record>
<record model="ir.action.keyword" id="open_invoice_keyword">
<field name="keyword">form_relate</field>
<field name="model">project.work,-1</field>
<field name="action" ref="open_invoice"/>
</record>
<record model="ir.action-res.group" id="open_invoice-group_invoice">
<field name="action" ref="open_invoice"/>
<field name="group" ref="account.group_account"/>
</record>
</data>
</tryton>
+13 -23
View File
@@ -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')
@@ -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')
+13 -23
View File
@@ -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')
+10 -18
View File
@@ -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')
-1
View File
@@ -10,6 +10,5 @@ depends:
product
xml:
project.xml
work.xml
timesheet.xml
message.xml
+5 -7
View File
@@ -8,15 +8,13 @@ this repository contains the full copyright notices and license terms. -->
<label name="project_invoice_method"/>
<field name="project_invoice_method"/>
</xpath>
<xpath expr="/form/notebook/page[@id='general']/separator[@name='comment']"
position="before">
<newline/>
<label name="invoiced_duration"/>
<field name="invoiced_duration"/>
<xpath expr="/form/notebook/page[@id='general']/field[@name='total_effort']" position="after">
<label name="amount_to_invoice"/>
<field name="amount_to_invoice"/>
</xpath>
<xpath expr="/form/notebook/page[@id='general']/field[@name='revenue']" position="after">
<label name="invoiced_amount"/>
<field name="invoiced_amount"/>
<label name="duration_to_invoice"/>
<field name="duration_to_invoice"/>
</xpath>
<xpath expr="/form/notebook/page[@id='general']/group/group[@id='buttons']"
position="inside">
+5 -2
View File
@@ -5,8 +5,11 @@ this repository contains the full copyright notices and license terms. -->
<label name="work"/>
<field name="work"/>
<newline/>
<label name="effort_duration"/>
<field name="effort_duration"/>
<label name="progress"/>
<group col="2" id="progress">
<field name="progress" factor="100" xexpand="0"/>
<label name="progress" string="%" xalign="0.0" xexpand="1"/>
</group>
<label name="invoice_line"/>
<field name="invoice_line"/>
</form>
+3 -1
View File
@@ -3,6 +3,8 @@
this repository contains the full copyright notices and license terms. -->
<tree>
<field name="work" expand="2"/>
<field name="effort_duration"/>
<field name="progress" factor="100">
<suffix string="%" name="progress"/>
</field>
<field name="invoice_line" expand="1"/>
</tree>
-62
View File
@@ -1,62 +0,0 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="work_view_list">
<field name="model">project.work</field>
<field name="inherit" ref="project.work_view_list"/>
<field name="name">work_list</field>
</record>
<record model="ir.ui.view" id="work_view_form">
<field name="model">project.work</field>
<field name="inherit" ref="project.work_view_form"/>
<field name="name">work_form</field>
</record>
<record model="ir.model.button" id="work_invoice_button">
<field name="name">invoice</field>
<field name="string">Invoice</field>
<field name="model" search="[('model', '=', 'project.work')]"/>
</record>
<record model="ir.model.button-res.group"
id="work_invoice_button_group_project_invoice">
<field name="button" ref="work_invoice_button"/>
<field name="group" ref="group_project_invoice"/>
</record>
<record model="ir.ui.view" id="work_invoiced_progress_view_form">
<field name="model">project.work.invoiced_progress</field>
<field name="type">form</field>
<field name="name">work_invoiced_progress_form</field>
</record>
<record model="ir.ui.view" id="work_invoiced_progress_view_list">
<field name="model">project.work.invoiced_progress</field>
<field name="type">tree</field>
<field name="name">work_invoiced_progress_list</field>
</record>
<record model="ir.model.access" id="access_work_invoiced_progress">
<field name="model"
search="[('model', '=', 'project.work.invoiced_progress')]"/>
<field name="perm_read" eval="True"/>
<field name="perm_write" eval="False"/>
<field name="perm_create" eval="False"/>
<field name="perm_delete" eval="False"/>
</record>
<record model="ir.action.wizard" id="open_invoice">
<field name="name">Invoices</field>
<field name="wiz_name">project.open_invoice</field>
<field name="model">project.work</field>
</record>
<record model="ir.action.keyword" id="open_invoice_keyword">
<field name="keyword">form_relate</field>
<field name="model">project.work,-1</field>
<field name="action" ref="open_invoice"/>
</record>
<record model="ir.action-res.group" id="open_invoice-group_invoice">
<field name="action" ref="open_invoice"/>
<field name="group" ref="account.group_account"/>
</record>
</data>
</tryton>