changeset 0367786244cb in modules/project_invoice:default
details: 
https://hg.tryton.org/modules/project_invoice?cmd=changeset;node=0367786244cb
description:
        Remove duration fields and base invoicing on quantity and price

        issue7776
        review64441002
diffstat:

 CHANGELOG                                         |    3 +
 __init__.py                                       |    8 +-
 project.py                                        |  643 ++++++++++++++++++++++
 project.xml                                       |   56 +
 tests/scenario_project_invoice_effort.rst         |   36 +-
 tests/scenario_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.py                                           |  636 ---------------------
 work.xml                                          |   62 --
 14 files changed, 763 insertions(+), 791 deletions(-)

diffs (1788 lines):

diff -r 5a78f819e58c -r 0367786244cb CHANGELOG
--- a/CHANGELOG Sun Mar 01 16:12:39 2020 +0100
+++ b/CHANGELOG Fri Mar 06 23:34:09 2020 +0100
@@ -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 -r 5a78f819e58c -r 0367786244cb __init__.py
--- a/__init__.py       Sun Mar 01 16:12:39 2020 +0100
+++ b/__init__.py       Fri Mar 06 23:34:09 2020 +0100
@@ -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 -r 5a78f819e58c -r 0367786244cb project.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/project.py        Fri Mar 06 23:34:09 2020 +0100
@@ -0,0 +1,643 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+
+from itertools import groupby
+from collections import defaultdict
+from decimal import Decimal
+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
+from trytond.model import ModelSQL, ModelView, fields
+from trytond.pool import PoolMeta
+from trytond.pyson import Eval, Bool, PYSONEncoder
+from trytond.pool import Pool
+from trytond.transaction import Transaction
+from trytond.wizard import Wizard, StateAction
+from trytond.tools import reduce_ids, grouped_slice
+
+from .exceptions import InvoicingError
+
+
+class Effort:
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls.project_invoice_method.selection.append(
+            ('effort', "On Effort"))
+
+    @classmethod
+    def _get_quantity_to_invoice_effort(cls, works):
+        quantities = {}
+        for work in works:
+            if work.progress == 1 and not work.invoice_line:
+                quantities[work.id] = work.effort_hours
+        return quantities
+
+    @classmethod
+    def _get_invoiced_amount_effort(cls, works):
+        pool = Pool()
+        InvoiceLine = pool.get('account.invoice.line')
+        Currency = pool.get('currency.currency')
+
+        invoice_lines = InvoiceLine.browse([
+                w.invoice_line.id for w in works
+                if w.invoice_line])
+
+        id2invoice_lines = dict((l.id, l) for l in invoice_lines)
+        amounts = {}
+        for work in works:
+            currency = work.company.currency
+            if work.invoice_line:
+                invoice_line = id2invoice_lines[work.invoice_line.id]
+                invoice_currency = (invoice_line.invoice.currency
+                    if invoice_line.invoice else invoice_line.currency)
+                amounts[work.id] = Currency.compute(invoice_currency,
+                    Decimal(str(work.effort_hours)) * invoice_line.unit_price,
+                    currency)
+            else:
+                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()
+        Progress = pool.get('project.work.invoiced_progress')
+        InvoiceLine = pool.get('account.invoice.line')
+        Company = pool.get('company.company')
+        Currency = pool.get('currency.currency')
+
+        cursor = Transaction().connection.cursor()
+        table = cls.__table__()
+        progress = Progress.__table__()
+        invoice_line = InvoiceLine.__table__()
+        company = Company.__table__()
+
+        amounts = defaultdict(Decimal)
+        work2currency = {}
+        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.progress * invoice_line.unit_price),
+                    where=where,
+                    group_by=table.id))
+            for work_id, amount in cursor.fetchall():
+                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
+                    ).select(table.id, company.currency,
+                    where=where))
+            work2currency.update(cursor.fetchall())
+
+        currencies = Currency.browse(set(work2currency.values()))
+        id2currency = {c.id: c for c in currencies}
+
+        for work in works:
+            currency = id2currency[work2currency[work.id]]
+            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()
+        TimesheetWork = pool.get('timesheet.work')
+        TimesheetLine = pool.get('timesheet.line')
+        InvoiceLine = pool.get('account.invoice.line')
+        Company = pool.get('company.company')
+        Currency = pool.get('currency.currency')
+
+        cursor = Transaction().connection.cursor()
+        table = cls.__table__()
+        timesheet_work = TimesheetWork.__table__()
+        timesheet_line = TimesheetLine.__table__()
+        invoice_line = InvoiceLine.__table__()
+        company = Company.__table__()
+
+        amounts = {}
+        work2currency = {}
+        work_ids = [w.id for w in works]
+        for sub_ids in grouped_slice(work_ids):
+            where = reduce_ids(table.id, sub_ids)
+            cursor.execute(*table.join(timesheet_work,
+                    condition=(
+                        Concat(cls.__name__ + ',', table.id)
+                        == timesheet_work.origin)
+                    ).join(timesheet_line,
+                    condition=timesheet_line.work == timesheet_work.id
+                    ).join(invoice_line,
+                    condition=timesheet_line.invoice_line == invoice_line.id
+                    ).select(table.id,
+                    Sum(timesheet_line.duration * invoice_line.unit_price),
+                    where=where,
+                    group_by=table.id))
+            amounts.update(cursor.fetchall())
+
+            cursor.execute(*table.join(company,
+                    condition=table.company == company.id
+                    ).select(table.id, company.currency,
+                    where=where))
+            work2currency.update(cursor.fetchall())
+
+        currencies = Currency.browse(set(work2currency.values()))
+        id2currency = {c.id: c for c in currencies}
+
+        for work in works:
+            currency = id2currency[work2currency[work.id]]
+            amount = amounts.get(work.id, 0)
+            if isinstance(amount, datetime.timedelta):
+                amount = amount.total_seconds()
+            amount = amount / 60 / 60
+            amounts[work.id] = currency.round(Decimal(str(amount)))
+        return amounts
+
+    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
+
+
+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)
+        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
+            amounts.update(method(cls.browse(m_works)))
+        return amounts
+
+    @classmethod
+    def _get_invoiced_amount(cls, works):
+        return cls._get_invoice_values(works, 'invoiced_amount')
+
+    @classmethod
+    @ModelView.button
+    def invoice(cls, works):
+        pool = Pool()
+        Invoice = pool.get('account.invoice')
+
+        invoices = []
+        uninvoiced = works[:]
+        while uninvoiced:
+            work = uninvoiced.pop(0)
+            invoice_lines, uninvoiced_children = (
+                work._get_all_lines_to_invoice())
+            uninvoiced.extend(uninvoiced_children)
+            if not invoice_lines:
+                continue
+            invoice = work._get_invoice()
+            invoice.save()
+            invoices.append(invoice)
+            for key, lines in groupby(invoice_lines,
+                    key=work._group_lines_to_invoice_key):
+                lines = list(lines)
+                key = dict(key)
+                invoice_line = work._get_invoice_line(key, invoice, lines)
+                invoice_line.invoice = invoice.id
+                invoice_line.save()
+                origins = defaultdict(list)
+                for line in lines:
+                    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():
+                        klass.save(records)  # Store first new origins
+                        klass.write(records, {
+                                'invoice_line': invoice_line.id,
+                                })
+        Invoice.update_taxes(invoices)
+
+    def _get_invoice(self):
+        "Return invoice for the work"
+        pool = Pool()
+        Invoice = pool.get('account.invoice')
+        Journal = pool.get('account.journal')
+
+        journals = Journal.search([
+                ('type', '=', 'revenue'),
+                ], limit=1)
+        if journals:
+            journal, = journals
+        else:
+            journal = None
+
+        if not self.party:
+            raise InvoicingError(
+                gettext('project_invoice.msg_missing_party',
+                    work=self.rec_name))
+
+        return Invoice(
+            company=self.company,
+            type='out',
+            journal=journal,
+            party=self.party,
+            invoice_address=self.party.address_get(type='invoice'),
+            currency=self.company.currency,
+            account=self.party.account_receivable_used,
+            payment_term=self.party.customer_payment_term,
+            description=self.name,
+            )
+
+    def _group_lines_to_invoice_key(self, line):
+        "The key to group lines"
+        return (('product', line['product']),
+            ('unit', line['unit']),
+            ('unit_price', line['unit_price']),
+            ('description', line['description']))
+
+    def _get_invoice_line(self, key, invoice, lines):
+        "Return a invoice line for the lines"
+        pool = Pool()
+        InvoiceLine = pool.get('account.invoice.line')
+        Uom = pool.get('product.uom')
+
+        quantity = sum(l['quantity'] for l in lines)
+        product = key['product']
+
+        invoice_line = InvoiceLine()
+        invoice_line.type = 'line'
+        invoice_line.description = key['description']
+        invoice_line.account = product.account_revenue_used
+        if (key['unit']
+                and key['unit'].category == product.default_uom.category):
+            invoice_line.product = product
+            invoice_line.unit_price = Uom.compute_price(
+                key['unit'], key['unit_price'], product.default_uom)
+            invoice_line.quantity = Uom.compute_qty(
+                key['unit'], quantity, product.default_uom)
+            invoice_line.unit = product.default_uom
+        else:
+            invoice_line.unit_price = key['unit_price']
+            invoice_line.quantity = quantity
+            invoice_line.unit = key['unit']
+
+        taxes = []
+        pattern = invoice_line._get_tax_rule_pattern()
+        party = invoice.party
+        for tax in product.customer_taxes_used:
+            if party.customer_tax_rule:
+                tax_ids = party.customer_tax_rule.apply(tax, pattern)
+                if tax_ids:
+                    taxes.extend(tax_ids)
+                continue
+            taxes.append(tax.id)
+        if party.customer_tax_rule:
+            tax_ids = party.customer_tax_rule.apply(None, pattern)
+            if tax_ids:
+                taxes.extend(tax_ids)
+        invoice_line.taxes = taxes
+        return invoice_line
+
+    def _test_group_invoice(self):
+        return (self.company, self.party)
+
+    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 += 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_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)
+    progress = fields.Float('Progress', required=True,
+        domain=[
+            ('progress', '>=', 0),
+            ])
+    invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line',
+        ondelete='CASCADE')
+
+    @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):
+    'Open Invoice'
+    __name__ = 'project.open_invoice'
+    start_state = 'open_'
+    open_ = StateAction('account_invoice.act_invoice_form')
+
+    def do_open_(self, action):
+        pool = Pool()
+        Work = pool.get('project.work')
+        works = Work.search([
+                ('parent', 'child_of', Transaction().context['active_ids']),
+                ])
+        invoice_ids = set()
+        for work in works:
+            if work.invoice_line and work.invoice_line.invoice:
+                invoice_ids.add(work.invoice_line.invoice.id)
+            for twork in work.timesheet_works:
+                for timesheet_line in twork.timesheet_lines:
+                    if (timesheet_line.invoice_line
+                            and timesheet_line.invoice_line.invoice):
+                        invoice_ids.add(timesheet_line.invoice_line.invoice.id)
+            if work.invoiced_progress:
+                for progress in work.invoiced_progress:
+                    invoice_ids.add(progress.invoice_line.invoice.id)
+        encoder = PYSONEncoder()
+        action['pyson_domain'] = encoder.encode(
+            [('id', 'in', list(invoice_ids))])
+        action['pyson_search_value'] = encoder.encode([])
+        return action, {}
diff -r 5a78f819e58c -r 0367786244cb project.xml
--- a/project.xml       Sun Mar 01 16:12:39 2020 +0100
+++ b/project.xml       Fri Mar 06 23:34:09 2020 +0100
@@ -10,5 +10,61 @@
             <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>
