550 lines
20 KiB
Python
550 lines
20 KiB
Python
# The COPYRIGHT file at the top level of this repository contains the full
|
|
# copyright notices and license terms.
|
|
from decimal import Decimal
|
|
from sql.aggregate import Max, Sum, Avg
|
|
|
|
from trytond.model import ModelSQL, ModelView, fields
|
|
from trytond.pyson import Eval, If, Bool
|
|
from trytond.transaction import Transaction
|
|
from trytond.pool import Pool, PoolMeta
|
|
from trytond.config import config
|
|
DIGITS = int(config.get('digits', 'unit_price_digits', 4))
|
|
|
|
__all__ = ['ProjectMilestoneGroup', 'ProjectSaleLine', 'Project',
|
|
'ShipmentWork', 'Sale', 'Maintenance', 'MilestoneGroup']
|
|
__metaclass__ = PoolMeta
|
|
|
|
_ZERO = Decimal('0.0')
|
|
|
|
|
|
class ProjectMilestoneGroup(ModelSQL):
|
|
'Project - Milestone'
|
|
__name__ = 'asset.project-account.invoice.milestone.group'
|
|
_table = 'asset_project_milestone_group_rel'
|
|
project = fields.Many2One('asset.project', 'Project', ondelete='CASCADE',
|
|
required=True, select=True)
|
|
milestone_group = fields.Many2One('account.invoice.milestone.group',
|
|
'Milestone Group',
|
|
ondelete='CASCADE', required=True, select=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ProjectMilestoneGroup, cls).__setup__()
|
|
cls._sql_constraints += [
|
|
('project_unique', 'UNIQUE(project)',
|
|
'The Project must be unique.'),
|
|
('milestone_group_unique', 'UNIQUE(milestone_group)',
|
|
'The Milestone Group must be unique.'),
|
|
]
|
|
|
|
|
|
class ProjectSaleLine(ModelSQL, ModelView):
|
|
'Project Sale Line'
|
|
__name__ = 'asset.project.sale.line'
|
|
project = fields.Many2One('asset.project', 'Project', required=True)
|
|
product = fields.Many2One('product.product', 'Product')
|
|
quantity = fields.Float('Quantity',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits'])
|
|
unit = fields.Many2One('product.uom', 'Unit')
|
|
unit_digits = fields.Function(fields.Integer('Unit Digits'),
|
|
'on_change_with_unit_digits')
|
|
currency = fields.Many2One('currency.currency', 'Currency')
|
|
currency_digits = fields.Function(fields.Integer('Currency Digits'),
|
|
'on_change_with_currency_digits')
|
|
unit_price = fields.Numeric('Unit Price', digits=(16, DIGITS))
|
|
amount = fields.Function(fields.Numeric('Amount',
|
|
digits=(16, Eval('currency_digits', 2)),
|
|
depends=['currency_digits']),
|
|
'get_amount')
|
|
|
|
def get_amount(self, name):
|
|
return self.currency.round(
|
|
Decimal(str(self.quantity)) * self.unit_price)
|
|
|
|
@fields.depends('unit')
|
|
def on_change_with_unit_digits(self, name=None):
|
|
if self.unit:
|
|
return self.unit.digits
|
|
return 2
|
|
|
|
@fields.depends('currency')
|
|
def on_change_with_currency_digits(self, name=None):
|
|
return self.currency.digits
|
|
|
|
@classmethod
|
|
def table_query(cls):
|
|
pool = Pool()
|
|
Sale = pool.get('sale.sale')
|
|
SaleLine = pool.get('sale.line')
|
|
table = SaleLine.__table__()
|
|
sale = Sale.__table__()
|
|
|
|
columns = []
|
|
for name in ('id', 'create_uid', 'write_uid', 'create_date',
|
|
'write_date'):
|
|
columns.append(Max(getattr(table, name)).as_(name))
|
|
|
|
columns.extend([sale.project, table.product, sale.currency, table.unit,
|
|
Sum(table.quantity).as_('quantity'),
|
|
Avg(table.unit_price).as_('unit_price')])
|
|
|
|
return table.join(sale, condition=(sale.id == table.sale)).select(
|
|
*columns,
|
|
where=((table.type == 'line') &
|
|
(sale.project != None) &
|
|
(sale.state != 'cancel')
|
|
),
|
|
group_by=(sale.project, sale.currency, table.product, table.unit))
|
|
|
|
|
|
class Project(ModelSQL, ModelView):
|
|
'Asset Project'
|
|
__name__ = 'asset.project'
|
|
_rec_name = 'code'
|
|
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
select=True, domain=[
|
|
('id', If(Eval('context', {}).contains('company'), '=', '!='),
|
|
Eval('context', {}).get('company', -1)),
|
|
])
|
|
unit_digits = fields.Function(fields.Integer('Unit Digits'),
|
|
'on_change_with_unit_digits')
|
|
code = fields.Char('Code', required=True, select=True,
|
|
states={
|
|
'readonly': Eval('code_readonly', True),
|
|
},
|
|
depends=['code_readonly'])
|
|
code_readonly = fields.Function(fields.Boolean('Code Readonly'),
|
|
'get_code_readonly')
|
|
party = fields.Many2One('party.party', 'Party', required=True)
|
|
milestone_group = fields.One2One(
|
|
'asset.project-account.invoice.milestone.group',
|
|
'project', 'milestone_group', 'Milestone Group',
|
|
domain=[
|
|
('party', '=', Eval('party')),
|
|
('company', '=', Eval('company')),
|
|
],
|
|
states={
|
|
'readonly': (Eval('id', 0) > 0) & Bool(Eval('milestone_group', 0)),
|
|
},
|
|
depends=['id', 'party', 'company'])
|
|
milestones = fields.Function(fields.One2Many('account.invoice.milestone',
|
|
None, 'Milestones',
|
|
domain=[
|
|
('group', '=', Eval('milestone_group')),
|
|
],
|
|
depends=['milestone_group']),
|
|
'get_milestones', setter='set_milestones')
|
|
asset = fields.Many2One('asset', 'Asset', required=True,
|
|
select=True)
|
|
start_date = fields.Date('Start Date',
|
|
states={
|
|
'invisible': Bool(Eval('maintenance')),
|
|
},
|
|
depends=['maintenance'])
|
|
end_date = fields.Date('End Date',
|
|
states={
|
|
'invisible': Bool(Eval('maintenance')),
|
|
},
|
|
depends=['maintenance'])
|
|
maintenance = fields.Boolean('Maintenance')
|
|
asset_maintenances = fields.One2Many('asset.maintenance', 'reference',
|
|
'Maintenances', size=1)
|
|
category = fields.Function(fields.Many2One('asset.maintenance.category',
|
|
'Category',
|
|
states={
|
|
'required': Bool(Eval('maintenance')),
|
|
'invisible': ~Bool(Eval('maintenance')),
|
|
},
|
|
depends=['maintenance']),
|
|
'get_maintenance', setter='set_maintenance',
|
|
searcher='search_maintenance')
|
|
date_planned = fields.Function(fields.Date('Planned Date',
|
|
states={
|
|
'invisible': ~Bool(Eval('maintenance')),
|
|
},
|
|
depends=['maintenance']),
|
|
'get_maintenance', setter='set_maintenance',
|
|
searcher='search_maintenance')
|
|
date_start = fields.Function(fields.Date('Start Date',
|
|
states={
|
|
'invisible': ~Bool(Eval('maintenance')),
|
|
},
|
|
depends=['maintenance']),
|
|
'get_maintenance', setter='set_maintenance',
|
|
searcher='search_maintenance')
|
|
date_done = fields.Function(fields.Date('Done Date',
|
|
states={
|
|
'invisible': ~Bool(Eval('maintenance')),
|
|
},
|
|
depends=['maintenance']),
|
|
'get_maintenance', setter='set_maintenance',
|
|
searcher='search_maintenance')
|
|
date_next = fields.Function(fields.Date('Next maintenance',
|
|
states={
|
|
'invisible': ~Bool(Eval('maintenance')),
|
|
},
|
|
depends=['maintenance']),
|
|
'get_maintenance', setter='set_maintenance',
|
|
searcher='search_maintenance')
|
|
|
|
work_shipments = fields.One2Many('shipment.work', 'project',
|
|
'Shipment Works',
|
|
domain=[
|
|
('party', '=', Eval('party')),
|
|
],
|
|
depends=['party'])
|
|
sales = fields.One2Many('sale.sale', 'project', 'Sales',
|
|
domain=[
|
|
('party', '=', Eval('party', -1)),
|
|
],
|
|
add_remove=[
|
|
('state', 'in', ['quotation', 'confirmed', 'processing']),
|
|
],
|
|
depends=['party'])
|
|
sale_lines = fields.One2Many('asset.project.sale.line', 'project',
|
|
'Sale Lines')
|
|
amount_invoice = fields.Function(fields.Numeric('Amount To Invoice',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_amount_milestones')
|
|
amount_milestones = fields.Function(fields.Numeric('Amount In Milestones',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_amount_milestones')
|
|
amount_to_assign = fields.Function(fields.Numeric('Amount to Assign',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_amount_milestones')
|
|
shipments = fields.Function(fields.One2Many('stock.shipment.out',
|
|
None, 'Shipments'), 'get_shipments')
|
|
shipment_returns = fields.Function(fields.One2Many(
|
|
'stock.shipment.out.return', None, 'Shipment Returns'),
|
|
'get_shipment_returns')
|
|
moves = fields.Function(fields.One2Many('stock.move', None, 'Moves'),
|
|
'get_moves')
|
|
income_labor = fields.Function(fields.Numeric('Income Labor',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_income_labor')
|
|
income_material = fields.Function(fields.Numeric('Income Material',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_income_material')
|
|
income_other = fields.Function(fields.Numeric('Income Other',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_income_other')
|
|
expense_labor = fields.Function(fields.Numeric('Expense Labor',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_expense_labor')
|
|
expense_material = fields.Function(fields.Numeric('Expense Material',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_expense_material')
|
|
expense_other = fields.Function(fields.Numeric('Expense Other',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_expense_other')
|
|
margin_labor = fields.Function(fields.Numeric('Margin Labor',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_margins')
|
|
margin_material = fields.Function(fields.Numeric('Margin Material',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_margins')
|
|
margin_other = fields.Function(fields.Numeric('Margin Other',
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_margins')
|
|
margin_percent_labor = fields.Function(fields.Numeric('Margin(%) Labor',
|
|
digits=(16, 4)),
|
|
'get_margins')
|
|
margin_percent_material = fields.Function(fields.Numeric(
|
|
'Margin (%) Material', digits=(16, 4)),
|
|
'get_margins')
|
|
margin_percent_other = fields.Function(fields.Numeric('Margin (%) Other',
|
|
digits=(16, 4)),
|
|
'get_margins')
|
|
note = fields.Text('Note')
|
|
|
|
@staticmethod
|
|
def default_currency_digits():
|
|
Company = Pool().get('company.company')
|
|
if Transaction().context.get('company'):
|
|
company = Company(Transaction().context['company'])
|
|
return company.currency.digits
|
|
return 2
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company')
|
|
|
|
@staticmethod
|
|
def default_code_readonly():
|
|
Configuration = Pool().get('asset.project.configuration')
|
|
config = Configuration(1)
|
|
return bool(config.project_sequence)
|
|
|
|
@fields.depends('company')
|
|
def on_change_with_unit_digits(self, name=None):
|
|
if self.company:
|
|
return self.company.currency.digits
|
|
return 2
|
|
|
|
@classmethod
|
|
def get_amount_milestones(self, projects, names):
|
|
res = {}
|
|
for name in names:
|
|
res[name] = dict((p.id, _ZERO) for p in projects)
|
|
for project in projects:
|
|
skip = set()
|
|
for sale in project.sales:
|
|
if not sale.milestone_group or sale.milestone_group.id in skip:
|
|
continue
|
|
for name, field in [
|
|
('amount_milestones', 'amount'),
|
|
('amount_to_assign', 'amount_to_assign'),
|
|
]:
|
|
if name in names:
|
|
res[name][project.id] += getattr(
|
|
sale.milestone_group, field, _ZERO)
|
|
if 'amount_invoiced' in names:
|
|
res[name][project.id] += (sale.milestone_group.amount -
|
|
sale.milestone_group.amount_invoiced)
|
|
skip.add(sale.milestone_group.id)
|
|
return res
|
|
|
|
def get_income_labor(self, name):
|
|
amount = _ZERO
|
|
for sale in self.sales:
|
|
for line in sale.lines:
|
|
if line.product and line.product.type == 'service':
|
|
amount += line.amount
|
|
return amount
|
|
|
|
def get_income_material(self, name):
|
|
amount = _ZERO
|
|
for sale in self.sales:
|
|
for line in sale.lines:
|
|
if line.product and line.product.type != 'service':
|
|
amount += line.amount
|
|
return amount
|
|
|
|
def get_income_other(self, name):
|
|
amount = _ZERO
|
|
for sale in self.sales:
|
|
for line in sale.lines:
|
|
if not line.product:
|
|
amount += line.amount
|
|
return amount
|
|
|
|
def get_expense_labor(self, name):
|
|
amount = _ZERO
|
|
for shipment_work in self.work_shipments:
|
|
for line in shipment_work.timesheet_lines:
|
|
amount += line.compute_cost()
|
|
return amount
|
|
|
|
def get_expense_material(self, name):
|
|
amount = _ZERO
|
|
for sale in self.sales:
|
|
for line in sale.lines:
|
|
if line.product and line.product.type != 'service':
|
|
# Compatibility with sale_margin
|
|
if hasattr(line, 'cost_price'):
|
|
amount += sale.currency.round(
|
|
Decimal(str(line.quantity)) * line.cost_price)
|
|
else:
|
|
amount += sale.currency.round(line.product.cost_price *
|
|
Decimal(str(line.quantity)))
|
|
return amount
|
|
|
|
def get_expense_other(self, name):
|
|
amount = _ZERO
|
|
return amount
|
|
|
|
@classmethod
|
|
def get_margins(cls, projects, names):
|
|
res = {}
|
|
for name in names:
|
|
res[name] = dict((p.id, _ZERO) for p in projects)
|
|
for project in projects:
|
|
for name in names:
|
|
field_name = name.split('_')[-1]
|
|
income = getattr(project, 'income_%s' % field_name)
|
|
expense = getattr(project, 'expense_%s' % field_name)
|
|
if 'percent' in name:
|
|
if not expense:
|
|
value = Decimal('1.0')
|
|
else:
|
|
value = (income - expense) / expense
|
|
else:
|
|
value = income - expense
|
|
res[name][project.id] = value
|
|
return res
|
|
|
|
def get_code_readonly(self, name):
|
|
return True
|
|
|
|
def get_moves(self, name):
|
|
return [m.id for s in self.sales for m in s.moves]
|
|
|
|
def get_shipments(self, name):
|
|
return [m.id for s in self.sales for m in s.shipments]
|
|
|
|
def get_shipment_returns(self, name):
|
|
return [m.id for s in self.sales for m in s.shipment_returns]
|
|
|
|
@classmethod
|
|
def get_maintenance(cls, projects, names):
|
|
pool = Pool()
|
|
Maintenance = pool.get('asset.maintenance')
|
|
res = {}
|
|
for name in names:
|
|
res[name] = dict((m.id, None) for m in projects)
|
|
|
|
for maintenance in Maintenance.search([
|
|
('reference', 'in', [str(m) for m in projects]),
|
|
]):
|
|
project = maintenance.reference.id
|
|
for name in names:
|
|
value = getattr(maintenance, name)
|
|
if isinstance(value, ModelSQL):
|
|
value = value.id
|
|
res[name][project] = value
|
|
return res
|
|
|
|
@classmethod
|
|
def set_maintenance(cls, projects, name, value):
|
|
pool = Pool()
|
|
Maintenance = pool.get('asset.maintenance')
|
|
to_write = []
|
|
to_create = []
|
|
for project in projects:
|
|
if not project.maintenance:
|
|
continue
|
|
if not project.asset_maintenances:
|
|
to_create.append(project.asset_maintenance_vals(
|
|
{name: value}))
|
|
else:
|
|
to_write.extend(project.asset_maintenances)
|
|
if to_create:
|
|
Maintenance.create(to_create)
|
|
if to_write:
|
|
Maintenance.write(to_write, {
|
|
name: value,
|
|
})
|
|
|
|
@classmethod
|
|
def search_maintenance(cls, name, clause):
|
|
return [('projects.%s' % name,) + tuple(clause[1:])]
|
|
|
|
def asset_maintenance_vals(self, vals):
|
|
'Returns the values for the asset maintenance to be created for self'
|
|
vals['reference'] = str(self)
|
|
vals['asset'] = self.asset.id
|
|
return vals
|
|
|
|
def get_milestones(self, name=None):
|
|
return [m.id for m in self.milestone_group.lines]
|
|
|
|
@classmethod
|
|
def set_milestones(cls, projects, name, value):
|
|
pool = Pool()
|
|
MilestoneGroup = pool.get('account.invoice.milestone.group')
|
|
groups = [p.milestone_group for p in projects if p.milestone_group]
|
|
if groups:
|
|
MilestoneGroup.write(groups, {
|
|
'lines': value,
|
|
})
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
'Fill the reference field with the sale sequence'
|
|
pool = Pool()
|
|
Sequence = pool.get('ir.sequence')
|
|
MilestoneGroup = pool.get('account.invoice.milestone.group')
|
|
Config = pool.get('asset.project.configuration')
|
|
|
|
config = Config(1)
|
|
for value in vlist:
|
|
if not value.get('code'):
|
|
code = Sequence.get_id(config.project_sequence.id)
|
|
value['code'] = code
|
|
if not value.get('milestone_group'):
|
|
vals = {'party': value.get('party')}
|
|
group, = MilestoneGroup.create([vals])
|
|
value['milestone_group'] = group.id
|
|
return super(Project, cls).create(vlist)
|
|
|
|
|
|
class ShipmentWork:
|
|
__name__ = 'shipment.work'
|
|
project = fields.Many2One('asset.project', 'Project',
|
|
states={
|
|
'readonly': Eval('state').in_(['checked', 'cancel']),
|
|
},
|
|
depends=['state'])
|
|
|
|
|
|
class Sale:
|
|
__name__ = 'sale.sale'
|
|
project = fields.Many2One('asset.project', 'Project',
|
|
domain=[
|
|
('party', '=', Eval('party')),
|
|
],
|
|
depends=['party'])
|
|
|
|
@classmethod
|
|
def _ensure_milestone_project_relation(cls, sales):
|
|
to_write = []
|
|
for sale in sales:
|
|
if (sale.project and (not sale.milestone_group or
|
|
sale.milestone_group != sale.project.milestone_group)):
|
|
to_write.extend(([sale], {
|
|
'milestone_group': sale.project.milestone_group.id,
|
|
}))
|
|
if to_write:
|
|
cls.write(*to_write)
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
sales = super(Sale, cls).create(vlist)
|
|
cls._ensure_milestone_project_relation(sales)
|
|
return sales
|
|
|
|
@classmethod
|
|
def write(cls, *args):
|
|
actions = iter(args)
|
|
all_sales = []
|
|
for sales, _ in zip(actions, actions):
|
|
all_sales += sales
|
|
super(Sale, cls).write(*args)
|
|
cls._ensure_milestone_project_relation(all_sales)
|
|
|
|
|
|
class Maintenance:
|
|
__name__ = 'asset.maintenance'
|
|
|
|
@classmethod
|
|
def _get_reference(cls):
|
|
references = super(Maintenance, cls)._get_reference()
|
|
references.append('asset.project')
|
|
return references
|
|
|
|
|
|
class MilestoneGroup:
|
|
__name__ = 'account.invoice.milestone.group'
|
|
#TODO: Add this to the views
|
|
project = fields.One2One('asset.project-account.invoice.milestone.group',
|
|
'milestone_group', 'project', 'Project', readonly=True,
|
|
domain=[
|
|
('party', '=', Eval('party')),
|
|
('company', '=', Eval('company')),
|
|
],
|
|
depends=['party', 'company'])
|