[IMP] sale_contract: recurring view refactoring, new fields and unit of measure
- add contract_type field on account.analytic.account and real_quantity on account.analytic.invoice.line - refactor contract form and list views for better clarity (hide unnecessary fields depending on the type of contract) - divide account_analityc_account class according to contract type for future work - divide contract template menu in 3 menus depending on contract_type - add on_change_uom behaviour on account recurring lines - rewrite tests in python - add new report (pivot & graph) - end date of contract is now computed from the template recurring rules - small fixes & usability improvements
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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', '<>', 'view'), ('company_id', '<>', 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', '<>', 'view'), ('company_id', '<>', False)]}"/>
|
||||
<field name="company_id" on_change="on_change_company(company_id)" options="{'no_create': True}" groups="base.group_multi_company" attrs="{'required': [('type','<>','view')]}"/>
|
||||
<field name="date_start" attrs="{'invisible': [('type','in',['view','normal','template'])]}"/>
|
||||
<label for="date" string="End Date" attrs="{'invisible': [('type','in',['view','normal','template'])]}"/>
|
||||
<div name="duration" attrs="{'invisible': [('type','=','template')]}">
|
||||
<field name="date" class="oe_inline"/>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Contract Information" name="contract_page" attrs="{'invisible':[('type','not in',['contract', 'template'])]}">
|
||||
<group string="Renewal" name="contract">
|
||||
<p colspan="2" class="oe_grey oe_edit_only">
|
||||
Once the end date of the contract is
|
||||
passed or the maximum number of service
|
||||
units (e.g. support contract) is
|
||||
reached, the account manager is notified
|
||||
by email to renew the contract with the
|
||||
customer.
|
||||
</p>
|
||||
<field name="date_start"/>
|
||||
<label for="date" string="End Date"/>
|
||||
<div name="duration">
|
||||
<field name="date" class="oe_inline"/>
|
||||
</div>
|
||||
</group>
|
||||
<separator string="Terms and Conditions" name="description"/>
|
||||
<field name="description"/>
|
||||
</page>
|
||||
</notebook>
|
||||
<separator string="Terms and Conditions" name="description" attrs="{'invisible': [('type','in',['view','normal'])]}"/>
|
||||
<field name="description" attrs="{'invisible': [('type','in',['view','normal'])]}"/>
|
||||
</sheet>
|
||||
<div class="oe_chatter">
|
||||
<field name="message_follower_ids" widget="mail_followers"/>
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<separator name="description" position="before">
|
||||
<div name="user_function_price" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
|
||||
<div name="user_function_price" attrs="{'invisible': ['|',('invoice_on_timesheets','=',False),('contract_type','!=','regular')]}">
|
||||
<separator string="Invoice Price Rate per User"/>
|
||||
<p class="oe_grey oe_edit_only">
|
||||
Define a specific service (e.g. Senior Consultant)
|
||||
|
||||
@@ -33,7 +33,8 @@ class account_analytic_account(osv.osv):
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'pricelist_id': lambda self, cr, uid, c: self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'product.list0')
|
||||
'pricelist_id': lambda self, cr, uid, c: self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'product.list0'),
|
||||
'to_invoice': lambda self, cr, uid, c: self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'hr_timesheet_invoice.timesheet_invoice_factor1')
|
||||
}
|
||||
|
||||
def on_change_partner_id(self, cr, uid, ids, partner_id, name, context=None):
|
||||
|
||||
@@ -7,12 +7,6 @@
|
||||
<field name="priority">30</field>
|
||||
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//separator[@name="description"]' position='before'>
|
||||
<group string="Invoice on Timesheets Options" name="invoice_on_timesheets" col="4">
|
||||
<field name="pricelist_id" groups="product.group_sale_pricelist"/>
|
||||
<field name="to_invoice"/>
|
||||
</group>
|
||||
</xpath>
|
||||
<xpath expr="/form/sheet" position='before'>
|
||||
<header attrs="{'invisible': [('type','=','template')]}">
|
||||
<button name="set_pending" string="To Renew" type="object" states="open" />
|
||||
|
||||
@@ -137,9 +137,6 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_invoicing_contracts" parent="menu_project_billing" sequence="4"
|
||||
action="sale_contract.action_account_analytic_overdue"/>
|
||||
|
||||
<record model="ir.ui.view" id="view_task_form2_inherited">
|
||||
<field name="name">project.task.form.inherited</field>
|
||||
<field name="model">project.task</field>
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
|
||||
import models
|
||||
import report
|
||||
import test
|
||||
import tests
|
||||
|
||||
@@ -14,18 +14,19 @@ Adds menu to show relevant information to each manager.You can also view the rep
|
||||
""",
|
||||
'author': 'Camptocamp / Odoo',
|
||||
'website': 'https://www.odoo.com/page/billing',
|
||||
'depends': ['hr_timesheet_invoice', 'sale'], #although sale is technically not required to install this module, all menuitems are located under 'Sales' application
|
||||
'depends': ['hr_timesheet_invoice', 'sale', 'payment'], #although sale is technically not required to install this module, all menuitems are located under 'Sales' application
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/account_analytic_analysis_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'views/sale_contract_view.xml',
|
||||
'data/sale_contract_cron.xml',
|
||||
'data/sale_contract_data.xml',
|
||||
'views/res_config_view.xml',
|
||||
'views/account_analytic_analysis.xml',
|
||||
'views/product_template_view.xml',
|
||||
'report/sale_contract_report_view.xml',
|
||||
],
|
||||
'demo': ['demo/sale_contract_demo.xml'],
|
||||
'test': ['test/account_analytic_analysis.yml'],
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ Odoo Automatic Email
|
||||
</record>
|
||||
|
||||
<record model="ir.cron" id="account_analytic_cron_for_invoice">
|
||||
<field name="name">Generate Recurring Invoices from Contracts</field>
|
||||
<field name="name">Generate Recurring Invoices and Payments for Subscription Contracts</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
<odoo>
|
||||
<!-- Recurrent payment emails -->
|
||||
<record id="email_payment_close" model="mail.template">
|
||||
<field name="name">email_payment_close</field>
|
||||
<field name="email_from">${(object.manager_id.email or object.company_id.email or 'noreply@localhost')|safe}</field>
|
||||
<field name="subject">Termination of subscription ${object.code}</field>
|
||||
<field name="email_to">${ctx['email_to'] or ''}</field>
|
||||
<field name="model_id" ref="analytic.model_account_analytic_account"/>
|
||||
<field name="body_html">
|
||||
<![CDATA[
|
||||
<div style="font-family: 'Lucida Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
|
||||
|
||||
<p>Hello ${object.partner_id.name},</p>
|
||||
|
||||
<p>
|
||||
% if ctx.get('payment_method') and ctx.get('total_amount'):
|
||||
Our final attempt to process a payment for your subscription using your payment method
|
||||
${ctx['payment_method']}
|
||||
for ${ctx['total_amount']} ${ctx.get('currency')} failed.
|
||||
% if ctx.get('error'):
|
||||
Your bank or credit institution gave the following details about the issue: <pre>${ctx['error']}</pre>.
|
||||
%endif
|
||||
% else:
|
||||
Our final attempt to process a payment for your subscription failed because we have no payment method recorded for you.
|
||||
% endif
|
||||
</p>
|
||||
|
||||
<p>
|
||||
As your payment should have been made <strong>15 days ago</strong>, your subscription has been terminated.
|
||||
</p>
|
||||
|
||||
<br/>
|
||||
<p>Should you wish to resolve this issue, do not hesitate to contact us.</p>
|
||||
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
|
||||
<br/>
|
||||
<br/>
|
||||
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
|
||||
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #DDD;">
|
||||
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
|
||||
</div>
|
||||
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
|
||||
<span style="color: #222; margin-bottom: 5px; display: block; ">
|
||||
% if object.company_id.street:
|
||||
${object.company_id.street}<br/>
|
||||
% endif
|
||||
% if object.company_id.street2:
|
||||
${object.company_id.street2}<br/>
|
||||
% endif
|
||||
% if object.company_id.city or object.company_id.zip:
|
||||
${object.company_id.zip} ${object.company_id.city}<br/>
|
||||
% endif
|
||||
% if object.company_id.country_id:
|
||||
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
|
||||
% endif
|
||||
</span>
|
||||
% if object.company_id.phone:
|
||||
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
|
||||
Phone: ${object.company_id.phone}
|
||||
</div>
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
<div>
|
||||
Web : <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: ${object.company_id.phone}
|
||||
</div>
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
<div>
|
||||
Web : <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: ${object.company_id.phone}
|
||||
</div>
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
<div>
|
||||
Web : <a href="${object.company_id.website}">${object.company_id.website}</a>
|
||||
</div>
|
||||
% endif
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
]]>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Standard Closing Reason -->
|
||||
<record id="close_reason_1" model="account.analytic.close.reason">
|
||||
<field name="name">Contract is too expensive</field>
|
||||
</record>
|
||||
<record id="close_reason_2" model="account.analytic.close.reason">
|
||||
<field name="name">Contract does not meet my requirements</field>
|
||||
</record>
|
||||
<record id="close_reason_3" model="account.analytic.close.reason">
|
||||
<field name="name">I don't use it</field>
|
||||
</record>
|
||||
<record id="close_reason_4" model="account.analytic.close.reason">
|
||||
<field name="name">Other</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -9,6 +9,7 @@
|
||||
<field name="to_invoice" ref="hr_timesheet_invoice.timesheet_invoice_factor1"/>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="amount_max">100000</field>
|
||||
<field name="contract_type">regular</field>
|
||||
</record>
|
||||
|
||||
<record id="sale.sale_order_4" model="sale.order">
|
||||
@@ -23,6 +24,7 @@
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="amount_max">50000</field>
|
||||
<field name="to_invoice" ref="hr_timesheet_invoice.timesheet_invoice_factor1"/>
|
||||
<field name="contract_type">regular</field>
|
||||
</record>
|
||||
|
||||
<record id="sale.sale_order_6" model="sale.order">
|
||||
@@ -36,6 +38,7 @@
|
||||
<field name="hours_qtt_est">100</field>
|
||||
<field name="pricelist_id" ref="product.list0"/>
|
||||
<field name="to_invoice" ref="hr_timesheet_invoice.timesheet_invoice_factor1"/>
|
||||
<field name="contract_type">regular</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import sale_contract
|
||||
import sale
|
||||
import res_config
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import account_analytic_analysis_summary
|
||||
import account_analytic_analysis_summary
|
||||
import sale_contract_report
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from openerp import tools
|
||||
from openerp import fields, models
|
||||
|
||||
|
||||
class sale_contract_report(models.Model):
|
||||
_name = "sale.contract.report"
|
||||
_description = "Subscription Statistics"
|
||||
_auto = False
|
||||
|
||||
date_start = fields.Date('Date Start', readonly=True)
|
||||
date_end = fields.Date('Date End', readonly=True)
|
||||
product_id = fields.Many2one('product.product', 'Product', readonly=True)
|
||||
product_uom = fields.Many2one('product.uom', 'Unit of Measure', readonly=True)
|
||||
recurring_price = fields.Float('Recurring price(per period)', readonly=True)
|
||||
partner_id = fields.Many2one('res.partner', 'Customer', readonly=True)
|
||||
manager_id = fields.Many2one('res.users', 'Sales Rep', readonly=True)
|
||||
company_id = fields.Many2one('res.company', 'Company', readonly=True)
|
||||
state = fields.Selection([('template', 'Template'),
|
||||
('draft', 'New'),
|
||||
('open', 'In Progress'),
|
||||
('pending', 'To Renew'),
|
||||
('close', 'Closed'),
|
||||
('cancelled', 'Cancelled')], readonly=True)
|
||||
categ_id = fields.Many2one('product.category', 'Product Category', readonly=True)
|
||||
pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', readonly=True)
|
||||
parent_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True)
|
||||
template_id = fields.Many2one('account.analytic.account', 'Subscription Template', readonly=True)
|
||||
product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True)
|
||||
country_id = fields.Many2one('res.country', 'Country', readonly=True)
|
||||
commercial_partner_id = fields.Many2one('res.partner', 'Comercial Partner', readonly=True)
|
||||
|
||||
def _select(self):
|
||||
select_str = """
|
||||
SELECT min(l.id) as id,
|
||||
l.product_id as product_id,
|
||||
l.uom_id as product_uom,
|
||||
l.analytic_account_id,
|
||||
(l.price_unit * l.quantity) as recurring_price,
|
||||
a.date_start as date_start,
|
||||
a.date as date_end,
|
||||
a.partner_id as partner_id,
|
||||
a.manager_id as manager_id,
|
||||
a.company_id as company_id,
|
||||
a.state,
|
||||
a.template_id as template_id,
|
||||
t.categ_id as categ_id,
|
||||
a.pricelist_id as pricelist_id,
|
||||
a.parent_id as parent_id,
|
||||
p.product_tmpl_id,
|
||||
partner.country_id as country_id,
|
||||
partner.commercial_partner_id as commercial_partner_id
|
||||
"""
|
||||
return select_str
|
||||
|
||||
def _from(self):
|
||||
from_str = """
|
||||
account_analytic_invoice_line l
|
||||
join account_analytic_account a on (l.analytic_account_id=a.id) and (a.contract_type='subscription') and (a.type!='template')
|
||||
join res_partner partner on a.partner_id = partner.id
|
||||
left join product_product p on (l.product_id=p.id)
|
||||
left join product_template t on (p.product_tmpl_id=t.id)
|
||||
left join product_uom u on (u.id=l.uom_id)
|
||||
"""
|
||||
return from_str
|
||||
|
||||
def _group_by(self):
|
||||
group_by_str = """
|
||||
GROUP BY l.product_id,
|
||||
l.uom_id,
|
||||
t.categ_id,
|
||||
l.analytic_account_id,
|
||||
a.date_start,
|
||||
a.date,
|
||||
a.partner_id,
|
||||
a.manager_id,
|
||||
recurring_price,
|
||||
a.company_id,
|
||||
a.state,
|
||||
a.template_id,
|
||||
a.pricelist_id,
|
||||
a.parent_id,
|
||||
p.product_tmpl_id,
|
||||
partner.country_id,
|
||||
partner.commercial_partner_id
|
||||
"""
|
||||
return group_by_str
|
||||
|
||||
def init(self, cr):
|
||||
tools.drop_view_if_exists(cr, self._table)
|
||||
cr.execute("""CREATE or REPLACE VIEW %s as (
|
||||
%s
|
||||
FROM ( %s )
|
||||
%s
|
||||
)""" % (self._table, self._select(), self._from(), self._group_by()))
|
||||
@@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_subcription_report_pivot" model="ir.ui.view">
|
||||
<field name="name">subscription.report.pivot</field>
|
||||
<field name="model">sale.contract.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<pivot string="Subscription Analysis" disable_linking="True">
|
||||
<field name="template_id" type="row"/>
|
||||
<field name="manager_id" type="col"/>
|
||||
<field name="recurring_price" type="measure"/>
|
||||
</pivot>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_subcription_report_graph" model="ir.ui.view">
|
||||
<field name="name">subscription.report.graph</field>
|
||||
<field name="model">sale.contract.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<graph string="Subscription Analysis">
|
||||
<field name="template_id" type="row"/>
|
||||
<field name="recurring_price" type="measure"/>
|
||||
</graph>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_subscription_report_all" model="ir.actions.act_window">
|
||||
<field name="name">Subscription Analysis</field>
|
||||
<field name="res_model">sale.contract.report</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">pivot,graph</field>
|
||||
</record>
|
||||
|
||||
<menuitem action="action_subscription_report_all" id="menu_report_product_all" parent="base.menu_sale_report" sequence="10"/>
|
||||
</odoo>
|
||||
@@ -7,7 +7,7 @@
|
||||
</record>
|
||||
|
||||
<record id="group_template_required" model="res.groups">
|
||||
<field name="name">Mandatory use of templates in contracts</field>
|
||||
<field name="name">Mandatory use of contract templates</field>
|
||||
<field name="category_id" ref="base.module_category_hidden"/>
|
||||
<field name="comment">the field template of the analytic accounts and contracts will be required.</field>
|
||||
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||
@@ -17,5 +17,45 @@
|
||||
<field name="implied_ids" eval="[(4, ref('sale.group_analytic_accounting'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="account.group_account_invoice" model="res.groups">
|
||||
<field name="implied_ids" eval="[(4, ref('sale.group_analytic_accounting'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="group_sale_contract_view" model="res.groups">
|
||||
<field name="name">See Contracts</field>
|
||||
<field name="category_id" ref="base.module_category_sales_management"/>
|
||||
<field name="comment">The user will have read access to contracts.</field>
|
||||
</record>
|
||||
|
||||
<record id="group_sale_contract_manager" model="res.groups">
|
||||
<field name="name">Manage Contracts</field>
|
||||
<field name="category_id" ref="base.module_category_sales_management"/>
|
||||
<field name="comment">The user will have write access to contracts.</field>
|
||||
<field name="implied_ids" eval="[(4, ref('sale_contract.group_sale_contract_view'))]"/>
|
||||
<field name="users" eval="[(4, ref('base.user_root'))]"/>
|
||||
</record>
|
||||
|
||||
<!-- Reporting -->
|
||||
<record model="ir.rule" id="sale_contract_report_comp_rule">
|
||||
<field name="name">Subscription Analysis multi-company</field>
|
||||
<field name="model_id" ref="model_sale_contract_report"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
<record id="sale_contract_report_personal_rule" model="ir.rule">
|
||||
<field name="name">Personal Subscription Analysis</field>
|
||||
<field ref="model_sale_contract_report" name="model_id"/>
|
||||
<field name="domain_force">['|',('user_id','=',user.id),('user_id','=',False)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="sale_order_report_see_all" model="ir.rule">
|
||||
<field name="name">All Orders Analysis</field>
|
||||
<field ref="model_sale_contract_report" name="model_id"/>
|
||||
<field name="domain_force">[(1,'=',1)]</field>
|
||||
<field name="groups" eval="[(4, ref('base.group_sale_salesman_all_leads'))]"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -4,4 +4,12 @@ access_account_analytic_analysis_summary_month_sale,account_analytic_analysis.su
|
||||
access_account_analytic_analysis_summary_user,account_analytic_analysis.summary.user,model_account_analytic_analysis_summary_user,account.group_account_manager,1,0,0,0
|
||||
access_account_analytic_analysis_summary_month,account_analytic_analysis.summary.month,model_account_analytic_analysis_summary_month,account.group_account_manager,1,0,0,0
|
||||
access_account_analytic_invoice_line,account_analytic_invoice_line.user,model_account_analytic_invoice_line,account.group_account_user,1,1,1,1
|
||||
access_account_analytic_invoice_line,account_analytic_invoice_line.user,model_account_analytic_invoice_line,base.group_sale_salesman,1,1,1,1
|
||||
access_sale_contract_report_salesman,sale.contract.report,model_sale_contract_report,base.group_sale_salesman,1,0,0,0
|
||||
access_sale_contract_report_manager,sale.contract.report,model_sale_contract_report,base.group_sale_manager,1,1,1,1
|
||||
sale.access_account_analytic_account_salesman,account_analytic_account salesman,analytic.model_account_analytic_account,base.group_sale_salesman,0,0,0,0
|
||||
access_account_analytic_account_manager,account_analytic_account contract manager,analytic.model_account_analytic_account,sale_contract.group_sale_contract_manager,1,1,1,1
|
||||
access_account_analytic_invoice_line_manager,account_analytic_invoice_line contract manager,model_account_analytic_invoice_line,sale_contract.group_sale_contract_manager,1,1,1,1
|
||||
access_account_analytic_close_reason_manager,account.analytic.close.reason,model_account_analytic_close_reason,sale_contract.group_sale_contract_manager,1,1,1,1
|
||||
access_account_analytic_account_view,account_analytic_account view,analytic.model_account_analytic_account,sale_contract.group_sale_contract_view,1,0,0,0
|
||||
access_account_analytic_invoice_line_view,account_analytic_invoice_line view,model_account_analytic_invoice_line,sale_contract.group_sale_contract_view,1,0,0,0
|
||||
access_account_analytic_close_reason_view,account.analytic.close.reason,model_account_analytic_close_reason,sale_contract.group_sale_contract_view,1,0,0,0
|
||||
|
||||
|
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import common_sale_contract
|
||||
import test_sale_contract
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp.tests import common
|
||||
|
||||
|
||||
class TestContractCommon(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestContractCommon, self).setUp()
|
||||
Contract = self.env['account.analytic.account']
|
||||
SaleOrder = self.env['sale.order']
|
||||
Product = self.env['product.product']
|
||||
ProductTmpl = self.env['product.template']
|
||||
|
||||
# Test products
|
||||
self.product_tmpl = ProductTmpl.create({
|
||||
'name': 'TestProduct',
|
||||
'type': 'service',
|
||||
'recurring_invoice': True,
|
||||
})
|
||||
self.product = Product.create({
|
||||
'product_tmpl_id': self.product_tmpl.id,
|
||||
'price': 50.0
|
||||
})
|
||||
|
||||
# Test user
|
||||
TestUsersEnv = self.env['res.users'].with_context({'no_reset_password': True})
|
||||
group_portal_id = self.ref('base.group_portal')
|
||||
self.user_portal = TestUsersEnv.create({
|
||||
'name': 'Beatrice Portal',
|
||||
'login': 'Beatrice',
|
||||
'alias_name': 'beatrice',
|
||||
'email': 'beatrice.employee@example.com',
|
||||
'groups_id': [(6, 0, [group_portal_id])]
|
||||
})
|
||||
|
||||
# Test Contract
|
||||
self.contract_tmpl = Contract.create({
|
||||
'name': 'TestContractTemplate',
|
||||
'type': 'template',
|
||||
'contract_type': 'subscription',
|
||||
'recurring_invoice_line_ids': [(0, 0, {'product_id': self.product.id, 'name': 'TestRecurringLine', 'price_unit': self.product.price, 'uom_id': self.product_tmpl.uom_id.id})]
|
||||
})
|
||||
self.contract = Contract.create({
|
||||
'name': 'TestContract',
|
||||
'type': 'contract',
|
||||
'state': 'open',
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
})
|
||||
self.sale_order = SaleOrder.create({
|
||||
'name': 'TestSO',
|
||||
'project_id': self.contract.id,
|
||||
'partner_id': self.user_portal.partner_id.id,
|
||||
'order_line': [(0, 0, {'product_id': self.product.id, 'name': 'TestLine'})]
|
||||
})
|
||||
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from openerp.addons.sale_contract.tests.common_sale_contract import TestContractCommon
|
||||
from openerp.tools import mute_logger
|
||||
|
||||
|
||||
class TestContract(TestContractCommon):
|
||||
|
||||
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
|
||||
def test_template(self):
|
||||
""" Test behaviour of on_change_template """
|
||||
Contract = self.env['account.analytic.account']
|
||||
|
||||
# on_change_template on existing record (present in the db)
|
||||
self.contract.write(self.contract.on_change_template(template_id=self.contract_tmpl.id)['value'])
|
||||
self.assertEqual(self.contract_tmpl.contract_type, self.contract.contract_type, 'sale_contract: contract_type not copied when changing template')
|
||||
self.assertTrue(len(self.contract.recurring_invoice_line_ids.ids) == 0, 'sale_contract: recurring_invoice_line_ids copied on existing account.analytic.account record')
|
||||
|
||||
# on_change_template on cached record (NOT present in the db)
|
||||
temp = Contract.new({'name': 'CachedContract',
|
||||
'type': 'contract',
|
||||
'state': 'open',
|
||||
'partner_id': self.user_portal.partner_id.id
|
||||
})
|
||||
temp.update(temp.on_change_template(template_id=self.contract_tmpl.id)['value'])
|
||||
self.assertEqual(self.contract_tmpl.contract_type, temp.contract_type, 'sale_contract: contract_type not copied when changing template')
|
||||
self.assertTrue(temp.recurring_invoice_line_ids.name, 'sale_contract: recurring_invoice_line_ids not copied on new cached account.analytic.account record')
|
||||
|
||||
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
|
||||
def test_sale_order(self):
|
||||
""" Test sale order line copying for recurring products on confirm"""
|
||||
self.sale_order.action_button_confirm()
|
||||
self.assertTrue(len(self.contract.recurring_invoice_line_ids.ids) == 1, 'sale_contract: recurring_invoice_line_ids not created when confirming sale_order with recurring_product')
|
||||
|
||||
@mute_logger('openerp.addons.base.ir.ir_model', 'openerp.models')
|
||||
def test_invoice(self):
|
||||
""" Test invoice generation"""
|
||||
self.contract.write(self.contract.on_change_template(template_id=self.contract_tmpl.id)['value'])
|
||||
self.contract.write({'recurring_invoice_line_ids': [(0, 0, {'product_id': self.product.id, 'name': 'TestRecurringLine', 'price_unit': 31415.9, 'uom_id': self.product_tmpl.uom_id.id})]})
|
||||
invoice_id = self.contract._recurring_create_invoice()
|
||||
invoice = self.env['account.invoice'].browse(invoice_id)
|
||||
self.assertEqual(invoice.amount_untaxed, 31415.9, 'sale_contract: recurring invoice generation problem')
|
||||
|
||||
def test_renewal(self):
|
||||
""" Test contract renewal """
|
||||
res = self.contract.prepare_renewal_order()
|
||||
renewal_so_id = res['res_id']
|
||||
renewal_so = self.env['sale.order'].browse(renewal_so_id)
|
||||
self.assertTrue(renewal_so.update_contract, 'sale_contract: renewal quotation generation is wrong')
|
||||
self.contract.write({'recurring_invoice_line_ids': [(0, 0, {'product_id': self.product.id, 'name': 'TestRecurringLine', 'price_unit': 50, 'uom_id': self.product.uom_id.id})]})
|
||||
renewal_so.write({'order_line': [(0, 0, {'product_id': self.product.id, 'name': 'TestRenewalLine'})]})
|
||||
renewal_so.action_button_confirm()
|
||||
lines = [line.name for line in self.contract.recurring_invoice_line_ids]
|
||||
self.assertTrue('TestRecurringLine' not in lines, 'sale_contract: old line still present after renewal quotation confirmation')
|
||||
self.assertTrue('TestRenewalLine' in lines, 'sale_contract: new line not present after renewal quotation confirmation')
|
||||
@@ -21,162 +21,190 @@
|
||||
<field name="name">account.analytic.account.invoice.form.inherit</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
|
||||
<field eval="40" name="priority"/>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr='//div[@name="duration"]' position="after">
|
||||
<label for="quantity_max"/>
|
||||
<div>
|
||||
<field name="quantity_max"/>
|
||||
<div attrs="{'invisible': [('hours_quantity','=',0)]}" class="oe_grey">
|
||||
<field name="hours_quantity" class="oe_inline"/> Units Consumed
|
||||
</div>
|
||||
<div attrs="{'invisible': [('quantity_max','=',0)]}" class="oe_grey">
|
||||
<field name="remaining_hours" class="oe_inline"/> Units Remaining
|
||||
</div>
|
||||
</div>
|
||||
<xpath expr="/form/sheet" position='before'>
|
||||
<header attrs="{'invisible': [('type','!=','template')]}">
|
||||
<button name="set_close" string="Set to Inactive" type="object" states="open" />
|
||||
<button name="set_open" string="Set to Active" type="object" states="close" />
|
||||
</header>
|
||||
</xpath>
|
||||
<field name="partner_id" position="attributes">
|
||||
<attribute name="attrs">{'required': [('type','=','contract'),'|','|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True), ('recurring_invoices', '=', True)]}</attribute>
|
||||
</field>
|
||||
<xpath expr='//group[@name="invoice_on_timesheets"]' position="replace">
|
||||
<xpath expr='//field[@name="partner_id"]' position='after'>
|
||||
<field name="pricelist_id"
|
||||
class="oe_inline"
|
||||
attrs="{'required': ['|',('invoice_on_timesheets', '=', True),('contract_type','=','subscription')], 'invisible': [('contract_type','=','prepaid')]}"
|
||||
domain="[('type', '=', 'sale')]"
|
||||
groups="product.group_sale_pricelist"/>
|
||||
</xpath>
|
||||
<xpath expr='//separator[@name="description"]' position='before'>
|
||||
<separator name="toinvoice" string="Invoicing"/>
|
||||
<table class="oe_form_analytic_account">
|
||||
<tr>
|
||||
<th class="oe_timesheet_grey" width="160px"></th>
|
||||
<th class="oe_timesheet_grey" width="25px"></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="Expected"/></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="Invoiced"/></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="Remaining"/></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="To Invoice"/></th>
|
||||
<th width="30px"></th>
|
||||
<th></th>
|
||||
</tr><tr>
|
||||
<td class="oe_timesheet_grey">
|
||||
<label for="fix_price_invoices"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="fix_price_invoices" class="oe_inline"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="amount_max" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="ca_invoiced" attrs="{'invisible': [('fix_price_invoices','=',False)]}" widget="monetary"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="remaining_ca" attrs="{'invisible': [('fix_price_invoices','=',False)]}" widget="monetary"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="fix_price_to_invoice" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
|
||||
</td><td attrs="{'invisible': [('fix_price_invoices','=',False)]}" class="oe_timesheet_action">
|
||||
<span attrs="{'invisible': [('fix_price_to_invoice','=',0.0)]}" class="oe_grey">
|
||||
<button name="open_sale_order_lines"
|
||||
class="oe_link"
|
||||
string="⇒ Invoice" type="object"
|
||||
context="{'default_partner_id': partner_id,'default_project_id': active_id,'search_default_uninvoiced': 1,'search_default_project_id': active_id,'search_default_partner_id': [partner_id]}"/>
|
||||
or view
|
||||
</span>
|
||||
|
||||
<span attrs="{'invisible': [('fix_price_to_invoice','<>',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','<>',0.0)]}" class="oe_grey">
|
||||
Nothing to invoice, create
|
||||
</span>
|
||||
|
||||
<button name="%(hr_timesheet.act_hr_timesheet_line_evry1_all_form)d"
|
||||
string="Timesheets" type="action"
|
||||
class="oe_link"
|
||||
context="{'default_account_id': active_id,'search_default_account_id': active_id}"/>
|
||||
</td>
|
||||
</tr><tr name='total'>
|
||||
<th class="oe_timesheet_grey">
|
||||
<label string="Total"/>
|
||||
</th><td class="oe_timesheet_grey">
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="est_total" class="oe_inline" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="invoiced_total" class="oe_inline"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="remaining_total" class="oe_inline"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="toinvoice_total" class="oe_inline"/>
|
||||
</td><td>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<group name='invoice_on_timesheets'>
|
||||
<p name='invoice_on_timesheets_label' class="oe_grey oe_edit_only" colspan="2" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
|
||||
When reinvoicing costs, Odoo uses the
|
||||
pricelist of the contract which uses the price
|
||||
defined on the product related (e.g timesheet
|
||||
products are defined on each employee).
|
||||
</p>
|
||||
<xpath expr='//div[@name="duration"]' position='after'>
|
||||
<field name="close_reason_id" attrs="{'invisible': ['|',('type','!=','contract'),('state','in',['open','pending'])]}"/>
|
||||
<field name="to_invoice"
|
||||
widget="selection"
|
||||
attrs="{'required': [('invoice_on_timesheets', '=', True)], 'invisible': [('invoice_on_timesheets','=',False)]}"/>
|
||||
</xpath>
|
||||
<notebook position="inside">
|
||||
<page id="prepaid" string="Prepaid Support Hours" attrs="{'invisible': ['|',('contract_type','!=','prepaid'),('type','in',['view','normal'])]}" >
|
||||
<group>
|
||||
<field name="pricelist_id"
|
||||
class="oe_inline"
|
||||
attrs="{'required': [('invoice_on_timesheets', '=', True)]}"
|
||||
domain="[('type', '=', 'sale')]"
|
||||
groups="product.group_sale_pricelist"/>
|
||||
<field name="to_invoice"
|
||||
class="oe_inline"
|
||||
widget="selection"
|
||||
attrs="{'required': [('invoice_on_timesheets', '=', True)], 'invisible': [('invoice_on_timesheets','=',False)]}"/>
|
||||
<label for="quantity_max"/>
|
||||
<div>
|
||||
<field name="quantity_max"/>
|
||||
<div attrs="{'invisible': [('hours_quantity','=',0)]}" class="oe_grey">
|
||||
<field name="hours_quantity" class="oe_inline"/> Units Consumed
|
||||
</div>
|
||||
<div attrs="{'invisible': [('quantity_max','=',0)]}" class="oe_grey">
|
||||
<field name="remaining_hours" class="oe_inline"/> Units Remaining
|
||||
</div>
|
||||
</div>
|
||||
</group>
|
||||
</group>
|
||||
<separator string="Recurring Invoices" attrs="{'invisible': [('recurring_invoices','!=',True)]}"/>
|
||||
<div>
|
||||
<div attrs="{'invisible': [('type','!=', 'contract'), ('recurring_invoices', '=', False)]}">
|
||||
<field name="recurring_invoices" on_change="onchange_recurring_invoices(recurring_invoices, date_start)" class="oe_inline" />
|
||||
<label for="recurring_invoices" />
|
||||
</div>
|
||||
<button class="oe_link" name="recurring_create_invoice" attrs="{'invisible': [('recurring_invoices','!=',True)]}" string="⇒ create invoices" type="object" groups="base.group_no_one"/>
|
||||
</div>
|
||||
<group attrs="{'invisible': [('recurring_invoices','!=',True)]}">
|
||||
<label for="recurring_interval"/>
|
||||
<group>
|
||||
<p colspan="2" class="oe_grey oe_edit_only">
|
||||
Once the end date of the contract is
|
||||
passed or the maximum number of service
|
||||
units (e.g. support contract) is
|
||||
reached, the account manager is notified
|
||||
by email to renew the contract with the
|
||||
customer.
|
||||
</p>
|
||||
</group>
|
||||
</page>
|
||||
<page id="billing_summary" string="Billing Summary" attrs="{'invisible': ['|',('contract_type','!=','regular'),('type','in',['view','normal'])]}">
|
||||
<separator name="toinvoice" string="Invoicing"/>
|
||||
<table class="oe_form_analytic_account">
|
||||
<tr>
|
||||
<th class="oe_timesheet_grey" width="160px"></th>
|
||||
<th class="oe_timesheet_grey" width="25px"></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="Expected"/></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="Invoiced"/></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="Remaining"/></th>
|
||||
<th class="oe_timesheet_grey" width="100px"><label string="To Invoice"/></th>
|
||||
<th width="30px"></th>
|
||||
<th></th>
|
||||
</tr><tr>
|
||||
<td class="oe_timesheet_grey">
|
||||
<label for="fix_price_invoices"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="fix_price_invoices" class="oe_inline"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="amount_max" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="ca_invoiced" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="remaining_ca" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
|
||||
</td><td>
|
||||
<field class="oe_inline" name="fix_price_to_invoice" attrs="{'invisible': [('fix_price_invoices','=',False)]}"/>
|
||||
</td><td attrs="{'invisible': [('fix_price_invoices','=',False)]}" class="oe_timesheet_action">
|
||||
<span attrs="{'invisible': [('fix_price_to_invoice','=',0.0)]}" class="oe_grey">
|
||||
<button name="open_sale_order_lines"
|
||||
class="oe_link"
|
||||
string="⇒ Invoice" type="object"
|
||||
context="{'default_partner_id': [partner_id],'default_project_id': active_id,'search_default_uninvoiced': 1,'search_default_project_id': active_id,'search_default_partner_id': [partner_id]}"/>
|
||||
or view
|
||||
</span>
|
||||
|
||||
<span attrs="{'invisible': [('fix_price_to_invoice','<>',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','<>',0.0)]}" class="oe_grey">
|
||||
Nothing to invoice, create
|
||||
</span>
|
||||
|
||||
<button name="%(hr_timesheet.act_hr_timesheet_line_evry1_all_form)d"
|
||||
string="Timesheets" type="action"
|
||||
class="oe_link"
|
||||
context="{'default_account_id': active_id,'search_default_account_id': active_id}"/>
|
||||
</td>
|
||||
</tr><tr name='total'>
|
||||
<th class="oe_timesheet_grey">
|
||||
<label string="Total"/>
|
||||
</th><td class="oe_timesheet_grey">
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="est_total" class="oe_inline" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="invoiced_total" class="oe_inline"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="remaining_total" class="oe_inline"/>
|
||||
</td><td class="oe_timesheet_grey">
|
||||
<field name="toinvoice_total" class="oe_inline"/>
|
||||
</td><td>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<group name='invoice_on_timesheets'>
|
||||
<p name='invoice_on_timesheets_label' class="oe_grey oe_edit_only" colspan="2" attrs="{'invisible': [('invoice_on_timesheets','=',False)]}">
|
||||
When reinvoicing costs, Odoo uses the
|
||||
pricelist of the contract which uses the price
|
||||
defined on the product related (e.g timesheet
|
||||
products are defined on each employee).
|
||||
</p>
|
||||
</group>
|
||||
</page>
|
||||
<page id="recurring_lines" string="Subscription" attrs="{'invisible': ['|',('contract_type','!=','subscription'),('type','in',['view','normal'])]}">
|
||||
<div>
|
||||
<field name="recurring_interval" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
|
||||
<field name="recurring_rule_type" class="oe_inline" attrs="{'required': [('recurring_invoices', '=', True)]}"/>
|
||||
<button string="=> Renewal Quotation" class="oe_link oe_right"
|
||||
help="Create a sale order that will overwrite this contract when confirmed (renewal quotation)"
|
||||
name="prepare_renewal_order" type="object" attrs="{'invisible': [('type','!=','contract')]}" />
|
||||
<field name="recurring_invoice_line_ids">
|
||||
<tree string="Account Analytic Lines" editable="bottom">
|
||||
<field name="product_id" on_change="product_id_change(product_id, uom_id, quantity, False, parent.partner_id, False, parent.pricelist_id, parent.company_id)"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity" readonly="1"/>
|
||||
<field name="actual_quantity"/>
|
||||
<field name="sold_quantity"/>
|
||||
<field name="uom_id" on_change="product_uom_change(product_id,uom_id,quantity,name,parent.partner_id, parent.pricelist_id,context)"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" groups="sale.group_discount_per_so_line"/>
|
||||
<field name="price_subtotal"/>
|
||||
</tree>
|
||||
</field>
|
||||
<group class="oe_subtotal_footer oe_right">
|
||||
<field name="recurring_total" class="oe_subtotal_footer_separator"
|
||||
widget="monetary" options="{'currency_field': 'pricelist_currency_id'}"
|
||||
modifiers="{'readonly': true}"
|
||||
/>
|
||||
</group>
|
||||
</div>
|
||||
<field name="recurring_next_date"/>
|
||||
</group>
|
||||
<label for="recurring_invoice_line_ids" attrs="{'invisible': [('recurring_invoices','=',False)]}"/>
|
||||
<div attrs="{'invisible': [('recurring_invoices','=',False)]}">
|
||||
<field name="recurring_invoice_line_ids">
|
||||
<tree string="Account Analytic Lines" editable="bottom">
|
||||
<field name="product_id" on_change="product_id_change(product_id, uom_id, quantity, False, parent.partner_id, False, parent.pricelist_id, parent.company_id)"/>
|
||||
<field name="name"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="price_subtotal" widget="monetary"/>
|
||||
</tree>
|
||||
</field>
|
||||
</page>
|
||||
</notebook>
|
||||
<field name="partner_id" position="attributes">
|
||||
<attribute name="attrs">{'invisible': [('type','in',['view','normal','template'])], 'required': [('type','=','contract'),'|',('fix_price_invoices','=',True), ('invoice_on_timesheets', '=', True)]}</attribute>
|
||||
</field>
|
||||
<xpath expr='//field[@name="contract_type"]' position='after'>
|
||||
<label for="recurring_interval" string="Contract Duration" attrs="{'invisible': [('type','!=','template')]}"/>
|
||||
<div attrs="{'invisible': [('type','!=','template')]}">
|
||||
<field name="recurring_interval" class="oe_inline"/>
|
||||
<field name="recurring_rule_type" class="oe_inline"/>
|
||||
</div>
|
||||
</xpath>
|
||||
|
||||
<label for="recurring_next_date" string="Date of Next Invoice" attrs="{'invisible': ['|',('type','=','template'),('contract_type','!=','subscription')]}"/>
|
||||
<div attrs="{'invisible': ['|',('type','=','template'),('contract_type','!=','subscription')]}">
|
||||
<field name="recurring_next_date" attrs="{'required': [('contract_type','=','subscription')]}"/>
|
||||
<button string="=> Generate Invoice" class="oe_link" name="recurring_invoice"
|
||||
type="object" attrs="{'invisible': [('type','!=','contract')]}" />
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -202,13 +230,29 @@
|
||||
<field name="arch" type="xml">
|
||||
<field name="date_start" position="before">
|
||||
<field name="last_invoice_date"/>
|
||||
<field name="toinvoice_total"/>
|
||||
<field name="remaining_hours"/>
|
||||
<field name="contract_type"/>
|
||||
<field name="pricelist_id" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Analytic Account list view for contract templates -->
|
||||
<record id="view_account_analytic_account_template_list" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.list</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field eval="10" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree toolbar="1" colors="grey:state=='close';" string="Analytic Accounts">
|
||||
<field name="complete_name"/>
|
||||
<field name="manager_id"/>
|
||||
<field name="contract_type"/>
|
||||
<field name="parent_id"/>
|
||||
<field name="state" invisible="1"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Analytic Account search view for contract -->
|
||||
<record id="view_account_analytic_account_overdue_search" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.search</field>
|
||||
@@ -251,6 +295,18 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- From view for Close Reasons -->
|
||||
<record id="account_analytic_close_reason_view_tree" model="ir.ui.view">
|
||||
<field name="name">account.analytic.close.reason.list</field>
|
||||
<field name="model">account.analytic.close.reason</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Close Reasons" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action Sales/Sales/Contracts -->
|
||||
<record id="action_account_analytic_overdue_all" model="ir.actions.act_window">
|
||||
<field name="name">Contracts</field>
|
||||
@@ -303,6 +359,10 @@
|
||||
<field name="res_model">account.invoice</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_ids"
|
||||
eval="[(5, 0, 0),
|
||||
(0, 0, {'view_mode': 'tree', 'view_id': ref('account.invoice_tree')}),
|
||||
(0, 0, {'view_mode': 'form', 'view_id': ref('account.invoice_form')})]"/>
|
||||
<field name="domain">[('invoice_line_ids.account_analytic_id', '=', active_id)]</field>
|
||||
<field name="context">{'create':False}</field>
|
||||
<field name="help" type="html">
|
||||
@@ -339,15 +399,35 @@
|
||||
</record>
|
||||
<menuitem action="action_account_analytic_overdue" id="menu_action_account_analytic_overdue" sequence="50" parent="base.menu_invoiced"/>
|
||||
|
||||
<!-- Search view for contract templates -->
|
||||
<!-- Analytic Account search view for contract -->
|
||||
<record id="account_analytic_account_template_view_search" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.search</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Contract Templates">
|
||||
<field name="name" filter_domain="['|', ('name','ilike',self),('code','ilike',self)]" string="Template"/>
|
||||
<filter name="open" string="Active" domain="[('state','in',('open','draft'))]" help="Contracts in progress (open, draft)"/>
|
||||
<filter name="closed" string="Inactive" domain="[('state','=','close')]" help="Closed contracts"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Parent" domain="[]" context="{'group_by':'parent_id'}"/>
|
||||
<filter string="Company" domain="[]" context="{'group_by':'company_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action Sales/Configuration/Contract template -->
|
||||
<record id="template_of_contract_action" model="ir.actions.act_window">
|
||||
<field name="name">Contract Template</field>
|
||||
<record id="template_of_regular_contract_action" model="ir.actions.act_window">
|
||||
<field name="name">Regular Contract Template</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.analytic.account</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="domain">[('type','=','template')]</field>
|
||||
<field name="context">{'search_default_type':'template','default_type' : 'template'}</field>
|
||||
<field name="view_id" ref="view_account_analytic_account_template_list"/>
|
||||
<field name="domain">[('type','=','template'),('contract_type','=','regular')]</field>
|
||||
<field name="context">{'search_default_type':'template','default_type' : 'template', 'default_contract_type': 'regular', 'search_default_open':1}</field>
|
||||
<field name="search_view_id" ref="account_analytic_account_template_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click here to create a template of contract.
|
||||
@@ -359,6 +439,56 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="template_of_prepaid_contract_action" model="ir.actions.act_window">
|
||||
<field name="name">Prepaid Contract Template</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.analytic.account</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_account_analytic_account_template_list"/>
|
||||
<field name="domain">[('type','=','template'),('contract_type','=','prepaid')]</field>
|
||||
<field name="context">{'search_default_type':'template','default_type' : 'template', 'default_contract_type': 'prepaid', 'search_default_open':1}</field>
|
||||
<field name="search_view_id" ref="account_analytic_account_template_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click here to create a template of contract.
|
||||
</p><p>
|
||||
Templates are used to prefigure contract/project that
|
||||
can be selected by the salespeople to quickly configure the
|
||||
terms and conditions of the contract.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="template_of_subscription_contract_action" model="ir.actions.act_window">
|
||||
<field name="name">Subscription Contract Template</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.analytic.account</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="view_account_analytic_account_template_list"/>
|
||||
<field name="domain">[('type','=','template'),('contract_type','=','subscription')]</field>
|
||||
<field name="context">{'search_default_type':'template','default_type' : 'template', 'default_contract_type': 'subscription', 'search_default_open':1}</field>
|
||||
<field name="search_view_id" ref="account_analytic_account_template_view_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="oe_view_nocontent_create">
|
||||
Click here to create a template of contract.
|
||||
</p><p>
|
||||
Templates are used to prefigure contract/project that
|
||||
can be selected by the salespeople to quickly configure the
|
||||
terms and conditions of the contract.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="analytic_account_lost_reason_action" model="ir.actions.act_window">
|
||||
<field name="name">Close Reasons</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.analytic.close.reason</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree</field>
|
||||
</record>
|
||||
|
||||
<menuitem
|
||||
id= "base.menu_sales_config"
|
||||
parent= "base.menu_sale_config"
|
||||
@@ -366,9 +496,12 @@
|
||||
name= "Sales"
|
||||
/>
|
||||
|
||||
<menuitem action="template_of_contract_action" id="menu_template_of_contract_action" parent="base.menu_sales_config" sequence="3"/>
|
||||
<menuitem action="template_of_regular_contract_action" id="menu_template_of_regular_contract_action" parent="base.menu_sales_config" sequence="3"/>
|
||||
<menuitem action="template_of_prepaid_contract_action" id="menu_template_of_prepaid_contract_action" parent="base.menu_sales_config" sequence="4"/>
|
||||
<menuitem action="template_of_subscription_contract_action" id="menu_template_of_subscription_contract_action" parent="base.menu_sales_config" sequence="5"/>
|
||||
<menuitem action="analytic_account_lost_reason_action" id="menu_analytic_account_lost_reason_action" parent="base.menu_sales_config" sequence="10"/>
|
||||
|
||||
<record id="sale_contract_form_form" model="ir.ui.view">
|
||||
<record id="account_analytic_analysis_form_form" model="ir.ui.view">
|
||||
<field name="name">account.analytic.account.invoice.form.inherit</field>
|
||||
<field name="model">account.analytic.account</field>
|
||||
<field name="inherit_id" ref="analytic.view_account_analytic_account_form"/>
|
||||
@@ -379,13 +512,19 @@
|
||||
string="To invoice"
|
||||
icon="fa-money"
|
||||
name="%(action_hr_tree_invoiced_all)d"
|
||||
type="action"
|
||||
type="action" attrs="{'invisible': [('type','=','template')]}"
|
||||
context="{'search_default_account_id': [active_id], 'default_account_id': active_id}"/>
|
||||
<button class="oe_stat_button" string="Invoices"
|
||||
icon="fa-book"
|
||||
name="%(action_contract_invoices_all)d"
|
||||
type="action"/>
|
||||
type="action" attrs="{'invisible': [('type','=','template')]}"/>
|
||||
<button class="oe_inline oe_stat_button" string="Contracts"
|
||||
icon="fa-book"
|
||||
name="%(action_account_analytic_overdue_all)d"
|
||||
context="{'search_default_template_id': [active_id], 'default_template_id': active_id}"
|
||||
type="action" attrs="{'invisible': [('type','!=','template')]}"/>
|
||||
</div>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user