first commit
This commit is contained in:
695
modules/project_invoice/project.py
Normal file
695
modules/project_invoice/project.py
Normal file
@@ -0,0 +1,695 @@
|
||||
# This file is part of Tryton. The COPYRIGHT file at the top level of
|
||||
# this repository contains the full copyright notices and license terms.
|
||||
|
||||
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from itertools import groupby
|
||||
|
||||
from sql import Cast, Null
|
||||
from sql.aggregate import Sum
|
||||
from sql.operators import Concat
|
||||
|
||||
from trytond import backend
|
||||
from trytond.i18n import gettext
|
||||
from trytond.model import ModelSQL, ModelView, fields
|
||||
from trytond.model.exceptions import AccessError
|
||||
from trytond.modules.currency.fields import Monetary
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Bool, Eval, Id, If
|
||||
from trytond.tools import grouped_slice, reduce_ids, sqlite_apply_types
|
||||
from trytond.transaction import Transaction
|
||||
from trytond.wizard import StateAction, Wizard
|
||||
|
||||
from .exceptions import InvoicingError
|
||||
|
||||
|
||||
class Effort:
|
||||
__slots__ = ()
|
||||
|
||||
@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 work.invoice_unit_price
|
||||
and not work.invoice_line):
|
||||
if work.price_list_hour:
|
||||
quantity = work.effort_hours
|
||||
else:
|
||||
quantity = 1
|
||||
if work.unit_to_invoice:
|
||||
quantity = work.unit_to_invoice.round(quantity)
|
||||
quantities[work.id] = quantity
|
||||
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)
|
||||
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
|
||||
|
||||
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:
|
||||
__slots__ = ()
|
||||
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 if x.invoice_unit_price])
|
||||
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))
|
||||
|
||||
for work in sub_works:
|
||||
delta = (
|
||||
(work.progress or 0)
|
||||
- invoiced_progress.get(work.id, 0.0))
|
||||
if work.invoice_unit_price and delta > 0:
|
||||
quantity = delta
|
||||
if work.price_list_hour:
|
||||
quantity *= work.effort_hours
|
||||
if work.unit_to_invoice:
|
||||
quantity = work.unit_to_invoice.round(quantity)
|
||||
quantities[work.id] = quantity
|
||||
return quantities
|
||||
|
||||
@property
|
||||
def progress_to_invoice(self):
|
||||
if self.quantity_to_invoice:
|
||||
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):
|
||||
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)
|
||||
query = (table.join(progress,
|
||||
condition=progress.work == table.id
|
||||
).join(invoice_line,
|
||||
condition=progress.invoice_line == invoice_line.id
|
||||
).select(
|
||||
table.id,
|
||||
Sum(Cast(progress.progress, 'NUMERIC')
|
||||
* invoice_line.unit_price).as_('amount'),
|
||||
where=where,
|
||||
group_by=table.id))
|
||||
if backend.name == 'sqlite':
|
||||
sqlite_apply_types(query, [None, 'NUMERIC'])
|
||||
cursor.execute(*query)
|
||||
for work_id, amount in cursor:
|
||||
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
|
||||
).select(table.id, company.currency,
|
||||
where=where))
|
||||
work2currency.update(cursor)
|
||||
|
||||
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(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
|
||||
|
||||
@classmethod
|
||||
def copy(cls, records, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
else:
|
||||
default = default.copy()
|
||||
default.setdefault('invoiced_progress', None)
|
||||
return super().copy(records, default=default)
|
||||
|
||||
|
||||
class Timesheet:
|
||||
__slots__ = ()
|
||||
project_invoice_timesheet_up_to = fields.Date(
|
||||
"Invoice up to",
|
||||
states={
|
||||
'invisible': Eval('project_invoice_method') != 'timesheet',
|
||||
},
|
||||
depends=['project_invoice_method'],
|
||||
help="Limits which timesheet lines get invoiced to "
|
||||
"only those before the date.")
|
||||
invoice_timesheet_up_to = fields.Function(fields.Date(
|
||||
"Invoice up to"), 'on_change_with_invoice_timesheet_up_to')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
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.add('invoice_method')
|
||||
|
||||
@fields.depends('type', 'project_invoice_timesheet_up_to',
|
||||
'parent', '_parent_parent.invoice_timesheet_up_to')
|
||||
def on_change_with_invoice_timesheet_up_to(self, name=None):
|
||||
if self.type == 'project':
|
||||
return self.project_invoice_timesheet_up_to
|
||||
elif self.parent:
|
||||
return self.parent.invoice_timesheet_up_to
|
||||
|
||||
@classmethod
|
||||
def _get_quantity_to_invoice_timesheet(cls, works):
|
||||
pool = Pool()
|
||||
TimesheetLine = pool.get('timesheet.line')
|
||||
cursor = Transaction().connection.cursor()
|
||||
line = TimesheetLine.__table__()
|
||||
|
||||
upto2tworks = defaultdict(list)
|
||||
twork2work = {}
|
||||
for work in works:
|
||||
upto = work.invoice_timesheet_up_to
|
||||
for timesheet_work in work.timesheet_works:
|
||||
twork2work[timesheet_work.id] = work.id
|
||||
upto2tworks[upto].append(timesheet_work.id)
|
||||
|
||||
durations = defaultdict(datetime.timedelta)
|
||||
query = line.select(
|
||||
line.work, Sum(line.duration),
|
||||
group_by=line.work)
|
||||
for upto, tworks in upto2tworks.items():
|
||||
for sub_ids in grouped_slice(tworks):
|
||||
query.where = (reduce_ids(line.work, sub_ids)
|
||||
& (line.invoice_line == Null))
|
||||
if upto:
|
||||
query.where &= (line.date <= upto)
|
||||
cursor.execute(*query)
|
||||
|
||||
for twork_id, duration in cursor:
|
||||
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.invoice_unit_price:
|
||||
hours = duration.total_seconds() / 60 / 60
|
||||
if work.unit_to_invoice:
|
||||
hours = work.unit_to_invoice.round(hours)
|
||||
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)
|
||||
|
||||
cursor.execute(*table.join(company,
|
||||
condition=table.company == company.id
|
||||
).select(table.id, company.currency,
|
||||
where=where))
|
||||
work2currency.update(cursor)
|
||||
|
||||
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':
|
||||
up_to = self.invoice_timesheet_up_to or datetime.date.max
|
||||
origins.extend(
|
||||
l for tw in self.timesheet_works
|
||||
for l in tw.timesheet_lines
|
||||
if not l.invoice_line and l.date <= up_to)
|
||||
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(Monetary(
|
||||
"Amount to Invoice", currency='currency', digits='currency',
|
||||
states={
|
||||
'invisible': Eval('invoice_method') == 'manual',
|
||||
},
|
||||
depends=['invoice_method']),
|
||||
'get_total')
|
||||
invoiced_amount = fields.Function(Monetary(
|
||||
"Invoiced Amount", currency='currency', digits='currency',
|
||||
states={
|
||||
'invisible': Eval('invoice_method') == 'manual',
|
||||
},
|
||||
depends=['invoice_method']),
|
||||
'get_total')
|
||||
invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line',
|
||||
readonly=True)
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
super().__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().copy(records, default=default)
|
||||
|
||||
@classmethod
|
||||
def check_modification(cls, mode, works, values=None, external=False):
|
||||
super().check_modification(
|
||||
mode, works, values=values, external=external)
|
||||
if mode == 'write':
|
||||
if ('effort_duration' in values
|
||||
and any(w.invoice_line for w in works)):
|
||||
work = next((w for w in works if w.invoice_line))
|
||||
raise AccessError(gettext(
|
||||
'project_invoice.msg_invoiced_work_modify_effort',
|
||||
work=work.rec_name))
|
||||
elif mode == 'delete':
|
||||
if any(w.invoice_line for w in works):
|
||||
work = next((w for w in works if w.invoice_line))
|
||||
raise AccessError(gettext(
|
||||
'project_invoice.msg_invoiced_work_delete',
|
||||
work=work.rec_name))
|
||||
|
||||
@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 = defaultdict(Decimal)
|
||||
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 = defaultdict(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)
|
||||
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'] or ''))
|
||||
|
||||
def _get_invoice_line(self, key, invoice, lines):
|
||||
"Return a invoice line for the lines"
|
||||
pool = Pool()
|
||||
InvoiceLine = pool.get('account.invoice.line')
|
||||
|
||||
quantity = sum(l['quantity'] for l in lines)
|
||||
product = key['product']
|
||||
|
||||
invoice_line = InvoiceLine(invoice=invoice)
|
||||
invoice_line.on_change_invoice()
|
||||
invoice_line.type = 'line'
|
||||
invoice_line.description = key['description']
|
||||
invoice_line.unit_price = key['unit_price']
|
||||
invoice_line.quantity = quantity
|
||||
invoice_line.unit = key['unit']
|
||||
invoice_line.product = product
|
||||
invoice_line.on_change_product()
|
||||
if not getattr(invoice_line, 'account', None):
|
||||
if invoice_line.product:
|
||||
raise InvoicingError(
|
||||
gettext(
|
||||
'project_invoice.msg_product_missing_account_revenue',
|
||||
work=self.rec_name,
|
||||
product=invoice_line.product.rec_name))
|
||||
else:
|
||||
raise InvoicingError(
|
||||
gettext('project_invoice.msg_missing_account_revenue',
|
||||
work=self.rec_name))
|
||||
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 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')
|
||||
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()
|
||||
|
||||
|
||||
class WorkInvoicedProgress(ModelView, ModelSQL):
|
||||
__name__ = 'project.work.invoiced_progress'
|
||||
work = fields.Many2One('project.work', "Work", ondelete='RESTRICT')
|
||||
progress = fields.Float('Progress', required=True,
|
||||
domain=[
|
||||
('progress', '>=', 0),
|
||||
])
|
||||
invoice_line = fields.Many2One('account.invoice.line', 'Invoice Line',
|
||||
ondelete='CASCADE')
|
||||
|
||||
|
||||
class OpenInvoice(Wizard):
|
||||
__name__ = 'project.open_invoice'
|
||||
start_state = 'open_'
|
||||
_readonly = True
|
||||
open_ = StateAction('account_invoice.act_invoice_form')
|
||||
|
||||
def do_open_(self, action):
|
||||
works = self.model.search([
|
||||
('parent', 'child_of', list(map(int, self.records))),
|
||||
])
|
||||
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)
|
||||
return action, {'res_id': list(invoice_ids)}
|
||||
Reference in New Issue
Block a user