first commit
This commit is contained in:
227
modules/project_revenue/work.py
Normal file
227
modules/project_revenue/work.py
Normal file
@@ -0,0 +1,227 @@
|
||||
# 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 as dt
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
|
||||
from sql.aggregate import Sum
|
||||
from sql.operators import Concat
|
||||
|
||||
from trytond.model import fields
|
||||
from trytond.modules.currency.fields import Monetary
|
||||
from trytond.modules.product import price_digits, round_price
|
||||
from trytond.pool import Pool, PoolMeta
|
||||
from trytond.pyson import Eval
|
||||
from trytond.tools import grouped_slice, reduce_ids
|
||||
from trytond.transaction import Transaction
|
||||
|
||||
|
||||
class Work(metaclass=PoolMeta):
|
||||
__name__ = 'project.work'
|
||||
product = fields.Many2One('product.product', 'Product',
|
||||
domain=[
|
||||
('type', '=', 'service'),
|
||||
],
|
||||
context={
|
||||
'company': Eval('company', -1),
|
||||
},
|
||||
depends={'company'})
|
||||
list_price = Monetary(
|
||||
"List Price", currency='currency', digits=price_digits)
|
||||
revenue = fields.Function(Monetary(
|
||||
"Revenue", currency='currency', digits='currency'),
|
||||
'get_total')
|
||||
cost = fields.Function(Monetary(
|
||||
"Cost", currency='currency', digits='currency'),
|
||||
'get_total')
|
||||
currency = fields.Function(fields.Many2One('currency.currency',
|
||||
'Currency'), 'on_change_with_currency')
|
||||
|
||||
@classmethod
|
||||
def __setup__(cls):
|
||||
pool = Pool()
|
||||
|
||||
super().__setup__()
|
||||
|
||||
try:
|
||||
pool.get('purchase.line')
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
# Add purchase lines if purchase is activated
|
||||
cls.purchase_lines = fields.One2Many('purchase.line', 'work',
|
||||
'Purchase Lines', domain=[
|
||||
('purchase.company', '=', Eval('company', -1)),
|
||||
('type', '=', 'line'),
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def get_total(cls, works, names):
|
||||
result = super().get_total(works, names)
|
||||
currencies = []
|
||||
for name in names:
|
||||
field = getattr(cls, name)
|
||||
if getattr(field, 'digits', None) == 'currency':
|
||||
currencies.append(name)
|
||||
if currencies:
|
||||
for work in works:
|
||||
for name in currencies:
|
||||
result[name][work.id] = work.company.currency.round(
|
||||
result[name][work.id])
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
def _get_cost(cls, works):
|
||||
costs = defaultdict(Decimal)
|
||||
|
||||
for work_id, cost in cls._timesheet_cost(works):
|
||||
costs[work_id] += cost
|
||||
|
||||
for work_id, cost in cls._purchase_cost(works):
|
||||
costs[work_id] += cost
|
||||
return costs
|
||||
|
||||
@classmethod
|
||||
def _timesheet_cost(cls, works):
|
||||
pool = Pool()
|
||||
Line = pool.get('timesheet.line')
|
||||
Work = pool.get('timesheet.work')
|
||||
transaction = Transaction()
|
||||
cursor = transaction.connection.cursor()
|
||||
table = cls.__table__()
|
||||
work = Work.__table__()
|
||||
line = Line.__table__()
|
||||
|
||||
work_ids = [w.id for w in works]
|
||||
for sub_ids in grouped_slice(work_ids):
|
||||
red_sql = reduce_ids(table.id, sub_ids)
|
||||
cursor.execute(*table.join(work,
|
||||
condition=(
|
||||
Concat(cls.__name__ + ',', table.id) == work.origin)
|
||||
).join(line, condition=line.work == work.id
|
||||
).select(table.id, Sum(line.cost_price * line.duration),
|
||||
where=red_sql,
|
||||
group_by=[table.id]))
|
||||
for work_id, cost in cursor:
|
||||
# SQLite stores timedelta as float
|
||||
if isinstance(cost, dt.timedelta):
|
||||
cost = cost.total_seconds()
|
||||
# Convert from seconds
|
||||
cost /= 60 * 60
|
||||
yield work_id, Decimal(str(cost))
|
||||
|
||||
@classmethod
|
||||
def _purchase_cost(cls, works):
|
||||
'Compute direct purchase cost'
|
||||
if not hasattr(cls, 'purchase_lines'):
|
||||
return
|
||||
|
||||
pool = Pool()
|
||||
Currency = pool.get('currency.currency')
|
||||
PurchaseLine = pool.get('purchase.line')
|
||||
InvoiceLine = pool.get('account.invoice.line')
|
||||
Invoice = pool.get('account.invoice')
|
||||
Company = pool.get('company.company')
|
||||
|
||||
cursor = Transaction().connection.cursor()
|
||||
table = cls.__table__()
|
||||
purchase_line = PurchaseLine.__table__()
|
||||
invoice_line = InvoiceLine.__table__()
|
||||
invoice = Invoice.__table__()
|
||||
company = Company.__table__()
|
||||
|
||||
work_ids = [w.id for w in works]
|
||||
work2currency = {}
|
||||
iline2work = {}
|
||||
for sub_ids in grouped_slice(work_ids):
|
||||
where = reduce_ids(table.id, sub_ids)
|
||||
cursor.execute(*table.join(purchase_line,
|
||||
condition=purchase_line.work == table.id
|
||||
).join(invoice_line,
|
||||
condition=invoice_line.origin == Concat(
|
||||
'purchase.line,', purchase_line.id)
|
||||
).join(invoice,
|
||||
condition=invoice_line.invoice == invoice.id
|
||||
).select(invoice_line.id, table.id,
|
||||
where=where & ~invoice.state.in_(['draft', 'cancelled'])))
|
||||
iline2work.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}
|
||||
|
||||
invoice_lines = InvoiceLine.browse(list(iline2work.keys()))
|
||||
for invoice_line in invoice_lines:
|
||||
invoice = invoice_line.invoice
|
||||
work_id = iline2work[invoice_line.id]
|
||||
currency_id = work2currency[work_id]
|
||||
currency = id2currency[currency_id]
|
||||
if currency != invoice.currency:
|
||||
with Transaction().set_context(date=invoice.currency_date):
|
||||
amount = Currency.compute(invoice.currency,
|
||||
invoice_line.amount, currency)
|
||||
else:
|
||||
amount = invoice_line.amount
|
||||
yield work_id, amount
|
||||
|
||||
@classmethod
|
||||
def _get_revenue(cls, works):
|
||||
revenues = defaultdict(Decimal)
|
||||
for work in works:
|
||||
if not work.list_price:
|
||||
continue
|
||||
if work.price_list_hour:
|
||||
revenue = work.list_price * Decimal(str(work.effort_hours))
|
||||
else:
|
||||
revenue = work.list_price
|
||||
revenues[work.id] = revenue
|
||||
return revenues
|
||||
|
||||
@fields.depends('company')
|
||||
def on_change_with_currency(self, name=None):
|
||||
return self.company.currency if self.company else None
|
||||
|
||||
@fields.depends('product')
|
||||
def on_change_product(self):
|
||||
pool = Pool()
|
||||
ModelData = pool.get('ir.model.data')
|
||||
Uom = pool.get('product.uom')
|
||||
|
||||
if not self.product:
|
||||
return
|
||||
|
||||
list_price = self.product.list_price_used
|
||||
if list_price is not None:
|
||||
if self.price_list_hour:
|
||||
hour_uom = Uom(ModelData.get_id('product', 'uom_hour'))
|
||||
list_price = Uom.compute_price(
|
||||
self.product.default_uom, list_price, hour_uom)
|
||||
list_price = round_price(list_price)
|
||||
self.list_price = list_price
|
||||
|
||||
@property
|
||||
def price_list_hour(self):
|
||||
pool = Pool()
|
||||
ModelData = pool.get('ir.model.data')
|
||||
Category = pool.get('product.uom.category')
|
||||
if not self.product:
|
||||
return
|
||||
time = Category(ModelData.get_id('product', 'uom_cat_time'))
|
||||
return self.product.default_uom_category == time
|
||||
|
||||
@classmethod
|
||||
def copy(cls, records, default=None):
|
||||
if default is None:
|
||||
default = {}
|
||||
else:
|
||||
default = default.copy()
|
||||
if hasattr(cls, 'purchase_lines'):
|
||||
# Do not copy purchase lines if purchase is activated
|
||||
default.setdefault('purchase_lines', None)
|
||||
return super().copy(records, default=default)
|
||||
Reference in New Issue
Block a user