diff -r 5a78f819e58c -r 0367786244cb tests/scenario_project_invoice_effort.rst
--- a/tests/scenario_project_invoice_effort.rst Sun Mar 01 16:12:39 2020 +0100
+++ b/tests/scenario_project_invoice_effort.rst Fri Mar 06 23:34:09 2020 +0100
@@ -125,39 +125,33 @@
     >>> 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 @@
     >>> 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 @@
 
     >>> 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 -r 5a78f819e58c -r 0367786244cb 
tests/scenario_project_invoice_multiple_party.rst
--- a/tests/scenario_project_invoice_multiple_party.rst Sun Mar 01 16:12:39 
2020 +0100
+++ b/tests/scenario_project_invoice_multiple_party.rst Fri Mar 06 23:34:09 
2020 +0100
@@ -83,13 +83,11 @@
     >>> 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 @@
     >>> 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 -r 5a78f819e58c -r 0367786244cb tests/scenario_project_invoice_progress.rst
--- a/tests/scenario_project_invoice_progress.rst       Sun Mar 01 16:12:39 
2020 +0100
+++ b/tests/scenario_project_invoice_progress.rst       Fri Mar 06 23:34:09 
2020 +0100
@@ -123,13 +123,11 @@
     >>> 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 @@
     >>> 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 @@
 
     >>> 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 @@
     >>> 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 @@
 
     >>> 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 -r 5a78f819e58c -r 0367786244cb 
