[IMP] sale_contract: recurring view refactoring, new fields and unit of measure

- add contract_type field on account.analytic.account and real_quantity on account.analytic.invoice.line
- refactor contract form and list views for better clarity (hide unnecessary fields depending on the type of contract)
- divide account_analityc_account class according to contract type for future work
- divide contract template menu in 3 menus depending on contract_type
- add on_change_uom behaviour on account recurring lines
- rewrite tests in python
- add new report (pivot & graph)
- end date of contract is now computed from the template recurring rules
- small fixes & usability improvements
This commit is contained in:
Damien Bouvy
2015-07-23 11:40:30 +02:00
parent d8f0231afc
commit ecdb8a60cb
25 changed files with 1378 additions and 378 deletions
View File
+153
View File
@@ -0,0 +1,153 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields
from openerp.osv import osv
from openerp.tools.translate import _
from openerp.exceptions import UserError
class account_analytic_line(osv.osv):
_inherit = 'account.analytic.line'
_description = 'Analytic Line'
_columns = {
'product_uom_id': fields.many2one('product.uom', 'Unit of Measure'),
'product_id': fields.many2one('product.product', 'Product'),
'general_account_id': fields.many2one('account.account', 'Financial Account', required=True, ondelete='restrict'),
'move_id': fields.many2one('account.move.line', 'Move Line', ondelete='cascade', select=True),
'journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal', required=True, ondelete='restrict', select=True),
'code': fields.char('Code', size=8),
'ref': fields.char('Ref.'),
'currency_id': fields.related('move_id', 'currency_id', type='many2one', relation='res.currency', string='Account Currency', store=True, help="The related account currency if not equal to the company one.", readonly=True),
'amount_currency': fields.related('move_id', 'amount_currency', type='float', string='Amount Currency', store=True, help="The amount expressed in the related account currency if not equal to the company one.", readonly=True),
}
_defaults = {
'company_id': lambda self,cr,uid,c: self.pool.get('res.company')._company_default_get(cr, uid, 'account.analytic.line', context=c),
}
_order = 'date desc'
def search(self, cr, uid, args, offset=0, limit=None, order=None, context=None, count=False):
if context is None:
context = {}
if context.get('from_date',False):
args.append(['date', '>=', context['from_date']])
if context.get('to_date',False):
args.append(['date','<=', context['to_date']])
return super(account_analytic_line, self).search(cr, uid, args, offset, limit,
order, context=context, count=count)
def _check_company(self, cr, uid, ids, context=None):
lines = self.browse(cr, uid, ids, context=context)
for l in lines:
if l.move_id and not l.account_id.company_id.id == l.move_id.account_id.company_id.id:
return False
return True
# Compute the cost based on the price type define into company
# property_valuation_price_type property
def on_change_unit_amount(self, cr, uid, id, prod_id, quantity, company_id,
unit=False, journal_id=False, context=None):
if context==None:
context={}
if not journal_id:
j_ids = self.pool.get('account.analytic.journal').search(cr, uid, [('type','=','purchase')])
journal_id = j_ids and j_ids[0] or False
if not journal_id or not prod_id:
return {}
product_obj = self.pool.get('product.product')
analytic_journal_obj =self.pool.get('account.analytic.journal')
product_price_type_obj = self.pool.get('product.price.type')
product_uom_obj = self.pool.get('product.uom')
j_id = analytic_journal_obj.browse(cr, uid, journal_id, context=context)
prod = product_obj.browse(cr, uid, prod_id, context=context)
result = 0.0
if prod_id:
unit_obj = False
if unit:
unit_obj = product_uom_obj.browse(cr, uid, unit, context=context)
if not unit_obj or prod.uom_id.category_id.id != unit_obj.category_id.id:
unit = prod.uom_id.id
if j_id.type == 'purchase':
if not unit_obj or prod.uom_po_id.category_id.id != unit_obj.category_id.id:
unit = prod.uom_po_id.id
if j_id.type <> 'sale':
a = prod.property_account_expense.id
if not a:
a = prod.categ_id.property_account_expense_categ.id
if not a:
raise UserError(
_('There is no expense account defined ' \
'for this product: "%s" (id:%d).') % \
(prod.name, prod.id,))
else:
a = prod.property_account_income.id
if not a:
a = prod.categ_id.property_account_income_categ.id
if not a:
raise UserError(
_('There is no income account defined ' \
'for this product: "%s" (id:%d).') % \
(prod.name, prod_id,))
flag = False
# Compute based on pricetype
product_price_type_ids = product_price_type_obj.search(cr, uid, [('field','=','standard_price')], context=context)
pricetype = product_price_type_obj.browse(cr, uid, product_price_type_ids, context=context)[0]
if journal_id:
journal = analytic_journal_obj.browse(cr, uid, journal_id, context=context)
if journal.type == 'sale':
product_price_type_ids = product_price_type_obj.search(cr, uid, [('field','=','list_price')], context=context)
if product_price_type_ids:
pricetype = product_price_type_obj.browse(cr, uid, product_price_type_ids, context=context)[0]
# Take the company currency as the reference one
if pricetype.field == 'list_price':
flag = True
ctx = context.copy()
if unit:
# price_get() will respect a 'uom' in its context, in order
# to return a default price for those units
ctx['uom'] = unit
amount_unit = prod.price_get(pricetype.field, context=ctx)[prod.id]
prec = self.pool.get('decimal.precision').precision_get(cr, uid, 'Account')
amount = amount_unit * quantity or 0.0
result = round(amount, prec)
if not flag:
result *= -1
return {'value': {
'amount': result,
'general_account_id': a,
'product_uom_id': unit
}
}
def view_header_get(self, cr, user, view_id, view_type, context=None):
if context is None:
context = {}
if context.get('account_id', False):
# account_id in context may also be pointing to an account.account.id
cr.execute('select name from account_analytic_account where id=%s', (context['account_id'],))
res = cr.fetchone()
if res:
res = _('Entries: ')+ (res[0] or '')
return res
return False
+19 -2
View File
@@ -156,6 +156,11 @@ class account_analytic_account(osv.osv):
result[rec.id] = rec.currency_id.id
return result
def _get_contract_type_selection(self, cr, uid, context=None):
return [('regular', 'Regular'), ('prepaid', 'Prepaid Support Hours')]
_contract_type_selection = lambda self, *args, **kwargs: self._get_contract_type_selection(*args, **kwargs)
_columns = {
'name': fields.char('Account/Contract Name', required=True, track_visibility='onchange'),
'complete_name': fields.function(_get_full_name, type='char', string='Full Name'),
@@ -178,7 +183,7 @@ class account_analytic_account(osv.osv):
'quantity_max': fields.float('Prepaid Service Units', help='Sets the higher limit of time to work on the contract, based on the timesheet. (for instance, number of hours in a limited support contract.)'),
'partner_id': fields.many2one('res.partner', 'Customer'),
'user_id': fields.many2one('res.users', 'Project Manager', track_visibility='onchange'),
'manager_id': fields.many2one('res.users', 'Account Manager', track_visibility='onchange'),
'manager_id': fields.many2one('res.users', 'Sales Rep', track_visibility='onchange'),
'date_start': fields.date('Start Date'),
'date': fields.date('Expiration Date', select=True, track_visibility='onchange'),
'company_id': fields.many2one('res.company', 'Company', required=False), #not required because we want to allow different companies to use the same chart of account, except for leaf accounts.
@@ -188,6 +193,7 @@ class account_analytic_account(osv.osv):
store = {
'res.company': (_get_analytic_account, ['currency_id'], 10),
}, string='Currency', type='many2one', relation='res.currency'),
'contract_type': fields.selection(_contract_type_selection, 'Type of Contract', required=True),
}
def create(self, cr, uid, vals, context=None):
@@ -217,6 +223,7 @@ class account_analytic_account(osv.osv):
res['value']['quantity_max'] = template.quantity_max
res['value']['parent_id'] = template.parent_id and template.parent_id.id or False
res['value']['description'] = template.description
res['value']['contract_type'] = template.contract_type
return res
def on_change_partner_id(self, cr, uid, ids,partner_id, name, context=None):
@@ -249,6 +256,7 @@ class account_analytic_account(osv.osv):
'manager_id': lambda self, cr, uid, ctx: ctx.get('manager_id', False),
'date_start': lambda *a: time.strftime('%Y-%m-%d'),
'currency_id': _get_default_currency,
'contract_type': 'regular',
}
def check_recursion(self, cr, uid, ids, context=None, parent=None):
@@ -335,6 +343,7 @@ class account_analytic_line(osv.osv):
'user_id': fields.many2one('res.users', 'User'),
'company_id': fields.related('account_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
'journal_id': fields.many2one('account.analytic.journal', 'Analytic Journal', required=True, ondelete='restrict', select=True),
'partner_id': fields.related('account_id', 'partner_id', type='many2one', relation='res.partner', string='Partner', store=True),
}
@@ -364,7 +373,6 @@ class account_analytic_line(osv.osv):
(_check_no_view, 'You cannot create analytic line on view account.', ['account_id']),
]
class account_analytic_journal(osv.osv):
_name = 'account.analytic.journal'
_description = 'Analytic Journal'
@@ -384,3 +392,12 @@ class account_analytic_journal(osv.osv):
'type': 'general',
'company_id': lambda self, cr, uid, c=None: self.pool.get('res.users').browse(cr, uid, uid, c).company_id.id,
}
class res_partner(osv.osv):
""" Inherits partner and adds contract information in the partner form """
_inherit = 'res.partner'
_columns = {
'contract_ids': fields.one2many('account.analytic.account', \
'partner_id', 'Contracts', readonly=True),
}
+15 -26
View File
@@ -6,7 +6,7 @@
<field name="name">analytic.analytic.account.form</field>
<field name="model">account.analytic.account</field>
<field name="arch" type="xml">
<form string="Analytic Account" version="7.0">
<form string="Analytic Account">
<sheet string="Analytic Account">
<div class="oe_button_box" name="button_box"/>
@@ -20,39 +20,28 @@
<group name="main">
<group>
<field name="partner_id" on_change="on_change_partner_id(partner_id, name)"/>
<field name="manager_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_manager']}"/>
<field name="currency_id" attrs="{'invisible': ['|',('type', '&lt;&gt;', 'view'), ('company_id', '&lt;&gt;', False)]}"/>
<field name="partner_id" on_change="on_change_partner_id(partner_id, name)" attrs="{'invisible': [('type','in',['view','normal','template'])]}"/>
<field name="manager_id" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_manager']}" attrs="{'invisible': [('type','in',['view','normal','template'])]}"/>
<field name="contract_type" widget="radio" attrs="{'invisible': ['|',('template_id','!=',False),('type','in',['view','normal'])]}"/>
</group>
<group>
<field name="type" invisible="context.get('default_type', False)"/>
<field name="template_id" on_change="on_change_template(template_id, date_start)" domain="[('type','=','template')]" attrs="{'invisible': [('type','in',['view', 'normal','template'])]}" context="{'default_type' : 'template'}"/>
<field name="code"/>
<field name="parent_id" on_change="on_change_parent(parent_id)"/>
<field name="template_id" on_change="on_change_template(template_id, date_start)" domain="[('type','=','template'),('state','=','open')]" attrs="{'invisible': [('type','in',['view', 'normal','template'])]}" context="{'default_type' : 'template'}"/>
<field name="code" attrs="{'invisible': [('type','=','template')]}"/>
<field name="parent_id" on_change="on_change_parent(parent_id)" attrs="{'invisible': [('type','in',['contract'])]}"/>
<field name="currency_id" attrs="{'invisible': ['|',('type', '&lt;&gt;', 'view'), ('company_id', '&lt;&gt;', False)]}"/>
<field name="company_id" on_change="on_change_company(company_id)" options="{'no_create': True}" groups="base.group_multi_company" attrs="{'required': [('type','&lt;&gt;','view')]}"/>
<field name="date_start" attrs="{'invisible': [('type','in',['view','normal','template'])]}"/>
<label for="date" string="End Date" attrs="{'invisible': [('type','in',['view','normal','template'])]}"/>
<div name="duration" attrs="{'invisible': [('type','=','template')]}">
<field name="date" class="oe_inline"/>
</div>
</group>
</group>
<notebook>
<page string="Contract Information" name="contract_page" attrs="{'invisible':[('type','not in',['contract', 'template'])]}">
<group string="Renewal" name="contract">
<p colspan="2" class="oe_grey oe_edit_only">
Once the end date of the contract is
passed or the maximum number of service
units (e.g. support contract) is
reached, the account manager is notified
by email to renew the contract with the
customer.
</p>
<field name="date_start"/>
<label for="date" string="End Date"/>
<div name="duration">
<field name="date" class="oe_inline"/>
</div>
</group>
<separator string="Terms and Conditions" name="description"/>
<field name="description"/>
</page>
</notebook>
<separator string="Terms and Conditions" name="description" attrs="{'invisible': [('type','in',['view','normal'])]}"/>
<field name="description" attrs="{'invisible': [('type','in',['view','normal'])]}"/>
</sheet>
<div class="oe_chatter">
<field name="message_follower_ids" widget="mail_followers"/>
@@ -39,7 +39,7 @@
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field name="arch" type="xml">
<separator name="description" position="before">
<div name="user_function_price" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
<div name="user_function_price" attrs="{'invisible': ['|',('invoice_on_timesheets','=',False),('contract_type','!=','regular')]}">
<separator string="Invoice Price Rate per User"/>
<p class="oe_grey oe_edit_only">
Define a specific service (e.g. Senior Consultant)
@@ -33,7 +33,8 @@ class account_analytic_account(osv.osv):
}
_defaults = {
'pricelist_id': lambda self, cr, uid, c: self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'product.list0')
'pricelist_id': lambda self, cr, uid, c: self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'product.list0'),
'to_invoice': lambda self, cr, uid, c: self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'hr_timesheet_invoice.timesheet_invoice_factor1')
}
def on_change_partner_id(self, cr, uid, ids, partner_id, name, context=None):
@@ -7,12 +7,6 @@
<field name="priority">30</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field name="arch" type="xml">
<xpath expr='//separator[@name="description"]' position='before'>
<group string="Invoice on Timesheets Options" name="invoice_on_timesheets" col="4">
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
<field name="to_invoice"/>
</group>
</xpath>
<xpath expr="/form/sheet" position='before'>
<header attrs="{'invisible': [('type','=','template')]}">
<button name="set_pending" string="To Renew" type="object" states="open" />
@@ -137,9 +137,6 @@
</field>
</record>
<menuitem id="menu_invoicing_contracts" parent="menu_project_billing" sequence="4"
action="sale_contract.action_account_analytic_overdue"/>
<record model="ir.ui.view" id="view_task_form2_inherited">
<field name="name">project.task.form.inherited</field>
<field name="model">project.task</field>
+1 -1
View File
@@ -3,4 +3,4 @@
import models
import report
import test
import tests
+4 -3
View File
@@ -14,18 +14,19 @@ Adds menu to show relevant information to each manager.You can also view the rep
""",
'author': 'Camptocamp / Odoo',
'website': 'https://www.odoo.com/page/billing',
'depends': ['hr_timesheet_invoice', 'sale'], #although sale is technically not required to install this module, all menuitems are located under 'Sales' application
'depends': ['hr_timesheet_invoice', 'sale', 'payment'], #although sale is technically not required to install this module, all menuitems are located under 'Sales' application
'data': [
'security/ir.model.access.csv',
'security/account_analytic_analysis_security.xml',
'security/ir.model.access.csv',
'views/sale_contract_view.xml',
'data/sale_contract_cron.xml',
'data/sale_contract_data.xml',
'views/res_config_view.xml',
'views/account_analytic_analysis.xml',
'views/product_template_view.xml',
'report/sale_contract_report_view.xml',
],
'demo': ['demo/sale_contract_demo.xml'],
'test': ['test/account_analytic_analysis.yml'],
'installable': True,
'auto_install': False,
}
@@ -88,7 +88,7 @@ Odoo Automatic Email
</record>
<record model="ir.cron" id="account_analytic_cron_for_invoice">
<field name="name">Generate Recurring Invoices from Contracts</field>
<field name="name">Generate Recurring Invoices and Payments for Subscription Contracts</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field>
@@ -0,0 +1,229 @@
<odoo>
<!-- Recurrent payment emails -->
<record id="email_payment_close" model="mail.template">
<field name="name">email_payment_close</field>
<field name="email_from">${(object.manager_id.email or object.company_id.email or 'noreply@localhost')|safe}</field>
<field name="subject">Termination of subscription ${object.code}</field>
<field name="email_to">${ctx['email_to'] or ''}</field>
<field name="model_id" ref="analytic.model_account_analytic_account"/>
<field name="body_html">
<![CDATA[
<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
<p>Hello ${object.partner_id.name},</p>
<p>
% if ctx.get('payment_method') and ctx.get('total_amount'):
Our final attempt to process a payment for your subscription using your payment method
${ctx['payment_method']}
for ${ctx['total_amount']} ${ctx.get('currency')} failed.
% if ctx.get('error'):
Your bank or credit institution gave the following details about the issue: <pre>${ctx['error']}</pre>.
%endif
% else:
Our final attempt to process a payment for your subscription failed because we have no payment method recorded for you.
% endif
</p>
<p>
As your payment should have been made <strong>15 days ago</strong>, your subscription has been terminated.
</p>
<br/>
<p>Should you wish to resolve this issue, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br/>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br/>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br/>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
% endif
<p></p>
</div>
</div>
]]>
</field>
</record>
<record id="email_payment_reminder" model="mail.template">
<field name="name">email_payment_reminder</field>
<field name="email_from">${(object.manager_id.email or object.company_id.email or 'noreply@localhost')|safe}</field>
<field name="subject">Payment reminder for subscription ${object.code}</field>
<field name="email_to">${ctx['email_to'] or ''}</field>
<field name="model_id" ref="analytic.model_account_analytic_account"/>
<field name="body_html">
<![CDATA[
<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
<p>Hello ${object.partner_id.name},</p>
<p>
% if ctx.get('payment_method') and ctx.get('total_amount'):
We were unable to process a payment for your subscription using your payment method
${ctx['payment_method']}
for ${ctx['total_amount']} ${ctx.get('currency')}.
% if ctx.get('error'):
Your bank or credit institution gave the following details about the issue: <pre>${ctx['error']}</pre>.
%endif
% else:
We were unable to process a payment for your subscription because we have no payment method recorded for you.
% endif
</p>
<p>
Your subscription ${ctx.get('code')} is still valid but will be <b>suspended</b>
on ${ctx.get('date_close')} unless the payment succeeds in the mean time (we will retry once every day).
Please double-check that you have sufficient funds.
</p>
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br/>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br/>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br/>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
% endif
<p></p>
</div>
</div>
]]>
</field>
</record>
<record id="email_payment_success" model="mail.template">
<field name="name">email_payment_success</field>
<field name="email_from">${(object.user_id.email or object.company_id.email or 'noreply@localhost')|safe}</field>
<field name="subject">Invoice for subscription ${ctx.get('code')}</field>
<field name="email_to">${ctx['email_to'] or ''}</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="report_template" ref="account.account_invoices" />
<field name="report_name">Invoice_${ctx.get('code')}_${ctx.get('previous_date')}</field>
<field name="body_html">
<![CDATA[
<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
<p>Hello ${object.partner_id.name},</p>
<p>
Your subscription (${ctx.get('code')}) has just been renewed
%if ctx.get('total_amount') and ctx.get('payment_method'):
via a payment of ${ctx['total_amount']} ${ctx['currency']} charged on ${ctx['payment_method']}
.
%endif
</p>
<p>You will find your invoice attached.</p>
%if ctx.get('next_date'):
<p>
Your next invoice will be on ${ctx.get('next_date')}.
</p>
%endif
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br/>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br/>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br/>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
% endif
<p></p>
</div>
</div>
]]>
</field>
</record>
<!-- Standard Closing Reason -->
<record id="close_reason_1" model="account.analytic.close.reason">
<field name="name">Contract is too expensive</field>
</record>
<record id="close_reason_2" model="account.analytic.close.reason">
<field name="name">Contract does not meet my requirements</field>
</record>
<record id="close_reason_3" model="account.analytic.close.reason">
<field name="name">I don't use it</field>
</record>
<record id="close_reason_4" model="account.analytic.close.reason">
<field name="name">Other</field>
</record>
</odoo>
@@ -9,6 +9,7 @@
<field name="to_invoice" ref="hr_timesheet_invoice.timesheet_invoice_factor1"/>
<field name="pricelist_id" ref="product.list0"/>
<field name="amount_max">100000</field>
<field name="contract_type">regular</field>
</record>
<record id="sale.sale_order_4" model="sale.order">
@@ -23,6 +24,7 @@
<field name="pricelist_id" ref="product.list0"/>
<field name="amount_max">50000</field>
<field name="to_invoice" ref="hr_timesheet_invoice.timesheet_invoice_factor1"/>
<field name="contract_type">regular</field>
</record>
<record id="sale.sale_order_6" model="sale.order">
@@ -36,6 +38,7 @@
<field name="hours_qtt_est">100</field>
<field name="pricelist_id" ref="product.list0"/>
<field name="to_invoice" ref="hr_timesheet_invoice.timesheet_invoice_factor1"/>
<field name="contract_type">regular</field>
</record>
</data>
-1
View File
@@ -1,4 +1,3 @@
import sale_contract
import sale
import res_config
+63 -15
View File
@@ -1,4 +1,9 @@
# -*- coding: utf-8 -*-
from dateutil.relativedelta import relativedelta
import datetime
from openerp import models, fields, api
from openerp.tools.translate import _
class product_template(models.Model):
@@ -15,23 +20,66 @@ class product_template(models.Model):
"the recurring invoices automatically")
class sale_order(models.Model):
_name = "sale.order"
_inherit = "sale.order"
update_contract = fields.Boolean("Update Contract", help="If set, the associated contract will be overwritten by this sale order (every recurring line of the contract not in this sale order will be deleted).")
@api.multi
def action_button_confirm(self):
for order in self:
if order.project_id and order.update_contract:
to_remove = [(2, line.id, 0) for line in order.project_id.recurring_invoice_line_ids]
order.project_id.sudo().write({'recurring_invoice_line_ids': to_remove, 'description': order.note})
order.project_id.sudo().increment_period()
return super(sale_order, self).action_button_confirm()
@api.model
def _prepare_invoice(self, order, lines):
invoice_vals = super(sale_order, self)._prepare_invoice(order, lines)
if order.project_id and order.update_contract:
next_date = datetime.datetime.strptime(order.project_id.recurring_next_date, "%Y-%m-%d")
periods = {'daily': 'days', 'weekly': 'weeks', 'monthly': 'months', 'yearly': 'years'}
invoicing_period = relativedelta(**{periods[order.project_id.recurring_rule_type]: order.project_id.recurring_interval})
previous_date = next_date - invoicing_period
invoice_vals['comment'] = _("This invoice covers the following period: %s - %s") % (previous_date.date(), (next_date - relativedelta(days=1)).date())
return invoice_vals
class sale_order_line(models.Model):
_name = "sale.order.line"
_inherit = "sale.order.line"
@api.one
@api.multi
def button_confirm(self):
if self.product_id.recurring_invoice and self.order_id.project_id:
invoice_line_ids = [((0, 0, {
'product_id': self.product_id.id,
'analytic_account_id': self.order_id.project_id.id,
'name': self.name,
'quantity': self.product_uom_qty,
'uom_id': self.product_uom.id,
'price_unit': self.price_unit,
'price_subtotal': self.price_subtotal
}))]
analytic_values = {'recurring_invoices': True, 'recurring_invoice_line_ids': invoice_line_ids}
if not self.order_id.project_id.partner_id:
analytic_values['partner_id'] = self.order_id.partner_id.id
self.order_id.project_id.write(analytic_values)
for order_line in self:
if order_line.product_id.recurring_invoice and order_line.order_id.project_id:
recurring_line_id = False
if order_line.product_id in [line.product_id for line in order_line.order_id.project_id.recurring_invoice_line_ids]:
for line in order_line.order_id.project_id.recurring_invoice_line_ids:
if line.product_id == order_line.product_id and line.uom_id == order_line.product_uom:
recurring_line_id = line.id
quantity = line.sold_quantity
break
if recurring_line_id:
invoice_line_ids = [((1, recurring_line_id, {
'sold_quantity': quantity + order_line.product_uom_qty,
}))]
else:
invoice_line_ids = [((0, 0, {
'product_id': order_line.product_id.id,
'analytic_account_id': order_line.order_id.project_id.id,
'name': order_line.name,
'sold_quantity': order_line.product_uom_qty,
'uom_id': order_line.product_uom.id,
'price_unit': order_line.price_unit,
'discount': order_line.discount if order_line.order_id.update_contract else False,
}))]
analytic_values = {'recurring_invoice_line_ids': invoice_line_ids}
if not order_line.order_id.project_id.partner_id:
analytic_values['partner_id'] = order_line.order_id.partner_id.id
order_line.order_id.project_id.sudo().write(analytic_values)
return super(sale_order_line, self).button_confirm()
+299 -157
View File
@@ -5,6 +5,7 @@ import datetime
import logging
import time
from openerp import SUPERUSER_ID
from openerp.osv import osv, fields
import openerp.tools
from openerp.tools.translate import _
@@ -14,68 +15,7 @@ from openerp.addons.decimal_precision import decimal_precision as dp
_logger = logging.getLogger(__name__)
class account_analytic_invoice_line(osv.osv):
_name = "account.analytic.invoice.line"
def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict, context=None):
res = {}
for line in self.browse(cr, uid, ids, context=context):
res[line.id] = line.quantity * line.price_unit
if line.analytic_account_id.pricelist_id:
cur = line.analytic_account_id.pricelist_id.currency_id
res[line.id] = self.pool.get('res.currency').round(cr, uid, cur, res[line.id])
return res
_columns = {
'product_id': fields.many2one('product.product','Product',required=True),
'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account', ondelete='cascade'),
'name': fields.text('Description', required=True),
'quantity': fields.float('Quantity', required=True),
'uom_id': fields.many2one('product.uom', 'Unit of Measure',required=True),
'price_unit': fields.float('Unit Price', required=True),
'price_subtotal': fields.function(_amount_line, string='Sub Total', type="float",digits=0),
}
_defaults = {
'quantity' : 1,
}
def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', partner_id=False, price_unit=False, pricelist_id=False, company_id=None, context=None):
context = context or {}
uom_obj = self.pool.get('product.uom')
company_id = company_id or False
local_context = dict(context, company_id=company_id, force_company=company_id, pricelist=pricelist_id)
if not product:
return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
if partner_id:
part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=local_context)
if part.lang:
local_context.update({'lang': part.lang})
result = {}
res = self.pool.get('product.product').browse(cr, uid, product, context=local_context)
price = False
if price_unit is not False:
price = price_unit
elif pricelist_id:
price = res.price
if price is False:
price = res.list_price
if not name:
name = self.pool.get('product.product').name_get(cr, uid, [res.id], context=local_context)[0][1]
if res.description_sale:
name += '\n'+res.description_sale
result.update({'name': name or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': price})
res_final = {'value':result}
if result['uom_id'] != res.uom_id.id:
selected_uom = uom_obj.browse(cr, uid, result['uom_id'], context=local_context)
new_price = uom_obj._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uom_id'])
res_final['value']['price_unit'] = new_price
return res_final
# Regular
class account_analytic_account(osv.osv):
_name = "account.analytic.account"
_inherit = "account.analytic.account"
@@ -287,23 +227,6 @@ class account_analytic_account(osv.osv):
res_final = res
return res_final
def _remaining_hours_calc(self, cr, uid, ids, name, arg, context=None):
res = {}
for account in self.browse(cr, uid, ids, context=context):
if account.quantity_max != 0:
res[account.id] = account.quantity_max - account.hours_quantity
else:
res[account.id] = 0.0
for id in ids:
res[id] = round(res.get(id, 0.0),2)
return res
def _remaining_hours_to_invoice_calc(self, cr, uid, ids, name, arg, context=None):
res = {}
for account in self.browse(cr, uid, ids, context=context):
res[account.id] = max(account.hours_qtt_est - account.timesheet_ca_invoiced, account.ca_to_invoice)
return res
def _hours_qtt_invoiced_calc(self, cr, uid, ids, name, arg, context=None):
res = {}
for account in self.browse(cr, uid, ids, context=context):
@@ -389,21 +312,6 @@ class account_analytic_account(osv.osv):
res[id] = round(res.get(id, 0.0),2)
return res
def _is_overdue_quantity(self, cr, uid, ids, fieldnames, args, context=None):
result = dict.fromkeys(ids, 0)
for record in self.browse(cr, uid, ids, context=context):
if record.quantity_max > 0.0:
result[record.id] = int(record.hours_quantity > record.quantity_max)
else:
result[record.id] = 0
return result
def _get_analytic_account(self, cr, uid, ids, context=None):
result = set()
for line in self.pool.get('account.analytic.line').browse(cr, uid, ids, context=context):
result.add(line.account_id.id)
return list(result)
def _get_total_estimation(self, account):
tot_est = 0.0
if account.fix_price_invoices:
@@ -446,11 +354,6 @@ class account_analytic_account(osv.osv):
return res
_columns = {
'is_overdue_quantity' : fields.function(_is_overdue_quantity, method=True, type='boolean', string='Overdue Quantity',
store={
'account.analytic.line' : (_get_analytic_account, None, 20),
'account.analytic.account': (lambda self, cr, uid, ids, c=None: ids, ['quantity_max'], 10),
}),
'ca_invoiced': fields.function(_ca_invoiced_calc, type='float', string='Invoiced Amount',
help="Total customer invoiced amount for this account.",
digits=0),
@@ -475,10 +378,6 @@ class account_analytic_account(osv.osv):
help="Number of time (hours/days) (from journal of type 'general') that can be invoiced if you invoice based on analytic account."),
'hours_qtt_invoiced': fields.function(_hours_qtt_invoiced_calc, type='float', string='Invoiced Time',
help="Number of time (hours/days) that can be invoiced plus those that already have been invoiced."),
'remaining_hours': fields.function(_remaining_hours_calc, type='float', string='Remaining Time',
help="Computed using the formula: Maximum Time - Total Worked Time"),
'remaining_hours_to_invoice': fields.function(_remaining_hours_to_invoice_calc, type='float', string='Remaining Time',
help="Computed using the formula: Expected on timesheets - Total invoiced on timesheets"),
'fix_price_to_invoice': fields.function(_fix_price_to_invoice_calc, type='float', string='Remaining Time',
help="Sum of quotations for this contract."),
'timesheet_ca_invoiced': fields.function(_timesheet_ca_invoiced_calc, type='float', string='Remaining Time',
@@ -506,22 +405,6 @@ class account_analytic_account(osv.osv):
'invoiced_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Invoiced"),
'remaining_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total Remaining", help="Expectation of remaining income for this contract. Computed as the sum of remaining subtotals which, in turn, are computed as the maximum between '(Estimation - Invoiced)' and 'To Invoice' amounts"),
'toinvoice_total' : fields.function(_sum_of_fields, type="float",multi="sum_of_all", string="Total to Invoice", help=" Sum of everything that could be invoiced for this contract."),
'recurring_invoice_line_ids': fields.one2many('account.analytic.invoice.line', 'analytic_account_id', 'Invoice Lines', copy=True),
'recurring_invoices' : fields.boolean('Generate recurring invoices automatically'),
'recurring_rule_type': fields.selection([
('daily', 'Day(s)'),
('weekly', 'Week(s)'),
('monthly', 'Month(s)'),
('yearly', 'Year(s)'),
], 'Recurrency', help="Invoice automatically repeat at specified interval"),
'recurring_interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
'recurring_next_date': fields.date('Date of Next Invoice'),
}
_defaults = {
'recurring_interval': 1,
'recurring_next_date': lambda *a: time.strftime('%Y-%m-%d'),
'recurring_rule_type':'monthly'
}
def open_sale_order_lines(self,cr,uid,ids,context=None):
@@ -550,41 +433,6 @@ class account_analytic_account(osv.osv):
return self.name_get(cr, uid, [rec_id], context)[0]
return super(account_analytic_account, self).name_create(cr, uid, name, context=context)
def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
if not template_id:
return {}
res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
template = self.browse(cr, uid, template_id, context=context)
if not ids:
res['value']['fix_price_invoices'] = template.fix_price_invoices
res['value']['amount_max'] = template.amount_max
if not ids:
res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets
res['value']['hours_qtt_est'] = template.hours_qtt_est
if template.to_invoice.id:
res['value']['to_invoice'] = template.to_invoice.id
if template.pricelist_id.id:
res['value']['pricelist_id'] = template.pricelist_id.id
if not ids:
invoice_line_ids = []
for x in template.recurring_invoice_line_ids:
invoice_line_ids.append((0, 0, {
'product_id': x.product_id.id,
'uom_id': x.uom_id.id,
'name': x.name,
'quantity': x.quantity,
'price_unit': x.price_unit,
'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False,
}))
res['value']['recurring_invoices'] = template.recurring_invoices
res['value']['recurring_interval'] = template.recurring_interval
res['value']['recurring_rule_type'] = template.recurring_rule_type
res['value']['recurring_invoice_line_ids'] = invoice_line_ids
return res
def onchange_recurring_invoices(self, cr, uid, ids, recurring_invoices, date_start=False, context=None):
value = {}
if date_start and recurring_invoices:
@@ -617,7 +465,10 @@ class account_analytic_account(osv.osv):
fill_remind("old", [('state', 'in', ['pending'])])
# Expires now
fill_remind("new", [('state', 'in', ['draft', 'open']), '|', '&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')), ('is_overdue_quantity', '=', True)], True)
fill_remind("new", [('state', 'in', ['draft', 'open']),
'|',
'&', ('date', '!=', False), ('date', '<=', time.strftime('%Y-%m-%d')),
'&', ('is_overdue_quantity', '=', True), ('contract_type', '=', 'prepaid')], True)
# Expires in less than 30 days
fill_remind("future", [('state', 'in', ['draft', 'open']), ('date', '!=', False), ('date', '<', (datetime.datetime.now() + datetime.timedelta(30)).strftime("%Y-%m-%d"))])
@@ -645,6 +496,142 @@ class account_analytic_account(osv.osv):
'res_model': 'account.analytic.line',
}
# Prepaid
class account_analytic_account(osv.osv):
_name = "account.analytic.account"
_inherit = "account.analytic.account"
def _get_analytic_account(self, cr, uid, ids, context=None):
result = set()
for line in self.pool.get('account.analytic.line').browse(cr, uid, ids, context=context):
result.add(line.account_id.id)
return list(result)
def _is_overdue_quantity(self, cr, uid, ids, fieldnames, args, context=None):
result = dict.fromkeys(ids, 0)
for record in self.browse(cr, uid, ids, context=context):
if record.quantity_max > 0.0:
result[record.id] = int(record.hours_quantity > record.quantity_max)
else:
result[record.id] = 0
return result
def _remaining_hours_calc(self, cr, uid, ids, name, arg, context=None):
res = {}
for account in self.browse(cr, uid, ids, context=context):
if account.quantity_max != 0:
res[account.id] = account.quantity_max - account.hours_quantity
else:
res[account.id] = 0.0
for id in ids:
res[id] = round(res.get(id, 0.0), 2)
return res
def _remaining_hours_to_invoice_calc(self, cr, uid, ids, name, arg, context=None):
res = {}
for account in self.browse(cr, uid, ids, context=context):
res[account.id] = max(account.hours_qtt_est - account.timesheet_ca_invoiced, account.ca_to_invoice)
return res
_columns = {
'is_overdue_quantity': fields.function(_is_overdue_quantity, method=True, type='boolean', string='Overdue Quantity',
store={
'account.analytic.line': (_get_analytic_account, None, 20),
'account.analytic.account': (lambda self, cr, uid, ids, c=None: ids, ['quantity_max'], 10),
}),
'remaining_hours': fields.function(_remaining_hours_calc, type='float', string='Remaining Time',
help="Computed using the formula: Maximum Time - Total Worked Time"),
'remaining_hours_to_invoice': fields.function(_remaining_hours_to_invoice_calc, type='float', string='Remaining Time',
help="Computed using the formula: Expected on timesheets - Total invoiced on timesheets"),
}
# Recurring
class account_analytic_account(osv.osv):
_name = "account.analytic.account"
_inherit = "account.analytic.account"
def _get_recurring_line_ids(self, cr, uid, ids, context=None):
result = []
for line in self.pool.get('account.analytic.invoice.line').browse(cr, uid, ids, context=context):
result.append(line.analytic_account_id.id)
return result
def _get_contract_type_selection(self, cr, uid, context=None):
select = super(account_analytic_account, self)._get_contract_type_selection(cr, uid, context=context)
select.append(('subscription', 'Susbcription'))
return select
def _get_recurring_price(self, cr, uid, ids, fieldnames, args, context=None):
result = dict.fromkeys(ids, 0.0)
for account in self.browse(cr, uid, ids, context=context):
result[account.id] = sum(line.price_subtotal for line in account.recurring_invoice_line_ids)
return result
_columns = {
'recurring_invoice_line_ids': fields.one2many('account.analytic.invoice.line', 'analytic_account_id', 'Invoice Lines', copy=True),
'recurring_rule_type': fields.selection([('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)'), ], 'Recurrency', help="Invoice automatically repeat at specified interval"),
'recurring_interval': fields.integer('Repeat Every', help="Repeat every (Days/Week/Month/Year)"),
'recurring_next_date': fields.date('Date of Next Invoice'),
'recurring_total': fields.function(_get_recurring_price, string="Recurring Price", type="float", store={
'account.analytic.account': (lambda s, cr, uid, ids, c={}: ids, ['recurring_invoice_line_ids'], 5),
'account.analytic.invoice.line': (_get_recurring_line_ids, ['product_id', 'quantity', 'actual_quantity', 'sold_quantity', 'uom_id', 'price_unit', 'discount', 'price_subtotal'], 5),
}, track_visibility='onchange'),
# Fields that only matters on template
'plan_description': fields.html(string='Plan Description', help="Describe this contract in a few lines",),
'user_selectable': fields.boolean(string='Allow Online Order', help="""Leave this unchecked if you don't want this contract template to be available to the customer in the frontend (for a free trial, for example)"""),
'close_reason_id': fields.many2one("account.analytic.close.reason", "Close Reason")
}
_defaults = {
'recurring_interval': 1,
'recurring_next_date': lambda *a: time.strftime('%Y-%m-%d'),
'recurring_rule_type': 'monthly',
'user_selectable': True,
'contract_type': 'regular',
}
def on_change_template(self, cr, uid, ids, template_id, date_start=False, context=None):
if not template_id:
return {}
res = super(account_analytic_account, self).on_change_template(cr, uid, ids, template_id, date_start=date_start, context=context)
template = self.browse(cr, uid, template_id, context=context)
if not ids:
res['value']['fix_price_invoices'] = template.fix_price_invoices
res['value']['amount_max'] = template.amount_max
if not ids:
res['value']['invoice_on_timesheets'] = template.invoice_on_timesheets
res['value']['hours_qtt_est'] = template.hours_qtt_est
if template.to_invoice.id:
res['value']['to_invoice'] = template.to_invoice.id
if template.pricelist_id.id:
res['value']['pricelist_id'] = template.pricelist_id.id
if not ids:
invoice_line_ids = []
for x in template.recurring_invoice_line_ids:
invoice_line_ids.append((0, 0, {
'product_id': x.product_id.id,
'uom_id': x.uom_id.id,
'name': x.name,
'quantity': x.quantity,
'price_unit': x.price_unit,
'analytic_account_id': x.analytic_account_id and x.analytic_account_id.id or False,
}))
res['value']['recurring_interval'] = template.recurring_interval
res['value']['recurring_rule_type'] = template.recurring_rule_type
res['value']['recurring_invoice_line_ids'] = invoice_line_ids
if template.contract_type == 'subscription':
res['value']['date'] = False
elif template.recurring_rule_type and template.recurring_interval:
periods = {'daily': 'days', 'weekly': 'weeks', 'monthly': 'months', 'yearly': 'years'}
contract_period = relativedelta(**{periods[template.recurring_rule_type]: template.recurring_interval})
res['value']['date'] = datetime.datetime.strftime(datetime.date.today() + contract_period, openerp.tools.DEFAULT_SERVER_DATE_FORMAT)
return res
def _prepare_invoice_data(self, cr, uid, contract, context=None):
context = context or {}
@@ -661,6 +648,11 @@ class account_analytic_account(osv.osv):
raise UserError(_('Please define a sale journal for the company "%s".') % (contract.company_id.name or '', ))
partner_payment_term = partner.property_payment_term_id and partner.property_payment_term_id.id or False
next_date = datetime.datetime.strptime(contract.recurring_next_date, "%Y-%m-%d")
periods = {'daily': 'days', 'weekly': 'weeks', 'monthly': 'months', 'yearly': 'years'}
invoicing_period = relativedelta(**{periods[contract.recurring_rule_type]: contract.recurring_interval})
new_date = next_date + invoicing_period
currency_id = False
if contract.pricelist_id:
@@ -681,6 +673,7 @@ class account_analytic_account(osv.osv):
'fiscal_position_id': fpos_id,
'payment_term_id': partner_payment_term,
'company_id': contract.company_id.id or False,
'comment': _("This invoice covers the following period: %s - %s") % (next_date.date(), new_date.date()),
}
return invoice
@@ -699,6 +692,7 @@ class account_analytic_account(osv.osv):
'account_id': account_id,
'account_analytic_id': line.analytic_account_id.id,
'price_unit': line.price_unit or 0.0,
'discount': line.discount,
'quantity': line.quantity,
'uos_id': line.uom_id.id or False,
'product_id': line.product_id.id or False,
@@ -722,7 +716,7 @@ class account_analytic_account(osv.osv):
invoice['invoice_line_ids'] = self._prepare_invoice_lines(cr, uid, contract, invoice['fiscal_position_id'], context=context)
return invoice
def recurring_create_invoice(self, cr, uid, ids, context=None):
def recurring_invoice(self, cr, uid, ids, context=None):
return self._recurring_create_invoice(cr, uid, ids, context=context)
def _cron_recurring_create_invoice(self, cr, uid, context=None):
@@ -735,7 +729,7 @@ class account_analytic_account(osv.osv):
if ids:
contract_ids = ids
else:
contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('recurring_invoices','=', True), ('type', '=', 'contract')])
contract_ids = self.search(cr, uid, [('recurring_next_date','<=', current_date), ('state','=', 'open'), ('type', '=', 'contract'), ('contract_type', '=', 'subscription')])
if contract_ids:
cr.execute('SELECT company_id, array_agg(id) as ids FROM account_analytic_account WHERE id IN %s GROUP BY company_id', (tuple(contract_ids),))
for company_id, ids in cr.fetchall():
@@ -763,3 +757,151 @@ class account_analytic_account(osv.osv):
else:
raise
return invoice_ids
def _prepare_renewal_order_values(self, cr, uid, ids, context=None):
res = dict()
for contract in self.browse(cr, uid, ids, context=context):
order_lines = []
order_seq_id = self.pool['ir.sequence'].search(cr, uid, [('code', '=', 'sale.order')], context=context)
order_seq = self.pool['ir.sequence'].browse(cr, uid, order_seq_id, context=context)
for line in contract.recurring_invoice_line_ids:
order_lines.append((0, 0, {
'product_id': line.product_id.id,
'name': line.product_id.name_template,
'description': line.name,
'product_uom': line.uom_id.id,
'product_uom_qty': line.quantity,
'price_unit': line.price_unit,
'discount': line.discount,
}))
res[contract.id] = {
'name': order_seq.next_by_id() + ' - Renewal',
'pricelist_id': contract.pricelist_id.id,
'partner_id': contract.partner_id.id,
'currency_id': contract.pricelist_id.currency_id.id,
'order_line': order_lines,
'project_id': contract.id,
'update_contract': True,
'note': contract.description,
'user_id': contract.manager_id.id,
}
return res
def prepare_renewal_order(self, cr, uid, ids, context=None):
values = self._prepare_renewal_order_values(cr, uid, ids, context=context)
for contract in self.browse(cr, uid, ids, context=context):
order_id = self.pool['sale.order'].create(cr, uid, values[contract.id], context=context)
return {
"type": "ir.actions.act_window",
"res_model": "sale.order",
"views": [[False, "form"]],
"res_id": order_id,
}
def increment_period(self, cr, uid, ids, context=None):
for account in self.browse(cr, uid, ids, context=context):
next_date = datetime.datetime.strptime(account.recurring_next_date, "%Y-%m-%d")
periods = {'daily': 'days', 'weekly': 'weeks', 'monthly': 'months', 'yearly': 'years'}
invoicing_period = relativedelta(**{periods[account.recurring_rule_type]: account.recurring_interval})
new_date = next_date + invoicing_period
self.write(cr, uid, account.id, {'recurring_next_date': new_date}, context=context)
class account_analytic_invoice_line(osv.osv):
_name = "account.analytic.invoice.line"
def _amount_line(self, cr, uid, ids, prop, unknow_none, unknow_dict, context=None):
res = {}
for line in self.browse(cr, uid, ids, context=context):
res[line.id] = line.quantity * line.price_unit * (100.0 - line.discount) / 100.0
if line.analytic_account_id.pricelist_id:
cur = line.analytic_account_id.pricelist_id.currency_id
res[line.id] = self.pool.get('res.currency').round(cr, uid, cur, res[line.id])
return res
def _compute_quantity(self, cr, uid, ids, name, arg, context=None):
res = {}
for line in self.browse(cr, uid, ids, context=context):
res[line.id] = max(line.sold_quantity, line.actual_quantity)
return res
def _set_quantity(self, cr, uid, ids, name, value, args=None, context=None):
for line in self.browse(cr, uid, ids, context=context):
self.write(cr, uid, line.id, {'actual_quantity': value}, context=context)
_columns = {
'product_id': fields.many2one('product.product', 'Product', required=True),
'analytic_account_id': fields.many2one('account.analytic.account', 'Analytic Account'),
'name': fields.text('Description', required=True),
'quantity': fields.function(_compute_quantity, string='Quantity',
store=True,
help="Max between actual and sold quantities; this quantity will be invoiced"),
'actual_quantity': fields.float('Actual Quantity', help="Quantity actually used by the customer"),
'sold_quantity': fields.float('Sold Quantity', help="Quantity sold to the customer", required=True),
'uom_id': fields.many2one('product.uom', 'Unit of Measure', required=True),
'price_unit': fields.float('Unit Price', required=True),
'discount': fields.float('Discount (%)', digits_compute=dp.get_precision('Discount')),
'price_subtotal': fields.function(_amount_line, string='Sub Total', type="float", digits_compute=dp.get_precision('Account')),
}
_defaults = {
'sold_quantity': 1,
'actual_quantity': 0,
}
def product_id_change(self, cr, uid, ids, product, uom_id, qty=0, name='', partner_id=False, price_unit=False, pricelist_id=False, company_id=None, context=None):
context = context or {}
uom_obj = self.pool.get('product.uom')
company_id = company_id or False
local_context = dict(context, company_id=company_id, force_company=company_id, pricelist=pricelist_id)
if not product:
return {'value': {'price_unit': 0.0}, 'domain':{'product_uom':[]}}
if partner_id:
part = self.pool.get('res.partner').browse(cr, uid, partner_id, context=local_context)
if part.lang:
local_context.update({'lang': part.lang})
result = {}
res = self.pool.get('product.product').browse(cr, uid, product, context=local_context)
price = False
if price_unit is not False:
price = price_unit
elif pricelist_id:
price = res.price
if price is False:
price = res.list_price
if not name:
name = self.pool.get('product.product').name_get(cr, uid, [res.id], context=local_context)[0][1]
if res.description_sale:
name += '\n'+res.description_sale
result.update({'name': name or False,'uom_id': uom_id or res.uom_id.id or False, 'price_unit': price})
res_final = {'value':result}
if result['uom_id'] != res.uom_id.id:
selected_uom = uom_obj.browse(cr, uid, result['uom_id'], context=local_context)
new_price = uom_obj._compute_price(cr, uid, res.uom_id.id, res_final['value']['price_unit'], result['uom_id'])
res_final['value']['price_unit'] = new_price
if not uom_id:
res_final['domain'] = {'uom_id': [('category_id', '=', res.uom_id.category_id.id)]}
return res_final
def product_uom_change(self, cr, uid, ids, product, uom_id, qty=0, name='', partner_id=False, pricelist_id=False, context=None):
context = context or {}
if not uom_id:
return {'value': {'price_unit': 0.0, 'uom_id': uom_id or False}}
return self.product_id_change(cr, uid, ids, product, uom_id=uom_id, qty=qty, name=name, partner_id=partner_id, pricelist_id=pricelist_id, context=context)
class account_analytic_close_reason(osv.osv):
_name = "account.analytic.close.reason"
_order = "sequence, id"
_columns = {
'name': fields.char('Name', required=True),
'sequence': fields.integer('Sequence')
}
_defaults = {
'sequence': 10
}
+2 -1
View File
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
import account_analytic_analysis_summary
import account_analytic_analysis_summary
import sale_contract_report
@@ -0,0 +1,94 @@
from openerp import tools
from openerp import fields, models
class sale_contract_report(models.Model):
_name = "sale.contract.report"
_description = "Subscription Statistics"
_auto = False
date_start = fields.Date('Date Start', readonly=True)
date_end = fields.Date('Date End', readonly=True)
product_id = fields.Many2one('product.product', 'Product', readonly=True)
product_uom = fields.Many2one('product.uom', 'Unit of Measure', readonly=True)
recurring_price = fields.Float('Recurring price(per period)', readonly=True)
partner_id = fields.Many2one('res.partner', 'Customer', readonly=True)
manager_id = fields.Many2one('res.users', 'Sales Rep', readonly=True)
company_id = fields.Many2one('res.company', 'Company', readonly=True)
state = fields.Selection([('template', 'Template'),
('draft', 'New'),
('open', 'In Progress'),
('pending', 'To Renew'),
('close', 'Closed'),
('cancelled', 'Cancelled')], readonly=True)
categ_id = fields.Many2one('product.category', 'Product Category', readonly=True)
pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True)
parent_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True)
template_id = fields.Many2one('account.analytic.account', 'Subscription Template', readonly=True)
product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True)
country_id = fields.Many2one('res.country', 'Country', readonly=True)
commercial_partner_id = fields.Many2one('res.partner', 'Comercial Partner', readonly=True)
def _select(self):
select_str = """
SELECT min(l.id) as id,
l.product_id as product_id,
l.uom_id as product_uom,
l.analytic_account_id,
(l.price_unit * l.quantity) as recurring_price,
a.date_start as date_start,
a.date as date_end,
a.partner_id as partner_id,
a.manager_id as manager_id,
a.company_id as company_id,
a.state,
a.template_id as template_id,
t.categ_id as categ_id,
a.pricelist_id as pricelist_id,
a.parent_id as parent_id,
p.product_tmpl_id,
partner.country_id as country_id,
partner.commercial_partner_id as commercial_partner_id
"""
return select_str
def _from(self):
from_str = """
account_analytic_invoice_line l
join account_analytic_account a on (l.analytic_account_id=a.id) and (a.contract_type='subscription') and (a.type!='template')
join res_partner partner on a.partner_id = partner.id
left join product_product p on (l.product_id=p.id)
left join product_template t on (p.product_tmpl_id=t.id)
left join product_uom u on (u.id=l.uom_id)
"""
return from_str
def _group_by(self):
group_by_str = """
GROUP BY l.product_id,
l.uom_id,
t.categ_id,
l.analytic_account_id,
a.date_start,
a.date,
a.partner_id,
a.manager_id,
recurring_price,
a.company_id,
a.state,
a.template_id,
a.pricelist_id,
a.parent_id,
p.product_tmpl_id,
partner.country_id,
partner.commercial_partner_id
"""
return group_by_str
def init(self, cr):
tools.drop_view_if_exists(cr, self._table)
cr.execute("""CREATE or REPLACE VIEW %s as (
%s
FROM ( %s )
%s
)""" % (self._table, self._select(), self._from(), self._group_by()))
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_subcription_report_pivot" model="ir.ui.view">
<field name="name">subscription.report.pivot</field>
<field name="model">sale.contract.report</field>
<field name="arch" type="xml">
<pivot string="Subscription Analysis" disable_linking="True">
<field name="template_id" type="row"/>
<field name="manager_id" type="col"/>
<field name="recurring_price" type="measure"/>
</pivot>
</field>
</record>
<record id="view_subcription_report_graph" model="ir.ui.view">
<field name="name">subscription.report.graph</field>
<field name="model">sale.contract.report</field>
<field name="arch" type="xml">
<graph string="Subscription Analysis">
<field name="template_id" type="row"/>
<field name="recurring_price" type="measure"/>
</graph>
</field>
</record>
<record id="action_subscription_report_all" model="ir.actions.act_window">
<field name="name">Subscription Analysis</field>
<field name="res_model">sale.contract.report</field>
<field name="view_type">form</field>
<field name="view_mode">pivot,graph</field>
</record>
<menuitem action="action_subscription_report_all" id="menu_report_product_all" parent="base.menu_sale_report" sequence="10"/>
</odoo>
@@ -7,7 +7,7 @@
</record>
<record id="group_template_required" model="res.groups">
<field name="name">Mandatory use of templates in contracts</field>
<field name="name">Mandatory use of contract templates</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="comment">the field template of the analytic accounts and contracts will be required.</field>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
@@ -17,5 +17,45 @@
<field name="implied_ids" eval="[(4, ref('sale.group_analytic_accounting'))]"/>
</record>
<record id="account.group_account_invoice" model="res.groups">
<field name="implied_ids" eval="[(4, ref('sale.group_analytic_accounting'))]"/>
</record>
<record id="group_sale_contract_view" model="res.groups">
<field name="name">See Contracts</field>
<field name="category_id" ref="base.module_category_sales_management"/>
<field name="comment">The user will have read access to contracts.</field>
</record>
<record id="group_sale_contract_manager" model="res.groups">
<field name="name">Manage Contracts</field>
<field name="category_id" ref="base.module_category_sales_management"/>
<field name="comment">The user will have write access to contracts.</field>
<field name="implied_ids" eval="[(4, ref('sale_contract.group_sale_contract_view'))]"/>
<field name="users" eval="[(4, ref('base.user_root'))]"/>
</record>
<!-- Reporting -->
<record model="ir.rule" id="sale_contract_report_comp_rule">
<field name="name">Subscription Analysis multi-company</field>
<field name="model_id" ref="model_sale_contract_report"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record id="sale_contract_report_personal_rule" model="ir.rule">
<field name="name">Personal Subscription Analysis</field>
<field ref="model_sale_contract_report" name="model_id"/>
<field name="domain_force">['|',('user_id','=',user.id),('user_id','=',False)]</field>
<field name="groups" eval="[(4, ref('base.group_sale_salesman'))]"/>
</record>
<record id="sale_order_report_see_all" model="ir.rule">
<field name="name">All Orders Analysis</field>
<field ref="model_sale_contract_report" name="model_id"/>
<field name="domain_force">[(1,'=',1)]</field>
<field name="groups" eval="[(4, ref('base.group_sale_salesman_all_leads'))]"/>
</record>
</data>
</openerp>
@@ -4,4 +4,12 @@ access_account_analytic_analysis_summary_month_sale,account_analytic_analysis.su
access_account_analytic_analysis_summary_user,account_analytic_analysis.summary.user,model_account_analytic_analysis_summary_user,account.group_account_manager,1,0,0,0
access_account_analytic_analysis_summary_month,account_analytic_analysis.summary.month,model_account_analytic_analysis_summary_month,account.group_account_manager,1,0,0,0
access_account_analytic_invoice_line,account_analytic_invoice_line.user,model_account_analytic_invoice_line,account.group_account_user,1,1,1,1
access_account_analytic_invoice_line,account_analytic_invoice_line.user,model_account_analytic_invoice_line,base.group_sale_salesman,1,1,1,1
access_sale_contract_report_salesman,sale.contract.report,model_sale_contract_report,base.group_sale_salesman,1,0,0,0
access_sale_contract_report_manager,sale.contract.report,model_sale_contract_report,base.group_sale_manager,1,1,1,1
sale.access_account_analytic_account_salesman,account_analytic_account salesman,analytic.model_account_analytic_account,base.group_sale_salesman,0,0,0,0
access_account_analytic_account_manager,account_analytic_account contract manager,analytic.model_account_analytic_account,sale_contract.group_sale_contract_manager,1,1,1,1
access_account_analytic_invoice_line_manager,account_analytic_invoice_line contract manager,model_account_analytic_invoice_line,sale_contract.group_sale_contract_manager,1,1,1,1
access_account_analytic_close_reason_manager,account.analytic.close.reason,model_account_analytic_close_reason,sale_contract.group_sale_contract_manager,1,1,1,1
access_account_analytic_account_view,account_analytic_account view,analytic.model_account_analytic_account,sale_contract.group_sale_contract_view,1,0,0,0
access_account_analytic_invoice_line_view,account_analytic_invoice_line view,model_account_analytic_invoice_line,sale_contract.group_sale_contract_view,1,0,0,0
access_account_analytic_close_reason_view,account.analytic.close.reason,model_account_analytic_close_reason,sale_contract.group_sale_contract_view,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_account_analytic_analysis_summary_user account_analytic_analysis.summary.user model_account_analytic_analysis_summary_user account.group_account_manager 1 0 0 0
5 access_account_analytic_analysis_summary_month account_analytic_analysis.summary.month model_account_analytic_analysis_summary_month account.group_account_manager 1 0 0 0
6 access_account_analytic_invoice_line account_analytic_invoice_line.user model_account_analytic_invoice_line account.group_account_user 1 1 1 1
7 access_account_analytic_invoice_line access_sale_contract_report_salesman account_analytic_invoice_line.user sale.contract.report model_account_analytic_invoice_line model_sale_contract_report base.group_sale_salesman 1 1 0 1 0 1 0
8 access_sale_contract_report_manager sale.contract.report model_sale_contract_report base.group_sale_manager 1 1 1 1
9 sale.access_account_analytic_account_salesman account_analytic_account salesman analytic.model_account_analytic_account base.group_sale_salesman 0 0 0 0
10 access_account_analytic_account_manager account_analytic_account contract manager analytic.model_account_analytic_account sale_contract.group_sale_contract_manager 1 1 1 1
11 access_account_analytic_invoice_line_manager account_analytic_invoice_line contract manager model_account_analytic_invoice_line sale_contract.group_sale_contract_manager 1 1 1 1
12 access_account_analytic_close_reason_manager account.analytic.close.reason model_account_analytic_close_reason sale_contract.group_sale_contract_manager 1 1 1 1
13 access_account_analytic_account_view account_analytic_account view analytic.model_account_analytic_account sale_contract.group_sale_contract_view 1 0 0 0
14 access_account_analytic_invoice_line_view account_analytic_invoice_line view model_account_analytic_invoice_line sale_contract.group_sale_contract_view 1 0 0 0
15 access_account_analytic_close_reason_view account.analytic.close.reason model_account_analytic_close_reason sale_contract.group_sale_contract_view 1 0 0 0
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
import common_sale_contract
import test_sale_contract
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from openerp.tests import common
class TestContractCommon(common.TransactionCase):
def setUp(self):
super(TestContractCommon, self).setUp()
Contract = self.env['account.analytic.account']
SaleOrder = self.env['sale.order']
Product = self.env['product.product']
ProductTmpl = self.env['product.template']
# Test products
self.product_tmpl = ProductTmpl.create({
'name': 'TestProduct',
'type': 'service',
'recurring_invoice': True,
})
self.product = Product.create({
'product_tmpl_id': self.product_tmpl.id,
'price': 50.0
})
# Test user
TestUsersEnv = self.env['res.users'].with_context({'no_reset_password': True})
group_portal_id = self.ref('base.group_portal')
self.user_portal = TestUsersEnv.create({
'name': 'Beatrice Portal',
'login': 'Beatrice',
'alias_name': 'beatrice',
'email': 'beatrice.employee@example.com',
'groups_id': [(6, 0, [group_portal_id])]
})
# Test Contract
self.contract_tmpl = Contract.create({
'name': 'TestContractTemplate',
'type': 'template',
'contract_type': 'subscription',
'recurring_invoice_line_ids': [(0, 0, {'product_id': self.product.id, 'name': 'TestRecurringLine', 'price_unit': self.product.price, 'uom_id': self.product_tmpl.uom_id.id})]
})
self.contract = Contract.create({
'name': 'TestContract',
'type': 'contract',
'state': 'open',
'partner_id': self.user_portal.partner_id.id,
})
self.sale_order = SaleOrder.create({
'name': 'TestSO',
'project_id': self.contract.id,
'partner_id': self.user_portal.partner_id.id,
'order_line': [(0, 0, {'product_id': self.product.id, 'name': 'TestLine'})]
})
@@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from openerp.addons.sale_contract.tests.common_sale_contract import TestContractCommon
from openerp.tools import mute_logger
class TestContract(TestContractCommon):
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
def test_template(self):
""" Test behaviour of on_change_template """
Contract = self.env['account.analytic.account']
# on_change_template on existing record (present in the db)
self.contract.write(self.contract.on_change_template(template_id=self.contract_tmpl.id)['value'])
self.assertEqual(self.contract_tmpl.contract_type, self.contract.contract_type, 'sale_contract: contract_type not copied when changing template')
self.assertTrue(len(self.contract.recurring_invoice_line_ids.ids) == 0, 'sale_contract: recurring_invoice_line_ids copied on existing account.analytic.account record')
# on_change_template on cached record (NOT present in the db)
temp = Contract.new({'name': 'CachedContract',
'type': 'contract',
'state': 'open',
'partner_id': self.user_portal.partner_id.id
})
temp.update(temp.on_change_template(template_id=self.contract_tmpl.id)['value'])
self.assertEqual(self.contract_tmpl.contract_type, temp.contract_type, 'sale_contract: contract_type not copied when changing template')
self.assertTrue(temp.recurring_invoice_line_ids.name, 'sale_contract: recurring_invoice_line_ids not copied on new cached account.analytic.account record')
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
def test_sale_order(self):
""" Test sale order line copying for recurring products on confirm"""
self.sale_order.action_button_confirm()
self.assertTrue(len(self.contract.recurring_invoice_line_ids.ids) == 1, 'sale_contract: recurring_invoice_line_ids not created when confirming sale_order with recurring_product')
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
def test_invoice(self):
""" Test invoice generation"""
self.contract.write(self.contract.on_change_template(template_id=self.contract_tmpl.id)['value'])
self.contract.write({'recurring_invoice_line_ids': [(0, 0, {'product_id': self.product.id, 'name': 'TestRecurringLine', 'price_unit': 31415.9, 'uom_id': self.product_tmpl.uom_id.id})]})
invoice_id = self.contract._recurring_create_invoice()
invoice = self.env['account.invoice'].browse(invoice_id)
self.assertEqual(invoice.amount_untaxed, 31415.9, 'sale_contract: recurring invoice generation problem')
def test_renewal(self):
""" Test contract renewal """
res = self.contract.prepare_renewal_order()
renewal_so_id = res['res_id']
renewal_so = self.env['sale.order'].browse(renewal_so_id)
self.assertTrue(renewal_so.update_contract, 'sale_contract: renewal quotation generation is wrong')
self.contract.write({'recurring_invoice_line_ids': [(0, 0, {'product_id': self.product.id, 'name': 'TestRecurringLine', 'price_unit': 50, 'uom_id': self.product.uom_id.id})]})
renewal_so.write({'order_line': [(0, 0, {'product_id': self.product.id, 'name': 'TestRenewalLine'})]})
renewal_so.action_button_confirm()
lines = [line.name for line in self.contract.recurring_invoice_line_ids]
self.assertTrue('TestRecurringLine' not in lines, 'sale_contract: old line still present after renewal quotation confirmation')
self.assertTrue('TestRenewalLine' in lines, 'sale_contract: new line not present after renewal quotation confirmation')
+297 -158
View File
@@ -21,162 +21,190 @@
<field name="name">account.analytic.account.invoice.form.inherit</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
<field eval="40" name="priority"/>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
<xpath expr='//div[@name="duration"]' position="after">
<label for="quantity_max"/>
<div>
<field name="quantity_max"/>
<div attrs="{'invisible': [('hours_quantity','=',0)]}" class="oe_grey">
<field name="hours_quantity" class="oe_inline"/> Units Consumed
</div>
<div attrs="{'invisible': [('quantity_max','=',0)]}" class="oe_grey">
<field name="remaining_hours" class="oe_inline"/> Units Remaining
</div>
</div>
<xpath expr="/form/sheet" position='before'>
<header attrs="{'invisible': [('type','!=','template')]}">
<button name="set_close" string="Set to Inactive" type="object" states="open" />
<button name="set_open" string="Set to Active" type="object" states="close" />
</header>
</xpath>
<field name="partner_id" position="attributes">
<attribute name="attrs">{'required': [('type','=','contract'),'|','|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True), ('recurring_invoices', '=', True)]}</attribute>
</field>
<xpath expr='//group[@name="invoice_on_timesheets"]' position="replace">
<xpath expr='//field[@name="partner_id"]' position='after'>
<field name="pricelist_id"
class="oe_inline"
attrs="{'required': ['|',('invoice_on_timesheets', '=', True),('contract_type','=','subscription')], 'invisible': [('contract_type','=','prepaid')]}"
domain="[('type', '=', 'sale')]"
groups="product.group_sale_pricelist"/>
</xpath>
<xpath expr='//separator[@name="description"]' position='before'>
<separator name="toinvoice" string="Invoicing"/>
<table class="oe_form_analytic_account">
<tr>
<th class="oe_timesheet_grey" width="160px"></th>
<th class="oe_timesheet_grey" width="25px"></th>
<th class="oe_timesheet_grey" width="100px"><label string="Expected"/></th>
<th class="oe_timesheet_grey" width="100px"><label string="Invoiced"/></th>
<th class="oe_timesheet_grey" width="100px"><label string="Remaining"/></th>
<th class="oe_timesheet_grey" width="100px"><label string="To Invoice"/></th>
<th width="30px"></th>
<th></th>
</tr><tr>
<td class="oe_timesheet_grey">
<label for="fix_price_invoices"/>
</td><td class="oe_timesheet_grey">
<field name="fix_price_invoices" class="oe_inline"/>
</td><td>
<field class="oe_inline" name="amount_max" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
</td><td>
<field class="oe_inline" name="ca_invoiced" attrs="{'invisible': [('fix_price_invoices','=',False)]}" widget="monetary"/>
</td><td>
<field class="oe_inline" name="remaining_ca" attrs="{'invisible': [('fix_price_invoices','=',False)]}" widget="monetary"/>
</td><td>
<field class="oe_inline" name="fix_price_to_invoice" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
</td><td attrs="{'invisible': [('fix_price_invoices','=',False)]}" class="oe_timesheet_action">
<span attrs="{'invisible': [('fix_price_to_invoice','=',0.0)]}" class="oe_grey">
<button name="open_sale_order_lines"
class="oe_link"
string="⇒ Invoice" type="object"
context="{'default_partner_id': partner_id,'default_project_id': active_id,'search_default_uninvoiced': 1,'search_default_project_id': active_id,'search_default_partner_id': [partner_id]}"/>
or view
</span>
<span attrs="{'invisible': [('fix_price_to_invoice','&lt;&gt;',0.0 )]}" class="oe_grey">
No order to invoice, create
</span>
<button name="%(action_sales_order)d" string="Sales Orders"
type="action"
class="oe_link"
context="{'default_partner_id': partner_id, 'search_default_project_id': [active_id],'default_project_id': [active_id], 'default_pricelist_id': pricelist_id}"
/>
</td>
</tr><tr attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
<td class="oe_timesheet_grey">
<label string="On Timesheets"/>
</td><td class="oe_timesheet_grey">
</td><td>
<field class="oe_inline" name="hours_qtt_est"/>
</td><td>
<field class="oe_inline" name="timesheet_ca_invoiced"/>
</td><td>
<field class="oe_inline" name="remaining_hours_to_invoice"/>
</td><td>
<field class="oe_inline" name="ca_to_invoice"/>
</td><td class="oe_timesheet_action" attrs="{'invisible': ['|',('invoice_on_timesheets','=',False),('type','=','template')]}">
<span attrs="{'invisible': [('ca_to_invoice','=',0.0)]}" class="oe_grey">
<button name="hr_to_invoice_timesheets"
type="object"
class="oe_link"
string="⇒ Invoice"/>
or view
</span>
<span attrs="{'invisible': [('ca_to_invoice','&lt;&gt;',0.0)]}" class="oe_grey">
Nothing to invoice, create
</span>
<button name="%(hr_timesheet.act_hr_timesheet_line_evry1_all_form)d"
string="Timesheets" type="action"
class="oe_link"
context="{'default_account_id': active_id,'search_default_account_id': active_id}"/>
</td>
</tr><tr name='total'>
<th class="oe_timesheet_grey">
<label string="Total"/>
</th><td class="oe_timesheet_grey">
</td><td class="oe_timesheet_grey">
<field name="est_total" class="oe_inline" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
</td><td class="oe_timesheet_grey">
<field name="invoiced_total" class="oe_inline"/>
</td><td class="oe_timesheet_grey">
<field name="remaining_total" class="oe_inline"/>
</td><td class="oe_timesheet_grey">
<field name="toinvoice_total" class="oe_inline"/>
</td><td>
</td>
</tr>
</table>
<group name='invoice_on_timesheets'>
<p name='invoice_on_timesheets_label' class="oe_grey oe_edit_only" colspan="2" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
When reinvoicing costs, Odoo uses the
pricelist of the contract which uses the price
defined on the product related (e.g timesheet
products are defined on each employee).
</p>
<xpath expr='//div[@name="duration"]' position='after'>
<field name="close_reason_id" attrs="{'invisible': ['|',('type','!=','contract'),('state','in',['open','pending'])]}"/>
<field name="to_invoice"
widget="selection"
attrs="{'required': [('invoice_on_timesheets', '=', True)], 'invisible': [('invoice_on_timesheets','=',False)]}"/>
</xpath>
<notebook position="inside">
<page id="prepaid" string="Prepaid Support Hours" attrs="{'invisible': ['|',('contract_type','!=','prepaid'),('type','in',['view','normal'])]}" >
<group>
<field name="pricelist_id"
class="oe_inline"
attrs="{'required': [('invoice_on_timesheets', '=', True)]}"
domain="[('type', '=', 'sale')]"
groups="product.group_sale_pricelist"/>
<field name="to_invoice"
class="oe_inline"
widget="selection"
attrs="{'required': [('invoice_on_timesheets', '=', True)], 'invisible': [('invoice_on_timesheets','=',False)]}"/>
<label for="quantity_max"/>
<div>
<field name="quantity_max"/>
<div attrs="{'invisible': [('hours_quantity','=',0)]}" class="oe_grey">
<field name="hours_quantity" class="oe_inline"/> Units Consumed
</div>
<div attrs="{'invisible': [('quantity_max','=',0)]}" class="oe_grey">
<field name="remaining_hours" class="oe_inline"/> Units Remaining
</div>
</div>
</group>
</group>
<separator string="Recurring Invoices" attrs="{'invisible': [('recurring_invoices','!=',True)]}"/>
<div>
<div attrs="{'invisible': [('type','!=', 'contract'), ('recurring_invoices', '=', False)]}">
<field name="recurring_invoices" on_change="onchange_recurring_invoices(recurring_invoices, date_start)" class="oe_inline" />
<label for="recurring_invoices" />
</div>
<button class="oe_link" name="recurring_create_invoice" attrs="{'invisible': [('recurring_invoices','!=',True)]}" string="⇒ create invoices" type="object" groups="base.group_no_one"/>
</div>
<group attrs="{'invisible': [('recurring_invoices','!=',True)]}">
<label for="recurring_interval"/>
<group>
<p colspan="2" class="oe_grey oe_edit_only">
Once the end date of the contract is
passed or the maximum number of service
units (e.g. support contract) is
reached, the account manager is notified
by email to renew the contract with the
customer.
</p>
</group>
</page>
<page id="billing_summary" string="Billing Summary" attrs="{'invisible': ['|',('contract_type','!=','regular'),('type','in',['view','normal'])]}">
<separator name="toinvoice" string="Invoicing"/>
<table class="oe_form_analytic_account">
<tr>
<th class="oe_timesheet_grey" width="160px"></th>
<th class="oe_timesheet_grey" width="25px"></th>
<th class="oe_timesheet_grey" width="100px"><label string="Expected"/></th>
<th class="oe_timesheet_grey" width="100px"><label string="Invoiced"/></th>
<th class="oe_timesheet_grey" width="100px"><label string="Remaining"/></th>
<th class="oe_timesheet_grey" width="100px"><label string="To Invoice"/></th>
<th width="30px"></th>
<th></th>
</tr><tr>
<td class="oe_timesheet_grey">
<label for="fix_price_invoices"/>
</td><td class="oe_timesheet_grey">
<field name="fix_price_invoices" class="oe_inline"/>
</td><td>
<field class="oe_inline" name="amount_max" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
</td><td>
<field class="oe_inline" name="ca_invoiced" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
</td><td>
<field class="oe_inline" name="remaining_ca" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
</td><td>
<field class="oe_inline" name="fix_price_to_invoice" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
</td><td attrs="{'invisible': [('fix_price_invoices','=',False)]}" class="oe_timesheet_action">
<span attrs="{'invisible': [('fix_price_to_invoice','=',0.0)]}" class="oe_grey">
<button name="open_sale_order_lines"
class="oe_link"
string="⇒ Invoice" type="object"
context="{'default_partner_id': [partner_id],'default_project_id': active_id,'search_default_uninvoiced': 1,'search_default_project_id': active_id,'search_default_partner_id': [partner_id]}"/>
or view
</span>
<span attrs="{'invisible': [('fix_price_to_invoice','&lt;&gt;',0.0 )]}" class="oe_grey">
No order to invoice, create
</span>
<button name="%(action_sales_order)d" string="Sales Orders"
type="action"/>
</td>
</tr><tr>
<td class="oe_timesheet_grey">
<label string="On Timesheets"/>
</td><td class="oe_timesheet_grey">
</td><td>
<field class="oe_inline" name="hours_qtt_est" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
</td><td>
<field class="oe_inline" name="timesheet_ca_invoiced" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
</td><td>
<field class="oe_inline" name="remaining_hours_to_invoice" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
</td><td>
<field class="oe_inline" name="ca_to_invoice" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
</td><td class="oe_timesheet_action" attrs="{'invisible': ['|',('invoice_on_timesheets','=',False),('type','=','template')]}">
<span attrs="{'invisible': [('ca_to_invoice','=',0.0)]}" class="oe_grey">
<button name="hr_to_invoice_timesheets"
type="object"
class="oe_link"
string="⇒ Invoice"/>
or view
</span>
<span attrs="{'invisible': [('ca_to_invoice','&lt;&gt;',0.0)]}" class="oe_grey">
Nothing to invoice, create
</span>
<button name="%(hr_timesheet.act_hr_timesheet_line_evry1_all_form)d"
string="Timesheets" type="action"
class="oe_link"
context="{'default_account_id': active_id,'search_default_account_id': active_id}"/>
</td>
</tr><tr name='total'>
<th class="oe_timesheet_grey">
<label string="Total"/>
</th><td class="oe_timesheet_grey">
</td><td class="oe_timesheet_grey">
<field name="est_total" class="oe_inline" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
</td><td class="oe_timesheet_grey">
<field name="invoiced_total" class="oe_inline"/>
</td><td class="oe_timesheet_grey">
<field name="remaining_total" class="oe_inline"/>
</td><td class="oe_timesheet_grey">
<field name="toinvoice_total" class="oe_inline"/>
</td><td>
</td>
</tr>
</table>
<group name='invoice_on_timesheets'>
<p name='invoice_on_timesheets_label' class="oe_grey oe_edit_only" colspan="2" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
When reinvoicing costs, Odoo uses the
pricelist of the contract which uses the price
defined on the product related (e.g timesheet
products are defined on each employee).
</p>
</group>
</page>
<page id="recurring_lines" string="Subscription" attrs="{'invisible': ['|',('contract_type','!=','subscription'),('type','in',['view','normal'])]}">
<div>
<field name="recurring_interval" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
<field name="recurring_rule_type" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
<button string="=> Renewal Quotation" class="oe_link oe_right"
help="Create a sale order that will overwrite this contract when confirmed (renewal quotation)"
name="prepare_renewal_order" type="object" attrs="{'invisible': [('type','!=','contract')]}" />
<field name="recurring_invoice_line_ids">
<tree string="Account Analytic Lines" editable="bottom">
<field name="product_id" on_change="product_id_change(product_id, uom_id, quantity, False, parent.partner_id, False, parent.pricelist_id, parent.company_id)"/>
<field name="name"/>
<field name="quantity" readonly="1"/>
<field name="actual_quantity"/>
<field name="sold_quantity"/>
<field name="uom_id" on_change="product_uom_change(product_id,uom_id,quantity,name,parent.partner_id, parent.pricelist_id,context)"/>
<field name="price_unit"/>
<field name="discount" groups="sale.group_discount_per_so_line"/>
<field name="price_subtotal"/>
</tree>
</field>
<group class="oe_subtotal_footer oe_right">
<field name="recurring_total" class="oe_subtotal_footer_separator"
widget="monetary" options="{'currency_field': 'pricelist_currency_id'}"
modifiers="{'readonly': true}"
/>
</group>
</div>
<field name="recurring_next_date"/>
</group>
<label for="recurring_invoice_line_ids" attrs="{'invisible': [('recurring_invoices','=',False)]}"/>
<div attrs="{'invisible': [('recurring_invoices','=',False)]}">
<field name="recurring_invoice_line_ids">
<tree string="Account Analytic Lines" editable="bottom">
<field name="product_id" on_change="product_id_change(product_id, uom_id, quantity, False, parent.partner_id, False, parent.pricelist_id, parent.company_id)"/>
<field name="name"/>
<field name="quantity"/>
<field name="uom_id"/>
<field name="price_unit"/>
<field name="price_subtotal" widget="monetary"/>
</tree>
</field>
</page>
</notebook>
<field name="partner_id" position="attributes">
<attribute name="attrs">{'invisible': [('type','in',['view','normal','template'])], 'required': [('type','=','contract'),'|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True)]}</attribute>
</field>
<xpath expr='//field[@name="contract_type"]' position='after'>
<label for="recurring_interval" string="Contract Duration" attrs="{'invisible': [('type','!=','template')]}"/>
<div attrs="{'invisible': [('type','!=','template')]}">
<field name="recurring_interval" class="oe_inline"/>
<field name="recurring_rule_type" class="oe_inline"/>
</div>
</xpath>
<label for="recurring_next_date" string="Date of Next Invoice" attrs="{'invisible': ['|',('type','=','template'),('contract_type','!=','subscription')]}"/>
<div attrs="{'invisible': ['|',('type','=','template'),('contract_type','!=','subscription')]}">
<field name="recurring_next_date" attrs="{'required': [('contract_type','=','subscription')]}"/>
<button string="=> Generate Invoice" class="oe_link" name="recurring_invoice"
type="object" attrs="{'invisible': [('type','!=','contract')]}" />
</div>
</xpath>
</field>
</record>
@@ -202,13 +230,29 @@
<field name="arch" type="xml">
<field name="date_start" position="before">
<field name="last_invoice_date"/>
<field name="toinvoice_total"/>
<field name="remaining_hours"/>
<field name="contract_type"/>
<field name="pricelist_id" invisible="1"/>
</field>
</field>
</record>
<!-- Analytic Account list view for contract templates -->
<record id="view_account_analytic_account_template_list" model="ir.ui.view">
<field name="name">account.analytic.account.list</field>
<field name="model">account.analytic.account</field>
<field eval="10" name="priority"/>
<field name="arch" type="xml">
<tree toolbar="1" colors="grey:state=='close';" string="Analytic Accounts">
<field name="complete_name"/>
<field name="manager_id"/>
<field name="contract_type"/>
<field name="parent_id"/>
<field name="state" invisible="1"/>
<field name="company_id" groups="base.group_multi_company"/>
</tree>
</field>
</record>
<!-- Analytic Account search view for contract -->
<record id="view_account_analytic_account_overdue_search" model="ir.ui.view">
<field name="name">account.analytic.account.search</field>
@@ -251,6 +295,18 @@
</field>
</record>
<!-- From view for Close Reasons -->
<record id="account_analytic_close_reason_view_tree" model="ir.ui.view">
<field name="name">account.analytic.close.reason.list</field>
<field name="model">account.analytic.close.reason</field>
<field name="arch" type="xml">
<tree string="Close Reasons" editable="bottom">
<field name="sequence" widget="handle"/>
<field name="name"/>
</tree>
</field>
</record>
<!-- Action Sales/Sales/Contracts -->
<record id="action_account_analytic_overdue_all" model="ir.actions.act_window">
<field name="name">Contracts</field>
@@ -303,6 +359,10 @@
<field name="res_model">account.invoice</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_ids"
eval="[(5, 0, 0),
(0, 0, {'view_mode': 'tree', 'view_id': ref('account.invoice_tree')}),
(0, 0, {'view_mode': 'form', 'view_id': ref('account.invoice_form')})]"/>
<field name="domain">[('invoice_line_ids.account_analytic_id', '=', active_id)]</field>
<field name="context">{'create':False}</field>
<field name="help" type="html">
@@ -339,15 +399,35 @@
</record>
<menuitem action="action_account_analytic_overdue" id="menu_action_account_analytic_overdue" sequence="50" parent="base.menu_invoiced"/>
<!-- Search view for contract templates -->
<!-- Analytic Account search view for contract -->
<record id="account_analytic_account_template_view_search" model="ir.ui.view">
<field name="name">account.analytic.account.search</field>
<field name="model">account.analytic.account</field>
<field name="arch" type="xml">
<search string="Contract Templates">
<field name="name" filter_domain="['|', ('name','ilike',self),('code','ilike',self)]" string="Template"/>
<filter name="open" string="Active" domain="[('state','in',('open','draft'))]" help="Contracts in progress (open, draft)"/>
<filter name="closed" string="Inactive" domain="[('state','=','close')]" help="Closed contracts"/>
<group expand="0" string="Group By">
<filter string="Parent" domain="[]" context="{'group_by':'parent_id'}"/>
<filter string="Company" domain="[]" context="{'group_by':'company_id'}"/>
</group>
</search>
</field>
</record>
<!-- Action Sales/Configuration/Contract template -->
<record id="template_of_contract_action" model="ir.actions.act_window">
<field name="name">Contract Template</field>
<record id="template_of_regular_contract_action" model="ir.actions.act_window">
<field name="name">Regular Contract Template</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('type','=','template')]</field>
<field name="context">{'search_default_type':'template','default_type' : 'template'}</field>
<field name="view_id" ref="view_account_analytic_account_template_list"/>
<field name="domain">[('type','=','template'),('contract_type','=','regular')]</field>
<field name="context">{'search_default_type':'template','default_type' : 'template', 'default_contract_type': 'regular', 'search_default_open':1}</field>
<field name="search_view_id" ref="account_analytic_account_template_view_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a template of contract.
@@ -359,6 +439,56 @@
</field>
</record>
<record id="template_of_prepaid_contract_action" model="ir.actions.act_window">
<field name="name">Prepaid Contract Template</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_account_analytic_account_template_list"/>
<field name="domain">[('type','=','template'),('contract_type','=','prepaid')]</field>
<field name="context">{'search_default_type':'template','default_type' : 'template', 'default_contract_type': 'prepaid', 'search_default_open':1}</field>
<field name="search_view_id" ref="account_analytic_account_template_view_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a template of contract.
</p><p>
Templates are used to prefigure contract/project that
can be selected by the salespeople to quickly configure the
terms and conditions of the contract.
</p>
</field>
</record>
<record id="template_of_subscription_contract_action" model="ir.actions.act_window">
<field name="name">Subscription Contract Template</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.analytic.account</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_account_analytic_account_template_list"/>
<field name="domain">[('type','=','template'),('contract_type','=','subscription')]</field>
<field name="context">{'search_default_type':'template','default_type' : 'template', 'default_contract_type': 'subscription', 'search_default_open':1}</field>
<field name="search_view_id" ref="account_analytic_account_template_view_search"/>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
Click here to create a template of contract.
</p><p>
Templates are used to prefigure contract/project that
can be selected by the salespeople to quickly configure the
terms and conditions of the contract.
</p>
</field>
</record>
<record id="analytic_account_lost_reason_action" model="ir.actions.act_window">
<field name="name">Close Reasons</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">account.analytic.close.reason</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
</record>
<menuitem
id= "base.menu_sales_config"
parent= "base.menu_sale_config"
@@ -366,9 +496,12 @@
name= "Sales"
/>
<menuitem action="template_of_contract_action" id="menu_template_of_contract_action" parent="base.menu_sales_config" sequence="3"/>
<menuitem action="template_of_regular_contract_action" id="menu_template_of_regular_contract_action" parent="base.menu_sales_config" sequence="3"/>
<menuitem action="template_of_prepaid_contract_action" id="menu_template_of_prepaid_contract_action" parent="base.menu_sales_config" sequence="4"/>
<menuitem action="template_of_subscription_contract_action" id="menu_template_of_subscription_contract_action" parent="base.menu_sales_config" sequence="5"/>
<menuitem action="analytic_account_lost_reason_action" id="menu_analytic_account_lost_reason_action" parent="base.menu_sales_config" sequence="10"/>
<record id="sale_contract_form_form" model="ir.ui.view">
<record id="account_analytic_analysis_form_form" model="ir.ui.view">
<field name="name">account.analytic.account.invoice.form.inherit</field>
<field name="model">account.analytic.account</field>
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
@@ -379,13 +512,19 @@
string="To invoice"
icon="fa-money"
name="%(action_hr_tree_invoiced_all)d"
type="action"
type="action" attrs="{'invisible': [('type','=','template')]}"
context="{'search_default_account_id': [active_id], 'default_account_id': active_id}"/>
<button class="oe_stat_button" string="Invoices"
icon="fa-book"
name="%(action_contract_invoices_all)d"
type="action"/>
type="action" attrs="{'invisible': [('type','=','template')]}"/>
<button class="oe_inline oe_stat_button" string="Contracts"
icon="fa-book"
name="%(action_account_analytic_overdue_all)d"
context="{'search_default_template_id': [active_id], 'default_template_id': active_id}"
type="action" attrs="{'invisible': [('type','!=','template')]}"/>
</div>
</field>
</record>