summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorDamien Bouvy <dbo@odoo.com>2015-03-03 14:25:44 +0100
committerDamien Bouvy <dbo@odoo.com>2015-07-23 11:40:30 +0200
commitecdb8a60cb8645d400a06bcf80f49484bbe671b7 (patch)
tree0d30644fc280f043d9ea7b1e78222dce0fb30427 /addons
parentd8f0231afc5ce94e2feb91b11fbfb45c52ae849e (diff)
[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
Diffstat (limited to 'addons')
-rw-r--r--addons/account/account_analytic_line.py153
-rw-r--r--addons/analytic/models/analytic.py21
-rw-r--r--addons/analytic/views/analytic_view.xml41
-rw-r--r--addons/analytic_user_function/analytic_user_function_view.xml2
-rw-r--r--addons/hr_timesheet_invoice/hr_timesheet_invoice.py3
-rw-r--r--addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml6
-rw-r--r--addons/project_timesheet/project_timesheet_view.xml3
-rw-r--r--addons/sale_contract/__init__.py2
-rw-r--r--addons/sale_contract/__openerp__.py7
-rw-r--r--addons/sale_contract/data/sale_contract_cron.xml2
-rw-r--r--addons/sale_contract/data/sale_contract_data.xml229
-rw-r--r--addons/sale_contract/demo/sale_contract_demo.xml3
-rw-r--r--addons/sale_contract/models/__init__.py1
-rw-r--r--addons/sale_contract/models/sale.py78
-rw-r--r--addons/sale_contract/models/sale_contract.py456
-rw-r--r--addons/sale_contract/report/__init__.py3
-rw-r--r--addons/sale_contract/report/sale_contract_report.py94
-rw-r--r--addons/sale_contract/report/sale_contract_report_view.xml34
-rw-r--r--addons/sale_contract/security/account_analytic_analysis_security.xml42
-rw-r--r--addons/sale_contract/security/ir.model.access.csv10
-rw-r--r--addons/sale_contract/tests/__init__.py3
-rw-r--r--addons/sale_contract/tests/common_sale_contract.py54
-rw-r--r--addons/sale_contract/tests/test_sale_contract.py54
-rw-r--r--addons/sale_contract/views/sale_contract_view.xml453
24 files changed, 1377 insertions, 377 deletions
diff --git a/addons/account/account_analytic_line.py b/addons/account/account_analytic_line.py
new file mode 100644
index 00000000000..2f7d4b1a2d9
--- /dev/null
+++ b/addons/account/account_analytic_line.py
@@ -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
+
+
diff --git a/addons/analytic/models/analytic.py b/addons/analytic/models/analytic.py
index 411476b1823..346d1d00877 100644
--- a/addons/analytic/models/analytic.py
+++ b/addons/analytic/models/analytic.py
@@ -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),
+ }
diff --git a/addons/analytic/views/analytic_view.xml b/addons/analytic/views/analytic_view.xml
index 22620c0d7c3..a848b3d0a24 100644
--- a/addons/analytic/views/analytic_view.xml
+++ b/addons/analytic/views/analytic_view.xml
@@ -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"/>
diff --git a/addons/analytic_user_function/analytic_user_function_view.xml b/addons/analytic_user_function/analytic_user_function_view.xml
index ff434cdd437..00743547e80 100644
--- a/addons/analytic_user_function/analytic_user_function_view.xml
+++ b/addons/analytic_user_function/analytic_user_function_view.xml
@@ -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)
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
index ef33bb2938f..51aa1e75408 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py
@@ -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):
diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
index 77b3b25e14f..8ad54199fcc 100644
--- a/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
+++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice_view.xml
@@ -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" />
diff --git a/addons/project_timesheet/project_timesheet_view.xml b/addons/project_timesheet/project_timesheet_view.xml
index 1ce6617152b..b4e0ff6153a 100644
--- a/addons/project_timesheet/project_timesheet_view.xml
+++ b/addons/project_timesheet/project_timesheet_view.xml
@@ -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>
diff --git a/addons/sale_contract/__init__.py b/addons/sale_contract/__init__.py
index 6e1a92f403e..b6a299a53ad 100644
--- a/addons/sale_contract/__init__.py
+++ b/addons/sale_contract/__init__.py
@@ -3,4 +3,4 @@
import models
import report
-import test
+import tests
diff --git a/addons/sale_contract/__openerp__.py b/addons/sale_contract/__openerp__.py
index e52743e660d..888e1f56ca0 100644
--- a/addons/sale_contract/__openerp__.py
+++ b/addons/sale_contract/__openerp__.py
@@ -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,
}
diff --git a/addons/sale_contract/data/sale_contract_cron.xml b/addons/sale_contract/data/sale_contract_cron.xml
index 376a2029e5e..a98269eeaf5 100644
--- a/addons/sale_contract/data/sale_contract_cron.xml
+++ b/addons/sale_contract/data/sale_contract_cron.xml
@@ -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>
diff --git a/addons/sale_contract/data/sale_contract_data.xml b/addons/sale_contract/data/sale_contract_data.xml
new file mode 100644
index 00000000000..5afeaf10fe3
--- /dev/null
+++ b/addons/sale_contract/data/sale_contract_data.xml
@@ -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> \ No newline at end of file
diff --git a/addons/sale_contract/demo/sale_contract_demo.xml b/addons/sale_contract/demo/sale_contract_demo.xml
index 1f9ce31fe61..63d576bd078 100644
--- a/addons/sale_contract/demo/sale_contract_demo.xml
+++ b/addons/sale_contract/demo/sale_contract_demo.xml
@@ -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>
diff --git a/addons/sale_contract/models/__init__.py b/addons/sale_contract/models/__init__.py
index 616ffabaed9..427c45f78f9 100644
--- a/addons/sale_contract/models/__init__.py
+++ b/addons/sale_contract/models/__init__.py
@@ -1,4 +1,3 @@
import sale_contract
import sale
import res_config
-
diff --git a/addons/sale_contract/models/sale.py b/addons/sale_contract/models/sale.py
index 0174749b875..a48d3ba7f02 100644
--- a/addons/sale_contract/models/sale.py
+++ b/addons/sale_contract/models/sale.py
@@ -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() \ No newline at end of file
diff --git a/addons/sale_contract/models/sale_contract.py b/addons/sale_contract/models/sale_contract.py
index cd0a6a77e32..6fa1ddf9114 100644
--- a/addons/sale_contract/models/sale_contract.py
+++ b/addons/sale_contract/models/sale_contract.py
@@ -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
+ }
diff --git a/addons/sale_contract/report/__init__.py b/addons/sale_contract/report/__init__.py
index 6f892af3ae4..a37c52fe09a 100644
--- a/addons/sale_contract/report/__init__.py
+++ b/addons/sale_contract/report/__init__.py
@@ -1,2 +1,3 @@
# -*- coding: utf-8 -*-
-import account_analytic_analysis_summary \ No newline at end of file
+import account_analytic_analysis_summary
+import sale_contract_report
diff --git a/addons/sale_contract/report/sale_contract_report.py b/addons/sale_contract/report/sale_contract_report.py
new file mode 100644
index 00000000000..2adf0257055
--- /dev/null
+++ b/addons/sale_contract/report/sale_contract_report.py
@@ -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())) \ No newline at end of file
diff --git a/addons/sale_contract/report/sale_contract_report_view.xml b/addons/sale_contract/report/sale_contract_report_view.xml
new file mode 100644
index 00000000000..81954f01862
--- /dev/null
+++ b/addons/sale_contract/report/sale_contract_report_view.xml
@@ -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> \ No newline at end of file
diff --git a/addons/sale_contract/security/account_analytic_analysis_security.xml b/addons/sale_contract/security/account_analytic_analysis_security.xml
index 1dc3c3d4973..5c7946f4a2d 100644
--- a/addons/sale_contract/security/account_analytic_analysis_security.xml
+++ b/addons/sale_contract/security/account_analytic_analysis_security.xml
@@ -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>
diff --git a/addons/sale_contract/security/ir.model.access.csv b/addons/sale_contract/security/ir.model.access.csv
index 8147affa379..a6337202b75 100644
--- a/addons/sale_contract/security/ir.model.access.csv
+++ b/addons/sale_contract/security/ir.model.access.csv
@@ -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
diff --git a/addons/sale_contract/tests/__init__.py b/addons/sale_contract/tests/__init__.py
new file mode 100644
index 00000000000..d985fecb374
--- /dev/null
+++ b/addons/sale_contract/tests/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+import common_sale_contract
+import test_sale_contract
diff --git a/addons/sale_contract/tests/common_sale_contract.py b/addons/sale_contract/tests/common_sale_contract.py
new file mode 100644
index 00000000000..bcc9e801bf1
--- /dev/null
+++ b/addons/sale_contract/tests/common_sale_contract.py
@@ -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'})]
+ })
diff --git a/addons/sale_contract/tests/test_sale_contract.py b/addons/sale_contract/tests/test_sale_contract.py
new file mode 100644
index 00000000000..04cef7cc083
--- /dev/null
+++ b/addons/sale_contract/tests/test_sale_contract.py
@@ -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')
diff --git a/addons/sale_contract/views/sale_contract_view.xml b/addons/sale_contract/views/sale_contract_view.xml
index cfd002256e7..d55d7a00769 100644
--- a/addons/sale_contract/views/sale_contract_view.xml
+++ b/addons/sale_contract/views/sale_contract_view.xml
@@ -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>
+ <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>
+ <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>
+ <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"
- 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>
+ <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>
- <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)]}"/>
+ <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>
- </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"/>
+ </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>
+
+ <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>
+ </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,56 @@
</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="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.
+ </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_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="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','=','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.
@@ -359,6 +460,35 @@
</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>