tests/scenario_project_invoice_timesheet.rst
--- a/tests/scenario_project_invoice_timesheet.rst      Sun Mar 01 16:12:39 
2020 +0100
+++ b/tests/scenario_project_invoice_timesheet.rst      Fri Mar 06 23:34:09 
2020 +0100
@@ -143,13 +143,11 @@
     >>> 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 @@
 
     >>> 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 @@
     >>> 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 @@
 
     >>> 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 -r 5a78f819e58c -r 0367786244cb tryton.cfg
--- a/tryton.cfg        Sun Mar 01 16:12:39 2020 +0100
+++ b/tryton.cfg        Fri Mar 06 23:34:09 2020 +0100
@@ -10,6 +10,5 @@
     product
 xml:
     project.xml
-    work.xml
     timesheet.xml
     message.xml
diff -r 5a78f819e58c -r 0367786244cb view/work_form.xml
--- a/view/work_form.xml        Sun Mar 01 16:12:39 2020 +0100
+++ b/view/work_form.xml        Fri Mar 06 23:34:09 2020 +0100
@@ -8,15 +8,13 @@
         <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">
diff -r 5a78f819e58c -r 0367786244cb view/work_invoiced_progress_form.xml
--- a/view/work_invoiced_progress_form.xml      Sun Mar 01 16:12:39 2020 +0100
+++ b/view/work_invoiced_progress_form.xml      Fri Mar 06 23:34:09 2020 +0100
@@ -5,8 +5,11 @@
     <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>
