Manage invoicing not time based product

issue7853
review46671002
This commit is contained in:
Cédric Krier committed 2020-03-28 13:05:56 +01:00
1 parent 60ea21f4a9
commit 55f990c8ab
6 files changed
+124 -46

No files matched your search

+1
View File
@@ -1,3 +1,4 @@
* Manage invoicing of non-time based products
* Base invoicing on quantity and price
* Remove duration fields
+5 -2
View File
@@ -9,8 +9,11 @@ this repository contains the full copyright notices and license terms. -->
<record model="ir.message" id="msg_delete_invoiced_line">
<field name="text">You cannot delete an invoiced line.</field>
</record>
<record model="ir.message" id="msg_missing_product">
<field name="text">There is no product on work "%(work)s".</field>
<record model="ir.message" id="msg_product_missing_account_revenue">
<field name="text">To invoice work "%(work)s" you must define an account revenue for product "%(product)s".</field>
</record>
<record model="ir.message" id="msg_missing_account_revenue">
<field name="text">To invoice work "%(work)s" you must configure a default account revenue.</field>
</record>
<record model="ir.message" id="msg_missing_list_price">
<field name="text">There is no list price on work "%(work)s".</field>
+66 -31
View File
@@ -15,7 +15,7 @@ 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.pyson import Eval, Bool, If, Id, PYSONEncoder
from trytond.pool import Pool
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateAction
@@ -36,8 +36,14 @@ class Effort:
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
if (work.progress == 1
and work.list_price
and not work.invoice_line):
if work.price_list_hour:
quantity = work.effort_hours
else:
quantity = 1
quantities[work.id] = work.unit_to_invoice.round(quantity)
return quantities
@classmethod
@@ -58,9 +64,14 @@ class Effort:
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)
if work.price_list_hour:
amount = (
Decimal(str(work.effort_hours))
* invoice_line.unit_price)
else:
amount = invoice_line.unit_price
amounts[work.id] = Currency.compute(
invoice_currency, amount, currency)
else:
amounts[work.id] = Decimal(0)
return amounts
@@ -112,13 +123,19 @@ class Progress:
(work.progress or 0)
- invoiced_progress.get(work.id, 0.0))
if delta > 0:
quantities[work.id] = delta * work.effort_hours
quantity = delta
if work.price_list_hour:
quantity *= work.effort_hours
quantities[work.id] = work.unit_to_invoice.round(quantity)
return quantities
@property
def progress_to_invoice(self):
if self.quantity_to_invoice:
return self.quantity_to_invoice / self.effort_hours
if self.price_list_hour:
return self.quantity_to_invoice / self.effort_hours
else:
return self.quantity_to_invoice
@classmethod
def _get_invoiced_amount_progress(cls, works):
@@ -150,8 +167,10 @@ class Progress:
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)))
work = ids2work[work_id]
if work.price_list_hour:
amount *= Decimal(str(work.effort_hours))
amounts[work_id] = amount
cursor.execute(*table.join(company,
condition=table.company == company.id
@@ -188,6 +207,14 @@ class Timesheet:
super().__setup__()
cls.project_invoice_method.selection.append(
('timesheet', 'On Timesheet'))
cls.product.domain = [
cls.product.domain,
If(Eval('invoice_method') == 'timesheet',
('default_uom_category', '=', Id('product', 'uom_cat_time')),
()),
]
if 'invoice_method' not in cls.product.depends:
cls.product.depends.append('invoice_method')
@classmethod
def _get_quantity_to_invoice_timesheet(cls, works):
@@ -215,7 +242,7 @@ class Timesheet:
duration = durations[work.id]
if work.list_price:
hours = duration.total_seconds() / 60 / 60
quantities[work.id] = hours
quantities[work.id] = work.unit_to_invoice.round(hours)
return quantities
@classmethod
@@ -481,7 +508,8 @@ class Work(Effort, Progress, Timesheet, metaclass=PoolMeta):
"Return a invoice line for the lines"
pool = Pool()
InvoiceLine = pool.get('account.invoice.line')
Uom = pool.get('product.uom')
AccountConfiguration = pool.get('account.configuration')
account_config = AccountConfiguration(1)
quantity = sum(l['quantity'] for l in lines)
product = key['product']
@@ -489,24 +517,32 @@ class Work(Effort, Progress, Timesheet, metaclass=PoolMeta):
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
if product:
invoice_line.account = product.account_revenue_used
if not invoice_line.account:
raise InvoicingError(
gettext(
'project_invoice.msg_product_missing_account_revenue',
work=self.rec_name,
product=product.rec_name))
else:
invoice_line.unit_price = key['unit_price']
invoice_line.quantity = quantity
invoice_line.unit = key['unit']
invoice_line.account = account_config.get_multivalue(
'default_category_account_revenue')
if not invoice_line.account:
raise InvoicingError(
gettext('project_invoice.msg_missing_account_revenue',
work=self.rec_name))
invoice_line.product = product
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:
original_taxes = (
product.customer_taxes_used if product else invoice.account.taxes)
for tax in original_taxes:
if party.customer_tax_rule:
tax_ids = party.customer_tax_rule.apply(tax, pattern)
if tax_ids:
@@ -543,11 +579,7 @@ class Work(Effort, Progress, Timesheet, metaclass=PoolMeta):
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:
if self.invoice_unit_price is None:
raise InvoicingError(
gettext('project_invoice.msg_missing_list_price',
work=self.rec_name))
@@ -570,7 +602,10 @@ class Work(Effort, Progress, Timesheet, metaclass=PoolMeta):
pool = Pool()
ModelData = pool.get('ir.model.data')
Uom = pool.get('product.uom')
return Uom(ModelData.get_id('product', 'uom_hour'))
if self.price_list_hour:
return Uom(ModelData.get_id('product', 'uom_hour'))
elif self.product:
return self.product.default_uom
def get_origins_to_invoice(self):
return super().get_origins_to_invoice()
+21 -4
View File
@@ -87,10 +87,11 @@ Create account category::
>>> account_category.account_revenue = revenue
>>> account_category.save()
Create product::
Create products::
>>> ProductUom = Model.get('product.uom')
>>> hour, = ProductUom.find([('name', '=', 'Hour')])
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> ProductTemplate = Model.get('product.template')
>>> template = ProductTemplate()
@@ -102,6 +103,15 @@ Create product::
>>> template.save()
>>> product, = template.products
>>> template = ProductTemplate()
>>> template.name = 'Service Fixed'
>>> template.default_uom = unit
>>> template.type = 'service'
>>> template.list_price = Decimal('50')
>>> template.account_category = account_category
>>> template.save()
>>> product_fixed, = template.products
Create a Project::
>>> set_user(project_user)
@@ -122,8 +132,13 @@ Create a Project::
>>> task_no_effort.name = "Task 2"
>>> task_no_effort.type = 'task'
>>> task_no_effort.effort_duration = None
>>> task_fixed = project.children.new()
>>> task_fixed.name = "Task 2"
>>> task_fixed.type = 'task'
>>> task_fixed.effort_duration = datetime.timedelta(hours=2)
>>> task_fixed.product = product_fixed
>>> project.save()
>>> task, task_no_effort = project.children
>>> task, task_no_effort, task_fixed = project.children
Check project amounts::
@@ -160,6 +175,8 @@ Do project::
>>> set_user(project_user)
>>> task_no_effort.progress = 1
>>> task_no_effort.save()
>>> task_fixed.progress = 1
>>> task_fixed.save()
>>> project.progress = 1
>>> project.save()
@@ -167,7 +184,7 @@ Check project amounts::
>>> project.reload()
>>> project.amount_to_invoice
Decimal('20.00')
Decimal('70.00')
>>> project.invoiced_amount
Decimal('100.00')
@@ -178,4 +195,4 @@ Invoice again project::
>>> project.amount_to_invoice
Decimal('0.00')
>>> project.invoiced_amount
Decimal('120.00')
Decimal('170.00')
+30 -9
View File
@@ -91,6 +91,10 @@ Create product::
>>> ProductUom = Model.get('product.uom')
>>> hour, = ProductUom.find([('name', '=', 'Hour')])
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> unit.rounding = 0.01
>>> unit.digits = 2
>>> unit.save()
>>> ProductTemplate = Model.get('product.template')
>>> template = ProductTemplate()
@@ -102,6 +106,15 @@ Create product::
>>> template.save()
>>> product, = template.products
>>> template = ProductTemplate()
>>> template.name = 'Service Fixed'
>>> template.default_uom = unit
>>> template.type = 'service'
>>> template.list_price = Decimal('50')
>>> template.account_category = account_category
>>> template.save()
>>> product_fixed, = template.products
Create a Project::
>>> set_user(project_user)
@@ -114,14 +127,18 @@ Create a Project::
>>> project.project_invoice_method = 'progress'
>>> project.product = product
>>> project.effort_duration = datetime.timedelta(hours=1)
>>> task = ProjectWork()
>>> task = project.children.new()
>>> task.name = 'Task 1'
>>> task.type = 'task'
>>> task.product = product
>>> task.effort_duration = datetime.timedelta(hours=5)
>>> project.children.append(task)
>>> task_fixed = project.children.new()
>>> task_fixed.name = 'Task 2'
>>> task_fixed.type = 'task'
>>> task_fixed.product = product
>>> task_fixed.product = product_fixed
>>> project.save()
>>> task, = project.children
>>> task, task_fixed = project.children
Check project amounts::
@@ -135,12 +152,14 @@ Do 50% of task::
>>> task.progress = 0.5
>>> task.save()
>>> task_fixed.progress = 0.5
>>> task_fixed.save()
Check project amounts::
>>> project.reload()
>>> project.amount_to_invoice
Decimal('50.00')
Decimal('75.00')
>>> project.invoiced_amount
Decimal('0.00')
@@ -151,13 +170,15 @@ Invoice project::
>>> project.amount_to_invoice
Decimal('0.00')
>>> project.invoiced_amount
Decimal('50.00')
Decimal('75.00')
Do 75% of task and 80% of project::
Do 75% and 90% of tasks and 80% of project::
>>> set_user(project_user)
>>> task.progress = 0.75
>>> task.save()
>>> task_fixed.progress = 0.9
>>> task_fixed.save()
>>> project.progress = 0.80
>>> project.save()
@@ -165,9 +186,9 @@ Check project amounts::
>>> project.reload()
>>> project.amount_to_invoice
Decimal('41.00')
Decimal('61.00')
>>> project.invoiced_amount
Decimal('50.00')
Decimal('75.00')
Invoice again project::
@@ -176,4 +197,4 @@ Invoice again project::
>>> project.amount_to_invoice
Decimal('0.00')
>>> project.invoiced_amount
Decimal('91.00')
Decimal('136.00')
+1
View File
@@ -7,6 +7,7 @@ depends:
timesheet
account
account_invoice
account_product
product
xml:
project.xml