770 lines
28 KiB
Python
770 lines
28 KiB
Python
# The COPYRIGHT file at the top level of this repository contains the full
|
|
# copyright notices and license terms.
|
|
from trytond.model import Workflow, ModelSQL, ModelView, fields
|
|
from trytond.pool import Pool
|
|
from trytond.pyson import Bool, Eval, If
|
|
from trytond.transaction import Transaction
|
|
from dateutil.relativedelta import relativedelta
|
|
from decimal import Decimal
|
|
from itertools import groupby
|
|
from jinja2 import Template as Jinja2Template
|
|
|
|
__all__ = ['MilestoneTypeGroup', 'MilestoneType', 'Milestone']
|
|
|
|
_KIND = [
|
|
('manual', 'Manual'),
|
|
('system', 'System'),
|
|
]
|
|
_TRIGGER = [
|
|
('', ''),
|
|
('start_project', 'On Project Start'),
|
|
('progress', 'On % Progress'),
|
|
('finish_project', 'On Project Finish'),
|
|
]
|
|
_ZERO = Decimal('0.0')
|
|
|
|
|
|
class MilestoneMixin:
|
|
kind = fields.Selection(_KIND, 'Kind', required=True, select=True)
|
|
trigger = fields.Selection(_TRIGGER, 'Trigger', sort=False,
|
|
states={
|
|
'required': Eval('kind') == 'system',
|
|
'invisible': Eval('kind') != 'system',
|
|
}, depends=['kind'],
|
|
help='Defines when the Milestone will be confirmed and its Invoice '
|
|
'Date calculated.')
|
|
trigger_progress = fields.Numeric('On Progress',
|
|
digits=(16, 8),
|
|
domain=[
|
|
['OR',
|
|
('trigger_progress', '=', None),
|
|
[
|
|
('trigger_progress', '>=', 0),
|
|
('trigger_progress', '<=', 1),
|
|
],
|
|
],
|
|
],
|
|
states={
|
|
'required': ((Eval('kind') == 'system') &
|
|
(Eval('trigger') == 'progress')),
|
|
'invisible': ((Eval('kind') != 'system') |
|
|
(Eval('trigger') != 'progress')),
|
|
}, depends=['kind', 'trigger'],
|
|
help="The percentage of progress over the total amount of Project.")
|
|
invoice_method = fields.Selection([
|
|
('fixed', 'Fixed'),
|
|
('percent', 'Percent'),
|
|
('progress', 'Progress'),
|
|
('remainder', 'Remainder'),
|
|
], 'Invoice Method', required=True, sort=False)
|
|
invoice_percent = fields.Numeric('Invoice Percent',
|
|
digits=(16, 8),
|
|
states={
|
|
'required': Eval('invoice_method') == 'percent',
|
|
'invisible': Eval('invoice_method') != 'percent',
|
|
},
|
|
depends=['invoice_method', 'currency_digits'])
|
|
advancement_product = fields.Many2One('product.product',
|
|
'Advancement Product', states={
|
|
'required': Eval('invoice_method') == 'fixed',
|
|
'invisible': Eval('invoice_method').in_(
|
|
['progress', 'remainder']),
|
|
}, depends=['invoice_method'])
|
|
advancement_amount = fields.Numeric('Advancement Amount',
|
|
digits=(16, Eval('currency_digits', 2)),
|
|
states={
|
|
'required': Eval('invoice_method') == 'fixed',
|
|
'invisible': Eval('invoice_method') != 'fixed',
|
|
},
|
|
depends=['invoice_method', 'currency_digits'])
|
|
compensation_product = fields.Many2One('product.product',
|
|
'Compensation Product', states={
|
|
'required': Eval('invoice_method').in_(['progress', 'remainder']),
|
|
'invisible': Eval('invoice_method') == 'percent'
|
|
}, depends=['invoice_method'])
|
|
currency = fields.Many2One('currency.currency', 'Currency',
|
|
states={
|
|
'required': Eval('invoice_method') == 'fixed',
|
|
'invisible': Eval('invoice_method') != 'fixed',
|
|
},
|
|
depends=['invoice_method'])
|
|
currency_digits = fields.Function(fields.Integer('Currency Digits'),
|
|
'on_change_with_currency_digits')
|
|
months = fields.Integer('Number of Months', required=True)
|
|
month = fields.Selection([
|
|
(None, ''),
|
|
('1', 'January'),
|
|
('2', 'February'),
|
|
('3', 'March'),
|
|
('4', 'April'),
|
|
('5', 'May'),
|
|
('6', 'June'),
|
|
('7', 'July'),
|
|
('8', 'August'),
|
|
('9', 'September'),
|
|
('10', 'October'),
|
|
('11', 'November'),
|
|
('12', 'December'),
|
|
], 'Month', sort=False)
|
|
weeks = fields.Integer('Number of Weeks', required=True)
|
|
weekday = fields.Selection([
|
|
(None, ''),
|
|
('0', 'Monday'),
|
|
('1', 'Tuesday'),
|
|
('2', 'Wednesday'),
|
|
('3', 'Thursday'),
|
|
('4', 'Friday'),
|
|
('5', 'Saturday'),
|
|
('6', 'Sunday'),
|
|
], 'Day of Week', sort=False)
|
|
days = fields.Integer('Number of Days', required=True)
|
|
day = fields.Integer('Day of Month', domain=[
|
|
['OR', [
|
|
('day', '=', None),
|
|
], [
|
|
('day', '>=', 1),
|
|
('day', '<=', 31),
|
|
]],
|
|
])
|
|
description = fields.Text('Description',
|
|
help='It will be used to prepare the description field of invoice '
|
|
'lines.\nYou can use tags and they will be replaced by these '
|
|
'fields from the record related to milestone: {{ record.rec_name }}.')
|
|
|
|
@staticmethod
|
|
def default_kind():
|
|
return 'manual'
|
|
|
|
@staticmethod
|
|
def default_invoice_method():
|
|
return 'fixed'
|
|
|
|
@staticmethod
|
|
def default_advancement_product():
|
|
pool = Pool()
|
|
Config = pool.get('project.invoice_milestone.configuration')
|
|
config = Config.get_singleton()
|
|
if getattr(config, 'advancement_product', None):
|
|
return config.advancement_product.id
|
|
|
|
@staticmethod
|
|
def default_compensation_product():
|
|
pool = Pool()
|
|
Config = pool.get('project.invoice_milestone.configuration')
|
|
config = Config.get_singleton()
|
|
if config and config.compensation_product:
|
|
return config.compensation_product.id
|
|
|
|
@staticmethod
|
|
def default_currency():
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
company_id = Transaction().context.get('company')
|
|
if company_id:
|
|
return Company(company_id).currency.id
|
|
|
|
@staticmethod
|
|
def default_currency_digits():
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
company_id = Transaction().context.get('company')
|
|
if company_id:
|
|
return Company(company_id).currency.digits
|
|
return 2
|
|
|
|
@staticmethod
|
|
def default_months():
|
|
return 0
|
|
|
|
@staticmethod
|
|
def default_weeks():
|
|
return 0
|
|
|
|
@staticmethod
|
|
def default_days():
|
|
return 0
|
|
|
|
@fields.depends('currency')
|
|
def on_change_with_currency_digits(self, name=None):
|
|
if self.currency:
|
|
return self.currency.digits
|
|
return 2
|
|
|
|
def _calc_delta(self):
|
|
return {
|
|
'day': self.day,
|
|
'month': int(self.month) if self.month else None,
|
|
'days': self.days,
|
|
'weeks': self.weeks,
|
|
'months': self.months,
|
|
'weekday': int(self.weekday) if self.weekday else None,
|
|
}
|
|
|
|
|
|
class MilestoneTypeGroup(ModelSQL, ModelView):
|
|
'Project Invoice Milestone Type Group'
|
|
__name__ = 'project.invoice_milestone.type.group'
|
|
name = fields.Char('Name', required=True, translate=True)
|
|
active = fields.Boolean('Active')
|
|
description = fields.Char('Description', translate=True)
|
|
lines = fields.One2Many('project.invoice_milestone.type', 'group', 'Lines')
|
|
|
|
@staticmethod
|
|
def default_active():
|
|
return True
|
|
|
|
def compute(self, project):
|
|
milestones = []
|
|
for line in self.lines:
|
|
milestone = line.compute_milestone(project)
|
|
milestones.append(milestone)
|
|
return milestones
|
|
|
|
|
|
class MilestoneType(ModelSQL, ModelView, MilestoneMixin):
|
|
'Milestone Type'
|
|
__name__ = 'project.invoice_milestone.type'
|
|
group = fields.Many2One('project.invoice_milestone.type.group',
|
|
'Group', required=True, select=True, ondelete='CASCADE')
|
|
sequence = fields.Integer('Sequence',
|
|
help='Use to order lines in ascending order')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(MilestoneType, cls).__setup__()
|
|
cls._order.insert(0, ('sequence', 'ASC'))
|
|
|
|
@staticmethod
|
|
def order_sequence(tables):
|
|
table, _ = tables[None]
|
|
return [table.sequence == None, table.sequence]
|
|
|
|
def compute_milestone(self, project):
|
|
pool = Pool()
|
|
Milestone = pool.get('project.invoice_milestone')
|
|
|
|
milestone = Milestone()
|
|
milestone.project = project
|
|
milestone.kind = self.kind
|
|
milestone.invoice_percent = self.invoice_percent
|
|
if self.kind == 'system':
|
|
milestone.trigger = self.trigger
|
|
milestone.trigger_progress = self.trigger_progress
|
|
milestone.invoice_method = self.invoice_method
|
|
if self.invoice_method not in ['progress', 'remainder']:
|
|
milestone.advancement_product = self.advancement_product
|
|
milestone.advancement_amount = self.advancement_amount
|
|
milestone.currency = self.currency
|
|
if self.invoice_method not in ['percent']:
|
|
milestone.compensation_product = self.compensation_product
|
|
|
|
for fname in ('months', 'month', 'weeks', 'weekday', 'days', 'day',
|
|
'description'):
|
|
setattr(milestone, fname, getattr(self, fname))
|
|
# invoice_date, planned_invoice_date
|
|
return milestone
|
|
|
|
|
|
class Milestone(Workflow, ModelSQL, ModelView, MilestoneMixin):
|
|
'Milestone'
|
|
__name__ = 'project.invoice_milestone'
|
|
_rec_name = 'number'
|
|
|
|
number = fields.Char('Number', readonly=True, select=True)
|
|
project = fields.Many2One('project.work', 'Project', required=True,
|
|
ondelete='CASCADE', select=True, domain=[
|
|
('type', '=', 'project'),
|
|
('project_invoice_method', '=', 'milestone'),
|
|
('company', If(Eval('context', {}).contains('company'), '=', '!='),
|
|
Eval('context', {}).get('company', -1)),
|
|
],
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
depends=['state'])
|
|
project_company = fields.Function(fields.Many2One('company.company',
|
|
'Project Company'),
|
|
'on_change_with_project_company', searcher='search_project_field')
|
|
project_party = fields.Function(fields.Many2One('party.party',
|
|
'Project Party'),
|
|
'on_change_with_project_party', searcher='search_project_field')
|
|
|
|
is_credit = fields.Boolean('Is Credit?', readonly=True)
|
|
invoice_date = fields.Date('Invoice Date', states={
|
|
'readonly': ~Eval('state', '').in_(['draft', 'confirmed']),
|
|
'required': Eval('state', '') == 'invoiced',
|
|
}, depends=['state'])
|
|
planned_invoice_date = fields.Date('Planned Invoice Date',
|
|
states={
|
|
'readonly': Eval('state') != 'draft',
|
|
},
|
|
depends=['state'])
|
|
invoice = fields.One2One('account.invoice-project.invoice_milestone',
|
|
'milestone', 'invoice', 'Invoice', domain=[
|
|
('company', '=', Eval('project_company', -1)),
|
|
('party', '=', Eval('project_party', -1)),
|
|
], readonly=True, depends=['project_company', 'project_party'])
|
|
# Selection items set in __setup__
|
|
invoice_state = fields.Function(fields.Selection([], 'Invoice State'),
|
|
'get_invoice_state', searcher='search_invoice_state')
|
|
invoiced_amount = fields.Function(fields.Numeric('Invoiced Amount'),
|
|
'get_invoiced_amount')
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('confirmed', 'Confirmed'),
|
|
('invoiced', 'Invoiced'),
|
|
('cancel', 'Cancelled'),
|
|
], 'State', readonly=True, select=True)
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
Invoice = Pool().get('account.invoice')
|
|
super(Milestone, cls).__setup__()
|
|
for field_name in ['kind', 'trigger', 'trigger_progress',
|
|
'invoice_method', 'advancement_product', 'advancement_amount',
|
|
'compensation_product', 'currency', 'months', 'month', 'weeks',
|
|
'weekday', 'days', 'day', 'description']:
|
|
field = getattr(cls, field_name)
|
|
if not field.states.get('readonly'):
|
|
field.states['readonly'] = Eval('state') != 'draft'
|
|
else:
|
|
field.states['readonly'] = (field.states['readonly']
|
|
| (Eval('state') != 'draft'))
|
|
field.depends.append('state')
|
|
cls.invoice_state.selection = Invoice.state.selection[:]
|
|
cls._transitions |= set((
|
|
('draft', 'confirmed'),
|
|
('confirmed', 'invoiced'),
|
|
('draft', 'cancel'),
|
|
('confirmed', 'cancel'),
|
|
('invoiced', 'cancel'),
|
|
('cancel', 'draft'),
|
|
))
|
|
cls.invoice_state.selection += [(None, '')]
|
|
cls._buttons.update({
|
|
'draft': {
|
|
'invisible': ((Eval('state') != 'cancel')
|
|
| Bool(Eval('invoice'))),
|
|
'icon': 'tryton-clear',
|
|
},
|
|
'confirm': {
|
|
'invisible': Eval('state') != 'draft',
|
|
'icon': 'tryton-ok',
|
|
},
|
|
'check_trigger': {
|
|
'invisible': ((Eval('state') != 'confirmed')
|
|
| (Eval('kind') == 'manual')),
|
|
'icon': 'tryton-executable',
|
|
},
|
|
'do_invoice': {
|
|
'invisible': (
|
|
(Eval('state') != 'confirmed') |
|
|
(Eval('kind') != 'manual') |
|
|
Eval('invoice')),
|
|
'icon': 'tryton-ok',
|
|
},
|
|
'cancel': {
|
|
'invisible': Eval('state').in_(['invoiced', 'cancel']),
|
|
'icon': 'tryton-cancel',
|
|
},
|
|
|
|
})
|
|
cls._error_messages.update({
|
|
'missing_project_invoice_milestone_sequence': (
|
|
'The "Milestone Sequence" configuration param is empty in '
|
|
'Project\'s "Invoice Milestones Configuration".'),
|
|
'reset_milestone_with_invoice': (
|
|
'You cannot reset to draft the Milestone "%s" because it '
|
|
'has an invoice. Duplicate it to reinvoice.'),
|
|
'reset_milestone_done_project': (
|
|
'You cannot reset to draft the Milestone "%s" because its '
|
|
'project is already done.'),
|
|
'reset_credit_milestone': (
|
|
'You cannot reset to draft the Milestone "%s" because it '
|
|
'is a credit milestone.'),
|
|
})
|
|
|
|
@classmethod
|
|
def view_attributes(cls):
|
|
return [
|
|
('/form//group[@id="invoice_date_calculator"]', 'states',
|
|
{'invisible': Eval('kind') != 'system'}),
|
|
]
|
|
|
|
@fields.depends('project')
|
|
def on_change_with_project_company(self, name=None):
|
|
return self.project.company.id if self.project else None
|
|
|
|
@fields.depends('project')
|
|
def on_change_with_project_party(self, name=None):
|
|
return (self.project.party.id
|
|
if self.project and self.project.party else None)
|
|
|
|
@classmethod
|
|
def search_project_field(cls, name, clause):
|
|
project_fname = name.replace('project_', '')
|
|
return [
|
|
('project.%s' % project_fname,) + tuple(clause[1:]),
|
|
]
|
|
|
|
@fields.depends('project', 'invoice_method')
|
|
def on_change_with_currency(self):
|
|
if self.invoice_method == 'fixed' and self.project:
|
|
return self.project.company.currency.id
|
|
|
|
@classmethod
|
|
def search_invoice_state(cls, name, clause):
|
|
return [('invoice.state',) + tuple(clause[1:])]
|
|
|
|
def get_invoice_state(self, name):
|
|
return self.invoice.state if self.invoice else None
|
|
|
|
def get_invoiced_amount(self, name):
|
|
return self.invoice.untaxed_amount if self.invoice else Decimal(0)
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@classmethod
|
|
def set_number(cls, milestones):
|
|
'''
|
|
Fill the number field with the milestone sequence
|
|
'''
|
|
pool = Pool()
|
|
Sequence = pool.get('ir.sequence')
|
|
Config = pool.get('project.invoice_milestone.configuration')
|
|
|
|
config = Config(1)
|
|
if not config.milestone_sequence:
|
|
cls.raise_user_error('missing_project_invoice_milestone_sequence')
|
|
|
|
for milestone in milestones:
|
|
if milestone.number:
|
|
continue
|
|
milestone.number = Sequence.get_id(config.milestone_sequence.id)
|
|
|
|
def _credit(self, credit_invoice):
|
|
pool = Pool()
|
|
Config = pool.get('project.invoice_milestone.configuration')
|
|
Date = pool.get('ir.date')
|
|
|
|
config = Config(1)
|
|
milestone = self.__class__()
|
|
|
|
for fname in ('project', 'advancement_product', 'compensation_product',
|
|
'currency', 'description'):
|
|
setattr(milestone, fname, getattr(self, fname))
|
|
|
|
milestone.kind = 'manual'
|
|
milestone.invoice_method = 'fixed'
|
|
milestone.is_credit = True
|
|
|
|
if self.invoice_method == 'fixed':
|
|
milestone.advancement_amount = -self.advancement_amount
|
|
else:
|
|
milestone.currency = self.invoice.currency
|
|
compensation_amount = Decimal(0)
|
|
for inv_line in self.invoice.lines:
|
|
if inv_line.origin == self:
|
|
compensation_amount = -inv_line.amount
|
|
break
|
|
milestone.advancement_amount = compensation_amount
|
|
milestone.advancement_product = config.advancement_product
|
|
|
|
milestone.project = self.project
|
|
milestone.invoice_date = credit_invoice.invoice_date or Date.today()
|
|
milestone.invoice = credit_invoice
|
|
return milestone
|
|
|
|
@classmethod
|
|
def cron_check_triggers(cls):
|
|
'Cron Check Triggers'
|
|
milestones = cls.search([
|
|
('state', '=', 'confirmed'),
|
|
('kind', '=', 'system'),
|
|
('invoice', '=', None),
|
|
('invoice_date', '!=', None),
|
|
])
|
|
if milestones:
|
|
cls.check_trigger(milestones)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, milestones):
|
|
for milestone in milestones:
|
|
if milestone.invoice:
|
|
cls.raise_user_error('reset_milestone_with_invoice',
|
|
(milestone.rec_name,))
|
|
if milestone.project.state == 'done':
|
|
cls.raise_user_error('reset_milestone_done_project',
|
|
(milestone.rec_name,))
|
|
if milestone.is_credit:
|
|
cls.raise_user_error('reset_credit_milestone',
|
|
(milestone.rec_name,))
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('confirmed')
|
|
def confirm(cls, milestones):
|
|
cls.set_number(milestones)
|
|
cls.save(milestones)
|
|
|
|
@classmethod
|
|
@Workflow.transition('invoiced')
|
|
def invoiced(cls, milestones):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('cancel')
|
|
def cancel(cls, milestones):
|
|
assert all(m.invoice == None for m in milestones)
|
|
# TODO
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def check_trigger(cls, milestones):
|
|
triggered_milestones = cls.check_trigger_condition(milestones)
|
|
with Transaction().set_user(0):
|
|
cls.do_invoice(triggered_milestones)
|
|
|
|
@classmethod
|
|
def check_trigger_condition(cls, milestones):
|
|
triggered_milestones = []
|
|
for milestone in milestones:
|
|
if (milestone.state != 'confirmed' or milestone.kind == 'manual' or
|
|
milestone.invoice):
|
|
continue
|
|
method = getattr(milestone, 'check_trigger_%s'
|
|
% milestone.trigger)
|
|
triggered = method()
|
|
if not triggered:
|
|
continue
|
|
triggered_milestones.append(milestone)
|
|
return triggered_milestones
|
|
|
|
def check_trigger_start_project(self):
|
|
if self.project.state == 'opened':
|
|
return True
|
|
return False
|
|
|
|
def check_trigger_finish_project(self):
|
|
if self.project.state == 'done':
|
|
return True
|
|
return False
|
|
|
|
def check_trigger_progress(self):
|
|
if self.project.percent_progress_amount >= self.trigger_progress:
|
|
return True
|
|
return False
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def do_invoice(cls, milestones):
|
|
"""
|
|
It's a replica of project.work.invoice()
|
|
"""
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
Invoice = pool.get('account.invoice')
|
|
|
|
today = Date.today()
|
|
invoices = []
|
|
to_invoice = []
|
|
for milestone in milestones:
|
|
if not milestone.invoice_date:
|
|
milestone.invoice_date = milestone._calc_invoice_date()
|
|
milestone.save()
|
|
if(milestone.kind == 'system' and milestone.invoice_date > today):
|
|
continue
|
|
if milestone.invoice:
|
|
to_invoice.append(milestone)
|
|
continue
|
|
|
|
inv_line_vals = milestone._get_line_vals_to_invoice()
|
|
if not inv_line_vals and milestone.invoice_method != 'remainder':
|
|
continue
|
|
|
|
invoice = milestone._get_invoice()
|
|
invoice.project_milestone = milestone
|
|
invoice.lines = []
|
|
if inv_line_vals:
|
|
invoice.save()
|
|
elif milestone.invoice_method not in ('percent', 'progress',
|
|
'remainder'):
|
|
continue
|
|
|
|
invoice_amount = Decimal(0)
|
|
for key, grouped_inv_line_vals in groupby(inv_line_vals,
|
|
key=milestone.project._group_lines_to_invoice_key):
|
|
grouped_inv_line_vals = list(grouped_inv_line_vals)
|
|
key = dict(key)
|
|
invoice_line = milestone.project._get_invoice_line(
|
|
key, invoice, grouped_inv_line_vals)
|
|
invoice_line.invoice = invoice
|
|
invoice_line.origin = milestone
|
|
invoice_line.save()
|
|
invoice_amount += invoice_line.amount
|
|
|
|
origins = {}
|
|
for line_vals in grouped_inv_line_vals:
|
|
origin = line_vals.get('origin', None)
|
|
|
|
if origin:
|
|
origin.invoice_line = invoice_line
|
|
origins.setdefault(origin.__class__, []).append(origin)
|
|
for klass, records in origins.iteritems():
|
|
klass.save(records) # Store first new origins
|
|
|
|
if milestone.invoice_method in ('percent', 'progress', 'remainder'):
|
|
invoice_line = milestone._get_compensation_invoice_line(
|
|
invoice_amount)
|
|
if invoice_line:
|
|
if not inv_line_vals:
|
|
invoice.save()
|
|
invoice_line.invoice = invoice
|
|
invoice_line.save()
|
|
elif not inv_line_vals:
|
|
# not progress/remainder lines nor compensation
|
|
continue
|
|
|
|
invoices.append(invoice)
|
|
to_invoice.append(milestone)
|
|
|
|
if invoices:
|
|
Invoice.update_taxes(Invoice.browse([i.id for i in invoices]))
|
|
if to_invoice:
|
|
cls.invoiced(to_invoice)
|
|
|
|
def _calc_invoice_date(self):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
today = Date.today()
|
|
return today + relativedelta(**self._calc_delta())
|
|
|
|
def _get_invoice(self):
|
|
invoice = self.project._get_invoice()
|
|
if hasattr(self.project.party, 'agent'):
|
|
# Compatibility with commission_party
|
|
invoice.agent = self.project.party.agent
|
|
invoice.invoice_date = self.invoice_date
|
|
return invoice
|
|
|
|
def _get_line_vals_to_invoice(self, work=None, test=False):
|
|
"""Return line vals for work and children.
|
|
If work is not supplied, it use milestone's project"""
|
|
if self.invoice_method == 'fixed':
|
|
return self._get_line_vals_to_invoice_fixed()
|
|
|
|
if self.invoice_method == 'percent':
|
|
return self._get_line_vals_to_invoice_percent()
|
|
|
|
lines = []
|
|
if work is None:
|
|
work = self.project
|
|
if test is None:
|
|
test = work._test_group_invoice()
|
|
|
|
lines += getattr(
|
|
work, '_get_lines_to_invoice_%s' % self.invoice_method)()
|
|
for children in work.children:
|
|
if children.type == 'project':
|
|
if test != children._test_group_invoice():
|
|
continue
|
|
lines += self._get_line_vals_to_invoice(work=children, test=test)
|
|
return lines
|
|
|
|
def _get_line_vals_to_invoice_fixed(self):
|
|
if self.state != 'confirmed' or self.invoice:
|
|
return []
|
|
amount = self.advancement_amount
|
|
return [{
|
|
'product': self.advancement_product,
|
|
'quantity': 1.0 if amount > _ZERO else -1.0,
|
|
'unit': self.advancement_product.default_uom,
|
|
'unit_price': abs(amount),
|
|
'description': self._calc_invoice_line_description(),
|
|
}]
|
|
|
|
def _get_line_vals_to_invoice_percent(self):
|
|
InvoicedProgress = Pool().get('project.work.invoiced_progress')
|
|
|
|
if self.state != 'confirmed' or self.invoice:
|
|
return []
|
|
advancement_product = self.advancement_product
|
|
uom = advancement_product.default_uom
|
|
quantity = float(Decimal(
|
|
self.project.quantity * float(self.invoice_percent)).quantize(
|
|
Decimal(1) / 10 ** uom.digits))
|
|
invoiced_progress = InvoicedProgress(work=self.project,
|
|
quantity=quantity)
|
|
return [{
|
|
'product': advancement_product,
|
|
'quantity': quantity,
|
|
'unit': uom,
|
|
'unit_price': abs(self.project.list_price),
|
|
'origin': invoiced_progress,
|
|
'description': self._calc_invoice_line_description(),
|
|
}]
|
|
|
|
def _get_compensation_invoice_line(self, current_invoice_amount):
|
|
InvoiceLine = Pool().get('account.invoice.line')
|
|
|
|
amount = self.project.pending_to_compensate_advanced_amount
|
|
# TODO: review
|
|
# if self.invoice_method == 'remainder':
|
|
# if (self.group.merited_amount == self.group.total_amount
|
|
# and (self.group.invoiced_amount - amount + current_invoice_amount)
|
|
# == self.group.merited_amount):
|
|
# # It closes the milestone group
|
|
# current_invoice_amount = None
|
|
|
|
if (current_invoice_amount is not None
|
|
and self.invoice_method != 'remainder'
|
|
and current_invoice_amount < amount):
|
|
# If it is remainder => if compensates all, generating a negative invoice if it corresponds
|
|
# Otherwise, it never generates a negative invoice
|
|
amount = current_invoice_amount
|
|
if amount == _ZERO:
|
|
return
|
|
|
|
with Transaction().set_user(0, set_context=True):
|
|
invoice_line = InvoiceLine()
|
|
invoice_line.invoice_type = 'out'
|
|
invoice_line.type = 'line'
|
|
invoice_line.sequence = 1
|
|
invoice_line.origin = self
|
|
invoice_line.party = self.project.party
|
|
invoice_line.product = self.compensation_product
|
|
invoice_line.unit = self.compensation_product.default_uom
|
|
invoice_line.on_change_product()
|
|
invoice_line.quantity = -1.0
|
|
invoice_line.unit_price = amount
|
|
return invoice_line
|
|
|
|
@staticmethod
|
|
def template_context(record):
|
|
"""Generate the tempalte context"""
|
|
return {
|
|
'record': record,
|
|
}
|
|
|
|
def _calc_invoice_line_description(self):
|
|
if self.description:
|
|
template = Jinja2Template(self.description)
|
|
template_context = self.template_context(self)
|
|
return template.render(template_context)
|
|
return self.number
|
|
|
|
@classmethod
|
|
def copy(cls, milestones, default=None):
|
|
if default is None:
|
|
default = {}
|
|
default.setdefault('number', None)
|
|
default.setdefault('invoice_date', None)
|
|
default.setdefault('invoice', None)
|
|
return super(Milestone, cls).copy(milestones, default)
|