diff -r 5a78f819e58c -r 0367786244cb view/work_invoiced_progress_list.xml
--- a/view/work_invoiced_progress_list.xml      Sun Mar 01 16:12:39 2020 +0100
+++ b/view/work_invoiced_progress_list.xml      Fri Mar 06 23:34:09 2020 +0100
@@ -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>
diff -r 5a78f819e58c -r 0367786244cb work.py
--- a/work.py   Sun Mar 01 16:12:39 2020 +0100
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -1,636 +0,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.
-
-
-from itertools import groupby
-from collections import defaultdict
-from decimal import Decimal
-import datetime
-
-from sql import Null
-from sql.aggregate import Sum
-from sql.operators import Concat
-
-from trytond.i18n import gettext
-from trytond.model import ModelSQL, ModelView, fields
-from trytond.pool import PoolMeta
-from trytond.pyson import Eval, Bool, PYSONEncoder
-from trytond.pool import Pool
-from trytond.transaction import Transaction
-from trytond.wizard import Wizard, StateAction
-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)
-
-    @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'
-
-    @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 = {}
-        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
-
-    @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):
-        pool = Pool()
-        InvoiceLine = pool.get('account.invoice.line')
-        Currency = pool.get('currency.currency')
-
-        invoice_lines = InvoiceLine.browse([
-                w.invoice_line.id for w in works
-                if w.invoice_line])
-
-        id2invoice_lines = dict((l.id, l) for l in invoice_lines)
-        amounts = {}
-        for work in works:
-            currency = work.company.currency
-            if work.invoice_line:
-                invoice_line = id2invoice_lines[work.invoice_line.id]
-                invoice_currency = (invoice_line.invoice.currency
-                    if invoice_line.invoice else invoice_line.currency)
-                amounts[work.id] = Currency.compute(invoice_currency,
-                    Decimal(str(work.effort_hours)) * invoice_line.unit_price,
-                    currency)
-            else:
-                amounts[work.id] = Decimal(0)
-        return amounts
-
-    @classmethod
-    def _get_invoiced_amount_progress(cls, works):
-        pool = Pool()
-        Progress = pool.get('project.work.invoiced_progress')
-        InvoiceLine = pool.get('account.invoice.line')
-        Company = pool.get('company.company')
-        Currency = pool.get('currency.currency')
-
-        cursor = Transaction().connection.cursor()
-        table = cls.__table__()
-        progress = Progress.__table__()
-        invoice_line = InvoiceLine.__table__()
-        company = Company.__table__()
-
-        amounts = defaultdict(Decimal)
-        work2currency = {}
-        work_ids = [w.id for w in works]
-        for sub_ids in grouped_slice(work_ids):
-            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),
-                    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
-
-            cursor.execute(*table.join(company,
-                    condition=table.company == company.id
-                    ).select(table.id, company.currency,
-                    where=where))
-            work2currency.update(cursor.fetchall())
-
-        currencies = Currency.browse(set(work2currency.values()))
-        id2currency = {c.id: c for c in currencies}
-
-        for work in works:
-            currency = id2currency[work2currency[work.id]]
-            amounts[work.id] = currency.round(Decimal(amounts[work.id]))
-        return amounts
-
-    @classmethod
-    def _get_invoiced_amount_timesheet(cls, works):
-        pool = Pool()
-        TimesheetWork = pool.get('timesheet.work')
-        TimesheetLine = pool.get('timesheet.line')
-        InvoiceLine = pool.get('account.invoice.line')
-        Company = pool.get('company.company')
-        Currency = pool.get('currency.currency')
-
-        cursor = Transaction().connection.cursor()
-        table = cls.__table__()
-        timesheet_work = TimesheetWork.__table__()
-        timesheet_line = TimesheetLine.__table__()
-        invoice_line = InvoiceLine.__table__()
-        company = Company.__table__()
-
-        amounts = {}
-        work2currency = {}
-        work_ids = [w.id for w in works]
-        for sub_ids in grouped_slice(work_ids):
-            where = reduce_ids(table.id, sub_ids)
-            cursor.execute(*table.join(timesheet_work,
-                    condition=(
-                        Concat(cls.__name__ + ',', table.id)
-                        == timesheet_work.origin)
-                    ).join(timesheet_line,
-                    condition=timesheet_line.work == timesheet_work.id
-                    ).join(invoice_line,
-                    condition=timesheet_line.invoice_line == invoice_line.id
-                    ).select(table.id,
-                    Sum(timesheet_line.duration * invoice_line.unit_price),
-                    where=where,
-                    group_by=table.id))
-            amounts.update(cursor.fetchall())
-
-            cursor.execute(*table.join(company,
-                    condition=table.company == company.id
-                    ).select(table.id, company.currency,
-                    where=where))
-            work2currency.update(cursor.fetchall())
-
-        currencies = Currency.browse(set(work2currency.values()))
-        id2currency = {c.id: c for c in currencies}
-
-        for work in works:
-            currency = id2currency[work2currency[work.id]]
-            amount = amounts.get(work.id, 0)
-            if isinstance(amount, datetime.timedelta):
-                amount = amount.total_seconds()
-            amount = amount / 60 / 60
-            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__()
-
-        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
-
-    @classmethod
-    def _get_invoice_values(cls, works, name):
-        default = getattr(cls, 'default_%s' % name)
-        durations = 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')
-
-    @classmethod
-    def _get_invoiced_amount(cls, works):
-        return cls._get_invoice_values(works, 'invoiced_amount')
-
-    @classmethod
-    @ModelView.button
-    def invoice(cls, works):
-        pool = Pool()
-        Invoice = pool.get('account.invoice')
-
-        invoices = []
-        uninvoiced = works[:]
-        while uninvoiced:
-            work = uninvoiced.pop(0)
-            invoice_lines, uninvoiced_children = work._get_lines_to_invoice()
-            uninvoiced.extend(uninvoiced_children)
-            if not invoice_lines:
-                continue
-            invoice = work._get_invoice()
-            invoice.save()
-            invoices.append(invoice)
-            for key, lines in groupby(invoice_lines,
-                    key=work._group_lines_to_invoice_key):
-                lines = list(lines)
-                key = dict(key)
-                invoice_line = work._get_invoice_line(key, invoice, lines)
-                invoice_line.invoice = invoice.id
-                invoice_line.save()
-                origins = {}
-                for line in lines:
-                    origin = line['origin']
-                    origins.setdefault(origin.__class__, []).append(origin)
-                # TODO: remove when _check_access ignores record rule
-                with Transaction().set_user(0):
-                    for klass, records in origins.items():
-                        klass.save(records)  # Store first new origins
-                        klass.write(records, {
-                                'invoice_line': invoice_line.id,
-                                })
-        Invoice.update_taxes(invoices)
-
-    def _get_invoice(self):
-        "Return invoice for the work"
-        pool = Pool()
-        Invoice = pool.get('account.invoice')
-        Journal = pool.get('account.journal')
-
-        journals = Journal.search([
-                ('type', '=', 'revenue'),
-                ], limit=1)
-        if journals:
-            journal, = journals
-        else:
-            journal = None
-
-        if not self.party:
-            raise InvoicingError(
-                gettext('project_invoice.msg_missing_party',
-                    work=self.rec_name))
-
-        return Invoice(
-            company=self.company,
-            type='out',
-            journal=journal,
-            party=self.party,
-            invoice_address=self.party.address_get(type='invoice'),
-            currency=self.company.currency,
-            account=self.party.account_receivable_used,
-            payment_term=self.party.customer_payment_term,
-            description=self.name,
-            )
-
-    def _group_lines_to_invoice_key(self, line):
-        "The key to group lines"
-        return (('product', line['product']),
-            ('unit', line['unit']),
-            ('unit_price', line['unit_price']),
-            ('description', line['description']))
-
-    def _get_invoice_line(self, key, invoice, lines):
-        "Return a invoice line for the lines"
-        pool = Pool()
-        InvoiceLine = pool.get('account.invoice.line')
-        Uom = pool.get('product.uom')
-
-        quantity = sum(l['quantity'] for l in lines)
-        product = key['product']
-
-        invoice_line = InvoiceLine()
-        invoice_line.type = 'line'
-        invoice_line.description = key['description']
-        invoice_line.account = product.account_revenue_used
-        if (key['unit']
-                and key['unit'].category == product.default_uom.category):
-            invoice_line.product = product
-            invoice_line.unit_price = Uom.compute_price(
-                key['unit'], key['unit_price'], product.default_uom)
-            invoice_line.quantity = Uom.compute_qty(
-                key['unit'], quantity, product.default_uom)
-            invoice_line.unit = product.default_uom
-        else:
-            invoice_line.unit_price = key['unit_price']
-            invoice_line.quantity = quantity
-            invoice_line.unit = key['unit']
-
-        taxes = []
-        pattern = invoice_line._get_tax_rule_pattern()
-        party = invoice.party
-        for tax in product.customer_taxes_used:
-            if party.customer_tax_rule:
-                tax_ids = party.customer_tax_rule.apply(tax, pattern)
-                if tax_ids:
-                    taxes.extend(tax_ids)
-                continue
-            taxes.append(tax.id)
-        if party.customer_tax_rule:
-            tax_ids = party.customer_tax_rule.apply(None, pattern)
-            if tax_ids:
-                taxes.extend(tax_ids)
-        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):
-        "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)()
-        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)
-            lines.extend(child_lines)
-            uninvoiced_children.extend(uninvoiced)
-        return lines, uninvoiced_children
-
-
-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')
-    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
-
-
-class OpenInvoice(Wizard):
-    'Open Invoice'
-    __name__ = 'project.open_invoice'
-    start_state = 'open_'
-    open_ = StateAction('account_invoice.act_invoice_form')
-
-    def do_open_(self, action):
-        pool = Pool()
-        Work = pool.get('project.work')
-        works = Work.search([
-                ('parent', 'child_of', Transaction().context['active_ids']),
-                ])
-        invoice_ids = set()
-        for work in works:
-            if work.invoice_line and work.invoice_line.invoice:
-                invoice_ids.add(work.invoice_line.invoice.id)
-            for twork in work.timesheet_works:
-                for timesheet_line in twork.timesheet_lines:
-                    if (timesheet_line.invoice_line
-                            and timesheet_line.invoice_line.invoice):
-                        invoice_ids.add(timesheet_line.invoice_line.invoice.id)
-            if work.invoiced_progress:
-                for progress in work.invoiced_progress:
-                    invoice_ids.add(progress.invoice_line.invoice.id)
-        encoder = PYSONEncoder()
-        action['pyson_domain'] = encoder.encode(
-            [('id', 'in', list(invoice_ids))])
-        action['pyson_search_value'] = encoder.encode([])
-        return action, {}
diff -r 5a78f819e58c -r 0367786244cb work.xml
--- a/work.xml  Sun Mar 01 16:12:39 2020 +0100
+++ /dev/null   Thu Jan 01 00:00:00 1970 +0000
@@ -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>

Reply via email to