[MERGE] merged trunk up to revision 9251

bzr revid: qdp-launchpad@openerp.com-20140326152751-b6bo0fawrzmd71f0
This commit is contained in:
Quentin (OpenERP) 2014-03-26 16:27:51 +01:00
commit e0b27b4ee8
225 changed files with 7477 additions and 3367 deletions

View File

@ -30,7 +30,7 @@ from openerp import SUPERUSER_ID
from openerp import tools from openerp import tools
from openerp.osv import fields, osv, expression from openerp.osv import fields, osv, expression
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.tools.float_utils import float_round from openerp.tools.float_utils import float_round as round
import openerp.addons.decimal_precision as dp import openerp.addons.decimal_precision as dp
@ -1937,15 +1937,15 @@ class account_tax(osv.osv):
# #
'base_code_id': fields.many2one('account.tax.code', 'Account Base Code', help="Use this code for the tax declaration."), 'base_code_id': fields.many2one('account.tax.code', 'Account Base Code', help="Use this code for the tax declaration."),
'tax_code_id': fields.many2one('account.tax.code', 'Account Tax Code', help="Use this code for the tax declaration."), 'tax_code_id': fields.many2one('account.tax.code', 'Account Tax Code', help="Use this code for the tax declaration."),
'base_sign': fields.float('Base Code Sign', help="Usually 1 or -1."), 'base_sign': fields.float('Base Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()),
'tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1."), 'tax_sign': fields.float('Tax Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()),
# Same fields for refund invoices # Same fields for refund invoices
'ref_base_code_id': fields.many2one('account.tax.code', 'Refund Base Code', help="Use this code for the tax declaration."), 'ref_base_code_id': fields.many2one('account.tax.code', 'Refund Base Code', help="Use this code for the tax declaration."),
'ref_tax_code_id': fields.many2one('account.tax.code', 'Refund Tax Code', help="Use this code for the tax declaration."), 'ref_tax_code_id': fields.many2one('account.tax.code', 'Refund Tax Code', help="Use this code for the tax declaration."),
'ref_base_sign': fields.float('Refund Base Code Sign', help="Usually 1 or -1."), 'ref_base_sign': fields.float('Refund Base Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()),
'ref_tax_sign': fields.float('Refund Tax Code Sign', help="Usually 1 or -1."), 'ref_tax_sign': fields.float('Refund Tax Code Sign', help="Usually 1 or -1.", digits_compute=get_precision_tax()),
'include_base_amount': fields.boolean('Included in base amount', help="Indicates if the amount of tax must be included in the base amount for the computation of the next taxes"), 'include_base_amount': fields.boolean('Included in base amount', help="Indicates if the amount of tax must be included in the base amount for the computation of the next taxes"),
'company_id': fields.many2one('res.company', 'Company', required=True), 'company_id': fields.many2one('res.company', 'Company', required=True),
'description': fields.char('Tax Code'), 'description': fields.char('Tax Code'),
@ -2143,7 +2143,7 @@ class account_tax(osv.osv):
tax_compute_precision = precision tax_compute_precision = precision
if taxes and taxes[0].company_id.tax_calculation_rounding_method == 'round_globally': if taxes and taxes[0].company_id.tax_calculation_rounding_method == 'round_globally':
tax_compute_precision += 5 tax_compute_precision += 5
totalin = totalex = float_round(price_unit * quantity, precision) totalin = totalex = round(price_unit * quantity, precision)
tin = [] tin = []
tex = [] tex = []
for tax in taxes: for tax in taxes:

View File

@ -168,7 +168,7 @@
on_change="onchange_partner_id(type,partner_id,date_invoice,payment_term, partner_bank_id,company_id)" on_change="onchange_partner_id(type,partner_id,date_invoice,payment_term, partner_bank_id,company_id)"
context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}" context="{'default_customer': 0, 'search_default_supplier': 1, 'default_supplier': 1}"
domain="[('supplier', '=', True)]"/> domain="[('supplier', '=', True)]"/>
<field name="fiscal_position" widget="selection"/> <field name="fiscal_position" options="{'no_create': True}"/>
<field name="origin"/> <field name="origin"/>
<field name="supplier_invoice_number"/> <field name="supplier_invoice_number"/>
<label for="reference_type"/> <label for="reference_type"/>
@ -183,7 +183,7 @@
<field domain="[('company_id', '=', company_id), ('type', '=', 'payable')]" <field domain="[('company_id', '=', company_id), ('type', '=', 'payable')]"
name="account_id" groups="account.group_account_user"/> name="account_id" groups="account.group_account_user"/>
<field name="journal_id" groups="account.group_account_user" <field name="journal_id" groups="account.group_account_user"
on_change="onchange_journal_id(journal_id, context)" widget="selection"/> on_change="onchange_journal_id(journal_id, context)" options="{'no_create': True}"/>
<field name="currency_id" groups="base.group_multi_currency"/> <field name="currency_id" groups="base.group_multi_currency"/>
<field name="check_total" groups="account.group_supplier_inv_check_total"/> <field name="check_total" groups="account.group_supplier_inv_check_total"/>
</group> </group>
@ -253,7 +253,7 @@
<field domain="[('partner_id', '=', partner_id)]" name="partner_bank_id" on_change="onchange_partner_bank(partner_bank_id)"/> <field domain="[('partner_id', '=', partner_id)]" name="partner_bank_id" on_change="onchange_partner_bank(partner_bank_id)"/>
<field name="user_id" string="Responsible" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_invoice']}"/> <field name="user_id" string="Responsible" context="{'default_groups_ref': ['base.group_user', 'base.group_partner_manager', 'account.group_account_invoice']}"/>
<field name="name" invisible="1"/> <field name="name" invisible="1"/>
<field name="payment_term" widget="selection"/> <field name="payment_term" options="{'no_create': True}"/>
</group> </group>
<group> <group>
<field name="move_id" groups="account.group_account_user"/> <field name="move_id" groups="account.group_account_user"/>
@ -324,12 +324,12 @@
context="{'search_default_customer':1, 'show_address': 1}" context="{'search_default_customer':1, 'show_address': 1}"
options='{"always_reload": True}' options='{"always_reload": True}'
domain="[('customer', '=', True)]"/> domain="[('customer', '=', True)]"/>
<field name="fiscal_position" widget="selection" /> <field name="fiscal_position" options="{'no_create': True}" />
</group> </group>
<group> <group>
<field name="date_invoice"/> <field name="date_invoice"/>
<field name="journal_id" groups="account.group_account_user" <field name="journal_id" groups="account.group_account_user"
on_change="onchange_journal_id(journal_id, context)" widget="selection"/> on_change="onchange_journal_id(journal_id, context)" options="{'no_create': True}"/>
<field domain="[('company_id', '=', company_id),('type','=', 'receivable')]" <field domain="[('company_id', '=', company_id),('type','=', 'receivable')]"
name="account_id" groups="account.group_account_user"/> name="account_id" groups="account.group_account_user"/>

View File

@ -25,7 +25,7 @@
<separator/> <separator/>
<filter string="Draft" icon="terp-document-new" domain="[('state','=','draft')]" help = "Draft Invoices"/> <filter string="Draft" icon="terp-document-new" domain="[('state','=','draft')]" help = "Draft Invoices"/>
<filter string="Pro-forma" icon="terp-gtk-media-pause" domain="['|', ('state','=','proforma'),('state','=','proforma2')]" help = "Pro-forma Invoices"/> <filter string="Pro-forma" icon="terp-gtk-media-pause" domain="['|', ('state','=','proforma'),('state','=','proforma2')]" help = "Pro-forma Invoices"/>
<filter string="Invoiced" name="current" icon="terp-check" domain="[('state','not in', ('draft','cancel'))]" help = "Open and Paid Invoices"/> <filter string="Invoiced" name="current" icon="terp-check" domain="[('state','not in', ('draft','cancel','proforma','proforma2'))]" help = "Open and Paid Invoices"/>
<separator/> <separator/>
<filter icon="terp-personal" string="Customer" name="customer" domain="['|', ('type','=','out_invoice'),('type','=','out_refund')]" help="Customer Invoices And Refunds"/> <filter icon="terp-personal" string="Customer" name="customer" domain="['|', ('type','=','out_invoice'),('type','=','out_refund')]" help="Customer Invoices And Refunds"/>
<filter icon="terp-personal" string="Supplier" domain="['|', ('type','=','in_invoice'),('type','=','in_refund')]" help="Supplier Invoices And Refunds"/> <filter icon="terp-personal" string="Supplier" domain="['|', ('type','=','in_invoice'),('type','=','in_refund')]" help="Supplier Invoices And Refunds"/>

View File

@ -19,6 +19,7 @@
# #
############################################################################## ##############################################################################
from openerp.osv import osv
from openerp.addons.web import http from openerp.addons.web import http
from openerp.addons.web.http import request from openerp.addons.web.http import request
from common_report_header import common_report_header from common_report_header import common_report_header
@ -29,14 +30,12 @@ except ImportError:
import xlwt import xlwt
class tax_report(http.Controller, common_report_header): class tax_report(osv.AbstractModel, common_report_header):
_name = 'report.account.report_vat'
@http.route(['/report/account.report_vat'], type='http', auth='user', website=True, multilang=True) def render_html(self, cr, uid, ids, data=None, context=None):
def report_account_tax(self, **data):
report_obj = request.registry['report'] report_obj = request.registry['report']
self.cr, self.uid, self.pool = request.cr, request.uid, request.registry self.cr, self.uid, self.context = cr, uid, context
data = report_obj.eval_params(data)
res = {} res = {}
self.period_ids = [] self.period_ids = []
@ -54,23 +53,23 @@ class tax_report(http.Controller, common_report_header):
'based_on': self._get_basedon(data), 'based_on': self._get_basedon(data),
'period_from': self.get_start_period(data), 'period_from': self.get_start_period(data),
'period_to': self.get_end_period(data), 'period_to': self.get_end_period(data),
'taxlines': self._get_lines(self._get_basedon(data), company_id=data['form']['company_id']), 'taxlines': self._get_lines(self._get_basedon(data), company_id=data['form']['company_id'], cr=cr, uid=uid),
} }
return request.registry['report'].render(self.cr, self.uid, [], 'account.report_vat', docargs) return report_obj.render(self.cr, self.uid, [], 'account.report_vat', docargs, context=context)
def _get_basedon(self, form): def _get_basedon(self, form):
return form['form']['based_on'] return form['form']['based_on']
def _get_lines(self, based_on, company_id=False, parent=False, level=0, context=None): def _get_lines(self, based_on, company_id=False, parent=False, level=0, context=None, cr=None, uid=None):
period_list = self.period_ids period_list = self.period_ids
res = self._get_codes(based_on, company_id, parent, level, period_list, context=context) res = self._get_codes(based_on, company_id, parent, level, period_list, cr=cr, uid=uid, context=context)
if period_list: if period_list:
res = self._add_codes(based_on, res, period_list, context=context) res = self._add_codes(based_on, res, period_list, context=context)
else: else:
self.cr.execute ("select id from account_fiscalyear") cr.execute ("select id from account_fiscalyear")
fy = self.cr.fetchall() fy = cr.fetchall()
self.cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],)) cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],))
periods = self.cr.fetchall() periods = cr.fetchall()
for p in periods: for p in periods:
period_list.append(p[0]) period_list.append(p[0])
res = self._add_codes(based_on, res, period_list, context=context) res = self._add_codes(based_on, res, period_list, context=context)
@ -90,7 +89,7 @@ class tax_report(http.Controller, common_report_header):
} }
top_result.append(res_dict) top_result.append(res_dict)
res_general = self._get_general(res[i][1].id, period_list, company_id, based_on, context=context) res_general = self._get_general(res[i][1].id, period_list, company_id, based_on, cr=cr, uid=uid, context=context)
ind_general = 0 ind_general = 0
while ind_general < len(res_general): while ind_general < len(res_general):
res_general[ind_general]['type'] = 2 res_general[ind_general]['type'] = 2
@ -101,14 +100,14 @@ class tax_report(http.Controller, common_report_header):
i+=1 i+=1
return top_result return top_result
def _get_general(self, tax_code_id, period_list, company_id, based_on, context=None): def _get_general(self, tax_code_id, period_list, company_id, based_on, cr=None, uid=None, context=None):
if not self.display_detail: if not self.display_detail:
return [] return []
res = [] res = []
obj_account = self.pool.get('account.account') obj_account = self.pool.get('account.account')
periods_ids = tuple(period_list) periods_ids = tuple(period_list)
if based_on == 'payments': if based_on == 'payments':
self.cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \ cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \
SUM(line.debit) AS debit, \ SUM(line.debit) AS debit, \
SUM(line.credit) AS credit, \ SUM(line.credit) AS credit, \
COUNT(*) AS count, \ COUNT(*) AS count, \
@ -132,7 +131,7 @@ class tax_report(http.Controller, common_report_header):
company_id, periods_ids, 'paid',)) company_id, periods_ids, 'paid',))
else: else:
self.cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \ cr.execute('SELECT SUM(line.tax_amount) AS tax_amount, \
SUM(line.debit) AS debit, \ SUM(line.debit) AS debit, \
SUM(line.credit) AS credit, \ SUM(line.credit) AS credit, \
COUNT(*) AS count, \ COUNT(*) AS count, \
@ -149,23 +148,21 @@ class tax_report(http.Controller, common_report_header):
AND account.active \ AND account.active \
GROUP BY account.id,account.name,account.code', ('draft', tax_code_id, GROUP BY account.id,account.name,account.code', ('draft', tax_code_id,
company_id, periods_ids,)) company_id, periods_ids,))
res = self.cr.dictfetchall() res = cr.dictfetchall()
i = 0 i = 0
while i<len(res): while i<len(res):
res[i]['account'] = obj_account.browse(self.cr, self.uid, res[i]['account_id'], context=context) res[i]['account'] = obj_account.browse(cr, uid, res[i]['account_id'], context=context)
i+=1 i+=1
return res return res
def _get_codes(self, based_on, company_id, parent=False, level=0, period_list=None, context=None): def _get_codes(self, based_on, company_id, parent=False, level=0, period_list=None, cr=None, uid=None, context=None):
obj_tc = self.pool.get('account.tax.code') obj_tc = self.pool.get('account.tax.code')
ids = obj_tc.search(self.cr, self.uid, [('parent_id','=',parent),('company_id','=',company_id)], order='sequence', context=context) ids = obj_tc.search(cr, uid, [('parent_id', '=', parent), ('company_id', '=', company_id)], order='sequence', context=context)
res = [] res = []
for code in obj_tc.browse(self.cr, self.uid, ids, {'based_on': based_on}): for code in obj_tc.browse(cr, uid, ids, {'based_on': based_on}):
res.append(('.'*2*level, code)) res.append(('.'*2*level, code))
res += self._get_codes(based_on, company_id, code.id, level+1, cr=cr, uid=uid, context=context)
res += self._get_codes(based_on, company_id, code.id, level+1, context=context)
return res return res
def _add_codes(self, based_on, account_list=None, period_list=None, context=None): def _add_codes(self, based_on, account_list=None, period_list=None, context=None):
@ -187,9 +184,6 @@ class tax_report(http.Controller, common_report_header):
res.append((account[0], code)) res.append((account[0], code))
return res return res
def _get_currency(self, form, context=None):
return self.pool.get('res.company').browse(self.cr, self.uid, form['company_id'], context=context).currency_id.name
def sort_result(self, accounts, context=None): def sort_result(self, accounts, context=None):
result_accounts = [] result_accounts = []
ind=0 ind=0
@ -206,7 +200,8 @@ class tax_report(http.Controller, common_report_header):
bcl_rup_ind = ind - 1 bcl_rup_ind = ind - 1
while (bcl_current_level >= int(accounts[bcl_rup_ind]['level']) and bcl_rup_ind >= 0 ): while (bcl_current_level >= int(accounts[bcl_rup_ind]['level']) and bcl_rup_ind >= 0 ):
res_tot = { 'code': accounts[bcl_rup_ind]['code'], res_tot = {
'code': accounts[bcl_rup_ind]['code'],
'name': '', 'name': '',
'debit': 0, 'debit': 0,
'credit': 0, 'credit': 0,
@ -229,25 +224,23 @@ class tax_report(http.Controller, common_report_header):
return result_accounts return result_accounts
class tax_report_xls(http.Controller):
@http.route(['/report/account.report_vat_xls'], type='http', auth='user', website=True, multilang=True) @http.route(['/report/account.report_vat_xls'], type='http', auth='user', website=True, multilang=True)
def report_account_tax_xls(self, **data): def report_account_tax_xls(self, **data):
report_obj = request.registry['report']
self.cr, self.uid, self.pool = request.cr, request.uid, request.registry
data = report_obj.eval_params(data) # Very ugly lines, only for the proof of concept of 'controller' report
taxreport_obj = request.registry['report.account.report_vat']
from openerp.addons.report.controllers.main import ReportController
eval_params = ReportController()._eval_params
res = {} cr, uid = request.cr, request.uid
self.period_ids = [] data = eval_params(data)
period_obj = self.pool.get('account.period') data = {'form': data}
self.display_detail = data['form']['display_detail']
res['periods'] = ''
res['fiscalyear'] = data['form'].get('fiscalyear_id', False)
if data['form'].get('period_from', False) and data['form'].get('period_to', False): taxreport_obj.render_html(cr, uid, [], data=data)
self.period_ids = period_obj.build_ctx_periods(self.cr, self.uid, data['form']['period_from'], data['form']['period_to']) lines = taxreport_obj._get_lines(taxreport_obj._get_basedon(data), company_id=data['form']['company_id'], cr=cr, uid=uid)
content = ''
lines = self._get_lines(self._get_basedon(data), company_id=data['form']['company_id'])
if lines: if lines:
xls = StringIO.StringIO() xls = StringIO.StringIO()

View File

@ -19,6 +19,7 @@
<newline/> <newline/>
<field name="result_selection"/> <field name="result_selection"/>
<field name="direction_selection"/> <field name="direction_selection"/>
<field name="target_move"/>
</group> </group>
<field name="journal_ids" required="0" invisible="1"/> <field name="journal_ids" required="0" invisible="1"/>
<footer> <footer>

View File

@ -41,6 +41,23 @@ class account_voucher(osv.osv):
'number': fields.char('Number', size=32), 'number': fields.char('Number', size=32),
} }
def _amount_to_text(self, cr, uid, amount, currency_id, context=None):
# Currency complete name is not available in res.currency model
# Exceptions done here (EUR, USD, BRL) cover 75% of cases
# For other currencies, display the currency code
currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
if currency.name.upper() == 'EUR':
currency_name = 'Euro'
elif currency.name.upper() == 'USD':
currency_name = 'Dollars'
elif currency.name.upper() == 'BRL':
currency_name = 'reais'
else:
currency_name = currency.name
#TODO : generic amount_to_text is not ready yet, otherwise language (and country) and currency can be passed
#amount_in_word = amount_to_text(amount, context=context)
return amount_to_text(amount, currency=currency_name)
def onchange_amount(self, cr, uid, ids, amount, rate, partner_id, journal_id, currency_id, ttype, date, payment_rate_currency_id, company_id, context=None): def onchange_amount(self, cr, uid, ids, amount, rate, partner_id, journal_id, currency_id, ttype, date, payment_rate_currency_id, company_id, context=None):
""" Inherited - add amount_in_word and allow_check_writting in returned value dictionary """ """ Inherited - add amount_in_word and allow_check_writting in returned value dictionary """
if not context: if not context:
@ -48,22 +65,7 @@ class account_voucher(osv.osv):
default = super(account_voucher, self).onchange_amount(cr, uid, ids, amount, rate, partner_id, journal_id, currency_id, ttype, date, payment_rate_currency_id, company_id, context=context) default = super(account_voucher, self).onchange_amount(cr, uid, ids, amount, rate, partner_id, journal_id, currency_id, ttype, date, payment_rate_currency_id, company_id, context=context)
if 'value' in default: if 'value' in default:
amount = 'amount' in default['value'] and default['value']['amount'] or amount amount = 'amount' in default['value'] and default['value']['amount'] or amount
amount_in_word = self._amount_to_text(cr, uid, amount, currency_id, context=context)
# Currency complete name is not available in res.currency model
# Exceptions done here (EUR, USD, BRL) cover 75% of cases
# For other currencies, display the currency code
currency = self.pool['res.currency'].browse(cr, uid, currency_id, context=context)
if currency.name.upper() == 'EUR':
currency_name = 'Euro'
elif currency.name.upper() == 'USD':
currency_name = 'Dollars'
elif currency.name.upper() == 'BRL':
currency_name = 'reais'
else:
currency_name = currency.name
#TODO : generic amount_to_text is not ready yet, otherwise language (and country) and currency can be passed
#amount_in_word = amount_to_text(amount, context=context)
amount_in_word = amount_to_text(amount, currency=currency_name)
default['value'].update({'amount_in_word':amount_in_word}) default['value'].update({'amount_in_word':amount_in_word})
if journal_id: if journal_id:
allow_check_writing = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context).allow_check_writing allow_check_writing = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context).allow_check_writing
@ -92,6 +94,19 @@ class account_voucher(osv.osv):
}, },
'nodestroy': True 'nodestroy': True
} }
def create(self, cr, uid, vals, context=None):
if vals.get('amount') and vals.get('journal_id') and 'amount_in_word' not in vals:
vals['amount_in_word'] = self._amount_to_text(cr, uid, vals['amount'], vals.get('currency_id') or \
self.pool['account.journal'].browse(cr, uid, vals['journal_id'], context=context).currency.id or \
self.pool['res.company'].browse(cr, uid, vals['company_id']).currency_id.id, context=context)
return super(account_voucher, self).create(cr, uid, vals, context=context)
def write(self, cr, uid, ids, vals, context=None):
if vals.get('amount') and vals.get('journal_id') and 'amount_in_word' not in vals:
vals['amount_in_word'] = self._amount_to_text(cr, uid, vals['amount'], vals.get('currency_id') or \
self.pool['account.journal'].browse(cr, uid, vals['journal_id'], context=context).currency.id or \
self.pool['res.company'].browse(cr, uid, vals['company_id']).currency_id.id, context=context)
return super(account_voucher, self).write(cr, uid, ids, vals, context=context)
def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False): def fields_view_get(self, cr, uid, view_id=None, view_type=False, context=None, toolbar=False, submenu=False):
""" """

View File

@ -149,7 +149,7 @@
<para style="terp_default_8"> <para style="terp_default_8">
<font color="white"> </font> <font color="white"> </font>
</para> </para>
<pre style="terp_default_9_followup_id">[[ format(get_text(o,data['form']['followup_id'])) ]]</pre> <para style="terp_default_9"><pre style="terp_default_9_followup_id">[[ format(get_text(o,data['form']['followup_id'])) ]]</pre></para>
<para style="terp_default_9"> <para style="terp_default_9">
<font color="white"> </font> <font color="white"> </font>
</para> </para>

View File

@ -463,14 +463,12 @@ def process_data(cr, uid, pool, res_ids, model, method, old_values=None, new_val
# if at least one modification has been found # if at least one modification has been found
for model_id, resource_id in lines: for model_id, resource_id in lines:
line_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, model_id).model line_model = pool.get('ir.model').browse(cr, SUPERUSER_ID, model_id).model
name = pool.get(line_model).name_get(cr, uid, [resource_id])[0][1]
vals = { vals = {
'method': method, 'method': method,
'object_id': model_id, 'object_id': model_id,
'user_id': uid, 'user_id': uid,
'res_id': resource_id, 'res_id': resource_id,
'name': name,
} }
if (model_id, resource_id) not in old_values and method not in ('copy', 'read'): if (model_id, resource_id) not in old_values and method not in ('copy', 'read'):
# the resource was not existing so we are forcing the method to 'create' # the resource was not existing so we are forcing the method to 'create'
@ -481,7 +479,11 @@ def process_data(cr, uid, pool, res_ids, model, method, old_values=None, new_val
# the resource is not existing anymore so we are forcing the method to 'unlink' # the resource is not existing anymore so we are forcing the method to 'unlink'
# (because it could also come with the value 'write' if we are deleting the # (because it could also come with the value 'write' if we are deleting the
# record through a one2many field) # record through a one2many field)
name = old_values[(model_id, resource_id)]['value'].get('name',False)
vals.update({'method': 'unlink'}) vals.update({'method': 'unlink'})
else :
name = pool[line_model].name_get(cr, uid, [resource_id])[0][1]
vals.update({'name': name})
# create the audittrail log in super admin mode, only if a change has been detected # create the audittrail log in super admin mode, only if a change has been detected
if lines[(model_id, resource_id)]: if lines[(model_id, resource_id)]:
log_id = pool.get('audittrail.log').create(cr, SUPERUSER_ID, vals) log_id = pool.get('audittrail.log').create(cr, SUPERUSER_ID, vals)

View File

@ -2,3 +2,4 @@ import controllers
import auth_oauth import auth_oauth
import res_users import res_users
import res_config import res_config
import ir_configparameter

View File

@ -15,6 +15,7 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
#---------------------------------------------------------- #----------------------------------------------------------
# helpers # helpers
#---------------------------------------------------------- #----------------------------------------------------------
@ -30,11 +31,15 @@ def fragment_to_query_string(func):
var s = l.search ? (l.search === '?' ? '' : '&') : '?'; var s = l.search ? (l.search === '?' ? '' : '&') : '?';
r = l.pathname + l.search + s + q; r = l.pathname + l.search + s + q;
} }
if (r == l.pathname) {
r = '/';
}
window.location = r; window.location = r;
</script></head><body></body></html>""" </script></head><body></body></html>"""
return func(self, *a, **kw) return func(self, *a, **kw)
return wrapper return wrapper
#---------------------------------------------------------- #----------------------------------------------------------
# Controller # Controller
#---------------------------------------------------------- #----------------------------------------------------------
@ -73,6 +78,9 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home):
@http.route() @http.route()
def web_login(self, *args, **kw): def web_login(self, *args, **kw):
ensure_db() ensure_db()
if request.httprequest.method == 'GET' and request.session.uid and request.params.get('redirect'):
# Redirect if already logged in and redirect param is present
return http.redirect_with_hash(request.params.get('redirect'))
providers = self.list_providers() providers = self.list_providers()
response = super(OAuthLogin, self).web_login(*args, **kw) response = super(OAuthLogin, self).web_login(*args, **kw)
@ -111,6 +119,7 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home):
response.qcontext.update(providers=providers) response.qcontext.update(providers=providers)
return response return response
class OAuthController(http.Controller): class OAuthController(http.Controller):
@http.route('/auth_oauth/signin', type='http', auth='none') @http.route('/auth_oauth/signin', type='http', auth='none')

View File

@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
from openerp import SUPERUSER_ID
from openerp.osv import osv
class ir_configparameter(osv.Model):
_inherit = 'ir.config_parameter'
def init(self, cr, force=False):
super(ir_configparameter, self).init(cr, force=force)
if force:
IMD = self.pool['ir.model.data']
oauth_oe = IMD.xmlid_to_object(cr, SUPERUSER_ID, 'auth_oauth.provider_openerp')
dbuuid = self.get_param(cr, SUPERUSER_ID, 'database.uuid')
oauth_oe.write({'client_id': dbuuid})

View File

@ -37,6 +37,9 @@ class AuthSignupHome(openerp.addons.web.controllers.main.Home):
ensure_db() ensure_db()
response = super(AuthSignupHome, self).web_login(*args, **kw) response = super(AuthSignupHome, self).web_login(*args, **kw)
response.qcontext.update(self.get_auth_signup_config()) response.qcontext.update(self.get_auth_signup_config())
if request.httprequest.method == 'GET' and request.session.uid and request.params.get('redirect'):
# Redirect if already logged in and redirect param is present
return http.redirect_with_hash(request.params.get('redirect'))
return response return response
@http.route('/web/signup', type='http', auth='public', website=True, multilang=True) @http.route('/web/signup', type='http', auth='public', website=True, multilang=True)

View File

@ -20,8 +20,8 @@
############################################################################## ##############################################################################
from datetime import datetime, timedelta from datetime import datetime, timedelta
import random import random
from urllib import urlencode
from urlparse import urljoin from urlparse import urljoin
import werkzeug
from openerp.addons.base.ir.ir_mail_server import MailDeliveryException from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
from openerp.osv import osv, fields from openerp.osv import osv, fields
@ -53,7 +53,7 @@ class res_partner(osv.Model):
(not partner.signup_expiration or dt <= partner.signup_expiration) (not partner.signup_expiration or dt <= partner.signup_expiration)
return res return res
def _get_signup_url_for_action(self, cr, uid, ids, action='login', view_type=None, menu_id=None, res_id=None, model=None, context=None): def _get_signup_url_for_action(self, cr, uid, ids, action=None, view_type=None, menu_id=None, res_id=None, model=None, context=None):
""" generate a signup url for the given partner ids and action, possibly overriding """ generate a signup url for the given partner ids and action, possibly overriding
the url state components (menu_id, id, view_type) """ the url state components (menu_id, id, view_type) """
if context is None: if context is None:
@ -81,6 +81,8 @@ class res_partner(osv.Model):
continue # no signup token, no user, thus no signup url! continue # no signup token, no user, thus no signup url!
fragment = dict() fragment = dict()
if action:
fragment['action'] = action
if view_type: if view_type:
fragment['view_type'] = view_type fragment['view_type'] = view_type
if menu_id: if menu_id:
@ -90,7 +92,10 @@ class res_partner(osv.Model):
if res_id: if res_id:
fragment['id'] = res_id fragment['id'] = res_id
res[partner.id] = urljoin(base_url, "/web/%s?%s#%s" % (route, urlencode(query), urlencode(fragment))) if fragment:
query['redirect'] = '/web#' + werkzeug.url_encode(fragment)
res[partner.id] = urljoin(base_url, "/web/%s?%s" % (route, werkzeug.url_encode(query)))
return res return res

View File

@ -13,15 +13,15 @@
<template id="auth_signup.fields" name="Auth Signup/ResetPassword form fields"> <template id="auth_signup.fields" name="Auth Signup/ResetPassword form fields">
<t t-call="web.database_select"/> <t t-call="web.database_select"/>
<div class="form-group field-login">
<label for="login" class="control-label">Your Email</label>
<input type="text" name="login" t-att-value="login" id="login" class="form-control" autofocus="autofocus"
required="required" t-att-readonly="'readonly' if only_passwords else None"/>
</div>
<div class="form-group field-name"> <div class="form-group field-name">
<label for="name" class="control-label">Your Name</label> <label for="name" class="control-label">Your Name</label>
<input type="text" name="name" t-att-value="name" id="name" class="form-control" placeholder="e.g. John Doe" <input type="text" name="name" t-att-value="name" id="name" class="form-control" placeholder="e.g. John Doe"
required="required" t-att-autofocus="'autofocus' if not only_passwords else None" t-att-readonly="'readonly' if only_passwords else None"/>
</div>
<div class="form-group field-login">
<label for="login" class="control-label">Your Email</label>
<input type="text" name="login" t-att-value="login" id="login" class="form-control"
required="required" t-att-readonly="'readonly' if only_passwords else None"/> required="required" t-att-readonly="'readonly' if only_passwords else None"/>
</div> </div>

View File

@ -114,6 +114,8 @@ class ir_import(orm.TransientModel):
elif field['type'] == 'one2many' and depth: elif field['type'] == 'one2many' and depth:
f['fields'] = self.get_fields( f['fields'] = self.get_fields(
cr, uid, field['relation'], context=context, depth=depth-1) cr, uid, field['relation'], context=context, depth=depth-1)
if self.pool['res.users'].has_group(cr, uid, 'base.group_no_one'):
f['fields'].append({'id' : '.id', 'name': '.id', 'string': _("Database ID"), 'required': False, 'fields': []})
fields.append(f) fields.append(f)

View File

@ -37,8 +37,6 @@ from openerp.tools.translate import _
from openerp.http import request from openerp.http import request
from operator import itemgetter from operator import itemgetter
from werkzeug.exceptions import BadRequest
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -63,6 +61,7 @@ def calendar_id2real_id(calendar_id=None, with_date=False):
return int(real_id) return int(real_id)
return calendar_id and int(calendar_id) or calendar_id return calendar_id and int(calendar_id) or calendar_id
def get_real_ids(ids): def get_real_ids(ids):
if isinstance(ids, (str, int, long)): if isinstance(ids, (str, int, long)):
return calendar_id2real_id(ids) return calendar_id2real_id(ids)
@ -76,6 +75,7 @@ class calendar_attendee(osv.Model):
Calendar Attendee Information Calendar Attendee Information
""" """
_name = 'calendar.attendee' _name = 'calendar.attendee'
_rec_name = 'cn'
_description = 'Attendee information' _description = 'Attendee information'
def _compute_data(self, cr, uid, ids, name, arg, context=None): def _compute_data(self, cr, uid, ids, name, arg, context=None):
@ -223,7 +223,7 @@ class calendar_attendee(osv.Model):
}) })
for attendee in self.browse(cr, uid, ids, context=context): for attendee in self.browse(cr, uid, ids, context=context):
if attendee.email and email_from: if attendee.email and email_from and attendee.email != email_from:
ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context) ics_file = self.get_ics_file(cr, uid, attendee.event_id, context=context)
mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context) mail_id = template_pool.send_mail(cr, uid, template_id, attendee.id, context=local_context)
@ -334,22 +334,22 @@ class calendar_alarm_manager(osv.AbstractModel):
res = {} res = {}
base_request = """ base_request = """
SELECT SELECT
crm.id, cal.id,
crm.date - interval '1' minute * calcul_delta.max_delta AS first_alarm, cal.date - interval '1' minute * calcul_delta.max_delta AS first_alarm,
CASE CASE
WHEN crm.recurrency THEN crm.end_date - interval '1' minute * calcul_delta.min_delta WHEN cal.recurrency THEN cal.end_date - interval '1' minute * calcul_delta.min_delta
ELSE crm.date_deadline - interval '1' minute * calcul_delta.min_delta ELSE cal.date_deadline - interval '1' minute * calcul_delta.min_delta
END as last_alarm, END as last_alarm,
crm.date as first_event_date, cal.date as first_event_date,
CASE CASE
WHEN crm.recurrency THEN crm.end_date WHEN cal.recurrency THEN cal.end_date
ELSE crm.date_deadline ELSE cal.date_deadline
END as last_event_date, END as last_event_date,
calcul_delta.min_delta, calcul_delta.min_delta,
calcul_delta.max_delta, calcul_delta.max_delta,
crm.rrule AS rule cal.rrule AS rule
FROM FROM
calendar_event AS crm calendar_event AS cal
RIGHT JOIN RIGHT JOIN
( (
SELECT SELECT
@ -359,11 +359,11 @@ class calendar_alarm_manager(osv.AbstractModel):
LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id LEFT JOIN calendar_alarm AS alarm ON alarm.id = rel.calendar_alarm_id
WHERE alarm.type in %s WHERE alarm.type in %s
GROUP BY rel.calendar_event_id GROUP BY rel.calendar_event_id
) AS calcul_delta ON calcul_delta.calendar_event_id = crm.id ) AS calcul_delta ON calcul_delta.calendar_event_id = cal.id
""" """
filter_user = """ filter_user = """
LEFT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = crm.id RIGHT JOIN calendar_event_res_partner_rel AS part_rel ON part_rel.calendar_event_id = cal.id
AND part_rel.res_partner_id = %s AND part_rel.res_partner_id = %s
""" """
@ -384,21 +384,14 @@ class calendar_alarm_manager(osv.AbstractModel):
#Add filter on hours #Add filter on hours
tuple_params += (seconds, seconds,) tuple_params += (seconds, seconds,)
cr.execute(""" cr.execute("""SELECT *
SELECT FROM ( %s ) AS ALL_EVENTS
* WHERE ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%%s' second )
FROM ( AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%%s' second )
""" """ % base_request, tuple_params)
+ base_request
+ """
) AS ALL_EVENTS
WHERE
ALL_EVENTS.first_alarm < (now() at time zone 'utc' + interval '%s' second )
AND ALL_EVENTS.last_alarm > (now() at time zone 'utc' - interval '%s' second )
""", tuple_params)
for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall(): for event_id, first_alarm, last_alarm, first_meeting, last_meeting, min_duration, max_duration, rule in cr.fetchall():
res[event_id].update({ res[event_id] = {
'event_id': event_id, 'event_id': event_id,
'first_alarm': first_alarm, 'first_alarm': first_alarm,
'last_alarm': last_alarm, 'last_alarm': last_alarm,
@ -407,7 +400,7 @@ class calendar_alarm_manager(osv.AbstractModel):
'min_duration': min_duration, 'min_duration': min_duration,
'max_duration': max_duration, 'max_duration': max_duration,
'rrule': rule 'rrule': rule
}) }
return res return res
@ -433,83 +426,86 @@ class calendar_alarm_manager(osv.AbstractModel):
res.append(alert) res.append(alert)
return res return res
def get_next_mail(self, cr, uid, context=None):
def get_next_mail(self,cr,uid,context=None): cron = self.pool.get('ir.cron').search(cr, uid, [('model', 'ilike', self._name)], context=context)
cron = self.pool.get('ir.cron').search(cr,uid,[('model','ilike',self._name)],context=context)
if cron and len(cron) == 1: if cron and len(cron) == 1:
cron = self.pool.get('ir.cron').browse(cr,uid,cron[0],context=context) cron = self.pool.get('ir.cron').browse(cr, uid, cron[0], context=context)
else: else:
raise ("Cron for " + self._name + " not identified :( !") raise ("Cron for " + self._name + " not identified :( !")
if cron.interval_type=="weeks": if cron.interval_type == "weeks":
cron_interval = cron.interval_number * 7 * 24 * 60 * 60 cron_interval = cron.interval_number * 7 * 24 * 60 * 60
elif cron.interval_type=="days": elif cron.interval_type == "days":
cron_interval = cron.interval_number * 24 * 60 * 60 cron_interval = cron.interval_number * 24 * 60 * 60
elif cron.interval_type=="hours": elif cron.interval_type == "hours":
cron_interval = cron.interval_number * 60 * 60 cron_interval = cron.interval_number * 60 * 60
elif cron.interval_type=="minutes": elif cron.interval_type == "minutes":
cron_interval = cron.interval_number * 60 cron_interval = cron.interval_number * 60
elif cron.interval_type=="seconds": elif cron.interval_type == "seconds":
cron_interval = cron.interval_number cron_interval = cron.interval_number
if not cron_interval: if not cron_interval:
raise ("Cron delay for " + self._name + " can not be calculated :( !") raise ("Cron delay for " + self._name + " can not be calculated :( !")
all_events = self.get_next_potential_limit_alarm(cr,uid,cron_interval,notif=False,context=context) all_events = self.get_next_potential_limit_alarm(cr, uid, cron_interval, notif=False, context=context)
for event in all_events: #.values() for event in all_events: # .values()
max_delta = all_events[event]['max_duration']; max_delta = all_events[event]['max_duration']
curEvent = self.pool.get('calendar.event').browse(cr,uid,event,context=context) curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
if curEvent.recurrency: if curEvent.recurrency:
bFound = False bFound = False
LastFound = False LastFound = False
for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr,uid,curEvent, context=context) : for one_date in self.pool.get('calendar.event').get_recurrent_date_by_event(cr, uid, curEvent, context=context):
in_date_format = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S'); in_date_format = one_date.replace(tzinfo=None)
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,cron_interval,notif=False,context=context) LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
if LastFound: if LastFound:
for alert in LastFound: for alert in LastFound:
self.do_mail_reminder(cr,uid,alert,context=context) self.do_mail_reminder(cr, uid, alert, context=context)
if not bFound: # if it's the first alarm for this recurrent event if not bFound: # if it's the first alarm for this recurrent event
bFound = True bFound = True
if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event if bFound and not LastFound: # if the precedent event had an alarm but not this one, we can stop the search for this event
break break
else: else:
in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S'); in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,cron_interval,notif=False,context=context) LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, cron_interval, notif=False, context=context)
if LastFound: if LastFound:
for alert in LastFound: for alert in LastFound:
self.do_mail_reminder(cr,uid,alert,context=context) self.do_mail_reminder(cr, uid, alert, context=context)
def get_next_notif(self,cr,uid,context=None): def get_next_notif(self, cr, uid, context=None):
ajax_check_every_seconds = 300 ajax_check_every_seconds = 300
partner = self.pool.get('res.users').browse(cr,uid,uid,context=context).partner_id; partner = self.pool.get('res.users').browse(cr, uid, uid, context=context).partner_id
all_notif = [] all_notif = []
all_events = self.get_next_potential_limit_alarm(cr,uid,ajax_check_every_seconds,partner_id=partner.id,mail=False,context=context)
if not partner:
return []
all_events = self.get_next_potential_limit_alarm(cr, uid, ajax_check_every_seconds, partner_id=partner.id, mail=False, context=context)
for event in all_events: # .values() for event in all_events: # .values()
max_delta = all_events[event]['max_duration']; max_delta = all_events[event]['max_duration']
curEvent = self.pool.get('calendar.event').browse(cr,uid,event,context=context) curEvent = self.pool.get('calendar.event').browse(cr, uid, event, context=context)
if curEvent.recurrency: if curEvent.recurrency:
bFound = False bFound = False
LastFound = False LastFound = False
for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr,uid,curEvent, context=context) : for one_date in self.pool.get("calendar.event").get_recurrent_date_by_event(cr, uid, curEvent, context=context):
in_date_format = datetime.strptime(one_date, '%Y-%m-%d %H:%M:%S'); in_date_format = one_date.replace(tzinfo=None)
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,ajax_check_every_seconds,after=partner.cal_last_notif,mail=False,context=context) LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, after=partner.calendar_last_notif_ack, mail=False, context=context)
if LastFound: if LastFound:
for alert in LastFound: for alert in LastFound:
all_notif.append(self.do_notif_reminder(cr,uid,alert,context=context)) all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
if not bFound: #if it's the first alarm for this recurrent event if not bFound: # if it's the first alarm for this recurrent event
bFound = True bFound = True
if bFound and not LastFound: #if the precedent event had alarm but not this one, we can stop the search fot this event if bFound and not LastFound: # if the precedent event had alarm but not this one, we can stop the search fot this event
break break
else: else:
in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S'); in_date_format = datetime.strptime(curEvent.date, '%Y-%m-%d %H:%M:%S')
LastFound = self.do_check_alarm_for_one_date(cr,uid,in_date_format,curEvent,max_delta,ajax_check_every_seconds,partner.cal_last_notif,mail=False,context=context) LastFound = self.do_check_alarm_for_one_date(cr, uid, in_date_format, curEvent, max_delta, ajax_check_every_seconds, partner.calendar_last_notif_ack, mail=False, context=context)
if LastFound: if LastFound:
for alert in LastFound: for alert in LastFound:
all_notif.append(self.do_notif_reminder(cr,uid,alert,context=context)) all_notif.append(self.do_notif_reminder(cr, uid, alert, context=context))
return all_notif return all_notif
def do_mail_reminder(self, cr, uid, alert, context=None): def do_mail_reminder(self, cr, uid, alert, context=None):
if context is None: if context is None:
@ -520,7 +516,7 @@ class calendar_alarm_manager(osv.AbstractModel):
alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context) alarm = self.pool['calendar.alarm'].browse(cr, uid, alert['alarm_id'], context=context)
if alarm.type == 'email': if alarm.type == 'email':
res = self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, event.attendee_ids, template_xmlid='calendar_template_meeting_reminder', context=context) res = self.pool['calendar.attendee']._send_mail_to_attendees(cr, uid, [att.id for att in event.attendee_ids], template_xmlid='calendar_template_meeting_reminder', context=context)
return res return res
@ -684,19 +680,23 @@ class calendar_event(osv.Model):
return [d.astimezone(pytz.UTC) for d in rset1] return [d.astimezone(pytz.UTC) for d in rset1]
def _get_recurrency_end_date(self, data, context=None): def _get_recurrency_end_date(self, data, context=None):
if data.get('recurrency') and data.get('end_type') in ('count', unicode('count')): if not data.get('recurrency'):
data_date_deadline = datetime.strptime(data.get('date_deadline'), '%Y-%m-%d %H:%M:%S') return False
if data.get('rrule_type') in ('daily', unicode('count')):
rel_date = relativedelta(days=data.get('count') + 1) end_type = data.get('end_type')
elif data.get('rrule_type') in ('weekly', unicode('weekly')): end_date = data.get('end_date')
rel_date = relativedelta(days=(data.get('count') + 1) * 7)
elif data.get('rrule_type') in ('monthly', unicode('monthly')): if end_type == 'count' and all(data.get(key) for key in ['count', 'rrule_type', 'date_deadline']):
rel_date = relativedelta(months=data.get('count') + 1) count = data['count'] + 1
elif data.get('rrule_type') in ('yearly', unicode('yearly')): delay, mult = {
rel_date = relativedelta(years=data.get('count') + 1) 'daily': ('days', 1),
end_date = data_date_deadline + rel_date 'weekly': ('days', 7),
else: 'monthly': ('months', 1),
end_date = data.get('end_date') 'yearly': ('years', 1),
}[data['rrule_type']]
deadline = datetime.strptime(data['date_deadline'], tools.DEFAULT_SERVER_DATETIME_FORMAT)
return deadline + relativedelta(**{delay: count * mult})
return end_date return end_date
def _find_my_attendee(self, cr, uid, meeting_ids, context=None): def _find_my_attendee(self, cr, uid, meeting_ids, context=None):
@ -777,8 +777,12 @@ class calendar_event(osv.Model):
else: else:
result[event] = "" result[event] = ""
return result return result
# retro compatibility function
def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None): def _rrule_write(self, cr, uid, ids, field_name, field_value, args, context=None):
return self._set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=context)
def _set_rulestring(self, cr, uid, ids, field_name, field_value, args, context=None):
if not isinstance(ids, list): if not isinstance(ids, list):
ids = [ids] ids = [ids]
data = self._get_empty_rrule_data() data = self._get_empty_rrule_data()
@ -790,7 +794,7 @@ class calendar_event(osv.Model):
data.update(update_data) data.update(update_data)
self.write(cr, uid, ids, data, context=context) self.write(cr, uid, ids, data, context=context)
return True return True
def _tz_get(self, cr, uid, context=None): def _tz_get(self, cr, uid, context=None):
return [(x.lower(), x) for x in pytz.all_timezones] return [(x.lower(), x) for x in pytz.all_timezones]
@ -798,7 +802,7 @@ class calendar_event(osv.Model):
'location': { 'location': {
'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True, 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
}, },
'date': { 'date': {
'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True, 'calendar.subtype_invitation': lambda self, cr, uid, obj, ctx=None: True,
}, },
} }
@ -816,7 +820,7 @@ class calendar_event(osv.Model):
'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}), 'class': fields.selection([('public', 'Public'), ('private', 'Private'), ('confidential', 'Public for Employees')], 'Privacy', states={'done': [('readonly', True)]}),
'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}), 'location': fields.char('Location', help="Location of Event", track_visibility='onchange', states={'done': [('readonly', True)]}),
'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}), 'show_as': fields.selection([('free', 'Free'), ('busy', 'Busy')], 'Show Time as', states={'done': [('readonly', True)]}),
'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_rrule_write, store=True, string='Recurrent Rule'), 'rrule': fields.function(_get_rulestring, type='char', fnct_inv=_set_rulestring, store=True, string='Recurrent Rule'),
'rrule_type': fields.selection([('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)')], 'Recurrency', states={'done': [('readonly', True)]}, help="Let the event automatically repeat at that interval"), 'rrule_type': fields.selection([('daily', 'Day(s)'), ('weekly', 'Week(s)'), ('monthly', 'Month(s)'), ('yearly', 'Year(s)')], 'Recurrency', states={'done': [('readonly', True)]}, help="Let the event automatically repeat at that interval"),
'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"), 'recurrency': fields.boolean('Recurrent', help="Recurrent Meeting"),
'recurrent_id': fields.integer('Recurrent ID'), 'recurrent_id': fields.integer('Recurrent ID'),
@ -979,7 +983,7 @@ class calendar_event(osv.Model):
} }
return res return res
def get_search_fields(self,browse_event,order_fields,r_date=None): def get_search_fields(self, browse_event, order_fields, r_date=None):
sort_fields = {} sort_fields = {}
for ord in order_fields: for ord in order_fields:
if ord == 'id' and r_date: if ord == 'id' and r_date:
@ -989,17 +993,17 @@ class calendar_event(osv.Model):
'If we sort on FK, we obtain a browse_record, so we need to sort on name_get' 'If we sort on FK, we obtain a browse_record, so we need to sort on name_get'
if type(browse_event[ord]) is openerp.osv.orm.browse_record: if type(browse_event[ord]) is openerp.osv.orm.browse_record:
name_get = browse_event[ord].name_get() name_get = browse_event[ord].name_get()
if len(name_get) and len(name_get[0])>=2: if len(name_get) and len(name_get[0]) >= 2:
sort_fields[ord] = name_get[0][1] sort_fields[ord] = name_get[0][1]
return sort_fields return sort_fields
def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None): def get_recurrent_ids(self, cr, uid, event_id, domain, order=None, context=None):
"""Gives virtual event ids for recurring events """Gives virtual event ids for recurring events
This method gives ids of dates that comes between start date and end date of calendar views This method gives ids of dates that comes between start date and end date of calendar views
@param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted @param order: The fields (comma separated, format "FIELD {DESC|ASC}") on which the events should be sorted
""" """
if not context: if not context:
@ -1024,7 +1028,7 @@ class calendar_event(osv.Model):
for ev in self.browse(cr, uid, ids_to_browse, context=context): for ev in self.browse(cr, uid, ids_to_browse, context=context):
if not ev.recurrency or not ev.rrule: if not ev.recurrency or not ev.rrule:
result.append(ev.id) result.append(ev.id)
result_data.append(self.get_search_fields(ev,order_fields)) result_data.append(self.get_search_fields(ev, order_fields))
continue continue
rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context) rdates = self.get_recurrent_date_by_event(cr, uid, ev, context=context)
@ -1070,7 +1074,7 @@ class calendar_event(osv.Model):
if [True for item in new_pile if not item]: if [True for item in new_pile if not item]:
continue continue
result_data.append(self.get_search_fields(ev,order_fields,r_date=r_date)) result_data.append(self.get_search_fields(ev, order_fields, r_date=r_date))
if order_fields: if order_fields:
def comparer(left, right): def comparer(left, right):
@ -1081,7 +1085,7 @@ class calendar_event(osv.Model):
return 0 return 0
sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')] sort_params = [key.split()[0] if key[-4:].lower() != 'desc' else '-%s' % key.split()[0] for key in (order or self._order).split(',')]
comparers = [ ((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params] comparers = [((itemgetter(col[1:]), -1) if col[0] == '-' else (itemgetter(col), 1)) for col in sort_params]
ids = [r['id'] for r in sorted(result_data, cmp=comparer)] ids = [r['id'] for r in sorted(result_data, cmp=comparer)]
if isinstance(event_id, (str, int, long)): if isinstance(event_id, (str, int, long)):
@ -1089,7 +1093,6 @@ class calendar_event(osv.Model):
else: else:
return ids return ids
def compute_rule_string(self, data): def compute_rule_string(self, data):
""" """
Compute rule string according to value type RECUR of iCalendar from the values given. Compute rule string according to value type RECUR of iCalendar from the values given.
@ -1123,7 +1126,7 @@ class calendar_event(osv.Model):
data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z' data['end_date_new'] = ''.join((re.compile('\d')).findall(data.get('end_date'))) + 'T235959Z'
return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\ return (data.get('end_type') == 'count' and (';COUNT=' + str(data.get('count'))) or '') +\
((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '') ((data.get('end_date_new') and data.get('end_type') == 'end_date' and (';UNTIL=' + data.get('end_date_new'))) or '')
freq = data.get('rrule_type', False) # day/week/month/year freq = data.get('rrule_type', False) # day/week/month/year
res = '' res = ''
@ -1174,7 +1177,7 @@ class calendar_event(osv.Model):
#repeat monthly by nweekday ((weekday, weeknumber), ) #repeat monthly by nweekday ((weekday, weeknumber), )
if r._bynweekday: if r._bynweekday:
data['week_list'] = day_list[r._bynweekday[0][0]].upper() data['week_list'] = day_list[r._bynweekday[0][0]].upper()
data['byday'] = r._bynweekday[0][1] data['byday'] = str(r._bynweekday[0][1])
data['month_by'] = 'day' data['month_by'] = 'day'
data['rrule_type'] = 'monthly' data['rrule_type'] = 'monthly'
@ -1219,12 +1222,12 @@ class calendar_event(osv.Model):
res.update(self.check_partners_email(cr, uid, value[0][2], context=context)) res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
return res return res
def onchange_rec_day(self,cr,uid,id,date,mo,tu,we,th,fr,sa,su): def onchange_rec_day(self, cr, uid, id, date, mo, tu, we, th, fr, sa, su):
""" set the start date according to the first occurence of rrule""" """ set the start date according to the first occurence of rrule"""
rrule_obj = self._get_empty_rrule_data() rrule_obj = self._get_empty_rrule_data()
rrule_obj.update({ rrule_obj.update({
'byday':True, 'byday': True,
'rrule_type':'weekly', 'rrule_type': 'weekly',
'mo': mo, 'mo': mo,
'tu': tu, 'tu': tu,
'we': we, 'we': we,
@ -1232,12 +1235,11 @@ class calendar_event(osv.Model):
'fr': fr, 'fr': fr,
'sa': sa, 'sa': sa,
'su': su, 'su': su,
'interval':1 'interval': 1
}) })
str_rrule = self.compute_rule_string(rrule_obj) str_rrule = self.compute_rule_string(rrule_obj)
first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0] first_occurence = list(rrule.rrulestr(str_rrule + ";COUNT=1", dtstart=datetime.strptime(date, "%Y-%m-%d %H:%M:%S"), forceset=True))[0]
return {'value': { 'date' : first_occurence.strftime("%Y-%m-%d") + ' 00:00:00' } } return {'value': {'date': first_occurence.strftime("%Y-%m-%d") + ' 00:00:00'}}
def check_partners_email(self, cr, uid, partner_ids, context=None): def check_partners_email(self, cr, uid, partner_ids, context=None):
""" Verify that selected partner_ids have an email_address defined. """ Verify that selected partner_ids have an email_address defined.
@ -1251,12 +1253,10 @@ class calendar_event(osv.Model):
warning_msg = _('The following contacts have no email address :') warning_msg = _('The following contacts have no email address :')
for partner in partner_wo_email_lst: for partner in partner_wo_email_lst:
warning_msg += '\n- %s' % (partner.name) warning_msg += '\n- %s' % (partner.name)
return {'warning': return {'warning': {
{ 'title': _('Email addresses not found'),
'title': _('Email addresses not found'), 'message': warning_msg,
'message': warning_msg, }}
}
}
# ---------------------------------------- # ----------------------------------------
# OpenChatter # OpenChatter
@ -1365,8 +1365,7 @@ class calendar_event(osv.Model):
rrule_type=False, rrule_type=False,
rrule='', rrule='',
recurrency=False, recurrency=False,
end_date = datetime.strptime(values.get('date', False) or data.get('date'),"%Y-%m-%d %H:%M:%S") end_date=datetime.strptime(values.get('date', False) or data.get('date'), "%Y-%m-%d %H:%M:%S") + timedelta(hours=values.get('duration', False) or data.get('duration'))
+ timedelta(hours=values.get('duration', False) or data.get('duration'))
) )
#do not copy the id #do not copy the id
@ -1381,29 +1380,25 @@ class calendar_event(osv.Model):
new_id = self._detach_one_event(cr, uid, ids[0], context=context) new_id = self._detach_one_event(cr, uid, ids[0], context=context)
return { return {
'type': 'ir.actions.act_window', 'type': 'ir.actions.act_window',
'res_model': 'calendar.event', 'res_model': 'calendar.event',
'view_mode': 'form', 'view_mode': 'form',
'res_id': new_id, 'res_id': new_id,
'target': 'current', 'target': 'current',
'flags': {'form': {'action_buttons': True, 'options' : { 'mode' : 'edit' } } } 'flags': {'form': {'action_buttons': True, 'options': {'mode': 'edit'}}}
} }
def write(self, cr, uid, ids, values, context=None): def write(self, cr, uid, ids, values, context=None):
def _only_changes_to_apply_on_real_ids(field_names): def _only_changes_to_apply_on_real_ids(field_names):
''' return True if changes are only to be made on the real ids''' ''' return True if changes are only to be made on the real ids'''
for field in field_names: for field in field_names:
if field in ['date','active']: if field in ['date', 'active']:
return True return True
return False return False
context = context or {} context = context or {}
if isinstance(ids, (str,int, long)): if isinstance(ids, (str, int, long)):
if len(str(ids).split('-')) == 1: if len(str(ids).split('-')) == 1:
ids = [int(ids)] ids = [int(ids)]
else: else:
@ -1428,15 +1423,15 @@ class calendar_event(osv.Model):
continue continue
else: else:
data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration']) data = self.read(cr, uid, event_id, ['date', 'date_deadline', 'rrule', 'duration'])
if data.get('rrule'): if data.get('rrule'):
new_id = self._detach_one_event(cr, uid, event_id, values, context=None) new_id = self._detach_one_event(cr, uid, event_id, values, context=None)
res = super(calendar_event, self).write(cr, uid, ids, values, context=context) res = super(calendar_event, self).write(cr, uid, ids, values, context=context)
# set end_date for calendar searching # set end_date for calendar searching
if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \ if values.get('recurrency', True) and values.get('end_type', 'count') in ('count', unicode('count')) and \
(values.get('rrule_type') or values.get('count') or values.get('date') or values.get('date_deadline')): (values.get('rrule_type') or values.get('count') or values.get('date') or values.get('date_deadline')):
for data in self.read(cr, uid, ids, ['date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context): for data in self.read(cr, uid, ids, ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context):
end_date = self._get_recurrency_end_date(data, context=context) end_date = self._get_recurrency_end_date(data, context=context)
super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context) super(calendar_event, self).write(cr, uid, [data['id']], {'end_date': end_date}, context=context)
@ -1467,11 +1462,12 @@ class calendar_event(osv.Model):
if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user if not 'user_id' in vals: # Else bug with quick_create when we are filter on an other user
vals['user_id'] = uid vals['user_id'] = uid
if vals.get('recurrency', True) and vals.get('end_type', 'count') in ('count', unicode('count')) and \
(vals.get('rrule_type') or vals.get('count') or vals.get('date') or vals.get('date_deadline')):
vals['end_date'] = self._get_recurrency_end_date(vals, context=context)
res = super(calendar_event, self).create(cr, uid, vals, context=context) res = super(calendar_event, self).create(cr, uid, vals, context=context)
data = self.read(cr, uid, [res], ['end_date', 'date_deadline', 'recurrency', 'rrule_type', 'count', 'end_type'], context=context)[0]
end_date = self._get_recurrency_end_date(data, context=context)
self.write(cr, uid, [res], {'end_date': end_date}, context=context)
self.create_attendees(cr, uid, [res], context=context) self.create_attendees(cr, uid, [res], context=context)
return res return res
@ -1528,7 +1524,7 @@ class calendar_event(osv.Model):
continue continue
if r['class'] == 'private': if r['class'] == 'private':
for f in r.keys(): for f in r.keys():
if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count'): if f not in ('id', 'date', 'date_deadline', 'duration', 'user_id', 'state', 'interval', 'count', 'recurrent_id_date'):
if isinstance(r[f], list): if isinstance(r[f], list):
r[f] = [] r[f] = []
else: else:
@ -1554,7 +1550,7 @@ class calendar_event(osv.Model):
ids_to_unlink = [] ids_to_unlink = []
# One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it # One time moved to google_Calendar, we can specify, if not in google, and not rec or get_inst = 0, we delete it
for event_id in ids: for event_id in ids:
if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL if unlink_level == 1 and len(str(event_id).split('-')) == 1: # if ID REAL
if self.browse(cr, uid, event_id).recurrent_id: if self.browse(cr, uid, event_id).recurrent_id:
ids_to_exclure.append(event_id) ids_to_exclure.append(event_id)
@ -1587,7 +1583,7 @@ class mail_message(osv.Model):
def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None): def _find_allowed_model_wise(self, cr, uid, doc_model, doc_dict, context=None):
if doc_model == 'calendar.event': if doc_model == 'calendar.event':
order = context.get('order', self._order) order = context.get('order', self._order)
for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context): for virtual_id in self.pool[doc_model].get_recurrent_ids(cr, uid, doc_dict.keys(), [], order=order, context=context):
doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)]) doc_dict.setdefault(virtual_id, doc_dict[get_real_ids(virtual_id)])
return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context) return super(mail_message, self)._find_allowed_model_wise(cr, uid, doc_model, doc_dict, context=context)
@ -1619,26 +1615,27 @@ class ir_http(osv.AbstractModel):
def _auth_method_calendar(self): def _auth_method_calendar(self):
token = request.params['token'] token = request.params['token']
db = request.params['db'] db = request.params['db']
registry = openerp.modules.registry.RegistryManager.get(db) registry = openerp.modules.registry.RegistryManager.get(db)
attendee_pool = registry.get('calendar.attendee') attendee_pool = registry.get('calendar.attendee')
error_message = False error_message = False
with registry.cursor() as cr: with registry.cursor() as cr:
attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token)]) attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)])
if not attendee_id: if not attendee_id:
error_message = """Invalid Invitation Token.""" error_message = """Invalid Invitation Token."""
elif request.session.uid and request.session.login != 'anonymous': elif request.session.uid and request.session.login != 'anonymous':
# if valid session but user is not match # if valid session but user is not match
attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0]) attendee = attendee_pool.browse(cr, openerp.SUPERUSER_ID, attendee_id[0])
user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid) user = registry.get('res.users').browse(cr, openerp.SUPERUSER_ID, request.session.uid)
if attendee.partner_id.id != user.partner_id.id: if attendee.partner_id.id != user.partner_id.id:
error_message = """Invitation cannot be forwarded via email. This event/meeting belongs to %s and you are logged in as %s. Please ask organizer to add you.""" % (attendee.email, user.email) error_message = """Invitation cannot be forwarded via email. This event/meeting belongs to %s and you are logged in as %s. Please ask organizer to add you.""" % (attendee.email, user.email)
if error_message: if error_message:
raise BadRequest(error_message) raise BadRequest(error_message)
return True return True
class invite_wizard(osv.osv_memory): class invite_wizard(osv.osv_memory):
_inherit = 'mail.wizard.invite' _inherit = 'mail.wizard.invite'

View File

@ -136,7 +136,7 @@
<strong>${object.event_id.name}</strong> <strong>${object.event_id.name}</strong>
</div> </div>
<div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px"> <div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px">
<strong style="margin-left:12px">Hello ${object.cn}</strong> ,<br/><p style="margin-left:12px">${object.event_id.user_id.partner_id.name} invited you for the ${object.event_id.name} meeting of ${object.event_id.user_id.company_id.name}.</p> <strong style="margin-left:12px">Dear ${object.cn}</strong> ,<br/><p style="margin-left:12px">${object.event_id.user_id.partner_id.name} invited you for the ${object.event_id.name} meeting of ${object.event_id.user_id.company_id.name}.</p>
</div> </div>
<div style="height: auto;margin-left:12px;margin-top:30px;"> <div style="height: auto;margin-left:12px;margin-top:30px;">
<table> <table>
@ -151,50 +151,58 @@
</td> </td>
<td> <td>
<table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;"> <table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;">
% if object.event_id.location: <tr>
<tr style=" height: 30px;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.location:
Where <div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> Where
</td> </div>
<td colspan="1" style="vertical-align:top;"> % endif
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
</td> </td>
<td style="vertical-align:top;">
% if object.event_id.location:
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% endif
</td>
</tr> </tr>
% endif
% if object.event_id.description : <tr>
<tr style=" height:auto;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.description :
What <div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> What
</div>
% endif
</td> </td>
<td colspan="3" style="vertical-align:text-top;"> <td style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.description :
: ${object.event_id.description} <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> : ${object.event_id.description}
</div>
% endif
</td> </td>
</tr> </tr>
% endif
% if not object.event_id.allday and object.event_id.duration: <tr>
<tr style=" height:auto;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if not object.event_id.allday and object.event_id.duration:
Duration <div style="height:auto; width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> Duration
</div>
% endif
</td> </td>
<td colspan="3" style="vertical-align:text-top;"> <td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if not object.event_id.allday and object.event_id.duration:
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))} <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> : ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% endif
</td> </td>
</tr> </tr>
% endif
<tr style=" height: 30px;"> <tr style=" height: 30px;">
<td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> <td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
<div> <div>
@ -219,9 +227,9 @@
</table> </table>
</div> </div>
<div style="height: auto;width:450px; margin:0 auto;padding-top:20px;padding-bottom:40px;"> <div style="height: auto;width:450px; margin:0 auto;padding-top:20px;padding-bottom:40px;">
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#8A89BA;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a> <a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#8A89BA;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a> <a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a> <a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a>
</div> </div>
</div> </div>
</body> </body>
@ -259,13 +267,13 @@
<strong>${object.event_id.name}</strong> <strong>${object.event_id.name}</strong>
</div> </div>
<div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px"> <div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px">
<strong style="margin-left:12px">Hello ${object.cn}</strong> ,<br/> <strong style="margin-left:12px">Dear ${object.cn}</strong> ,<br/>
<p style="margin-left:12px">The date of the meeting has been changed...<br/> <p style="margin-left:12px">The date of the meeting has been changed...<br/>
The meeting created by ${object.event_id.user_id.partner_id.name} is now scheduled for : ${object.event_id.date}.</p> The meeting created by ${object.event_id.user_id.partner_id.name} is now scheduled for : ${object.event_id.display_time}.</p>
</div> </div>
<div style="height: auto;margin-left:12px;margin-top:30px;"> <div style="height: auto;margin-left:12px;margin-top:30px;">
<table> <table>
<tr> <tr>
<td> <td>
<div style="border-top-left-radius:3px;border-top-right-radius:3px;font-size:12px;border-collapse:separate;text-align:center;font-weight:bold;color:#ffffff;width:130px;min-height: 18px;border-color:#ffffff;background:#8a89ba;padding-top: 4px;">${object.event_id.get_interval(object.event_id.date, 'dayname')}</div> <div style="border-top-left-radius:3px;border-top-right-radius:3px;font-size:12px;border-collapse:separate;text-align:center;font-weight:bold;color:#ffffff;width:130px;min-height: 18px;border-color:#ffffff;background:#8a89ba;padding-top: 4px;">${object.event_id.get_interval(object.event_id.date, 'dayname')}</div>
<div style="font-size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F;background-color: #E1E2F8;width: 130px;"> <div style="font-size:48px;min-height:auto;font-weight:bold;text-align:center;color: #5F5F5F;background-color: #E1E2F8;width: 130px;">
@ -276,50 +284,58 @@
</td> </td>
<td> <td>
<table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;"> <table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;">
% if object.event_id.location: <tr>
<tr style=" height: 30px;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.location:
Where <div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> Where
</td> </div>
<td colspan="1" style="vertical-align:top;"> % endif
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
</td> </td>
<td style="vertical-align:top;">
% if object.event_id.location:
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% endif
</td>
</tr> </tr>
% endif
% if object.event_id.description : <tr>
<tr style=" height:auto;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.description :
What <div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> What
</div>
% endif
</td> </td>
<td colspan="3" style="vertical-align:text-top;"> <td style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.description :
: ${object.event_id.description} <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> : ${object.event_id.description}
</div>
% endif
</td> </td>
</tr> </tr>
% endif
% if not object.event_id.allday and object.event_id.duration: <tr>
<tr style=" height:auto;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if not object.event_id.allday and object.event_id.duration:
Duration <div style="height:auto; width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> Duration
</div>
% endif
</td> </td>
<td colspan="3" style="vertical-align:text-top;"> <td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if not object.event_id.allday and object.event_id.duration:
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))} <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> : ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% endif
</td> </td>
</tr> </tr>
% endif
<tr style=" height: 30px;"> <tr style=" height: 30px;">
<td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> <td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
<div> <div>
@ -341,12 +357,12 @@
</table> </table>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
<div style="height: auto;width:450px; margin:0 auto;padding-top:20px;padding-bottom:40px;"> <div style="height: auto;width:450px; margin:0 auto;padding-top:20px;padding-bottom:40px;">
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#8A89BA;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a> <a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#8A89BA;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/accept?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Accept</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a> <a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#808080;margin : 0 15px 0 0;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/decline?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">Decline</a>
<a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="${ctx['base_url']}/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a> <a style="padding: 8px 30px 8px 30px;border-radius: 6px;border: 1px solid #CCCCCC;background:#D8D8D8;text-decoration: none;color:#FFFFFF;" href="/calendar/meeting/view?db=${ctx['dbname']}&token=${object.access_token}&action=${ctx['action_id']}&id=${object.event_id.id}">View</a>
</div> </div>
</div> </div>
</body> </body>
@ -384,8 +400,8 @@
<strong>${object.event_id.name}</strong> <strong>${object.event_id.name}</strong>
</div> </div>
<div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px"> <div style="height: 50px;text-align: left;font-size : 14px;border-collapse: separate;margin-top:10px">
<strong style="margin-left:12px">Hello ${object.cn}</strong> ,<br/> <strong style="margin-left:12px">Dear ${object.cn}</strong> ,<br/>
<p style="margin-left:12px">this it a rmeinder for the event below : </p> <p style="margin-left:12px">That is a reminder for the event below : </p>
</div> </div>
<div style="height: auto;margin-left:12px;margin-top:30px;"> <div style="height: auto;margin-left:12px;margin-top:30px;">
<table> <table>
@ -400,50 +416,58 @@
</td> </td>
<td> <td>
<table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;"> <table cellspacing="0" cellpadding="0" border="0" style="margin-top: 15px; margin-left: 10px;font-size: 16px;">
% if object.event_id.location: <tr>
<tr style=" height: 30px;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height: 25px; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.location:
Where <div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> Where
</td> </div>
<td colspan="1" style="vertical-align:top;"> % endif
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
</td> </td>
<td style="vertical-align:top;">
% if object.event_id.location:
<div style = "font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 14px" >
: ${object.event_id.location}
<span style= "color:#A9A9A9; ">(<a href="http://maps.google.com/maps?oi=map&q=${object.event_id.location}">View Map</a>)
</span>
</div>
% endif
</td>
</tr> </tr>
% endif
% if object.event_id.description : <tr>
<tr style=" height:auto;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.description :
What <div style="width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> What
</div>
% endif
</td> </td>
<td colspan="3" style="vertical-align:text-top;"> <td style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if object.event_id.description :
: ${object.event_id.description} <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> : ${object.event_id.description}
</div>
% endif
</td> </td>
</tr> </tr>
% endif
% if not object.event_id.allday and object.event_id.duration: <tr>
<tr style=" height:auto;">
<td style="vertical-align:top;"> <td style="vertical-align:top;">
<div style="height:auto; width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if not object.event_id.allday and object.event_id.duration:
Duration <div style="height:auto; width: 120px; background : #CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> Duration
</div>
% endif
</td> </td>
<td colspan="3" style="vertical-align:text-top;"> <td colspan="3" style="vertical-align:text-top;">
<div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> % if not object.event_id.allday and object.event_id.duration:
: ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))} <div style="font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
</div> : ${('%dH%02d' % (object.event_id.duration,(object.event_id.duration*60)%60))}
</div>
% endif
</td> </td>
</tr> </tr>
% endif
<tr style=" height: 30px;"> <tr style=" height: 30px;">
<td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;"> <td style="height: 25px;width: 120px; background : # CCCCCC; font-family: Lucica Grande', Ubuntu, Arial, Verdana, sans-serif;">
<div> <div>

View File

@ -20,7 +20,7 @@
<record id="calendar_event_1" model="calendar.event"> <record id="calendar_event_1" model="calendar.event">
<field eval="1" name="active"/> <field eval="1" name="active"/>
<field name="user_id" ref="base.user_root"/> <field name="user_id" ref="base.user_root"/>
<field name="partner_ids" eval="[(6,0,[ref('base.partner_root'),ref('base.res_partner_1'),ref('base.res_partner_6')])]"/> <field name="partner_ids" eval="[(6,0,[ref('base.res_partner_6')])]"/>
<field name="name">Follow-up for Project proposal</field> <field name="name">Follow-up for Project proposal</field>
<field name="description">Meeting to discuss project plan and hash out the details of implementation.</field> <field name="description">Meeting to discuss project plan and hash out the details of implementation.</field>
<field eval="time.strftime('%Y-%m-03 10:20:00')" name="date"/> <field eval="time.strftime('%Y-%m-03 10:20:00')" name="date"/>

View File

@ -1,21 +1,19 @@
import simplejson import simplejson
import urllib
import openerp import openerp
import openerp.addons.web.http as http import openerp.addons.web.http as http
from openerp.addons.web.http import request from openerp.addons.web.http import request
import openerp.addons.web.controllers.main as webmain import openerp.addons.web.controllers.main as webmain
import json import json
from openerp.addons.web.http import SessionExpiredException
from werkzeug.exceptions import BadRequest
class meeting_invitation(http.Controller): class meeting_invitation(http.Controller):
@http.route('/calendar/meeting/accept', type='http', auth="calendar") @http.route('/calendar/meeting/accept', type='http', auth="calendar")
def accept(self, db, token, action, id,**kwargs): def accept(self, db, token, action, id, **kwargs):
registry = openerp.modules.registry.RegistryManager.get(db) registry = openerp.modules.registry.RegistryManager.get(db)
attendee_pool = registry.get('calendar.attendee') attendee_pool = registry.get('calendar.attendee')
with registry.cursor() as cr: with registry.cursor() as cr:
attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token),('state','!=', 'accepted')]) attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token), ('state', '!=', 'accepted')])
if attendee_id: if attendee_id:
attendee_pool.do_accept(cr, openerp.SUPERUSER_ID, attendee_id) attendee_pool.do_accept(cr, openerp.SUPERUSER_ID, attendee_id)
return self.view(db, token, action, id, view='form') return self.view(db, token, action, id, view='form')
@ -25,7 +23,7 @@ class meeting_invitation(http.Controller):
registry = openerp.modules.registry.RegistryManager.get(db) registry = openerp.modules.registry.RegistryManager.get(db)
attendee_pool = registry.get('calendar.attendee') attendee_pool = registry.get('calendar.attendee')
with registry.cursor() as cr: with registry.cursor() as cr:
attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token','=',token),('state','!=', 'declined')]) attendee_id = attendee_pool.search(cr, openerp.SUPERUSER_ID, [('access_token', '=', token), ('state', '!=', 'declined')])
if attendee_id: if attendee_id:
attendee_pool.do_decline(cr, openerp.SUPERUSER_ID, attendee_id) attendee_pool.do_decline(cr, openerp.SUPERUSER_ID, attendee_id)
return self.view(db, token, action, id, view='form') return self.view(db, token, action, id, view='form')
@ -36,13 +34,13 @@ class meeting_invitation(http.Controller):
meeting_pool = registry.get('calendar.event') meeting_pool = registry.get('calendar.event')
attendee_pool = registry.get('calendar.attendee') attendee_pool = registry.get('calendar.attendee')
with registry.cursor() as cr: with registry.cursor() as cr:
attendee_data = meeting_pool.get_attendee(cr, openerp.SUPERUSER_ID, id); attendee_data = meeting_pool.get_attendee(cr, openerp.SUPERUSER_ID, id)
attendee = attendee_pool.search_read(cr, openerp.SUPERUSER_ID, [('access_token','=',token)],[]) attendee = attendee_pool.search_read(cr, openerp.SUPERUSER_ID, [('access_token', '=', token)], [])
if attendee: if attendee:
attendee_data['current_attendee'] = attendee[0] attendee_data['current_attendee'] = attendee[0]
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in webmain.manifest_list('js', db=db)) js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in webmain.manifest_list('js', db=db))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in webmain.manifest_list('css',db=db)) css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in webmain.manifest_list('css', db=db))
return webmain.html_template % { return webmain.html_template % {
'js': js, 'js': js,
@ -50,7 +48,7 @@ class meeting_invitation(http.Controller):
'modules': simplejson.dumps(webmain.module_boot(db)), 'modules': simplejson.dumps(webmain.module_boot(db)),
'init': "s.calendar.event('%s', '%s', '%s', '%s' , '%s');" % (db, action, id, 'form', json.dumps(attendee_data)), 'init': "s.calendar.event('%s', '%s', '%s', '%s' , '%s');" % (db, action, id, 'form', json.dumps(attendee_data)),
} }
# Function used, in RPC to check every 5 minutes, if notification to do for an event or not # Function used, in RPC to check every 5 minutes, if notification to do for an event or not
@http.route('/calendar/notify', type='json', auth="none") @http.route('/calendar/notify', type='json', auth="none")
def notify(self): def notify(self):
@ -58,15 +56,14 @@ class meeting_invitation(http.Controller):
uid = request.session.uid uid = request.session.uid
context = request.session.context context = request.session.context
with registry.cursor() as cr: with registry.cursor() as cr:
res = registry.get("calendar.alarm_manager").get_next_notif(cr,uid,context=context) res = registry.get("calendar.alarm_manager").get_next_notif(cr, uid, context=context)
return res return res
@http.route('/calendar/notify_ack', type='json', auth="none") @http.route('/calendar/notify_ack', type='json', auth="none")
def notify_ack(self, type=''): def notify_ack(self, type=''):
registry = openerp.modules.registry.RegistryManager.get(request.session.db) registry = openerp.modules.registry.RegistryManager.get(request.session.db)
uid = request.session.uid uid = request.session.uid
context = request.session.context context = request.session.context
with registry.cursor() as cr: with registry.cursor() as cr:
res = registry.get("res.partner").calendar_last_notif(cr,uid,context=context) res = registry.get("res.partner").calendar_last_notif_ack(cr, uid, context=context)
return res return res

View File

@ -1,17 +1,20 @@
openerp.calendar = function(instance) { openerp.calendar = function(instance) {
var _t = instance.web._t; var _t = instance.web._t;
var QWeb = instance.web.qweb; var QWeb = instance.web.qweb;
instance.calendar = {}; instance.calendar = {};
instance.web.WebClient = instance.web.WebClient.extend({ instance.web.WebClient = instance.web.WebClient.extend({
get_notif_box: function(me) { get_notif_box: function(me) {
return $(me).closest(".ui-notify-message-style"); return $(me).closest(".ui-notify-message-style");
}, },
get_next_notif: function() { get_next_notif: function() {
var self= this; var self= this;
this.rpc("/calendar/notify") this.rpc("/calendar/notify")
.then( .done(
function(result) { function(result) {
_.each(result, function(res) { _.each(result, function(res) {
setTimeout(function() { setTimeout(function() {
@ -44,21 +47,33 @@ openerp.calendar = function(instance) {
},res.timer * 1000); },res.timer * 1000);
}); });
} }
); )
.fail(function (err, ev) {
if (err.code === -32098) {
// Prevent the CrashManager to display an error
// in case of an xhr error not due to a server error
ev.preventDefault();
}
});
}, },
check_notifications: function() { check_notifications: function() {
var self= this; var self= this;
self.get_next_notif(); self.get_next_notif();
setInterval(function(){ self.intervalNotif = setInterval(function(){
self.get_next_notif(); self.get_next_notif();
}, 5 * 60 * 1000 ); }, 5 * 60 * 1000 );
}, },
//Override the show_application of addons/web/static/src/js/chrome.js //Override the show_application of addons/web/static/src/js/chrome.js
show_application: function() { show_application: function() {
this._super(); this._super();
this.check_notifications(); this.check_notifications();
}, },
//Override addons/web/static/src/js/chrome.js
on_logout: function() {
this._super();
clearInterval(self.intervalNotif);
},
}); });

View File

@ -65,3 +65,20 @@
- -
!python {model: calendar.event}: | !python {model: calendar.event}: |
self.write(cr, uid, [ref("calendar_event_alldaytestevent0")], {'alarm_ids': [(6,0,[ref("res_alarm_daybeforeeventstarts0")])]}) self.write(cr, uid, [ref("calendar_event_alldaytestevent0")], {'alarm_ids': [(6,0,[ref("res_alarm_daybeforeeventstarts0")])]})
-
I create a recuring rule for my event
-
!record {model: calendar.event, id: calendar.event_sprintreview1}:
name: Begin of month meeting
date: !eval time.strftime('%Y-%m-%d 12:00:00')
recurrency: true
rrule: FREQ=MONTHLY;INTERVAL=1;COUNT=12;BYDAY=1MO
-
I check that the attributes are set correctly
-
!assert {model: calendar.event, id: calendar.event_sprintreview1}:
- rrule_type == 'monthly'
- count == 12
- month_by == 'day'
- byday == '1'
- week_list == 'MO'

View File

@ -23,15 +23,13 @@ from openerp.osv import fields, osv
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
#
# calendar.event is defined in module calendar
#
class calendar_event(osv.Model): class calendar_event(osv.Model):
""" Model for Calendar Event """ """ Model for Calendar Event """
_inherit = 'calendar.event' _inherit = 'calendar.event'
_columns = { _columns = {
'phonecall_id': fields.many2one ('crm.phonecall', 'Phonecall'), 'phonecall_id': fields.many2one('crm.phonecall', 'Phonecall'),
'opportunity_id': fields.many2one ('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]"), 'opportunity_id': fields.many2one('crm.lead', 'Opportunity', domain="[('type', '=', 'opportunity')]"),
} }
def create(self, cr, uid, vals, context=None): def create(self, cr, uid, vals, context=None):
@ -48,31 +46,11 @@ class calendar_attendee(osv.osv):
_inherit = 'calendar.attendee' _inherit = 'calendar.attendee'
_description = 'Calendar Attendee' _description = 'Calendar Attendee'
def _compute_data(self, cr, uid, ids, name, arg, context=None): def _noop(self, cr, uid, ids, name, arg, context=None):
""" return dict.fromkeys(ids, False)
@param self: The object pointer
@param cr: the current row, from the database cursor,
@param uid: the current users ID for security checks,
@param ids: List of compute datas IDs
@param context: A standard dictionary for contextual values
"""
name = name[0]
result = super(calendar_attendee, self)._compute_data(cr, uid, ids, name, arg, context=context)
for attdata in self.browse(cr, uid, ids, context=context):
id = attdata.id
result[id] = {}
if name == 'categ_id':
if attdata.ref and 'categ_id' in attdata.ref._columns:
result[id][name] = (attdata.ref.categ_id.id, attdata.ref.categ_id.name,)
else:
result[id][name] = False
return result
_columns = { _columns = {
'categ_id': fields.function(_compute_data, \ 'categ_id': fields.function(_noop, string='Event Type', deprecated="Unused Field - TODO : Remove it in trunk", type="many2one", relation="crm.case.categ"),
string='Event Type', type="many2one", \
relation="crm.case.categ", multi='categ_id'),
} }
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -313,7 +313,10 @@ class crm_lead(format_address, osv.osv):
stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context) stage = self.pool.get('crm.case.stage').browse(cr, uid, stage_id, context=context)
if not stage.on_change: if not stage.on_change:
return {'value': {}} return {'value': {}}
return {'value': {'probability': stage.probability}} vals = {'probability': stage.probability}
if stage.probability >= 100 or (stage.probability == 0 and stage.sequence > 1):
vals['date_closed'] = fields.datetime.now()
return {'value': vals}
def on_change_partner_id(self, cr, uid, ids, partner_id, context=None): def on_change_partner_id(self, cr, uid, ids, partner_id, context=None):
values = {} values = {}

View File

@ -95,7 +95,6 @@
<form string="Leads Form" version="7.0"> <form string="Leads Form" version="7.0">
<header> <header>
<button name="%(crm.action_crm_lead2opportunity_partner)d" string="Convert to Opportunity" type="action" <button name="%(crm.action_crm_lead2opportunity_partner)d" string="Convert to Opportunity" type="action"
attrs="{'invisible': [('probability', '=', 100)]}"
help="Convert to Opportunity" class="oe_highlight"/> help="Convert to Opportunity" class="oe_highlight"/>
<field name="stage_id" widget="statusbar" clickable="True" <field name="stage_id" widget="statusbar" clickable="True"
domain="['&amp;', '|', ('case_default', '=', True), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]" domain="['&amp;', '|', ('case_default', '=', True), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"

View File

@ -58,7 +58,7 @@
<field name="view_mode">tree,calendar</field> <field name="view_mode">tree,calendar</field>
<field name="view_id" ref="crm_case_inbound_phone_tree_view"/> <field name="view_id" ref="crm_case_inbound_phone_tree_view"/>
<field name="domain">[]</field> <field name="domain">[]</field>
<field name="context">{}</field> <field name="context">{'search_default_state': 'done', 'default_state': 'done'}</field>
<field name="search_view_id" ref="crm.view_crm_case_phonecalls_filter"/> <field name="search_view_id" ref="crm.view_crm_case_phonecalls_filter"/>
<field name="help" type="html"> <field name="help" type="html">
<p class="oe_view_nocontent_create"> <p class="oe_view_nocontent_create">

View File

@ -167,6 +167,7 @@
<search string="Search Phonecalls"> <search string="Search Phonecalls">
<field name="name" string="Phonecalls"/> <field name="name" string="Phonecalls"/>
<field name="date"/> <field name="date"/>
<field name="state"/>
<separator/> <separator/>
<filter icon="terp-gtk-go-back-rtl" string="To Do" name="current" domain="[('state','=','open')]"/> <filter icon="terp-gtk-go-back-rtl" string="To Do" name="current" domain="[('state','=','open')]"/>
<separator/> <separator/>

View File

@ -66,7 +66,7 @@
- -
!python {model: crm.lead2opportunity.partner.mass}: | !python {model: crm.lead2opportunity.partner.mass}: |
context.update({'active_model': 'crm.lead', 'active_ids': [ref("test_crm_lead_01"), ref("test_crm_lead_02"), ref("test_crm_lead_03"), ref("test_crm_lead_04"), ref("test_crm_lead_05"), ref("test_crm_lead_06")], 'active_id': ref("test_crm_lead_01")}) context.update({'active_model': 'crm.lead', 'active_ids': [ref("test_crm_lead_01"), ref("test_crm_lead_02"), ref("test_crm_lead_03"), ref("test_crm_lead_04"), ref("test_crm_lead_05"), ref("test_crm_lead_06")], 'active_id': ref("test_crm_lead_01")})
id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('test_res_user_01'), ref('test_res_user_02'), ref('test_res_user_03'), ref('test_res_user_04')])], 'section_id': ref('crm.section_sales_department'), 'deduplicate': False}, context=context) id = self.create(cr, uid, {'user_ids': [(6, 0, [ref('test_res_user_01'), ref('test_res_user_02'), ref('test_res_user_03'), ref('test_res_user_04')])], 'section_id': ref('crm.section_sales_department'), 'deduplicate': False, 'force_assignation': True}, context=context)
self.mass_convert(cr, uid, [id], context=context) self.mass_convert(cr, uid, [id], context=context)
- -
The leads should now be opps with a salesman and a salesteam. Also, salesmen should have been assigned following a round-robin method. The leads should now be opps with a salesman and a salesteam. Also, salesmen should have been assigned following a round-robin method.

View File

@ -21,6 +21,7 @@
from openerp.osv import fields, osv from openerp.osv import fields, osv
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.tools import email_split
import re import re
class crm_lead2opportunity_partner(osv.osv_memory): class crm_lead2opportunity_partner(osv.osv_memory):
@ -41,21 +42,25 @@ class crm_lead2opportunity_partner(osv.osv_memory):
def onchange_action(self, cr, uid, ids, action, context=None): def onchange_action(self, cr, uid, ids, action, context=None):
return {'value': {'partner_id': False if action != 'exist' else self._find_matching_partner(cr, uid, context=context)}} return {'value': {'partner_id': False if action != 'exist' else self._find_matching_partner(cr, uid, context=context)}}
def _get_duplicated_leads(self, cr, uid, partner_id, email, context=None): def _get_duplicated_leads(self, cr, uid, partner_id, email, include_lost=False, context=None):
"""
Search for opportunities that have the same partner and that arent done or cancelled
"""
lead_obj = self.pool.get('crm.lead') lead_obj = self.pool.get('crm.lead')
results = [] emails = set(email_split(email) + [email])
final_stage_domain = [('stage_id.probability', '<', 100), '|', ('stage_id.probability', '>', 0), ('stage_id.sequence', '<=', 1)]
partner_match_domain = []
for email in emails:
partner_match_domain.append(('email_from', '=ilike', email))
if partner_id: if partner_id:
# Search for opportunities that have the same partner and that arent done or cancelled partner_match_domain.append(('partner_id', '=', partner_id))
ids = lead_obj.search(cr, uid, [('partner_id', '=', partner_id), '|', ('probability', '=', False), ('probability', '<', '100')]) partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain
for id in ids: if not partner_match_domain:
results.append(id) return []
email = re.findall(r'([^ ,<@]+@[^> ,]+)', email or '') domain = partner_match_domain
if email: if not include_lost:
ids = lead_obj.search(cr, uid, [('email_from', '=ilike', email[0]), '|', ('probability', '=', False), ('probability', '<', '100')]) domain += final_stage_domain
for id in ids: return lead_obj.search(cr, uid, domain)
results.append(id)
return list(set(results))
def default_get(self, cr, uid, fields, context=None): def default_get(self, cr, uid, fields, context=None):
""" """
@ -69,12 +74,11 @@ class crm_lead2opportunity_partner(osv.osv_memory):
if context.get('active_id'): if context.get('active_id'):
tomerge = [int(context['active_id'])] tomerge = [int(context['active_id'])]
email = False
partner_id = res.get('partner_id') partner_id = res.get('partner_id')
lead = lead_obj.browse(cr, uid, int(context['active_id']), context=context) lead = lead_obj.browse(cr, uid, int(context['active_id']), context=context)
email = lead.partner_id and lead.partner_id.email or lead.email_from
#TOFIX: use mail.mail_message.to_mail tomerge.extend(self._get_duplicated_leads(cr, uid, partner_id, email, include_lost=True, context=context))
tomerge.extend(self._get_duplicated_leads(cr, uid, partner_id, email))
tomerge = list(set(tomerge)) tomerge = list(set(tomerge))
if 'action' in fields: if 'action' in fields:
@ -100,10 +104,8 @@ class crm_lead2opportunity_partner(osv.osv_memory):
else: else:
user_in_section = False user_in_section = False
if not user_in_section: if not user_in_section:
section_id = False result = self.pool['crm.lead'].on_change_user(cr, uid, ids, user_id, context=context)
section_ids = self.pool.get('crm.case.section').search(cr, uid, ['|', ('user_id', '=', user_id), ('member_ids', '=', user_id)], context=context) section_id = result.get('value') and result['value'].get('section_id') and result['value']['section_id'] or False
if section_ids:
section_id = section_ids[0]
return {'value': {'section_id': section_id}} return {'value': {'section_id': section_id}}
def view_init(self, cr, uid, fields, context=None): def view_init(self, cr, uid, fields, context=None):
@ -126,14 +128,17 @@ class crm_lead2opportunity_partner(osv.osv_memory):
lead_ids = vals.get('lead_ids', []) lead_ids = vals.get('lead_ids', [])
team_id = vals.get('section_id', False) team_id = vals.get('section_id', False)
data = self.browse(cr, uid, ids, context=context)[0] data = self.browse(cr, uid, ids, context=context)[0]
for lead_id in lead_ids: leads = lead.browse(cr, uid, lead_ids, context=context)
partner_id = self._create_partner(cr, uid, lead_id, data.action, data.partner_id, context=context) for lead_id in leads:
# FIXME: cannot pass user_ids as the salesman allocation only works in batch partner_id = self._create_partner(cr, uid, lead_id.id, data.action, lead_id.partner_id.id, context=context)
res = lead.convert_opportunity(cr, uid, [lead_id], partner_id, [], team_id, context=context) res = lead.convert_opportunity(cr, uid, [lead_id.id], partner_id, [], False, context=context)
# FIXME: must perform salesman allocation in batch separately here
user_ids = vals.get('user_ids', False) user_ids = vals.get('user_ids', False)
if context.get('no_force_assignation'):
leads_to_allocate = [lead_id.id for lead_id in leads if not lead_id.user_id]
else:
leads_to_allocate = lead_ids
if user_ids: if user_ids:
lead.allocate_salesman(cr, uid, lead_ids, user_ids, team_id=team_id, context=context) lead.allocate_salesman(cr, uid, leads_to_allocate, user_ids, team_id=team_id, context=context)
return res return res
def action_apply(self, cr, uid, ids, context=None): def action_apply(self, cr, uid, ids, context=None):
@ -144,15 +149,19 @@ class crm_lead2opportunity_partner(osv.osv_memory):
if context is None: if context is None:
context = {} context = {}
lead_obj = self.pool['crm.lead']
w = self.browse(cr, uid, ids, context=context)[0] w = self.browse(cr, uid, ids, context=context)[0]
opp_ids = [o.id for o in w.opportunity_ids] opp_ids = [o.id for o in w.opportunity_ids]
if w.name == 'merge': if w.name == 'merge':
lead_id = self.pool.get('crm.lead').merge_opportunity(cr, uid, opp_ids, w.user_id.id, w.section_id.id, context=context) lead_id = lead_obj.merge_opportunity(cr, uid, opp_ids, context=context)
lead_ids = [lead_id] lead_ids = [lead_id]
lead = self.pool.get('crm.lead').read(cr, uid, lead_id, ['type'], context=context) lead = lead_obj.read(cr, uid, lead_id, ['type', 'user_id'], context=context)
if lead['type'] == "lead": if lead['type'] == "lead":
context.update({'active_ids': lead_ids}) context.update({'active_ids': lead_ids})
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'section_id': w.section_id.id}, context=context) self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'section_id': w.section_id.id}, context=context)
elif not context.get('no_force_assignation') or not lead['user_id']:
lead_obj.write(cr, uid, lead_id, {'user_id': w.user_id.id, 'section_id': w.section_id.id}, context=context)
else: else:
lead_ids = context.get('active_ids', []) lead_ids = context.get('active_ids', [])
self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'section_id': w.section_id.id}, context=context) self._convert_opportunity(cr, uid, ids, {'lead_ids': lead_ids, 'user_ids': [w.user_id.id], 'section_id': w.section_id.id}, context=context)
@ -186,11 +195,12 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
_columns = { _columns = {
'user_ids': fields.many2many('res.users', string='Salesmen'), 'user_ids': fields.many2many('res.users', string='Salesmen'),
'section_id': fields.many2one('crm.case.section', 'Sales Team'), 'section_id': fields.many2one('crm.case.section', 'Sales Team'),
'deduplicate': fields.boolean('Apply deduplication', help='Merge with existing leads/opportunities of each partner'), 'deduplicate': fields.boolean('Apply deduplication', help='Merge with existing leads/opportunities of each partner'),
'action': fields.selection([ 'action': fields.selection([
('each_exist_or_create', 'Use existing partner or create'), ('each_exist_or_create', 'Use existing partner or create'),
('nothing', 'Do not link to a customer') ('nothing', 'Do not link to a customer')
], 'Related Customer', required=True), ], 'Related Customer', required=True),
'force_assignation': fields.boolean('Force assignation', help='If unchecked, this will leave the salesman of duplicated opportunities'),
} }
_defaults = { _defaults = {
@ -266,6 +276,7 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
active_ids = active_ids.difference(merged_lead_ids) active_ids = active_ids.difference(merged_lead_ids)
active_ids = active_ids.union(remaining_lead_ids) active_ids = active_ids.union(remaining_lead_ids)
ctx['active_ids'] = list(active_ids) ctx['active_ids'] = list(active_ids)
ctx['no_force_assignation'] = not data.force_assignation
return self.action_apply(cr, uid, ids, context=ctx) return self.action_apply(cr, uid, ids, context=ctx)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -58,6 +58,8 @@
<group string="Assign opportunities to"> <group string="Assign opportunities to">
<field name="section_id" groups="base.group_multi_salesteams"/> <field name="section_id" groups="base.group_multi_salesteams"/>
<field name="user_ids" widget="many2many_tags"/> <field name="user_ids" widget="many2many_tags"/>
<!-- Uncomment me in trunk -->
<!-- <field name="force_assignation" /> -->
</group> </group>
<label for="opportunity_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/> <label for="opportunity_ids" string="Leads with existing duplicates (for information)" help="Leads that you selected that have duplicates. If the list is empty, it means that no duplicates were found" attrs="{'invisible': [('deduplicate', '=', False)]}"/>
<group attrs="{'invisible': [('deduplicate', '=', False)]}"> <group attrs="{'invisible': [('deduplicate', '=', False)]}">

View File

@ -9,7 +9,7 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Merge Leads/Opportunities" version="7.0"> <form string="Merge Leads/Opportunities" version="7.0">
<group string="Assign opportunities to"> <group string="Assign opportunities to">
<field name="user_id" class="oe_inline" on_change="on_change_user(user_id, context)"/> <field name="user_id" class="oe_inline" on_change="on_change_user(user_id, section_id, context)"/>
<field name="section_id" class="oe_inline"/> <field name="section_id" class="oe_inline"/>
</group> </group>
<group string="Select Leads/Opportunities"> <group string="Select Leads/Opportunities">

View File

@ -15,10 +15,10 @@
<div class="oe_title"> <div class="oe_title">
<h3> <h3>
<span class="oe_grey">( </span> <span class="oe_grey">( </span>
<field name="partner_latitude" nolabel="1" readonly="1" class="oe_inline"/> <field name="partner_latitude" nolabel="1" class="oe_inline"/>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&lt;=',0)]}">N </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&lt;=',0)]}">N </span>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&gt;=',0)]}">S </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&gt;=',0)]}">S </span>
<field name="partner_longitude" class="oe_inline" readonly="1" nolabel="1"/> <field name="partner_longitude" class="oe_inline" nolabel="1"/>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&lt;=',0)]}">E </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&lt;=',0)]}">E </span>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&gt;=',0)]}">W </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&gt;=',0)]}">W </span>
<span class="oe_grey">) </span> <span class="oe_grey">) </span>
@ -88,10 +88,10 @@
<div> <div>
<h3> <h3>
<span class="oe_grey">( </span> <span class="oe_grey">( </span>
<field name="partner_latitude" nolabel="1" readonly="1" class="oe_inline"/> <field name="partner_latitude" nolabel="1" class="oe_inline"/>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&lt;=',0)]}">N </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&lt;=',0)]}">N </span>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&gt;=',0)]}">S </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','&gt;=',0)]}">S </span>
<field name="partner_longitude" class="oe_inline" readonly="1" nolabel="1"/> <field name="partner_longitude" class="oe_inline" nolabel="1"/>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&lt;=',0)]}">E </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&lt;=',0)]}">E </span>
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&gt;=',0)]}">W </span> <span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','&gt;=',0)]}">W </span>
<span class="oe_grey">) </span> <span class="oe_grey">) </span>

View File

@ -1,18 +1,19 @@
openerp.crm_partner_assign = function (instance) { openerp.crm_partner_assign = function (instance) {
instance.crm_partner_assign = instance.crm_partner_assign || {}; instance.crm_partner_assign = instance.crm_partner_assign || {};
instance.crm_partner_assign.next_or_list = function(parent) { instance.crm_partner_assign.next_or_list = function(parent) {
if (parent.inner_widget.active_view === "form"){ var view = parent.inner_widget.active_view;
var form = parent.inner_widget.views.form.controller; var controller = parent.inner_widget.views[view].controller;
form.dataset.remove_ids([form.dataset.ids[form.dataset.index]]); if (view === "form"){
form.reload(); if (controller.dataset.size()) {
if (!form.dataset.ids.length){ controller.execute_pager_action('next');
parent.inner_widget.switch_mode('list'); } else {
} controller.do_action('history_back');
} }
else{ }
parent.inner_widget.views[parent.inner_widget.active_view].controller.reload(); controller.do_action({ type: 'ir.actions.act_window_close' });
} if (view === "list"){
parent.do_action({ type: 'ir.actions.act_window_close' }); controller.records.remove(controller.records.get(parent.dialog_widget.action.context.active_id));
}
}; };
instance.web.client_actions.add("next_or_list", "instance.crm_partner_assign.next_or_list"); instance.web.client_actions.add("next_or_list", "instance.crm_partner_assign.next_or_list");
} }

View File

@ -24,6 +24,8 @@ import base64
import datetime import datetime
import dateutil.relativedelta as relativedelta import dateutil.relativedelta as relativedelta
import logging import logging
import lxml
import urlparse
import openerp import openerp
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
@ -61,6 +63,15 @@ try:
'quote': quote, 'quote': quote,
'urlencode': urlencode, 'urlencode': urlencode,
'datetime': datetime, 'datetime': datetime,
'len': len,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'filter': filter,
'reduce': reduce,
'map': map,
'round': round,
# dateutil.relativedelta is an old-style class and cannot be directly # dateutil.relativedelta is an old-style class and cannot be directly
# instanciated wihtin a jinja2 expression, so a lambda "proxy" is # instanciated wihtin a jinja2 expression, so a lambda "proxy" is
@ -70,6 +81,7 @@ try:
except ImportError: except ImportError:
_logger.warning("jinja2 not available, templating features will not work!") _logger.warning("jinja2 not available, templating features will not work!")
class email_template(osv.osv): class email_template(osv.osv):
"Templates for sending email" "Templates for sending email"
_name = "email.template" _name = "email.template"
@ -82,7 +94,48 @@ class email_template(osv.osv):
res['model_id'] = self.pool['ir.model'].search(cr, uid, [('model', '=', res.pop('model'))], context=context)[0] res['model_id'] = self.pool['ir.model'].search(cr, uid, [('model', '=', res.pop('model'))], context=context)[0]
return res return res
def render_template_batch(self, cr, uid, template, model, res_ids, context=None): def _replace_local_links(self, cr, uid, html, context=None):
""" Post-processing of html content to replace local links to absolute
links, using web.base.url as base url. """
if not html:
return html
# form a tree
root = lxml.html.fromstring(html)
if not len(root) and root.text is None and root.tail is None:
html = '<div>%s</div>' % html
root = lxml.html.fromstring(html)
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
(base_scheme, base_netloc, bpath, bparams, bquery, bfragment) = urlparse.urlparse(base_url)
def _process_link(url):
new_url = url
(scheme, netloc, path, params, query, fragment) = urlparse.urlparse(url)
if not scheme and not netloc:
new_url = urlparse.urlunparse((base_scheme, base_netloc, path, params, query, fragment))
return new_url
# check all nodes, replace :
# - img src -> check URL
# - a href -> check URL
for node in root.iter():
if node.tag == 'a':
node.set('href', _process_link(node.get('href')))
elif node.tag == 'img' and not node.get('src', 'data').startswith('data'):
node.set('src', _process_link(node.get('src')))
html = lxml.html.tostring(root, pretty_print=False, method='html')
# this is ugly, but lxml/etree tostring want to put everything in a 'div' that breaks the editor -> remove that
if html.startswith('<div>') and html.endswith('</div>'):
html = html[5:-6]
return html
def render_post_process(self, cr, uid, html, context=None):
html = self._replace_local_links(cr, uid, html, context=context)
return html
def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):
"""Render the given template text, replace mako expressions ``${expr}`` """Render the given template text, replace mako expressions ``${expr}``
with the result of evaluating these expressions with with the result of evaluating these expressions with
an evaluation context containing: an evaluation context containing:
@ -125,6 +178,10 @@ class email_template(osv.osv):
if render_result == u"False": if render_result == u"False":
render_result = u"" render_result = u""
results[res_id] = render_result results[res_id] = render_result
if post_process:
for res_id, result in results.iteritems():
results[res_id] = self.render_post_process(cr, uid, result, context=context)
return results return results
def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None): def get_email_template_batch(self, cr, uid, template_id=False, res_ids=None, context=None):
@ -183,7 +240,7 @@ class email_template(osv.osv):
'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False, 'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing Mail Server', readonly=False,
help="Optional preferred server for outgoing mails. If not set, the highest " help="Optional preferred server for outgoing mails. If not set, the highest "
"priority one will be used."), "priority one will be used."),
'body_html': fields.html('Body', translate=True, help="Rich-text/HTML version of the message (placeholders may be used here)"), 'body_html': fields.html('Body', translate=True, sanitize=False, help="Rich-text/HTML version of the message (placeholders may be used here)"),
'report_name': fields.char('Report Filename', translate=True, 'report_name': fields.char('Report Filename', translate=True,
help="Name to use for the generated report file (may contain placeholders)\n" help="Name to use for the generated report file (may contain placeholders)\n"
"The extension can be omitted and will then come from the report type."), "The extension can be omitted and will then come from the report type."),
@ -356,17 +413,20 @@ class email_template(osv.osv):
results = dict() results = dict()
for template, template_res_ids in templates_to_res_ids.iteritems(): for template, template_res_ids in templates_to_res_ids.iteritems():
# generate fields value for all res_ids linked to the current template # generate fields value for all res_ids linked to the current template
for field in ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to']: for field in fields:
generated_field_values = self.render_template_batch(cr, uid, getattr(template, field), template.model, template_res_ids, context=context) generated_field_values = self.render_template_batch(
cr, uid, getattr(template, field), template.model, template_res_ids,
post_process=(field == 'body_html'),
context=context)
for res_id, field_value in generated_field_values.iteritems(): for res_id, field_value in generated_field_values.iteritems():
results.setdefault(res_id, dict())[field] = field_value results.setdefault(res_id, dict())[field] = field_value
# update values for all res_ids # update values for all res_ids
for res_id in template_res_ids: for res_id in template_res_ids:
values = results[res_id] values = results[res_id]
if template.user_signature: if 'body_html' in fields and template.user_signature:
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
values['body_html'] = tools.append_content_to_html(values['body_html'], signature) values['body_html'] = tools.append_content_to_html(values['body_html'], signature)
if values['body_html']: if values.get('body_html'):
values['body'] = tools.html_sanitize(values['body_html']) values['body'] = tools.html_sanitize(values['body_html'])
values.update( values.update(
mail_server_id=template.mail_server_id.id or False, mail_server_id=template.mail_server_id.id or False,
@ -389,7 +449,7 @@ class email_template(osv.osv):
ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, [res_id], context)[res_id] # take 0 ? ctx['lang'] = self.render_template_batch(cr, uid, template.lang, template.model, [res_id], context)[res_id] # take 0 ?
if report.report_type in ['qweb-html', 'qweb-pdf']: if report.report_type in ['qweb-html', 'qweb-pdf']:
result, format = self.pool['report'].get_pdf(report, res_id, context=ctx), 'pdf' result, format = self.pool['report'].get_pdf(cr, uid, [res_id], report_service, context=ctx), 'pdf'
else: else:
result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx) result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)

View File

@ -162,16 +162,18 @@ class mail_compose_message(osv.TransientModel):
partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context) partner_ids += self.pool['res.partner'].exists(cr, SUPERUSER_ID, tpl_partner_ids, context=context)
return partner_ids return partner_ids
def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None): def generate_email_for_composer_batch(self, cr, uid, template_id, res_ids, context=None, fields=None):
""" Call email_template.generate_email(), get fields relevant for """ Call email_template.generate_email(), get fields relevant for
mail.compose.message, transform email_cc and email_to into partner_ids """ mail.compose.message, transform email_cc and email_to into partner_ids """
# filter template values # filter template values
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'attachments', 'mail_server_id'] if fields is None:
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'attachment_ids', 'mail_server_id']
returned_fields = fields + ['attachments']
values = dict.fromkeys(res_ids, False) values = dict.fromkeys(res_ids, False)
template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, context=context) template_values = self.pool.get('email.template').generate_email_batch(cr, uid, template_id, res_ids, fields=fields, context=context)
for res_id in res_ids: for res_id in res_ids:
res_id_values = dict((field, template_values[res_id][field]) for field in fields if template_values[res_id].get(field)) res_id_values = dict((field, template_values[res_id][field]) for field in returned_fields if template_values[res_id].get(field))
res_id_values['body'] = res_id_values.pop('body_html', '') res_id_values['body'] = res_id_values.pop('body_html', '')
# transform email_to, email_cc into partner_ids # transform email_to, email_cc into partner_ids
@ -189,7 +191,10 @@ class mail_compose_message(osv.TransientModel):
""" Override to handle templates. """ """ Override to handle templates. """
# generate template-based values # generate template-based values
if wizard.template_id: if wizard.template_id:
template_values = self.generate_email_for_composer_batch(cr, uid, wizard.template_id.id, res_ids, context=context) template_values = self.generate_email_for_composer_batch(
cr, uid, wizard.template_id.id, res_ids,
fields=['email_to', 'partner_to', 'email_cc', 'attachment_ids', 'mail_server_id'],
context=context)
else: else:
template_values = dict.fromkeys(res_ids, dict()) template_values = dict.fromkeys(res_ids, dict())
# generate composer values # generate composer values
@ -206,8 +211,8 @@ class mail_compose_message(osv.TransientModel):
template_values[res_id].update(composer_values[res_id]) template_values[res_id].update(composer_values[res_id])
return template_values return template_values
def render_template_batch(self, cr, uid, template, model, res_ids, context=None): def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):
return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context) return self.pool.get('email.template').render_template_batch(cr, uid, template, model, res_ids, context=context, post_process=post_process)
# Compatibility methods # Compatibility methods
def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None): def generate_email_for_composer(self, cr, uid, template_id, res_id, context=None):

View File

@ -52,7 +52,7 @@ class report_event_registration(osv.osv):
# TOFIX this request won't select events that have no registration # TOFIX this request won't select events that have no registration
cr.execute(""" CREATE VIEW report_event_registration AS ( cr.execute(""" CREATE VIEW report_event_registration AS (
SELECT SELECT
e.id::char || '/' || coalesce(r.id::char,'') AS id, e.id::varchar || '/' || coalesce(r.id::varchar,'') AS id,
e.id AS event_id, e.id AS event_id,
e.user_id AS user_id, e.user_id AS user_id,
r.user_id AS user_id_registration, r.user_id AS user_id_registration,

View File

@ -35,7 +35,6 @@
<group expand="1" string="Group By..."> <group expand="1" string="Group By...">
<filter string="Participant / Contact" icon="terp-personal" context="{'group_by':'name_registration'}" help="Registration contact"/> <filter string="Participant / Contact" icon="terp-personal" context="{'group_by':'name_registration'}" help="Registration contact"/>
<filter string="Register" icon="terp-personal" context="{'group_by':'user_id_registration'}" help="Registration contact" groups="base.group_no_one"/> <filter string="Register" icon="terp-personal" context="{'group_by':'user_id_registration'}" help="Registration contact" groups="base.group_no_one"/>
<filter string="Speaker" name="speaker" icon="terp-personal+" context="{'group_by': 'speaker_id'}" groups="base.group_no_one"/>
<filter string="Event Responsible" name="user_id" icon="terp-personal" context="{'group_by': 'user_id'}"/> <filter string="Event Responsible" name="user_id" icon="terp-personal" context="{'group_by': 'user_id'}"/>
<filter string="Event" name="event" icon="terp-crm" context="{'group_by':'event_id', 'max_reg_event_visible':0}"/> <filter string="Event" name="event" icon="terp-crm" context="{'group_by':'event_id', 'max_reg_event_visible':0}"/>
<filter string="Event Type" icon="terp-crm" context="{'group_by':'event_type'}"/> <filter string="Event Type" icon="terp-crm" context="{'group_by':'event_type'}"/>

View File

@ -21,30 +21,22 @@
import operator import operator
import simplejson import simplejson
import re
import urllib import urllib
import warnings
from openerp import tools from openerp import tools
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.tools.translate import _
from openerp.addons.web.http import request from datetime import datetime, timedelta
import werkzeug.utils
from datetime import datetime, timedelta, date
from dateutil import parser from dateutil import parser
import pytz import pytz
from openerp.osv import fields, osv from openerp.osv import fields, osv
from openerp.osv import osv
from collections import namedtuple
import logging import logging
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
class Meta(type): class Meta(type):
""" This Meta class allow to define class as a structure, and so instancied variable """ This Meta class allow to define class as a structure, and so instancied variable
in __init__ to avoid to have side effect alike 'static' variable """ in __init__ to avoid to have side effect alike 'static' variable """
def __new__(typ, name, parents, attrs): def __new__(typ, name, parents, attrs):
methods = dict((k, v) for k, v in attrs.iteritems() methods = dict((k, v) for k, v in attrs.iteritems()
@ -63,9 +55,11 @@ class Meta(type):
methods['__getitem__'] = getattr methods['__getitem__'] = getattr
return type.__new__(typ, name, parents, methods) return type.__new__(typ, name, parents, methods)
class Struct(object): class Struct(object):
__metaclass__ = Meta __metaclass__ = Meta
class OpenerpEvent(Struct): class OpenerpEvent(Struct):
event = False event = False
found = False found = False
@ -77,6 +71,7 @@ class OpenerpEvent(Struct):
attendee_id = False attendee_id = False
synchro = False synchro = False
class GmailEvent(Struct): class GmailEvent(Struct):
event = False event = False
found = False found = False
@ -85,53 +80,51 @@ class GmailEvent(Struct):
update = False update = False
status = False status = False
class SyncEvent(object): class SyncEvent(object):
def __init__(self): def __init__(self):
self.OE = OpenerpEvent() self.OE = OpenerpEvent()
self.GG = GmailEvent() self.GG = GmailEvent()
self.OP = None self.OP = None
def __getitem__(self, key): def __getitem__(self, key):
return getattr(self,key) return getattr(self, key)
def compute_OP(self): def compute_OP(self):
#If event are already in Gmail and in OpenERP #If event are already in Gmail and in OpenERP
if self.OE.found and self.GG.found: if self.OE.found and self.GG.found:
#If the event has been deleted from one side, we delete on other side ! #If the event has been deleted from one side, we delete on other side !
if self.OE.status != self.GG.status: if self.OE.status != self.GG.status:
self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"), self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"),
'The event has been deleted from one side, we delete on other side !' ) 'The event has been deleted from one side, we delete on other side !')
#If event is not deleted ! #If event is not deleted !
elif self.OE.status and self.GG.status: elif self.OE.status and self.GG.status:
if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]: if self.OE.update.split('.')[0] != self.GG.update.split('.')[0]:
if self.OE.update < self.GG.update: if self.OE.update < self.GG.update:
tmpSrc = 'GG' tmpSrc = 'GG'
elif self.OE.update > self.GG.update: elif self.OE.update > self.GG.update:
tmpSrc = 'OE' tmpSrc = 'OE'
assert tmpSrc in ['GG','OE'] assert tmpSrc in ['GG', 'OE']
#if self.OP.action == None: #if self.OP.action == None:
if self[tmpSrc].isRecurrence: if self[tmpSrc].isRecurrence:
if self[tmpSrc].status: if self[tmpSrc].status:
self.OP = Update(tmpSrc, 'Only need to update, because i\'m active') self.OP = Update(tmpSrc, 'Only need to update, because i\'m active')
else: else:
self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence') self.OP = Exclude(tmpSrc, 'Need to Exclude (Me = First event from recurrence) from recurrence')
elif self[tmpSrc].isInstance: elif self[tmpSrc].isInstance:
self.OP= Update(tmpSrc, 'Only need to update, because already an exclu'); self.OP = Update(tmpSrc, 'Only need to update, because already an exclu')
else: else:
self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event'); self.OP = Update(tmpSrc, 'Simply Update... I\'m a single event')
#end-if self.OP.action == None:
else: else:
if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]: if not self.OE.synchro or self.OE.synchro.split('.')[0] < self.OE.update.split('.')[0]:
self.OP = Update('OE','Event already updated by another user, but not synchro with my google calendar') self.OP = Update('OE', 'Event already updated by another user, but not synchro with my google calendar')
#import ipdb; ipdb.set_trace(); #import ipdb; ipdb.set_trace();
else: else:
self.OP = NothingToDo("",'Not update needed') self.OP = NothingToDo("", 'Not update needed')
else: else:
self.OP = NothingToDo("", "Both are already deleted"); self.OP = NothingToDo("", "Both are already deleted")
# New in openERP... Create on create_events of synchronize function # New in openERP... Create on create_events of synchronize function
elif self.OE.found and not self.GG.found: elif self.OE.found and not self.GG.found:
@ -139,64 +132,73 @@ class SyncEvent(object):
if self.OE.status: if self.OE.status:
self.OP = Delete('OE', 'Removed from GOOGLE') self.OP = Delete('OE', 'Removed from GOOGLE')
else: else:
self.OP = NothingToDo("","Already Deleted in gmail and unlinked in OpenERP") self.OP = NothingToDo("", "Already Deleted in gmail and unlinked in OpenERP")
elif self.GG.found and not self.OE.found: elif self.GG.found and not self.OE.found:
tmpSrc = 'GG' tmpSrc = 'GG'
if not self.GG.status and not self.GG.isInstance: if not self.GG.status and not self.GG.isInstance:
# don't need to make something... because event has been created and deleted before the synchronization # don't need to make something... because event has been created and deleted before the synchronization
self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly') self.OP = NothingToDo("", 'Nothing to do... Create and Delete directly')
else: else:
if self.GG.isInstance: if self.GG.isInstance:
if self[tmpSrc].status: if self[tmpSrc].status:
self.OP = Exclude(tmpSrc, 'Need to create the new exclu') self.OP = Exclude(tmpSrc, 'Need to create the new exclu')
else: else:
self.OP = Exclude(tmpSrc, 'Need to copy and Exclude') self.OP = Exclude(tmpSrc, 'Need to copy and Exclude')
else: else:
self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL') self.OP = Create(tmpSrc, 'New EVENT CREATE from GMAIL')
def __str__(self): def __str__(self):
return self.__repr__() return self.__repr__()
def __repr__(self): def __repr__(self):
myPrint = "---- A SYNC EVENT ---" myPrint = "---- A SYNC EVENT ---"
myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id) myPrint += "\n ID OE: %s " % (self.OE.event and self.OE.event.id)
myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False)) myPrint += "\n ID GG: %s " % (self.GG.event and self.GG.event.get('id', False))
myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name) myPrint += "\n Name OE: %s " % (self.OE.event and self.OE.event.name)
myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', False)) myPrint += "\n Name GG: %s " % (self.GG.event and self.GG.event.get('summary', False))
myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found) myPrint += "\n Found OE:%5s vs GG: %5s" % (self.OE.found, self.GG.found)
myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence) myPrint += "\n Recurrence OE:%5s vs GG: %5s" % (self.OE.isRecurrence, self.GG.isRecurrence)
myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance) myPrint += "\n Instance OE:%5s vs GG: %5s" % (self.OE.isInstance, self.GG.isInstance)
myPrint += "\n Synchro OE: %10s " % (self.OE.synchro) myPrint += "\n Synchro OE: %10s " % (self.OE.synchro)
myPrint += "\n Update OE: %10s " % (self.OE.update) myPrint += "\n Update OE: %10s " % (self.OE.update)
myPrint += "\n Update GG: %10s " % (self.GG.update) myPrint += "\n Update GG: %10s " % (self.GG.update)
myPrint += "\n Status OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status) myPrint += "\n Status OE:%5s vs GG: %5s" % (self.OE.status, self.GG.status)
if (self.OP is None): if (self.OP is None):
myPrint += "\n Action %s" % "---!!!---NONE---!!!---" myPrint += "\n Action %s" % "---!!!---NONE---!!!---"
else: else:
myPrint += "\n Action %s" % type(self.OP).__name__ myPrint += "\n Action %s" % type(self.OP).__name__
myPrint += "\n Source %s" % (self.OP.src) myPrint += "\n Source %s" % (self.OP.src)
myPrint += "\n comment %s" % (self.OP.info) myPrint += "\n comment %s" % (self.OP.info)
return myPrint return myPrint
class SyncOperation(object): class SyncOperation(object):
def __init__(self, src,info, **kw): def __init__(self, src, info, **kw):
self.src = src self.src = src
self.info = info self.info = info
for k,v in kw.items(): for k, v in kw.items():
setattr(self,k,v) setattr(self, k, v)
def __str__(self): def __str__(self):
return 'in__STR__' return 'in__STR__'
class Create(SyncOperation): class Create(SyncOperation):
pass pass
class Update(SyncOperation): class Update(SyncOperation):
pass pass
class Delete(SyncOperation): class Delete(SyncOperation):
pass pass
class NothingToDo(SyncOperation): class NothingToDo(SyncOperation):
pass pass
class Exclude(SyncOperation): class Exclude(SyncOperation):
pass pass
@ -205,9 +207,9 @@ class google_calendar(osv.AbstractModel):
STR_SERVICE = 'calendar' STR_SERVICE = 'calendar'
_name = 'google.%s' % STR_SERVICE _name = 'google.%s' % STR_SERVICE
def generate_data(self, cr, uid, event, context=None): def generate_data(self, cr, uid, event, context=None):
if event.allday: if event.allday:
start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) , context=context).isoformat('T').split('T')[0] start_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT), context=context).isoformat('T').split('T')[0]
end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration), context=context).isoformat('T').split('T')[0] end_date = fields.datetime.context_timestamp(cr, uid, datetime.strptime(event.date, tools.DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(hours=event.duration), context=context).isoformat('T').split('T')[0]
type = 'date' type = 'date'
vstype = 'dateTime' vstype = 'dateTime'
@ -220,72 +222,71 @@ class google_calendar(osv.AbstractModel):
for attendee in event.attendee_ids: for attendee in event.attendee_ids:
attendee_list.append({ attendee_list.append({
'email':attendee.email or 'NoEmail@mail.com', 'email': attendee.email or 'NoEmail@mail.com',
'displayName':attendee.partner_id.name, 'displayName': attendee.partner_id.name,
'responseStatus':attendee.state or 'needsAction', 'responseStatus': attendee.state or 'needsAction',
}) })
data = { data = {
"summary": event.name or '', "summary": event.name or '',
"description": event.description or '', "description": event.description or '',
"start":{ "start": {
type:start_date, type: start_date,
vstype:None, vstype: None,
'timeZone':'UTC' 'timeZone': 'UTC'
}, },
"end":{ "end": {
type:end_date, type: end_date,
vstype:None, vstype: None,
'timeZone':'UTC' 'timeZone': 'UTC'
}, },
"attendees":attendee_list, "attendees": attendee_list,
"location":event.location or '', "location": event.location or '',
"visibility":event['class'] or 'public', "visibility": event['class'] or 'public',
} }
if event.recurrency and event.rrule: if event.recurrency and event.rrule:
data["recurrence"]=["RRULE:"+event.rrule] data["recurrence"] = ["RRULE:" + event.rrule]
if not event.active: if not event.active:
data["state"] = "cancelled" data["state"] = "cancelled"
if not self.get_need_synchro_attendee(cr,uid,context=context): if not self.get_need_synchro_attendee(cr, uid, context=context):
data.pop("attendees") data.pop("attendees")
return data return data
def create_an_event(self, cr, uid,event, context=None): def create_an_event(self, cr, uid, event, context=None):
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid,event, context=context) data = self.generate_data(cr, uid, event, context=context)
url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary',urllib.quote('id,updated'),self.get_token(cr,uid,context)) url = "/calendar/v3/calendars/%s/events?fields=%s&access_token=%s" % ('primary', urllib.quote('id,updated'), self.get_token(cr, uid, context))
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
data_json = simplejson.dumps(data) data_json = simplejson.dumps(data)
return gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context) return gs_pool._do_request(cr, uid, url, data_json, headers, type='POST', context=context)
def delete_an_event(self, cr, uid,event_id, context=None): def delete_an_event(self, cr, uid, event_id, context=None):
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
params = { params = {
'access_token' : self.get_token(cr,uid,context) 'access_token': self.get_token(cr, uid, context)
} }
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
url = "/calendar/v3/calendars/%s/events/%s" % ('primary',event_id) url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event_id)
return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context) return gs_pool._do_request(cr, uid, url, params, headers, type='DELETE', context=context)
def get_event_dict(self,cr,uid,token=False,nextPageToken=False,context=None): def get_event_dict(self, cr, uid, token=False, nextPageToken=False, context=None):
if not token: if not token:
token = self.get_token(cr,uid,context) token = self.get_token(cr, uid, context)
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
params = { params = {
'fields': 'items,nextPageToken', 'fields': 'items,nextPageToken',
'access_token' : token, 'access_token': token,
'maxResults':1000, 'maxResults': 1000,
'timeMin': self.get_start_time_to_synchro(cr,uid,context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"), 'timeMin': self.get_start_time_to_synchro(cr, uid, context=context).strftime("%Y-%m-%dT%H:%M:%S.%fz"),
} }
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
@ -301,50 +302,50 @@ class google_calendar(osv.AbstractModel):
google_events_dict[google_event['id']] = google_event google_events_dict[google_event['id']] = google_event
if content.get('nextPageToken', False): if content.get('nextPageToken', False):
google_events_dict.update(self.get_event_dict(cr,uid,token,content['nextPageToken'],context=context)) google_events_dict.update(self.get_event_dict(cr, uid, token, content['nextPageToken'], context=context))
return google_events_dict return google_events_dict
def update_to_google(self, cr, uid, oe_event, google_event, context): def update_to_google(self, cr, uid, oe_event, google_event, context):
calendar_event = self.pool['calendar.event'] calendar_event = self.pool['calendar.event']
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'],'id,updated', self.get_token(cr,uid,context)) url = "/calendar/v3/calendars/%s/events/%s?fields=%s&access_token=%s" % ('primary', google_event['id'], 'id,updated', self.get_token(cr, uid, context))
headers = {'Content-type': 'application/json', 'Accept': 'text/plain'} headers = {'Content-type': 'application/json', 'Accept': 'text/plain'}
data = self.generate_data(cr,uid ,oe_event, context) data = self.generate_data(cr, uid, oe_event, context)
data['sequence'] = google_event.get('sequence', 0) data['sequence'] = google_event.get('sequence', 0)
data_json = simplejson.dumps(data) data_json = simplejson.dumps(data)
content = gs_pool._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context) content = gs_pool._do_request(cr, uid, url, data_json, headers, type='PATCH', context=context)
update_date = datetime.strptime(content['updated'],"%Y-%m-%dT%H:%M:%S.%fz") update_date = datetime.strptime(content['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date':update_date}) calendar_event.write(cr, uid, [oe_event.id], {'oe_update_date': update_date})
if context['curr_attendee']: if context['curr_attendee']:
self.pool['calendar.attendee'].write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date},context) self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date}, context)
def update_an_event(self, cr, uid,event, context=None): def update_an_event(self, cr, uid, event, context=None):
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid,event, context=context) data = self.generate_data(cr, uid, event, context=context)
url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id) url = "/calendar/v3/calendars/%s/events/%s" % ('primary', event.google_internal_event_id)
headers = {} headers = {}
data['access_token'] = self.get_token(cr,uid,context) data['access_token'] = self.get_token(cr, uid, context)
response = gs_pool._do_request(cr, uid, url, data, headers, type='GET', context=context) response = gs_pool._do_request(cr, uid, url, data, headers, type='GET', context=context)
#TO_CHECK : , if http fail, no event, do DELETE ? #TO_CHECK : , if http fail, no event, do DELETE ?
return response return response
def update_recurrent_event_exclu(self, cr, uid,instance_id,event_ori_google_id,event_new, context=None): def update_recurrent_event_exclu(self, cr, uid, instance_id, event_ori_google_id, event_new, context=None):
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
data = self.generate_data(cr, uid,event_new, context=context) data = self.generate_data(cr, uid, event_new, context=context)
data['recurringEventId'] = event_ori_google_id data['recurringEventId'] = event_ori_google_id
data['originalStartTime'] = event_new.recurrent_id_date data['originalStartTime'] = event_new.recurrent_id_date
url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id,self.get_token(cr,uid,context)) url = "/calendar/v3/calendars/%s/events/%s?access_token=%s" % ('primary', instance_id, self.get_token(cr, uid, context))
headers = { 'Content-type': 'application/json'} headers = {'Content-type': 'application/json'}
data['sequence'] = self.get_sequence(cr, uid, instance_id, context) data['sequence'] = self.get_sequence(cr, uid, instance_id, context)
@ -353,41 +354,41 @@ class google_calendar(osv.AbstractModel):
def update_from_google(self, cr, uid, event, single_event_dict, type, context): def update_from_google(self, cr, uid, event, single_event_dict, type, context):
if context is None: if context is None:
context= [] context = []
calendar_event = self.pool['calendar.event'] calendar_event = self.pool['calendar.event']
res_partner_obj = self.pool['res.partner'] res_partner_obj = self.pool['res.partner']
calendar_attendee_obj = self.pool['calendar.attendee'] calendar_attendee_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users'] user_obj = self.pool['res.users']
myPartnerID = user_obj.browse(cr,uid,uid,context).partner_id.id myPartnerID = user_obj.browse(cr, uid, uid, context).partner_id.id
attendee_record = [] attendee_record = []
partner_record = [(4,myPartnerID)] partner_record = [(4, myPartnerID)]
result = {} result = {}
if single_event_dict.get('attendees',False): if single_event_dict.get('attendees', False):
for google_attendee in single_event_dict['attendees']: for google_attendee in single_event_dict['attendees']:
if type == "write": if type == "write":
for oe_attendee in event['attendee_ids']: for oe_attendee in event['attendee_ids']:
if oe_attendee.email == google_attendee['email']: if oe_attendee.email == google_attendee['email']:
calendar_attendee_obj.write(cr, uid,[oe_attendee.id] ,{'state' : google_attendee['responseStatus']},context=context) calendar_attendee_obj.write(cr, uid, [oe_attendee.id], {'state': google_attendee['responseStatus']}, context=context)
google_attendee['found'] = True google_attendee['found'] = True
continue continue
if google_attendee.get('found',False): if google_attendee.get('found', False):
continue continue
if self.get_need_synchro_attendee(cr,uid,context=context): if self.get_need_synchro_attendee(cr, uid, context=context):
attendee_id = res_partner_obj.search(cr, uid,[('email', '=', google_attendee['email'])], context=context) attendee_id = res_partner_obj.search(cr, uid, [('email', '=', google_attendee['email'])], context=context)
if not attendee_id: if not attendee_id:
attendee_id = [res_partner_obj.create(cr, uid,{'email': google_attendee['email'],'customer': False, 'name': google_attendee.get("displayName",False) or google_attendee['email'] }, context=context)] attendee_id = [res_partner_obj.create(cr, uid, {'email': google_attendee['email'], 'customer': False, 'name': google_attendee.get("displayName", False) or google_attendee['email']}, context=context)]
attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context) attendee = res_partner_obj.read(cr, uid, attendee_id[0], ['email'], context=context)
partner_record.append((4, attendee.get('id'))) partner_record.append((4, attendee.get('id')))
attendee['partner_id'] = attendee.pop('id') attendee['partner_id'] = attendee.pop('id')
attendee['state'] = google_attendee['responseStatus'] attendee['state'] = google_attendee['responseStatus']
attendee_record.append((0, 0, attendee)) attendee_record.append((0, 0, attendee))
UTC = pytz.timezone('UTC') UTC = pytz.timezone('UTC')
if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled if single_event_dict.get('start') and single_event_dict.get('end'): # If not cancelled
if single_event_dict['start'].get('dateTime',False) and single_event_dict['end'].get('dateTime',False): if single_event_dict['start'].get('dateTime', False) and single_event_dict['end'].get('dateTime', False):
date = parser.parse(single_event_dict['start']['dateTime']) date = parser.parse(single_event_dict['start']['dateTime'])
date_deadline = parser.parse(single_event_dict['end']['dateTime']) date_deadline = parser.parse(single_event_dict['end']['dateTime'])
delta = date_deadline.astimezone(UTC) - date.astimezone(UTC) delta = date_deadline.astimezone(UTC) - date.astimezone(UTC)
@ -402,8 +403,8 @@ class google_calendar(osv.AbstractModel):
delta = (d_end - d_start) delta = (d_end - d_start)
allday = True allday = True
result['duration'] = (delta.seconds / 60) / 60.0 + delta.days *24 result['duration'] = (delta.seconds / 60) / 60.0 + delta.days * 24
update_date = datetime.strptime(single_event_dict['updated'],"%Y-%m-%dT%H:%M:%S.%fz") update_date = datetime.strptime(single_event_dict['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
result.update({ result.update({
'date': date, 'date': date,
'date_deadline': date_deadline, 'date_deadline': date_deadline,
@ -413,17 +414,17 @@ class google_calendar(osv.AbstractModel):
'attendee_ids': attendee_record, 'attendee_ids': attendee_record,
'partner_ids': list(set(partner_record)), 'partner_ids': list(set(partner_record)),
'name': single_event_dict.get('summary','Event'), 'name': single_event_dict.get('summary', 'Event'),
'description': single_event_dict.get('description',False), 'description': single_event_dict.get('description', False),
'location':single_event_dict.get('location',False), 'location': single_event_dict.get('location', False),
'class':single_event_dict.get('visibility','public'), 'class': single_event_dict.get('visibility', 'public'),
'oe_update_date':update_date, 'oe_update_date': update_date,
# 'google_internal_event_id': single_event_dict.get('id',False), # 'google_internal_event_id': single_event_dict.get('id',False),
}) })
if single_event_dict.get("recurrence",False): if single_event_dict.get("recurrence", False):
rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:] rrule = [rule for rule in single_event_dict["recurrence"] if rule.startswith("RRULE:")][0][6:]
result['rrule']=rrule result['rrule'] = rrule
if type == "write": if type == "write":
res = calendar_event.write(cr, uid, event['id'], result, context=context) res = calendar_event.write(cr, uid, event['id'], result, context=context)
@ -435,77 +436,71 @@ class google_calendar(osv.AbstractModel):
res = calendar_event.create(cr, uid, result, context=context) res = calendar_event.create(cr, uid, result, context=context)
if context['curr_attendee']: if context['curr_attendee']:
self.pool['calendar.attendee'].write(cr,uid,[context['curr_attendee']], {'oe_synchro_date':update_date,'google_internal_event_id': single_event_dict.get('id',False)},context) self.pool['calendar.attendee'].write(cr, uid, [context['curr_attendee']], {'oe_synchro_date': update_date, 'google_internal_event_id': single_event_dict.get('id', False)}, context)
return res return res
def synchronize_events(self, cr, uid, ids, context=None): def synchronize_events(self, cr, uid, ids, context=None):
gc_obj = self.pool['google.calendar']
# Create all new events from OpenERP into Gmail, if that is not recurrent event # Create all new events from OpenERP into Gmail, if that is not recurrent event
self.create_new_events(cr, uid, context=context) self.create_new_events(cr, uid, context=context)
self.bind_recurring_events_to_google(cr, uid, context) self.bind_recurring_events_to_google(cr, uid, context)
res = self.update_events(cr, uid, context) res = self.update_events(cr, uid, context)
return { return {
"status" : res and "need_refresh" or "no_new_event_form_google", "status": res and "need_refresh" or "no_new_event_form_google",
"url" : '' "url": ''
} }
def create_new_events(self, cr, uid, context=None): def create_new_events(self, cr, uid, context=None):
gc_pool = self.pool['google.calendar']
ev_obj = self.pool['calendar.event'] ev_obj = self.pool['calendar.event']
att_obj = self.pool['calendar.attendee'] att_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users'] user_obj = self.pool['res.users']
myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
context_norecurrent = context.copy() context_norecurrent = context.copy()
context_norecurrent['virtual_id'] = False context_norecurrent['virtual_id'] = False
my_att_ids = att_obj.search(cr, uid,[ ('partner_id', '=', myPartnerID), my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
('google_internal_event_id', '=', False), ('google_internal_event_id', '=', False),
'|', '|',
('event_id.date_deadline','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), ('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
('event_id.end_date','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), ('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
], context=context_norecurrent) ], context=context_norecurrent)
for att in att_obj.browse(cr,uid,my_att_ids,context=context): for att in att_obj.browse(cr, uid, my_att_ids, context=context):
if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0: if not att.event_id.recurrent_id or att.event_id.recurrent_id == 0:
response = self.create_an_event(cr,uid,att.event_id,context=context) response = self.create_an_event(cr, uid, att.event_id, context=context)
update_date = datetime.strptime(response['updated'],"%Y-%m-%dT%H:%M:%S.%fz") update_date = datetime.strptime(response['updated'], "%Y-%m-%dT%H:%M:%S.%fz")
ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date':update_date}) ev_obj.write(cr, uid, att.event_id.id, {'oe_update_date': update_date})
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date':update_date}) att_obj.write(cr, uid, [att.id], {'google_internal_event_id': response['id'], 'oe_synchro_date': update_date})
cr.commit() cr.commit()
def bind_recurring_events_to_google(self, cr, uid, context): def bind_recurring_events_to_google(self, cr, uid, context):
ev_obj = self.pool['calendar.event'] ev_obj = self.pool['calendar.event']
att_obj = self.pool['calendar.attendee'] att_obj = self.pool['calendar.attendee']
user_obj = self.pool['res.users'] user_obj = self.pool['res.users']
myPartnerID = user_obj.browse(cr,uid,uid,context=context).partner_id.id myPartnerID = user_obj.browse(cr, uid, uid, context=context).partner_id.id
context_norecurrent = context.copy() context_norecurrent = context.copy()
context_norecurrent['virtual_id'] = False context_norecurrent['virtual_id'] = False
context_norecurrent['active_test'] = False context_norecurrent['active_test'] = False
my_att_ids = att_obj.search(cr, uid,[('partner_id', '=', myPartnerID),('google_internal_event_id', '=', False)], context=context_norecurrent) my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('google_internal_event_id', '=', False)], context=context_norecurrent)
for att in att_obj.browse(cr,uid,my_att_ids,context=context): for att in att_obj.browse(cr, uid, my_att_ids, context=context):
if att.event_id.recurrent_id and att.event_id.recurrent_id > 0: if att.event_id.recurrent_id and att.event_id.recurrent_id > 0:
new_google_internal_event_id = False new_google_internal_event_id = False
source_event_record = ev_obj.browse(cr, uid, att.event_id.recurrent_id, context) source_event_record = ev_obj.browse(cr, uid, att.event_id.recurrent_id, context)
source_attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',source_event_record.id)], context=context) source_attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', source_event_record.id)], context=context)
source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context)[0] source_attendee_record = att_obj.browse(cr, uid, source_attendee_record_id, context)[0]
if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id: if att.event_id.recurrent_id_date and source_event_record.allday and source_attendee_record.google_internal_event_id:
new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.split(' ')[0].replace('-','') new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.split(' ')[0].replace('-', '')
elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id: elif att.event_id.recurrent_id_date and source_attendee_record.google_internal_event_id:
new_google_internal_event_id = source_attendee_record.google_internal_event_id +'_'+ att.event_id.recurrent_id_date.replace('-','').replace(' ','T').replace(':','') + 'Z' new_google_internal_event_id = source_attendee_record.google_internal_event_id + '_' + att.event_id.recurrent_id_date.replace('-', '').replace(' ', 'T').replace(':', '') + 'Z'
if new_google_internal_event_id: if new_google_internal_event_id:
#TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE ! #TODO WARNING, NEED TO CHECK THAT EVENT and ALL instance NOT DELETE IN GMAIL BEFORE !
res = self.update_recurrent_event_exclu(cr, uid,new_google_internal_event_id, source_attendee_record.google_internal_event_id,att.event_id, context=context) self.update_recurrent_event_exclu(cr, uid, new_google_internal_event_id, source_attendee_record.google_internal_event_id, att.event_id, context=context)
att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context) att_obj.write(cr, uid, [att.id], {'google_internal_event_id': new_google_internal_event_id}, context=context)
cr.commit() cr.commit()
@ -523,16 +518,16 @@ class google_calendar(osv.AbstractModel):
context_novirtual['active_test'] = False context_novirtual['active_test'] = False
all_event_from_google = self.get_event_dict(cr, uid, context=context) all_event_from_google = self.get_event_dict(cr, uid, context=context)
# Select all events from OpenERP which have been already synchronized in gmail # Select all events from OpenERP which have been already synchronized in gmail
my_att_ids = att_obj.search(cr, uid,[ ('partner_id', '=', myPartnerID), my_att_ids = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID),
('google_internal_event_id', '!=', False), ('google_internal_event_id', '!=', False),
'|', '|',
('event_id.date_deadline','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), ('event_id.date_deadline', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
('event_id.end_date','>',self.get_start_time_to_synchro(cr,uid,context).strftime("%Y-%m-%d %H:%M:%S")), ('event_id.end_date', '>', self.get_start_time_to_synchro(cr, uid, context).strftime("%Y-%m-%d %H:%M:%S")),
], context=context_novirtual) ], context=context_novirtual)
event_to_synchronize = {} event_to_synchronize = {}
for att in att_obj.browse(cr,uid,my_att_ids,context=context): for att in att_obj.browse(cr, uid, my_att_ids, context=context):
event = att.event_id event = att.event_id
base_event_id = att.google_internal_event_id.split('_')[0] base_event_id = att.google_internal_event_id.split('_')[0]
@ -544,7 +539,7 @@ class google_calendar(osv.AbstractModel):
event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent() event_to_synchronize[base_event_id][att.google_internal_event_id] = SyncEvent()
ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id] ev_to_sync = event_to_synchronize[base_event_id][att.google_internal_event_id]
ev_to_sync.OE.attendee_id = att.id ev_to_sync.OE.attendee_id = att.id
ev_to_sync.OE.event = event ev_to_sync.OE.event = event
ev_to_sync.OE.found = True ev_to_sync.OE.found = True
@ -566,40 +561,35 @@ class google_calendar(osv.AbstractModel):
event_to_synchronize[base_event_id][event_id] = SyncEvent() event_to_synchronize[base_event_id][event_id] = SyncEvent()
ev_to_sync = event_to_synchronize[base_event_id][event_id] ev_to_sync = event_to_synchronize[base_event_id][event_id]
ev_to_sync.GG.event = event ev_to_sync.GG.event = event
ev_to_sync.GG.found = True ev_to_sync.GG.found = True
ev_to_sync.GG.isRecurrence = bool(event.get('recurrence','')) ev_to_sync.GG.isRecurrence = bool(event.get('recurrence', ''))
ev_to_sync.GG.isInstance = bool(event.get('recurringEventId',0)) ev_to_sync.GG.isInstance = bool(event.get('recurringEventId', 0))
ev_to_sync.GG.update = event.get('updated',None) # if deleted, no date without browse event ev_to_sync.GG.update = event.get('updated', None) # if deleted, no date without browse event
if ev_to_sync.GG.update: if ev_to_sync.GG.update:
ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T',' ').replace('Z','') ev_to_sync.GG.update = ev_to_sync.GG.update.replace('T', ' ').replace('Z', '')
ev_to_sync.GG.status = (event.get('status') != 'cancelled') ev_to_sync.GG.status = (event.get('status') != 'cancelled')
###################### ######################
# PRE-PROCESSING # # PRE-PROCESSING #
###################### ######################
for base_event in event_to_synchronize: for base_event in event_to_synchronize:
for current_event in event_to_synchronize[base_event]: for current_event in event_to_synchronize[base_event]:
event_to_synchronize[base_event][current_event].compute_OP() event_to_synchronize[base_event][current_event].compute_OP()
#print event_to_synchronize[base_event] #print event_to_synchronize[base_event]
#print "========================================================"
###################### ######################
# DO ACTION # # DO ACTION #
###################### ######################
for base_event in event_to_synchronize: for base_event in event_to_synchronize:
event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(),key=operator.itemgetter(0)) event_to_synchronize[base_event] = sorted(event_to_synchronize[base_event].iteritems(), key=operator.itemgetter(0))
for current_event in event_to_synchronize[base_event]: for current_event in event_to_synchronize[base_event]:
cr.commit() cr.commit()
event = current_event[1] # event is an Sync Event ! event = current_event[1] # event is an Sync Event !
actToDo = event.OP actToDo = event.OP
actSrc = event.OP.src actSrc = event.OP.src
# if not isinstance(actToDo, NothingToDo):
# print event
context['curr_attendee'] = event.OE.attendee_id context['curr_attendee'] = event.OE.attendee_id
if isinstance(actToDo, NothingToDo): if isinstance(actToDo, NothingToDo):
@ -607,135 +597,134 @@ class google_calendar(osv.AbstractModel):
elif isinstance(actToDo, Create): elif isinstance(actToDo, Create):
context_tmp = context.copy() context_tmp = context.copy()
context_tmp['NewMeeting'] = True context_tmp['NewMeeting'] = True
if actSrc == 'GG': if actSrc == 'GG':
res = self.update_from_google(cr, uid, False, event.GG.event, "create", context=context_tmp) res = self.update_from_google(cr, uid, False, event.GG.event, "create", context=context_tmp)
event.OE.event_id = res event.OE.event_id = res
meeting = calendar_event.browse(cr,uid,res,context=context) meeting = calendar_event.browse(cr, uid, res, context=context)
attendee_record_id = att_obj.search(cr, uid, [('partner_id','=', myPartnerID), ('event_id','=',res)], context=context) attendee_record_id = att_obj.search(cr, uid, [('partner_id', '=', myPartnerID), ('event_id', '=', res)], context=context)
self.pool['calendar.attendee'].write(cr, uid, attendee_record_id, {'oe_synchro_date':meeting.oe_update_date, 'google_internal_event_id':event.GG.event['id']}, context=context_tmp) self.pool['calendar.attendee'].write(cr, uid, attendee_record_id, {'oe_synchro_date': meeting.oe_update_date, 'google_internal_event_id': event.GG.event['id']}, context=context_tmp)
elif actSrc == 'OE': elif actSrc == 'OE':
raise "Should be never here, creation for OE is done before update !" raise "Should be never here, creation for OE is done before update !"
#TODO Add to batch #TODO Add to batch
elif isinstance(actToDo, Update): elif isinstance(actToDo, Update):
if actSrc == 'GG': if actSrc == 'GG':
self.update_from_google(cr, uid, event.OE.event, event.GG.event, 'write', context) self.update_from_google(cr, uid, event.OE.event, event.GG.event, 'write', context)
elif actSrc == 'OE': elif actSrc == 'OE':
self.update_to_google(cr, uid, event.OE.event, event.GG.event, context) self.update_to_google(cr, uid, event.OE.event, event.GG.event, context)
elif isinstance(actToDo, Exclude): elif isinstance(actToDo, Exclude):
if actSrc == 'OE': if actSrc == 'OE':
self.delete_an_event(cr,uid,current_event[0],context=context) self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'GG': elif actSrc == 'GG':
new_google_event_id = event.GG.event['id'].split('_')[1] new_google_event_id = event.GG.event['id'].split('_')[1]
if 'T' in new_google_event_id: if 'T' in new_google_event_id:
new_google_event_id = new_google_event_id.replace('T','')[:-1] new_google_event_id = new_google_event_id.replace('T', '')[:-1]
else: else:
new_google_event_id = new_google_event_id + "000000" new_google_event_id = new_google_event_id + "000000"
if event.GG.status: if event.GG.status:
parent_event = {} parent_event = {}
parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id , new_google_event_id) parent_event['id'] = "%s-%s" % (event_to_synchronize[base_event][0][1].OE.event_id, new_google_event_id)
res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context) res = self.update_from_google(cr, uid, parent_event, event.GG.event, "copy", context)
else: else:
if event_to_synchronize[base_event][0][1].OE.event_id: if event_to_synchronize[base_event][0][1].OE.event_id:
parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id parent_oe_id = event_to_synchronize[base_event][0][1].OE.event_id
calendar_event.unlink(cr,uid,"%s-%s" % (parent_oe_id,new_google_event_id),unlink_level=1,context=context) calendar_event.unlink(cr, uid, "%s-%s" % (parent_oe_id, new_google_event_id), unlink_level=1, context=context)
elif isinstance(actToDo, Delete): elif isinstance(actToDo, Delete):
if actSrc == 'GG': if actSrc == 'GG':
self.delete_an_event(cr,uid,current_event[0],context=context) self.delete_an_event(cr, uid, current_event[0], context=context)
elif actSrc == 'OE': elif actSrc == 'OE':
calendar_event.unlink(cr,uid,event.OE.event_id,unlink_level=0,context=context) calendar_event.unlink(cr, uid, event.OE.event_id, unlink_level=0, context=context)
return True return True
def check_and_sync(self, cr, uid, oe_event, google_event, context): def check_and_sync(self, cr, uid, oe_event, google_event, context):
if datetime.strptime(oe_event.oe_update_date,"%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'],"%Y-%m-%dT%H:%M:%S.%fz"): if datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") > datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"):
self.update_to_google(cr, uid, oe_event, google_event, context) self.update_to_google(cr, uid, oe_event, google_event, context)
elif datetime.strptime(oe_event.oe_update_date,"%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'],"%Y-%m-%dT%H:%M:%S.%fz"): elif datetime.strptime(oe_event.oe_update_date, "%Y-%m-%d %H:%M:%S.%f") < datetime.strptime(google_event['updated'], "%Y-%m-%dT%H:%M:%S.%fz"):
self.update_from_google(cr, uid, oe_event, google_event, 'write', context) self.update_from_google(cr, uid, oe_event, google_event, 'write', context)
def get_sequence(self,cr,uid,instance_id,context=None): def get_sequence(self, cr, uid, instance_id, context=None):
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
params = { params = {
'fields': 'sequence', 'fields': 'sequence',
'access_token' : self.get_token(cr,uid,context) 'access_token': self.get_token(cr, uid, context)
} }
headers = {'Content-type': 'application/json'} headers = {'Content-type': 'application/json'}
url = "/calendar/v3/calendars/%s/events/%s" % ('primary',instance_id) url = "/calendar/v3/calendars/%s/events/%s" % ('primary', instance_id)
content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context) content = gs_pool._do_request(cr, uid, url, params, headers, type='GET', context=context)
return content.get('sequence',0) return content.get('sequence', 0)
################################# #################################
## MANAGE CONNEXION TO GMAIL ## ## MANAGE CONNEXION TO GMAIL ##
################################# #################################
def get_token(self,cr,uid,context=None): def get_token(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr,uid,uid,context=context) current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)): if datetime.strptime(current_user.google_calendar_token_validity.split('.')[0], "%Y-%m-%d %H:%M:%S") < (datetime.now() + timedelta(minutes=1)):
self.do_refresh_token(cr,uid,context=context) self.do_refresh_token(cr, uid, context=context)
current_user.refresh() current_user.refresh()
return current_user.google_calendar_token return current_user.google_calendar_token
def do_refresh_token(self,cr,uid,context=None): def do_refresh_token(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr,uid,uid,context=context) current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
refresh = current_user.google_calendar_rtoken
all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken, self.STR_SERVICE, context=context) all_token = gs_pool._refresh_google_token_json(cr, uid, current_user.google_calendar_rtoken, self.STR_SERVICE, context=context)
vals = {} vals = {}
vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
self.pool['res.users'].write(cr,SUPERUSER_ID,uid,vals,context=context) self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
def need_authorize(self,cr,uid,context=None): def need_authorize(self, cr, uid, context=None):
current_user = self.pool['res.users'].browse(cr,uid,uid,context=context) current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
return current_user.google_calendar_rtoken == False return current_user.google_calendar_rtoken is False
def get_calendar_scope(self,RO=False): def get_calendar_scope(self, RO=False):
readonly = RO and '.readonly' or '' readonly = RO and '.readonly' or ''
return 'https://www.googleapis.com/auth/calendar%s' % (readonly) return 'https://www.googleapis.com/auth/calendar%s' % (readonly)
def authorize_google_uri(self,cr,uid,from_url='http://www.openerp.com',context=None): def authorize_google_uri(self, cr, uid, from_url='http://www.openerp.com', context=None):
url = self.pool['google.service']._get_authorize_uri(cr,uid,from_url,self.STR_SERVICE,scope=self.get_calendar_scope(),context=context) url = self.pool['google.service']._get_authorize_uri(cr, uid, from_url, self.STR_SERVICE, scope=self.get_calendar_scope(), context=context)
return url return url
def can_authorize_google(self,cr,uid,context=None): def can_authorize_google(self, cr, uid, context=None):
return self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager') return self.pool['res.users'].has_group(cr, uid, 'base.group_erp_manager')
def set_all_tokens(self,cr,uid,authorization_code,context=None): def set_all_tokens(self, cr, uid, authorization_code, context=None):
gs_pool = self.pool['google.service'] gs_pool = self.pool['google.service']
all_token = gs_pool._get_google_token_json(cr, uid, authorization_code,self.STR_SERVICE,context=context) all_token = gs_pool._get_google_token_json(cr, uid, authorization_code, self.STR_SERVICE, context=context)
vals = {} vals = {}
vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token') vals['google_%s_rtoken' % self.STR_SERVICE] = all_token.get('refresh_token')
vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in')) vals['google_%s_token_validity' % self.STR_SERVICE] = datetime.now() + timedelta(seconds=all_token.get('expires_in'))
vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token') vals['google_%s_token' % self.STR_SERVICE] = all_token.get('access_token')
self.pool['res.users'].write(cr,SUPERUSER_ID,uid,vals,context=context) self.pool['res.users'].write(cr, SUPERUSER_ID, uid, vals, context=context)
def get_start_time_to_synchro(self, cr, uid, context=None) : def get_start_time_to_synchro(self, cr, uid, context=None):
# WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4
number_of_week = 13 number_of_week = 13
return datetime.now()-timedelta(weeks=number_of_week) return datetime.now() - timedelta(weeks=number_of_week)
def get_need_synchro_attendee(self, cr, uid, context=None): def get_need_synchro_attendee(self, cr, uid, context=None):
# WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4 # WILL BE AN IR CONFIG PARAMETER - beginning from SAAS4
return True return True
class res_users(osv.Model):
class res_users(osv.Model):
_inherit = 'res.users' _inherit = 'res.users'
_columns = { _columns = {
'google_calendar_rtoken': fields.char('Refresh Token'), 'google_calendar_rtoken': fields.char('Refresh Token'),
'google_calendar_token': fields.char('User token'), 'google_calendar_token': fields.char('User token'),
'google_calendar_token_validity': fields.datetime('Token Validity'), 'google_calendar_token_validity': fields.datetime('Token Validity'),
} }
class calendar_event(osv.Model): class calendar_event(osv.Model):
@ -743,7 +732,7 @@ class calendar_event(osv.Model):
def write(self, cr, uid, ids, vals, context=None): def write(self, cr, uid, ids, vals, context=None):
if context is None: if context is None:
context= {} context = {}
sync_fields = set(['name', 'description', 'date', 'date_closed', 'date_deadline', 'attendee_ids', 'location', 'class']) sync_fields = set(['name', 'description', 'date', 'date_closed', 'date_deadline', 'attendee_ids', 'location', 'class'])
if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context: if (set(vals.keys()) & sync_fields) and 'oe_update_date' not in vals.keys() and 'NewMeeting' not in context:
vals['oe_update_date'] = datetime.now() vals['oe_update_date'] = datetime.now()
@ -773,19 +762,17 @@ class calendar_attendee(osv.Model):
'google_internal_event_id': fields.char('Google Calendar Event Id', size=256), 'google_internal_event_id': fields.char('Google Calendar Event Id', size=256),
'oe_synchro_date': fields.datetime('OpenERP Synchro Date'), 'oe_synchro_date': fields.datetime('OpenERP Synchro Date'),
} }
_sql_constraints = [('google_id_uniq', 'unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')]
_sql_constraints = [('google_id_uniq','unique(google_internal_event_id,partner_id,event_id)', 'Google ID should be unique!')]
def write(self, cr, uid, ids, vals, context=None): def write(self, cr, uid, ids, vals, context=None):
if context is None: if context is None:
context = {} context = {}
for id in ids: for id in ids:
ref = vals.get('event_id',self.browse(cr,uid,id,context=context).event_id.id) ref = vals.get('event_id', self.browse(cr, uid, id, context=context).event_id.id)
# If attendees are updated, we need to specify that next synchro need an action # If attendees are updated, we need to specify that next synchro need an action
# Except if it come from an update_from_google # Except if it come from an update_from_google
if not context.get('curr_attendee', False) and not context.get('NewMeeting', False): if not context.get('curr_attendee', False) and not context.get('NewMeeting', False):
self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date':datetime.now()},context) self.pool['calendar.event'].write(cr, uid, ref, {'oe_update_date': datetime.now()}, context)
return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context)
return super(calendar_attendee, self).write(cr, uid, ids, vals, context=context)

963
addons/hr/i18n/am.po Normal file
View File

@ -0,0 +1,963 @@
# Amharic translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2014-03-18 13:39+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Amharic <am@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-03-19 05:52+0000\n"
"X-Generator: Launchpad (build 16963)\n"
#. module: hr
#: model:process.node,name:hr.process_node_openerpuser0
msgid "Openerp user"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_timesheet_sheet:0
msgid "Allow timesheets validation by managers"
msgstr ""
#. module: hr
#: field:hr.job,requirements:0
msgid "Requirements"
msgstr "አስፈላጊ"
#. module: hr
#: model:process.transition,name:hr.process_transition_contactofemployee0
msgid "Link the employee to information"
msgstr "የሰራተኞች መረጃ ማገናኘት"
#. module: hr
#: field:hr.employee,sinid:0
msgid "SIN No"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,name:hr.open_board_hr
#: model:ir.ui.menu,name:hr.menu_hr_dashboard
#: model:ir.ui.menu,name:hr.menu_hr_main
#: model:ir.ui.menu,name:hr.menu_hr_reporting
#: model:ir.ui.menu,name:hr.menu_hr_root
#: model:ir.ui.menu,name:hr.menu_human_resources_configuration
msgid "Human Resources"
msgstr "የሰው ሀይል አስተዳደር"
#. module: hr
#: help:hr.employee,image_medium:0
msgid ""
"Medium-sized photo of the employee. It is automatically resized as a "
"128x128px image, with aspect ratio preserved. Use this field in form views "
"or some kanban views."
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Time Tracking"
msgstr ""
#. module: hr
#: view:hr.employee:0
#: view:hr.job:0
msgid "Group By..."
msgstr ""
#. module: hr
#: model:ir.actions.act_window,name:hr.view_department_form_installer
msgid "Create Your Departments"
msgstr ""
#. module: hr
#: help:hr.job,no_of_employee:0
msgid "Number of employees currently occupying this job position."
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_evaluation:0
msgid "Organize employees periodic evaluation"
msgstr ""
#. module: hr
#: view:hr.department:0
#: view:hr.employee:0
#: field:hr.employee,department_id:0
#: view:hr.job:0
#: field:hr.job,department_id:0
#: model:ir.model,name:hr.model_hr_department
msgid "Department"
msgstr ""
#. module: hr
#: field:hr.employee,work_email:0
msgid "Work Email"
msgstr ""
#. module: hr
#: help:hr.employee,image:0
msgid ""
"This field holds the image used as photo for the employee, limited to "
"1024x1024px."
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_holidays:0
msgid "This installs the module hr_holidays."
msgstr ""
#. module: hr
#: view:hr.job:0
msgid "Jobs"
msgstr ""
#. module: hr
#: view:hr.job:0
msgid "In Recruitment"
msgstr ""
#. module: hr
#: field:hr.job,message_unread:0
msgid "Unread Messages"
msgstr ""
#. module: hr
#: field:hr.department,company_id:0
#: view:hr.employee:0
#: view:hr.job:0
#: field:hr.job,company_id:0
msgid "Company"
msgstr ""
#. module: hr
#: field:hr.job,no_of_recruitment:0
msgid "Expected in Recruitment"
msgstr ""
#. module: hr
#: field:res.users,employee_ids:0
msgid "Related employees"
msgstr ""
#. module: hr
#: constraint:hr.employee.category:0
msgid "Error! You cannot create recursive Categories."
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_recruitment:0
msgid "This installs the module hr_recruitment."
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Birth"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,name:hr.open_view_categ_form
#: model:ir.ui.menu,name:hr.menu_view_employee_category_form
msgid "Employee Tags"
msgstr ""
#. module: hr
#: view:hr.job:0
msgid "Launch Recruitement"
msgstr ""
#. module: hr
#: model:process.transition,name:hr.process_transition_employeeuser0
msgid "Link a user to an employee"
msgstr ""
#. module: hr
#: field:hr.department,parent_id:0
msgid "Parent Department"
msgstr ""
#. module: hr
#: model:ir.ui.menu,name:hr.menu_open_view_attendance_reason_config
msgid "Leaves"
msgstr ""
#. module: hr
#: selection:hr.employee,marital:0
msgid "Married"
msgstr ""
#. module: hr
#: field:hr.job,message_ids:0
msgid "Messages"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Talent Management"
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_timesheet_sheet:0
msgid "This installs the module hr_timesheet_sheet."
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Mobile:"
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Position"
msgstr ""
#. module: hr
#: help:hr.job,message_unread:0
msgid "If checked new messages require your attention."
msgstr ""
#. module: hr
#: field:hr.employee,color:0
msgid "Color Index"
msgstr ""
#. module: hr
#: model:process.transition,note:hr.process_transition_employeeuser0
msgid ""
"The Related user field on the Employee form allows to link the OpenERP user "
"(and her rights) to the employee."
msgstr ""
#. module: hr
#: field:hr.employee,image_medium:0
msgid "Medium-sized photo"
msgstr ""
#. module: hr
#: field:hr.employee,identification_id:0
msgid "Identification No"
msgstr ""
#. module: hr
#: selection:hr.employee,gender:0
msgid "Female"
msgstr ""
#. module: hr
#: model:ir.ui.menu,name:hr.menu_open_view_attendance_reason_new_config
msgid "Attendance"
msgstr ""
#. module: hr
#: field:hr.employee,work_phone:0
msgid "Work Phone"
msgstr ""
#. module: hr
#: field:hr.employee.category,child_ids:0
msgid "Child Categories"
msgstr ""
#. module: hr
#: field:hr.job,description:0
#: model:ir.model,name:hr.model_hr_job
msgid "Job Description"
msgstr ""
#. module: hr
#: field:hr.employee,work_location:0
msgid "Office Location"
msgstr ""
#. module: hr
#: field:hr.job,message_follower_ids:0
msgid "Followers"
msgstr ""
#. module: hr
#: view:hr.employee:0
#: model:ir.model,name:hr.model_hr_employee
#: model:process.node,name:hr.process_node_employee0
msgid "Employee"
msgstr ""
#. module: hr
#: model:process.node,note:hr.process_node_employeecontact0
msgid "Other information"
msgstr ""
#. module: hr
#: help:hr.employee,image_small:0
msgid ""
"Small-sized photo of the employee. It is automatically resized as a 64x64px "
"image, with aspect ratio preserved. Use this field anywhere a small image is "
"required."
msgstr ""
#. module: hr
#: field:hr.employee,birthday:0
msgid "Date of Birth"
msgstr ""
#. module: hr
#: help:hr.job,no_of_recruitment:0
msgid "Number of new employees you expect to recruit."
msgstr ""
#. module: hr
#: model:ir.actions.client,name:hr.action_client_hr_menu
msgid "Open HR Menu"
msgstr ""
#. module: hr
#: help:hr.job,message_summary:0
msgid ""
"Holds the Chatter summary (number of messages, ...). This summary is "
"directly in html format in order to be inserted in kanban views."
msgstr ""
#. module: hr
#: help:hr.config.settings,module_account_analytic_analysis:0
msgid ""
"This installs the module account_analytic_analysis, which will install sales "
"management too."
msgstr ""
#. module: hr
#: view:board.board:0
msgid "Human Resources Dashboard"
msgstr ""
#. module: hr
#: view:hr.employee:0
#: field:hr.employee,job_id:0
#: view:hr.job:0
msgid "Job"
msgstr ""
#. module: hr
#: field:hr.job,no_of_employee:0
msgid "Current Number of Employees"
msgstr ""
#. module: hr
#: field:hr.department,member_ids:0
msgid "Members"
msgstr ""
#. module: hr
#: model:ir.ui.menu,name:hr.menu_hr_configuration
msgid "Configuration"
msgstr ""
#. module: hr
#: model:process.node,note:hr.process_node_employee0
msgid "Employee form and structure"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_expense:0
msgid "Manage employees expenses"
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Tel:"
msgstr ""
#. module: hr
#: selection:hr.employee,marital:0
msgid "Divorced"
msgstr ""
#. module: hr
#: field:hr.employee.category,parent_id:0
msgid "Parent Category"
msgstr ""
#. module: hr
#: view:hr.department:0
#: model:ir.actions.act_window,name:hr.open_module_tree_department
#: model:ir.ui.menu,name:hr.menu_hr_department_tree
msgid "Departments"
msgstr ""
#. module: hr
#: model:process.node,name:hr.process_node_employeecontact0
msgid "Employee Contact"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,help:hr.action_hr_job
msgid ""
"<p class=\"oe_view_nocontent_create\">\n"
" Click to define a new job position.\n"
" </p><p>\n"
" Job Positions are used to define jobs and their "
"requirements.\n"
" You can keep track of the number of employees you have per "
"job\n"
" position and follow the evolution according to what you "
"planned\n"
" for the future.\n"
" </p><p>\n"
" You can attach a survey to a job position. It will be used "
"in\n"
" the recruitment process to evaluate the applicants for this "
"job\n"
" position.\n"
" </p>\n"
" "
msgstr ""
#. module: hr
#: selection:hr.employee,gender:0
msgid "Male"
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid ""
"$('.oe_employee_picture').load(function() { if($(this).width() > "
"$(this).height()) { $(this).addClass('oe_employee_picture_wide') } });"
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_evaluation:0
msgid "This installs the module hr_evaluation."
msgstr ""
#. module: hr
#: constraint:hr.employee:0
msgid "Error! You cannot create recursive hierarchy of Employee(s)."
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_attendance:0
msgid "This installs the module hr_attendance."
msgstr ""
#. module: hr
#: field:hr.employee,image_small:0
msgid "Smal-sized photo"
msgstr ""
#. module: hr
#: view:hr.employee.category:0
#: model:ir.model,name:hr.model_hr_employee_category
msgid "Employee Category"
msgstr ""
#. module: hr
#: field:hr.employee,category_ids:0
msgid "Tags"
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_contract:0
msgid "This installs the module hr_contract."
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Related User"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "or"
msgstr ""
#. module: hr
#: field:hr.employee.category,name:0
msgid "Category"
msgstr ""
#. module: hr
#: view:hr.job:0
msgid "Stop Recruitment"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_attendance:0
msgid "Install attendances feature"
msgstr ""
#. module: hr
#: help:hr.employee,bank_account_id:0
msgid "Employee bank salary account"
msgstr ""
#. module: hr
#: field:hr.department,note:0
msgid "Note"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,name:hr.open_view_employee_tree
msgid "Employees Structure"
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Contact Information"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_holidays:0
msgid "Manage holidays, leaves and allocation requests"
msgstr ""
#. module: hr
#: field:hr.department,child_ids:0
msgid "Child Departments"
msgstr ""
#. module: hr
#: view:hr.employee:0
#: view:hr.job:0
#: field:hr.job,state:0
msgid "Status"
msgstr ""
#. module: hr
#: field:hr.employee,otherid:0
msgid "Other Id"
msgstr ""
#. module: hr
#: model:process.process,name:hr.process_process_employeecontractprocess0
msgid "Employee Contract"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Contracts"
msgstr ""
#. module: hr
#: help:hr.job,message_ids:0
msgid "Messages and communication history"
msgstr ""
#. module: hr
#: field:hr.employee,ssnid:0
msgid "SSN No"
msgstr ""
#. module: hr
#: field:hr.job,message_is_follower:0
msgid "Is a Follower"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_recruitment:0
msgid "Manage the recruitment process"
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Active"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Human Resources Management"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Install your country's payroll"
msgstr ""
#. module: hr
#: field:hr.employee,bank_account_id:0
msgid "Bank Account Number"
msgstr ""
#. module: hr
#: view:hr.department:0
msgid "Companies"
msgstr ""
#. module: hr
#: field:hr.job,message_summary:0
msgid "Summary"
msgstr ""
#. module: hr
#: model:process.transition,note:hr.process_transition_contactofemployee0
msgid ""
"In the Employee form, there are different kind of information like Contact "
"information."
msgstr ""
#. module: hr
#: model:ir.actions.act_window,help:hr.open_view_employee_list_my
msgid ""
"<p class=\"oe_view_nocontent_create\">\n"
" Click to add a new employee.\n"
" </p><p>\n"
" With just a quick glance on the OpenERP employee screen, "
"you\n"
" can easily find all the information you need for each "
"person;\n"
" contact data, job position, availability, etc.\n"
" </p>\n"
" "
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "HR Settings"
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Citizenship & Other Info"
msgstr ""
#. module: hr
#: constraint:hr.department:0
msgid "Error! You cannot create recursive departments."
msgstr ""
#. module: hr
#: field:hr.employee,address_id:0
msgid "Working Address"
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Public Information"
msgstr ""
#. module: hr
#: field:hr.employee,marital:0
msgid "Marital Status"
msgstr ""
#. module: hr
#: model:ir.model,name:hr.model_ir_actions_act_window
msgid "ir.actions.act_window"
msgstr ""
#. module: hr
#: field:hr.employee,last_login:0
msgid "Latest Connection"
msgstr ""
#. module: hr
#: field:hr.employee,image:0
msgid "Photo"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Cancel"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,help:hr.open_module_tree_department
msgid ""
"<p class=\"oe_view_nocontent_create\">\n"
" Click to create a department.\n"
" </p><p>\n"
" OpenERP's department structure is used to manage all "
"documents\n"
" related to employees by departments: expenses, timesheets,\n"
" leaves and holidays, recruitments, etc.\n"
" </p>\n"
" "
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_timesheet:0
msgid "This installs the module hr_timesheet."
msgstr ""
#. module: hr
#: help:hr.job,expected_employees:0
msgid ""
"Expected number of employees for this job position after new recruitment."
msgstr ""
#. module: hr
#: model:ir.actions.act_window,help:hr.view_department_form_installer
msgid ""
"<p class=\"oe_view_nocontent_create\">\n"
" Click to define a new department.\n"
" </p><p>\n"
" Your departments structure is used to manage all documents\n"
" related to employees by departments: expenses and "
"timesheets,\n"
" leaves and holidays, recruitments, etc.\n"
" </p>\n"
" "
msgstr ""
#. module: hr
#: view:hr.employee:0
msgid "Personal Information"
msgstr ""
#. module: hr
#: field:hr.employee,city:0
msgid "City"
msgstr ""
#. module: hr
#: field:hr.employee,passport_id:0
msgid "Passport No"
msgstr ""
#. module: hr
#: field:hr.employee,mobile_phone:0
msgid "Work Mobile"
msgstr ""
#. module: hr
#: selection:hr.job,state:0
msgid "Recruitement in Progress"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_account_analytic_analysis:0
msgid ""
"Allow invoicing based on timesheets (the sale application will be installed)"
msgstr ""
#. module: hr
#: view:hr.employee.category:0
msgid "Employees Categories"
msgstr ""
#. module: hr
#: field:hr.employee,address_home_id:0
msgid "Home Address"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_timesheet:0
msgid "Manage timesheets"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,name:hr.open_payroll_modules
msgid "Payroll"
msgstr ""
#. module: hr
#: selection:hr.employee,marital:0
msgid "Single"
msgstr ""
#. module: hr
#: field:hr.job,name:0
msgid "Job Name"
msgstr ""
#. module: hr
#: view:hr.job:0
msgid "In Position"
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_payroll:0
msgid "This installs the module hr_payroll."
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_contract:0
msgid "Record contracts per employee"
msgstr ""
#. module: hr
#: view:hr.department:0
msgid "department"
msgstr ""
#. module: hr
#: field:hr.employee,country_id:0
msgid "Nationality"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Additional Features"
msgstr ""
#. module: hr
#: field:hr.employee,notes:0
msgid "Notes"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,name:hr.action2
msgid "Subordinate Hierarchy"
msgstr ""
#. module: hr
#: field:hr.employee,resource_id:0
msgid "Resource"
msgstr ""
#. module: hr
#: field:hr.department,complete_name:0
#: field:hr.employee,name_related:0
#: field:hr.employee.category,complete_name:0
msgid "Name"
msgstr ""
#. module: hr
#: field:hr.employee,gender:0
msgid "Gender"
msgstr ""
#. module: hr
#: view:hr.employee:0
#: field:hr.employee.category,employee_ids:0
#: field:hr.job,employee_ids:0
#: model:ir.actions.act_window,name:hr.hr_employee_normal_action_tree
#: model:ir.actions.act_window,name:hr.open_view_employee_list
#: model:ir.actions.act_window,name:hr.open_view_employee_list_my
#: model:ir.ui.menu,name:hr.menu_open_view_employee_list_my
msgid "Employees"
msgstr ""
#. module: hr
#: help:hr.employee,sinid:0
msgid "Social Insurance Number"
msgstr ""
#. module: hr
#: field:hr.department,name:0
msgid "Department Name"
msgstr ""
#. module: hr
#: model:ir.ui.menu,name:hr.menu_hr_reporting_timesheet
msgid "Reports"
msgstr ""
#. module: hr
#: field:hr.config.settings,module_hr_payroll:0
msgid "Manage payroll"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
#: model:ir.actions.act_window,name:hr.action_human_resources_configuration
msgid "Configure Human Resources"
msgstr ""
#. module: hr
#: selection:hr.job,state:0
msgid "No Recruitment"
msgstr ""
#. module: hr
#: help:hr.employee,ssnid:0
msgid "Social Security Number"
msgstr ""
#. module: hr
#: model:process.node,note:hr.process_node_openerpuser0
msgid "Creation of a OpenERP user"
msgstr ""
#. module: hr
#: field:hr.employee,login:0
msgid "Login"
msgstr ""
#. module: hr
#: field:hr.job,expected_employees:0
msgid "Total Forecasted Employees"
msgstr ""
#. module: hr
#: help:hr.job,state:0
msgid ""
"By default 'In position', set it to 'In Recruitment' if recruitment process "
"is going on for this job position."
msgstr ""
#. module: hr
#: model:ir.model,name:hr.model_res_users
msgid "Users"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,name:hr.action_hr_job
#: model:ir.ui.menu,name:hr.menu_hr_job
msgid "Job Positions"
msgstr ""
#. module: hr
#: model:ir.actions.act_window,help:hr.open_board_hr
msgid ""
"<div class=\"oe_empty_custom_dashboard\">\n"
" <p>\n"
" <b>Human Resources dashboard is empty.</b>\n"
" </p><p>\n"
" To add your first report into this dashboard, go to any\n"
" menu, switch to list or graph view, and click <i>'Add "
"to\n"
" Dashboard'</i> in the extended search options.\n"
" </p><p>\n"
" You can filter and group data before inserting into the\n"
" dashboard using the search options.\n"
" </p>\n"
" </div>\n"
" "
msgstr ""
#. module: hr
#: view:hr.employee:0
#: field:hr.employee,coach_id:0
msgid "Coach"
msgstr ""
#. module: hr
#: sql_constraint:hr.job:0
msgid "The name of the job position must be unique per company!"
msgstr ""
#. module: hr
#: help:hr.config.settings,module_hr_expense:0
msgid "This installs the module hr_expense."
msgstr ""
#. module: hr
#: model:ir.model,name:hr.model_hr_config_settings
msgid "hr.config.settings"
msgstr ""
#. module: hr
#: field:hr.department,manager_id:0
#: view:hr.employee:0
#: field:hr.employee,parent_id:0
msgid "Manager"
msgstr ""
#. module: hr
#: selection:hr.employee,marital:0
msgid "Widower"
msgstr ""
#. module: hr
#: field:hr.employee,child_ids:0
msgid "Subordinates"
msgstr ""
#. module: hr
#: view:hr.config.settings:0
msgid "Apply"
msgstr ""

View File

@ -263,6 +263,10 @@ class hr_expense_expense(osv.osv):
#convert eml into an osv-valid format #convert eml into an osv-valid format
lines = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, exp.employee_id.address_home_id, exp.date_confirm, context=context)), eml) lines = map(lambda x:(0,0,self.line_get_convert(cr, uid, x, exp.employee_id.address_home_id, exp.date_confirm, context=context)), eml)
journal_id = move_obj.browse(cr, uid, move_id, context).journal_id
# post the journal entry if 'Skip 'Draft' State for Manual Entries' is checked
if journal_id.entry_posted:
move_obj.button_validate(cr, uid, [move_id], context)
move_obj.write(cr, uid, [move_id], {'line_id': lines}, context=context) move_obj.write(cr, uid, [move_id], {'line_id': lines}, context=context)
self.write(cr, uid, ids, {'account_move_id': move_id, 'state': 'done'}, context=context) self.write(cr, uid, ids, {'account_move_id': move_id, 'state': 'done'}, context=context)
return True return True

View File

@ -370,7 +370,8 @@ class hr_holidays(osv.osv):
'date': record.date_from, 'date': record.date_from,
'end_date': record.date_to, 'end_date': record.date_to,
'date_deadline': record.date_to, 'date_deadline': record.date_to,
'state': 'open', # to block that meeting date in the calendar 'state': 'open', # to block that meeting date in the calendar
'class': 'confidential'
} }
#Add the partner_id (if exist) as an attendee #Add the partner_id (if exist) as an attendee
if record.user_id and record.user_id.partner_id: if record.user_id and record.user_id.partner_id:

View File

@ -23,7 +23,7 @@ import time
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from openerp.osv import fields, osv from openerp.osv import fields, osv
from openerp.tools import config from openerp.tools import config, float_compare
from openerp.tools.translate import _ from openerp.tools.translate import _
class hr_payslip(osv.osv): class hr_payslip(osv.osv):
@ -86,6 +86,7 @@ class hr_payslip(osv.osv):
def process_sheet(self, cr, uid, ids, context=None): def process_sheet(self, cr, uid, ids, context=None):
move_pool = self.pool.get('account.move') move_pool = self.pool.get('account.move')
period_pool = self.pool.get('account.period') period_pool = self.pool.get('account.period')
precision = self.pool.get('decimal.precision').precision_get(cr, uid, 'Payroll')
timenow = time.strftime('%Y-%m-%d') timenow = time.strftime('%Y-%m-%d')
for slip in self.browse(cr, uid, ids, context=context): for slip in self.browse(cr, uid, ids, context=context):
@ -149,7 +150,7 @@ class hr_payslip(osv.osv):
line_ids.append(credit_line) line_ids.append(credit_line)
credit_sum += credit_line[2]['credit'] - credit_line[2]['debit'] credit_sum += credit_line[2]['credit'] - credit_line[2]['debit']
if debit_sum > credit_sum: if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1:
acc_id = slip.journal_id.default_credit_account_id.id acc_id = slip.journal_id.default_credit_account_id.id
if not acc_id: if not acc_id:
raise osv.except_osv(_('Configuration Error!'),_('The Expense Journal "%s" has not properly configured the Credit Account!')%(slip.journal_id.name)) raise osv.except_osv(_('Configuration Error!'),_('The Expense Journal "%s" has not properly configured the Credit Account!')%(slip.journal_id.name))
@ -165,7 +166,7 @@ class hr_payslip(osv.osv):
}) })
line_ids.append(adjust_credit) line_ids.append(adjust_credit)
elif debit_sum < credit_sum: elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1:
acc_id = slip.journal_id.default_debit_account_id.id acc_id = slip.journal_id.default_debit_account_id.id
if not acc_id: if not acc_id:
raise osv.except_osv(_('Configuration Error!'),_('The Expense Journal "%s" has not properly configured the Debit Account!')%(slip.journal_id.name)) raise osv.except_osv(_('Configuration Error!'),_('The Expense Journal "%s" has not properly configured the Debit Account!')%(slip.journal_id.name))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
# Chinese (Simplified) translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-11-24 02:53+0000\n"
"PO-Revision-Date: 2014-03-13 05:55+0000\n"
"Last-Translator: LaoBiao.JX <betty2349@gmail.com>\n"
"Language-Team: Chinese (Simplified) <zh_CN@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-03-14 06:36+0000\n"
"X-Generator: Launchpad (build 16963)\n"
#. module: l10n_be_invoice_bba
#: sql_constraint:account.invoice:0
msgid "Invoice Number must be unique per Company!"
msgstr "发票号必须在公司范围内唯一"
#. module: l10n_be_invoice_bba
#: model:ir.model,name:l10n_be_invoice_bba.model_account_invoice
msgid "Invoice"
msgstr "发票"
#. module: l10n_be_invoice_bba
#: constraint:res.partner:0
msgid "Error ! You cannot create recursive associated members."
msgstr "错误,您不能创建循环引用的会员用户"
#. module: l10n_be_invoice_bba
#: constraint:account.invoice:0
msgid "Invalid BBA Structured Communication !"
msgstr "BBA结构化传输有误"
#. module: l10n_be_invoice_bba
#: selection:res.partner,out_inv_comm_algorithm:0
msgid "Random"
msgstr "随机"
#. module: l10n_be_invoice_bba
#: help:res.partner,out_inv_comm_type:0
msgid "Select Default Communication Type for Outgoing Invoices."
msgstr ""
#. module: l10n_be_invoice_bba
#: help:res.partner,out_inv_comm_algorithm:0
msgid ""
"Select Algorithm to generate the Structured Communication on Outgoing "
"Invoices."
msgstr ""
#. module: l10n_be_invoice_bba
#: code:addons/l10n_be_invoice_bba/invoice.py:109
#: code:addons/l10n_be_invoice_bba/invoice.py:135
#, python-format
msgid ""
"The daily maximum of outgoing invoices with an automatically generated BBA "
"Structured Communications has been exceeded!\n"
"Please create manually a unique BBA Structured Communication."
msgstr "自动生成结构化BBA传输已超出每日销售发票的最大值,请手动建立BBA结构化传输"
#. module: l10n_be_invoice_bba
#: code:addons/l10n_be_invoice_bba/invoice.py:150
#, python-format
msgid "Error!"
msgstr ""
#. module: l10n_be_invoice_bba
#: code:addons/l10n_be_invoice_bba/invoice.py:121
#, python-format
msgid ""
"The Partner should have a 3-7 digit Reference Number for the generation of "
"BBA Structured Communications!\n"
"Please correct the Partner record."
msgstr ""
#. module: l10n_be_invoice_bba
#: constraint:res.partner:0
msgid "Error: Invalid ean code"
msgstr "错误无效的EAN编码"
#. module: l10n_be_invoice_bba
#: code:addons/l10n_be_invoice_bba/invoice.py:108
#: code:addons/l10n_be_invoice_bba/invoice.py:120
#: code:addons/l10n_be_invoice_bba/invoice.py:134
#: code:addons/l10n_be_invoice_bba/invoice.py:162
#: code:addons/l10n_be_invoice_bba/invoice.py:172
#: code:addons/l10n_be_invoice_bba/invoice.py:197
#, python-format
msgid "Warning!"
msgstr "警告!"
#. module: l10n_be_invoice_bba
#: selection:res.partner,out_inv_comm_algorithm:0
msgid "Customer Reference"
msgstr "客户参考号"
#. module: l10n_be_invoice_bba
#: field:res.partner,out_inv_comm_type:0
msgid "Communication Type"
msgstr "讯息类型"
#. module: l10n_be_invoice_bba
#: code:addons/l10n_be_invoice_bba/invoice.py:173
#: code:addons/l10n_be_invoice_bba/invoice.py:198
#, python-format
msgid ""
"The BBA Structured Communication has already been used!\n"
"Please create manually a unique BBA Structured Communication."
msgstr ""
#. module: l10n_be_invoice_bba
#: selection:res.partner,out_inv_comm_algorithm:0
msgid "Date"
msgstr "事务处理日期"
#. module: l10n_be_invoice_bba
#: model:ir.model,name:l10n_be_invoice_bba.model_res_partner
msgid "Partner"
msgstr "合作伙伴"
#. module: l10n_be_invoice_bba
#: code:addons/l10n_be_invoice_bba/invoice.py:151
#, python-format
msgid ""
"Unsupported Structured Communication Type Algorithm '%s' !\n"
"Please contact your OpenERP support channel."
msgstr "不支持的结构化传输算法类型\"%s\"!请联系你的OpenERP 维护人员"
#. module: l10n_be_invoice_bba
#: field:res.partner,out_inv_comm_algorithm:0
msgid "Communication Algorithm"
msgstr ""
#. module: l10n_be_invoice_bba
#: code:addons/l10n_be_invoice_bba/invoice.py:163
#, python-format
msgid ""
"Empty BBA Structured Communication!\n"
"Please fill in a unique BBA Structured Communication."
msgstr ""

View File

@ -138,7 +138,7 @@ class wizard_multi_charts_accounts(osv.osv_memory):
def _process_taxes_translations(self, cr, uid, obj_multi, company_id, langs, field, context=None): def _process_taxes_translations(self, cr, uid, obj_multi, company_id, langs, field, context=None):
obj_tax_template = self.pool.get('account.tax.template') obj_tax_template = self.pool.get('account.tax.template')
obj_tax = self.pool.get('account.tax') obj_tax = self.pool.get('account.tax')
in_ids = sorted([x.id for x in obj_multi.chart_template_id.tax_template_ids]) in_ids = [x.id for x in obj_multi.chart_template_id.tax_template_ids]
out_ids = obj_tax.search(cr, uid, [('company_id', '=', company_id)], order='id') out_ids = obj_tax.search(cr, uid, [('company_id', '=', company_id)], order='id')
return self.process_translations(cr, uid, langs, obj_tax_template, field, in_ids, obj_tax, out_ids, context=context) return self.process_translations(cr, uid, langs, obj_tax_template, field, in_ids, obj_tax, out_ids, context=context)

View File

@ -186,12 +186,11 @@ class IrAttachment(osv.Model):
def get_attachment_type(self, cr, uid, ids, name, args, context=None): def get_attachment_type(self, cr, uid, ids, name, args, context=None):
result = {} result = {}
for attachment in self.browse(cr, uid, ids, context=context): for attachment in self.browse(cr, uid, ids, context=context):
fileext = os.path.splitext(attachment.datas_fname)[1].lower() fileext = os.path.splitext(attachment.datas_fname or '')[1].lower()[1:]
if not fileext or not fileext[1:] in self._fileext_to_type: result[attachment.id] = self._fileext_to_type.get(fileext, 'unknown')
return 'unknown'
result[attachment.id] = self._fileext_to_type[fileext[1:]]
return result return result
_columns = { _columns = {
'file_type': fields.function(get_attachment_type, type='char', string='File Type'), 'file_type_icon': fields.function(get_attachment_type, type='char', string='File Type Icon'),
'file_type': fields.related('file_type_icon', type='char'), # FIXME remove in trunk
} }

View File

@ -160,8 +160,8 @@ class mail_mail(osv.Model):
fragment['message_id'] = mail.mail_message_id.id fragment['message_id'] = mail.mail_message_id.id
elif mail.model and mail.res_id: elif mail.model and mail.res_id:
fragment.update(model=mail.model, res_id=mail.res_id) fragment.update(model=mail.model, res_id=mail.res_id)
url = urljoin(base_url, "?%s#%s" % (urlencode(query), urlencode(fragment))) url = urljoin(base_url, "/web?%s#%s" % (urlencode(query), urlencode(fragment)))
return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small></span>""") % url return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents <a style='color:inherit' href="%s">in OpenERP</a></small></span>""") % url
else: else:
return None return None

View File

@ -208,7 +208,7 @@ class mail_message(osv.Model):
raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias.")) raise osv.except_osv(_('Invalid Action!'), _("Unable to send email, please configure the sender's email address or alias."))
def _get_default_author(self, cr, uid, context=None): def _get_default_author(self, cr, uid, context=None):
return self.pool.get('res.users').read(cr, SUPERUSER_ID, [uid], ['partner_id'], context=context)[0]['partner_id'][0] return self.pool.get('res.users').browse(cr, SUPERUSER_ID, uid, context=context).partner_id.id
_defaults = { _defaults = {
'type': 'email', 'type': 'email',
@ -351,12 +351,12 @@ class mail_message(osv.Model):
partner_tree = dict((partner[0], partner) for partner in partners) partner_tree = dict((partner[0], partner) for partner in partners)
# 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see # 2. Attachments as SUPERUSER, because could receive msg and attachments for doc uid cannot see
attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type'], context=context) attachments = ir_attachment_obj.read(cr, SUPERUSER_ID, list(attachment_ids), ['id', 'datas_fname', 'name', 'file_type_icon'], context=context)
attachments_tree = dict((attachment['id'], { attachments_tree = dict((attachment['id'], {
'id': attachment['id'], 'id': attachment['id'],
'filename': attachment['datas_fname'], 'filename': attachment['datas_fname'],
'name': attachment['name'], 'name': attachment['name'],
'file_type': attachment['file_type'], 'file_type_icon': attachment['file_type_icon'],
}) for attachment in attachments) }) for attachment in attachments)
# 3. Update message dictionaries # 3. Update message dictionaries

View File

@ -161,6 +161,7 @@ class mail_thread(osv.AbstractModel):
if res[id]['message_unread_count']: if res[id]['message_unread_count']:
title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message") title = res[id]['message_unread_count'] > 1 and _("You have %d unread messages") % res[id]['message_unread_count'] or _("You have one unread message")
res[id]['message_summary'] = "<span class='oe_kanban_mail_new' title='%s'><span class='oe_e'>9</span> %d %s</span>" % (title, res[id].pop('message_unread_count'), _("New")) res[id]['message_summary'] = "<span class='oe_kanban_mail_new' title='%s'><span class='oe_e'>9</span> %d %s</span>" % (title, res[id].pop('message_unread_count'), _("New"))
res[id].pop('message_unread_count', None)
return res return res
def read_followers_data(self, cr, uid, follower_ids, context=None): def read_followers_data(self, cr, uid, follower_ids, context=None):
@ -209,7 +210,7 @@ class mail_thread(osv.AbstractModel):
], context=context) ], context=context)
for fol in fol_obj.browse(cr, uid, fol_ids, context=context): for fol in fol_obj.browse(cr, uid, fol_ids, context=context):
thread_subtype_dict = res[fol.res_id]['message_subtype_data'] thread_subtype_dict = res[fol.res_id]['message_subtype_data']
for subtype in fol.subtype_ids: for subtype in [st for st in fol.subtype_ids if st.name in thread_subtype_dict]:
thread_subtype_dict[subtype.name]['followed'] = True thread_subtype_dict[subtype.name]['followed'] = True
res[fol.res_id]['message_subtype_data'] = thread_subtype_dict res[fol.res_id]['message_subtype_data'] = thread_subtype_dict

View File

@ -76,8 +76,8 @@ openerp.mail = function (session) {
*/ */
get_text2html: function (text) { get_text2html: function (text) {
return text return text
.replace(/[\n\r]/g,'<br/>')
.replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ') .replace(/((?:https?|ftp):\/\/[\S]+)/g,'<a href="$1">$1</a> ')
.replace(/[\n\r]/g,'<br/>')
}, },
/* Returns the complete domain with "&" /* Returns the complete domain with "&"

View File

@ -88,10 +88,10 @@
--> -->
<t t-name="mail.thread.message.attachments"> <t t-name="mail.thread.message.attachments">
<t t-foreach='widget.attachment_ids' t-as='attachment'> <t t-foreach='widget.attachment_ids' t-as='attachment'>
<t t-if="attachment.file_type !== 'webimage'"> <t t-if="attachment.file_type_icon !== 'webimage'">
<div t-attf-class="oe_attachment #{attachment.upload ? 'oe_uploading' : ''}"> <div t-attf-class="oe_attachment #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank"> <a t-att-href='attachment.url' target="_blank">
<img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type + '.png'"></img> <img t-att-src="'/mail/static/src/img/mimetypes/' + attachment.file_type_icon + '.png'"></img>
<div class='oe_name'><t t-raw='attachment.name' /></div> <div class='oe_name'><t t-raw='attachment.name' /></div>
</a> </a>
<div class='oe_delete oe_e' title="Delete this attachment" t-att-data-id="attachment.id">[</div> <div class='oe_delete oe_e' title="Delete this attachment" t-att-data-id="attachment.id">[</div>
@ -100,7 +100,7 @@
</div> </div>
</div> </div>
</t> </t>
<t t-if="attachment.file_type === 'webimage'"> <t t-if="attachment.file_type_icon === 'webimage'">
<div t-attf-class="oe_attachment oe_preview #{attachment.upload ? 'oe_uploading' : ''}"> <div t-attf-class="oe_attachment oe_preview #{attachment.upload ? 'oe_uploading' : ''}">
<a t-att-href='attachment.url' target="_blank"> <a t-att-href='attachment.url' target="_blank">
<img t-att-src="widget.attachments_resize_image(attachment.id, [100,80])"></img> <img t-att-src="widget.attachments_resize_image(attachment.id, [100,80])"></img>

View File

@ -352,10 +352,10 @@ class mail_compose_message(osv.TransientModel):
:return dict results: for each res_id, the generated template values for :return dict results: for each res_id, the generated template values for
subject, body, email_from and reply_to subject, body, email_from and reply_to
""" """
subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context) subjects = self.render_template_batch(cr, uid, wizard.subject, wizard.model, res_ids, context=context)
bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context) bodies = self.render_template_batch(cr, uid, wizard.body, wizard.model, res_ids, context=context, post_process=True)
emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context) emails_from = self.render_template_batch(cr, uid, wizard.email_from, wizard.model, res_ids, context=context)
replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context) replies_to = self.render_template_batch(cr, uid, wizard.reply_to, wizard.model, res_ids, context=context)
results = dict.fromkeys(res_ids, False) results = dict.fromkeys(res_ids, False)
for res_id in res_ids: for res_id in res_ids:
@ -367,7 +367,7 @@ class mail_compose_message(osv.TransientModel):
} }
return results return results
def render_template_batch(self, cr, uid, template, model, res_ids, context=None): def render_template_batch(self, cr, uid, template, model, res_ids, context=None, post_process=False):
""" Render the given template text, replace mako-like expressions ``${expr}`` """ Render the given template text, replace mako-like expressions ``${expr}``
with the result of evaluating these expressions with an evaluation context with the result of evaluating these expressions with an evaluation context
containing: containing:

View File

@ -6,7 +6,7 @@
<record id="membership_0" model="product.product"> <record id="membership_0" model="product.product">
<field name="membership">True</field> <field name="membership">True</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/> <field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-12-01')" name="membership_date_to"/> <field eval="time.strftime('%Y-12-31')" name="membership_date_to"/>
<field name="name">Gold Membership</field> <field name="name">Gold Membership</field>
<field name="list_price">180</field> <field name="list_price">180</field>
<field name="categ_id" ref="product.product_category_1"/> <field name="categ_id" ref="product.product_category_1"/>
@ -16,7 +16,7 @@
<record id="membership_1" model="product.product"> <record id="membership_1" model="product.product">
<field name="membership">True</field> <field name="membership">True</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/> <field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-12-01')" name="membership_date_to"/> <field eval="time.strftime('%Y-12-31')" name="membership_date_to"/>
<field name="name">Silver Membership</field> <field name="name">Silver Membership</field>
<field name="categ_id" ref="product.product_category_1"/> <field name="categ_id" ref="product.product_category_1"/>
<field name="list_price">80</field> <field name="list_price">80</field>
@ -26,7 +26,7 @@
<record id="membership_2" model="product.product"> <record id="membership_2" model="product.product">
<field name="membership">True</field> <field name="membership">True</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/> <field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-12-01')" name="membership_date_to"/> <field eval="time.strftime('%Y-12-31')" name="membership_date_to"/>
<field name="name">Basic Membership</field> <field name="name">Basic Membership</field>
<field name="categ_id" ref="product.product_category_1"/> <field name="categ_id" ref="product.product_category_1"/>
<field name="list_price">40</field> <field name="list_price">40</field>

View File

@ -21,7 +21,6 @@
import time import time
from datetime import datetime from datetime import datetime
import openerp.addons.decimal_precision as dp import openerp.addons.decimal_precision as dp
from openerp.osv import fields, osv, orm from openerp.osv import fields, osv, orm
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP
@ -29,6 +28,8 @@ from openerp.tools import float_compare
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp import tools, SUPERUSER_ID from openerp import tools, SUPERUSER_ID
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp.addons.product import _common
class mrp_property_group(osv.osv): class mrp_property_group(osv.osv):
""" """
@ -56,8 +57,6 @@ class mrp_property(osv.osv):
_defaults = { _defaults = {
'composition': lambda *a: 'min', 'composition': lambda *a: 'min',
} }
#---------------------------------------------------------- #----------------------------------------------------------
# Work Centers # Work Centers
#---------------------------------------------------------- #----------------------------------------------------------
@ -349,7 +348,7 @@ class mrp_bom(osv.osv):
""" """
routing_obj = self.pool.get('mrp.routing') routing_obj = self.pool.get('mrp.routing')
factor = factor / (bom.product_efficiency or 1.0) factor = factor / (bom.product_efficiency or 1.0)
factor = rounding(factor, bom.product_rounding) factor = _common.ceiling(factor, bom.product_rounding)
if factor < bom.product_rounding: if factor < bom.product_rounding:
factor = bom.product_rounding factor = bom.product_rounding
result = [] result = []
@ -403,12 +402,6 @@ class mrp_bom(osv.osv):
return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context) return super(mrp_bom, self).copy_data(cr, uid, id, default, context=context)
def rounding(f, r):
import math
if not r:
return f
return math.ceil(f / r) * r
class mrp_production(osv.osv): class mrp_production(osv.osv):
""" """
Production Orders / Manufacturing Orders Production Orders / Manufacturing Orders
@ -650,12 +643,11 @@ class mrp_production(osv.osv):
""" """
self.write(cr, uid, ids, {'state': 'picking_except'}) self.write(cr, uid, ids, {'state': 'picking_except'})
return True return True
def _action_compute_lines(self, cr, uid, ids, properties=None, context=None): def _action_compute_lines(self, cr, uid, ids, properties=None, context=None):
""" Compute product_lines and workcenter_lines from BoM structure """ Compute product_lines and workcenter_lines from BoM structure
@return: product_lines @return: product_lines
""" """
if properties is None: if properties is None:
properties = [] properties = []
results = [] results = []
@ -663,14 +655,11 @@ class mrp_production(osv.osv):
uom_obj = self.pool.get('product.uom') uom_obj = self.pool.get('product.uom')
prod_line_obj = self.pool.get('mrp.production.product.line') prod_line_obj = self.pool.get('mrp.production.product.line')
workcenter_line_obj = self.pool.get('mrp.production.workcenter.line') workcenter_line_obj = self.pool.get('mrp.production.workcenter.line')
for production in self.browse(cr, uid, ids, context=context): for production in self.browse(cr, uid, ids, context=context):
#unlink product_lines #unlink product_lines
prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context) prod_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.product_lines], context=context)
#unlink workcenter_lines #unlink workcenter_lines
workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context) workcenter_line_obj.unlink(cr, SUPERUSER_ID, [line.id for line in production.workcenter_lines], context=context)
# search BoM structure and route # search BoM structure and route
bom_point = production.bom_id bom_point = production.bom_id
bom_id = production.bom_id.id bom_id = production.bom_id.id
@ -680,7 +669,7 @@ class mrp_production(osv.osv):
bom_point = bom_obj.browse(cr, uid, bom_id) bom_point = bom_obj.browse(cr, uid, bom_id)
routing_id = bom_point.routing_id.id or False routing_id = bom_point.routing_id.id or False
self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id}) self.write(cr, uid, [production.id], {'bom_id': bom_id, 'routing_id': routing_id})
if not bom_id: if not bom_id:
raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product.")) raise osv.except_osv(_('Error!'), _("Cannot find a bill of material for this product."))
@ -689,7 +678,6 @@ class mrp_production(osv.osv):
res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id) res = bom_obj._bom_explode(cr, uid, bom_point, factor / bom_point.product_qty, properties, routing_id=production.routing_id.id)
results = res[0] # product_lines results = res[0] # product_lines
results2 = res[1] # workcenter_lines results2 = res[1] # workcenter_lines
# reset product_lines in production order # reset product_lines in production order
for line in results: for line in results:
line['production_id'] = production.id line['production_id'] = production.id

View File

@ -19,24 +19,22 @@
# #
############################################################################## ##############################################################################
from openerp.addons.web import http from openerp.osv import osv
from openerp.addons.web.http import request
class bom_structure(http.Controller): class bom_structure(osv.AbstractModel):
_name = 'report.mrp.report_mrpbomstructure'
@http.route(['/report/mrp.report_mrpbomstructure/<docids>'], type='http', auth='user', website=True, multilang=True) def render_html(self, cr, uid, ids, data=None, context=None):
def report_mrpbomstructure(self, docids): mrpbom_obj = self.pool['mrp.bom']
ids = [int(i) for i in docids.split(',')] report_obj = self.pool['report']
ids = list(set(ids)) docs = mrpbom_obj.browse(cr, uid, ids, context=context)
report_obj = request.registry['mrp.bom']
docs = report_obj.browse(request.cr, request.uid, ids, context=request.context)
docargs = { docargs = {
'docs': docs, 'docs': docs,
'get_children': self.get_children, 'get_children': self.get_children,
} }
return request.registry['report'].render(request.cr, request.uid, [], 'mrp.report_mrpbomstructure', docargs) return report_obj.render(cr, uid, [], 'mrp.report_mrpbomstructure', docargs, context=context)
def get_children(self, object, level=0): def get_children(self, object, level=0):
result = [] result = []

View File

@ -59,6 +59,7 @@ The following topics should be covered by this module:
'test/test_mrp_repair_b4inv.yml', 'test/test_mrp_repair_b4inv.yml',
'test/test_mrp_repair_afterinv.yml', 'test/test_mrp_repair_afterinv.yml',
'test/test_mrp_repair_cancel.yml', 'test/test_mrp_repair_cancel.yml',
'test/test_mrp_repair_fee.yml',
], ],
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,

View File

@ -24,7 +24,6 @@ from datetime import datetime
from openerp.tools.translate import _ from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp import openerp.addons.decimal_precision as dp
class mrp_repair(osv.osv): class mrp_repair(osv.osv):
_name = 'mrp.repair' _name = 'mrp.repair'
_inherit = 'mail.thread' _inherit = 'mail.thread'
@ -108,10 +107,10 @@ class mrp_repair(osv.osv):
return res return res
def _get_lines(self, cr, uid, ids, context=None): def _get_lines(self, cr, uid, ids, context=None):
result = {} return self.pool['mrp.repair'].search(cr, uid, [('operations', 'in', ids)], context=context)
for line in self.pool.get('mrp.repair.line').browse(cr, uid, ids, context=context):
result[line.repair_id.id] = True def _get_fee_lines(self, cr, uid, ids, context=None):
return result.keys() return self.pool['mrp.repair'].search(cr, uid, [('fees_lines', 'in', ids)], context=context)
_columns = { _columns = {
'name': fields.char('Repair Reference', size=24, required=True, states={'confirmed': [('readonly', True)]}), 'name': fields.char('Repair Reference', size=24, required=True, states={'confirmed': [('readonly', True)]}),
@ -161,18 +160,21 @@ class mrp_repair(osv.osv):
'repaired': fields.boolean('Repaired', readonly=True), 'repaired': fields.boolean('Repaired', readonly=True),
'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount', 'amount_untaxed': fields.function(_amount_untaxed, string='Untaxed Amount',
store={ store={
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10), 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10),
'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
}), }),
'amount_tax': fields.function(_amount_tax, string='Taxes', 'amount_tax': fields.function(_amount_tax, string='Taxes',
store={ store={
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10), 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10),
'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
}), }),
'amount_total': fields.function(_amount_total, string='Total', 'amount_total': fields.function(_amount_total, string='Total',
store={ store={
'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations'], 10), 'mrp.repair': (lambda self, cr, uid, ids, c={}: ids, ['operations', 'fees_lines'], 10),
'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10), 'mrp.repair.line': (_get_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
'mrp.repair.fee': (_get_fee_lines, ['price_unit', 'price_subtotal', 'product_id', 'tax_id', 'product_uom_qty', 'product_uom'], 10),
}), }),
} }

View File

@ -0,0 +1,23 @@
-
Testing total amount update function
-
I check the total amount of mrp_repair_rmrp1 is 100
-
!assert {model: mrp.repair, id: mrp_repair_rmrp1, string=amount_total should be 100}:
- amount_total == 100
-
I add a new fee line
-
!record {model: mrp.repair, id: mrp_repair_rmrp1}:
fees_lines:
- name: 'Assembly Service Cost'
product_id: product.product_assembly
product_uom_qty: 1.0
product_uom: product.product_uom_hour
price_unit: 12.0
to_invoice: True
-
I check the total amount of mrp_repair_rmrp1 is now 112
-
!assert {model: mrp.repair, id: mrp_repair_rmrp1, string=amount_total should be 112}:
- amount_total == 112

View File

@ -14,6 +14,10 @@ _logger = logging.getLogger(__name__)
class pad_common(osv.osv_memory): class pad_common(osv.osv_memory):
_name = 'pad.common' _name = 'pad.common'
def pad_is_configured(self, cr, uid, context=None):
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
return bool(user.company_id.pad_server)
def pad_generate_url(self, cr, uid, context=None): def pad_generate_url(self, cr, uid, context=None):
company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id; company = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id;

View File

@ -70,6 +70,7 @@
.oe_pad_loading{ .oe_pad_loading{
text-align: center; text-align: center;
opacity: 0.75; opacity: 0.75;
font-style: italic;
} }
.etherpad_readonly ul, .etherpad_readonly ol { .etherpad_readonly ul, .etherpad_readonly ol {

View File

@ -1,66 +1,82 @@
openerp.pad = function(instance) { openerp.pad = function(instance) {
var _t = instance.web._t;
instance.web.form.FieldPad = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeWidgetMixin, { instance.web.form.FieldPad = instance.web.form.AbstractField.extend(instance.web.form.ReinitializeWidgetMixin, {
template: 'FieldPad', template: 'FieldPad',
content: "", content: "",
init: function() { init: function() {
var self = this;
this._super.apply(this, arguments); this._super.apply(this, arguments);
this.set("configured", true); this._configured_deferred = this.view.dataset.call('pad_is_configured').done(function(data) {
this.on("change:configured", this, this.switch_configured); self.set("configured", !!data);
}).fail(function(data, event) {
event.preventDefault();
self.set("configured", true);
});
}, },
initialize_content: function() { initialize_content: function() {
var self = this; var self = this;
this.switch_configured();
this.$('.oe_pad_switch').click(function() { this.$('.oe_pad_switch').click(function() {
self.$el.toggleClass('oe_pad_fullscreen'); self.$el.toggleClass('oe_pad_fullscreen');
self.$el.find('.oe_pad_switch').toggleClass('fa-expand fa-compress'); self.$el.find('.oe_pad_switch').toggleClass('fa-expand fa-compress');
self.view.$el.find('.oe_chatter').toggle(); self.view.$el.find('.oe_chatter').toggle();
}); });
this._configured_deferred.always(function() {
var configured = self.get('configured');
self.$(".oe_unconfigured").toggle(!configured);
self.$(".oe_configured").toggle(configured);
});
this.render_value(); this.render_value();
}, },
switch_configured: function() {
this.$(".oe_unconfigured").toggle(! this.get("configured"));
this.$(".oe_configured").toggle(this.get("configured"));
},
render_value: function() { render_value: function() {
var self = this; var self = this;
if (this.get("configured") && ! this.get("value")) { this._configured_deferred.always(function() {
self.view.dataset.call('pad_generate_url', { if (! self.get('configured')) {
context: { return;
model: self.view.model, };
field_name: self.name, var value = self.get('value');
object_id: self.view.datarecord.id if (self.get('effective_readonly')) {
}, if (_.str.startsWith(value, 'http')) {
}).done(function(data) { this.pad_loading_request = self.view.dataset.call('pad_get_content', {url: value}).done(function(data) {
if (! data.url) { self.$('.oe_pad_content').removeClass('oe_pad_loading').html('<div class="oe_pad_readonly"><div>');
self.set("configured", false); self.$('.oe_pad_readonly').html(data);
}).fail(function() {
self.$('.oe_pad_content').text(_t('Unable to load pad'));
});
} else { } else {
self.set("value", data.url); self.$('.oe_pad_content').addClass('oe_pad_loading').show().text(_t("This pad will be initialized on first edit"));
} }
}); }
} else {
this.$('.oe_pad_content').html(""); var def = $.when();
var value = this.get('value'); if (! value || !_.str.startsWith(value, 'http')) {
if (this.pad_loading_request) { def = self.view.dataset.call('pad_generate_url', {
this.pad_loading_request.abort(); context: {
} model: self.view.model,
if (_.str.startsWith(value, 'http')) { field_name: self.name,
if (! this.get('effective_readonly')) { object_id: self.view.datarecord.id
var content = '<iframe width="100%" height="100%" frameborder="0" src="' + value + '?showChat=false&userName=' + this.session.username + '"></iframe>'; },
this.$('.oe_pad_content').html(content); }).done(function(data) {
this._dirty_flag = true; if (! data.url) {
} else { self.set("configured", false);
this.content = '<div class="oe_pad_loading">... Loading pad ...</div>'; } else {
this.pad_loading_request = $.get(value + '/export/html').done(function(data) { self.set("value", data.url);
groups = /\<\s*body\s*\>(.*?)\<\s*\/body\s*\>/.exec(data); }
data = (groups || []).length >= 2 ? groups[1] : ''; });
self.$('.oe_pad_content').html('<div class="oe_pad_readonly"><div>'); }
self.$('.oe_pad_readonly').html(data); def.then(function() {
}).fail(function() { value = self.get('value');
self.$('.oe_pad_content').text('Unable to load pad'); if (_.str.startsWith(value, 'http')) {
var content = '<iframe width="100%" height="100%" frameborder="0" src="' + value + '?showChat=false&userName=' + self.session.username + '"></iframe>';
self.$('.oe_pad_content').html(content);
self._dirty_flag = true;
}
else {
self.$('.oe_pad_content').text(value);
}
}); });
} }
} });
}, },
}); });

View File

@ -275,9 +275,12 @@ class PaymentAcquirer(osv.Model):
</div>""" % (amount, payment_header) </div>""" % (amount, payment_header)
return result % html_block.decode("utf-8") return result % html_block.decode("utf-8")
def render_payment_block(self, cr, uid, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, context=None): def render_payment_block(self, cr, uid, reference, amount, currency_id, tx_id=None, partner_id=False, partner_values=None, tx_values=None, company_id=None, context=None):
html_forms = [] html_forms = []
acquirer_ids = self.search(cr, uid, [('website_published', '=', True), ('validation', '=', 'automatic')], context=context) domain = [('website_published', '=', True), ('validation', '=', 'automatic')]
if company_id:
domain.append(('company_id', '=', company_id))
acquirer_ids = self.search(cr, uid, domain, context=context)
for acquirer_id in acquirer_ids: for acquirer_id in acquirer_ids:
button = self.render( button = self.render(
cr, uid, acquirer_id, cr, uid, acquirer_id,

View File

@ -9,14 +9,11 @@ class AccountPaymentConfig(osv.TransientModel):
_columns = { _columns = {
'module_payment_paypal': fields.boolean( 'module_payment_paypal': fields.boolean(
'Manage Payments Using Paypal', 'Manage Payments Using Paypal',
help='Blahblahblah\n' help='-It installs the module payment_paypal.'),
'-It installs the module payment_paypal.'),
'module_payment_ogone': fields.boolean( 'module_payment_ogone': fields.boolean(
'Manage Payments Using Ogone', 'Manage Payments Using Ogone',
help='Blahblahblah\n' help='-It installs the module payment_ogone.'),
'-It installs the module payment_ogone.'),
'module_payment_adyen': fields.boolean( 'module_payment_adyen': fields.boolean(
'Manage Payments Using Adyen', 'Manage Payments Using Adyen',
help='Blahblahblah\n' help='-It installs the module payment_adyen.'),
'-It installs the module payment_adyen.'),
} }

View File

@ -30,10 +30,10 @@ class PaymentAcquirerOgone(osv.Model):
@TDETODO: complete me @TDETODO: complete me
""" """
return { return {
'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard.asp' % env, 'ogone_standard_order_url': 'https://secure.ogone.com/ncol/%s/orderstandard_utf8.asp' % (env,),
'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect.asp' % env, 'ogone_direct_order_url': 'https://secure.ogone.com/ncol/%s/orderdirect_utf8.asp' % (env,),
'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect.asp' % env, 'ogone_direct_query_url': 'https://secure.ogone.com/ncol/%s/querydirect_utf8.asp' % (env,),
'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % env, 'ogone_afu_agree_url': 'https://secure.ogone.com/ncol/%s/AFU_agree.asp' % (env,),
} }
_columns = { _columns = {

View File

@ -70,6 +70,7 @@ class AcquirerPaypal(osv.Model):
else: else:
paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button') paypal_view = self.pool['ir.model.data'].get_object(cr, uid, 'payment_paypal', 'paypal_acquirer_button')
self.create(cr, uid, { self.create(cr, uid, {
'name': 'paypal',
'paypal_email_account': company_paypal_account, 'paypal_email_account': company_paypal_account,
'view_template_id': paypal_view.id, 'view_template_id': paypal_view.id,
}, context=context) }, context=context)

View File

@ -8,7 +8,7 @@
<field name="inherit_id" ref="account.view_account_config_settings"/> <field name="inherit_id" ref="account.view_account_config_settings"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<xpath expr="//div[@name='payment_acquirer']" version="7.0" position="inside"> <xpath expr="//div[@name='payment_acquirer']" version="7.0" position="inside">
<button name='%(payment.acquirer_list)d' type="action" <button name='%(payment.action_payment_acquirer)d' type="action"
string="Configure payment acquiring methods" class="oe_link"/> string="Configure payment acquiring methods" class="oe_link"/>
</xpath> </xpath>
</field> </field>

View File

@ -8,7 +8,7 @@ import random
from openerp import http from openerp import http
from openerp.http import request from openerp.http import request
from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template, login_redirect
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
@ -59,7 +59,7 @@ class PosController(http.Controller):
def a(self, debug=False, **k): def a(self, debug=False, **k):
if not request.session.uid: if not request.session.uid:
return http.local_redirect('/web/login?redirect=/pos/web') return login_redirect()
js_list = manifest_list('js',db=request.db, debug=debug) js_list = manifest_list('js',db=request.db, debug=debug)
css_list = manifest_list('css',db=request.db, debug=debug) css_list = manifest_list('css',db=request.db, debug=debug)

View File

@ -514,13 +514,17 @@ class pos_order(osv.osv):
_order = "id desc" _order = "id desc"
def create_from_ui(self, cr, uid, orders, context=None): def create_from_ui(self, cr, uid, orders, context=None):
#_logger.info("orders: %r", orders) # Keep only new orders
submitted_references = [o['data']['name'] for o in orders]
existing_orders = self.search_read(cr, uid, domain=[('pos_reference', 'in', submitted_references)], fields=['pos_reference'], context=context)
existing_references = set([o['pos_reference'] for o in existing_orders])
orders_to_save = [o for o in orders if o['data']['name'] not in existing_references]
order_ids = [] order_ids = []
for tmp_order in orders: for tmp_order in orders_to_save:
to_invoice = tmp_order['to_invoice'] to_invoice = tmp_order['to_invoice']
order = tmp_order['data'] order = tmp_order['data']
order_id = self.create(cr, uid, { order_id = self.create(cr, uid, {
'name': order['name'], 'name': order['name'],
'user_id': order['user_id'] or False, 'user_id': order['user_id'] or False,
@ -542,7 +546,6 @@ class pos_order(osv.osv):
if order['amount_return']: if order['amount_return']:
session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context) session = self.pool.get('pos.session').browse(cr, uid, order['pos_session_id'], context=context)
cash_journal = session.cash_journal_id cash_journal = session.cash_journal_id
cash_statement = False
if not cash_journal: if not cash_journal:
cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids) cash_journal_ids = filter(lambda st: st.journal_id.type=='cash', session.statement_ids)
if not len(cash_journal_ids): if not len(cash_journal_ids):
@ -556,7 +559,11 @@ class pos_order(osv.osv):
'journal': cash_journal.id, 'journal': cash_journal.id,
}, context=context) }, context=context)
order_ids.append(order_id) order_ids.append(order_id)
self.signal_paid(cr, uid, [order_id])
try:
self.signal_paid(cr, uid, [order_id])
except Exception as e:
_logger.error('Could not mark POS Order as Paid: %s', tools.ustr(e))
if to_invoice: if to_invoice:
self.action_invoice(cr, uid, [order_id], context) self.action_invoice(cr, uid, [order_id], context)

View File

@ -19,5 +19,11 @@
<field name="global" eval="True" /> <field name="global" eval="True" />
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field> <field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
</record> </record>
<record id="rule_pos_config_multi_company" model="ir.rule">
<field name="name">Point Of Sale Config</field>
<field name="model_id" ref="model_pos_config" />
<field name="global" eval="True" />
<field name="domain_force">[('warehouse_id.company_id','child_of',[user.company_id.id])]</field>
</record>
</data> </data>
</openerp> </openerp>

View File

@ -672,7 +672,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
// returns true if the ean is a valid EAN codebar number by checking the control digit. // returns true if the ean is a valid EAN codebar number by checking the control digit.
// ean must be a string // ean must be a string
check_ean: function(ean){ check_ean: function(ean){
return this.ean_checksum(ean) === Number(ean[ean.length-1]); return /^\d+$/.test(ean) && this.ean_checksum(ean) === Number(ean[ean.length-1]);
}, },
// returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string. // returns a valid zero padded ean13 from an ean prefix. the ean prefix must be a string.
sanitize_ean:function(ean){ sanitize_ean:function(ean){
@ -757,8 +757,13 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
scan: function(code){ scan: function(code){
if(code.length < 3){ if(code.length < 3){
return; return;
}else if(code.length === 13 && /^\d+$/.test(code)){ }else if(code.length === 13 && this.check_ean(code)){
var parse_result = this.parse_ean(code); var parse_result = this.parse_ean(code);
}else if(code.length === 12 && this.check_ean('0'+code)){
// many barcode scanners strip the leading zero of ean13 barcodes.
// This is because ean-13 are UCP-A with an additional zero at the beginning,
// so by stripping zeros you get retrocompatibility with UCP-A systems.
var parse_result = this.parse_ean('0'+code);
}else if(this.pos.db.get_product_by_reference(code)){ }else if(this.pos.db.get_product_by_reference(code)){
var parse_result = { var parse_result = {
encoding: 'reference', encoding: 'reference',
@ -767,12 +772,16 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
prefix: '', prefix: '',
}; };
}else{ }else{
var parse_result = {
encoding: 'error',
type: 'error',
code: code,
prefix: '',
};
return; return;
} }
if (parse_result.type === 'error') { //most likely a checksum error, raise warning if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
console.warn('WARNING: barcode checksum error:',parse_result);
}else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
if(this.action_callback['product']){ if(this.action_callback['product']){
this.action_callback['product'](parse_result); this.action_callback['product'](parse_result);
} }

View File

@ -421,62 +421,67 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// it is therefore important to only call this method from inside a mutex // it is therefore important to only call this method from inside a mutex
// this method returns a deferred indicating wether the sending was successful or not // this method returns a deferred indicating wether the sending was successful or not
// there is a timeout parameter which is set to 2 seconds by default. // there is a timeout parameter which is set to 2 seconds by default.
_flush_order: function(order_id, options){ _flush_order: function( order_id, options) {
var self = this; return this._flush_all_orders([this.db.get_order(order_id)], options);
options = options || {};
timeout = typeof options.timeout === 'number' ? options.timeout : 7500;
this.set('synch',{state:'connecting', pending: this.get('synch').pending});
var order = this.db.get_order(order_id);
order.to_invoice = options.to_invoice || false;
if(!order){
// flushing a non existing order always fails
return (new $.Deferred()).reject();
}
// we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice,
// then we want to notify the user that we are waiting on something )
var rpc = (new instance.web.Model('pos.order')).call('create_from_ui',[[order]],undefined,{shadow: !options.to_invoice, timeout:timeout});
rpc.fail(function(unused,event){
// prevent an error popup creation by the rpc failure
// we want the failure to be silent as we send the orders in the background
event.preventDefault();
console.error('Failed to send order:',order);
});
rpc.done(function(){
self.db.remove_order(order_id);
var pending = self.db.get_orders().length;
self.set('synch',{state: pending ? 'connecting' : 'connected', pending:pending});
});
return rpc;
}, },
// attempts to send all the locally stored orders. As with _flush_order, it should only be // attempts to send all the locally stored orders. As with _flush_order, it should only be
// called from within a mutex. // called from within a mutex.
// this method returns a deferred that always succeeds when all orders have been tried to be sent, // this method returns a deferred that always succeeds when all orders have been tried to be sent,
// even if none of them could actually be sent. // even if none of them could actually be sent.
_flush_all_orders: function(){ _flush_all_orders: function () {
var self = this; var self = this;
var orders = this.db.get_orders(); self.set('synch', {
var tried_all = new $.Deferred(); state: 'connecting',
pending: self.get('synch').pending
});
return self._save_to_server(self.db.get_orders()).done(function () {
var pending = self.db.get_orders().length;
self.set('synch', {
state: pending ? 'connecting' : 'connected',
pending: pending
});
});
},
function rec_flush(index){ // send an array of orders to the server
if(index < orders.length){ // available options:
self._flush_order(orders[index].id).always(function(){ // - timeout: timeout for the rpc call in ms
rec_flush(index+1); _save_to_server: function (orders, options) {
}) if (!orders || !orders.length) {
}else{ var result = $.Deferred();
tried_all.resolve(); result.resolve();
} return result;
} }
rec_flush(0);
options = options || {};
return tried_all; var self = this;
var timeout = typeof options.timeout === 'number' ? options.timeout : 7500 * orders.length;
// we try to send the order. shadow prevents a spinner if it takes too long. (unless we are sending an invoice,
// then we want to notify the user that we are waiting on something )
var posOrderModel = new instance.web.Model('pos.order');
return posOrderModel.call('create_from_ui',
[_.map(orders, function (order) {
order.to_invoice = options.to_invoice || false;
return order;
})],
undefined,
{
shadow: !options.to_invoice,
timeout: timeout
}
).then(function () {
_.each(orders, function (order) {
self.db.remove_order(order.id);
});
}).fail(function (unused, event){
// prevent an error popup creation by the rpc failure
// we want the failure to be silent as we send the orders in the background
event.preventDefault();
console.error('Failed to send orders:', orders);
});
}, },
scan_product: function(parsed_code){ scan_product: function(parsed_code){

View File

@ -10,7 +10,7 @@
<separator string="Select your Point of Sale" colspan="4" attrs="{'invisible' : [('show_config', '=', False)]}" /> <separator string="Select your Point of Sale" colspan="4" attrs="{'invisible' : [('show_config', '=', False)]}" />
<group attrs="{'invisible' : [('show_config', '=', False)]}"> <group attrs="{'invisible' : [('show_config', '=', False)]}">
<field name="pos_config_id" on_change="on_change_config(pos_config_id)" <field name="pos_config_id" on_change="on_change_config(pos_config_id)"
widget="selection" domain="[('state','=','active')]" options="{'no_create': True}" domain="[('state','=','active')]"
class="oe_inline"/> class="oe_inline"/>
<field name="pos_state" invisible="1" /> <field name="pos_state" invisible="1" />
</group> </group>

View File

@ -39,7 +39,7 @@ class mail_mail(osv.Model):
if partner and not partner.user_ids: if partner and not partner.user_ids:
contex_signup = dict(context, signup_valid=True) contex_signup = dict(context, signup_valid=True)
signup_url = partner_obj._get_signup_url_for_action(cr, SUPERUSER_ID, [partner.id], signup_url = partner_obj._get_signup_url_for_action(cr, SUPERUSER_ID, [partner.id],
action='login', model=mail.model, res_id=mail.res_id, model=mail.model, res_id=mail.res_id,
context=contex_signup)[partner.id] context=contex_signup)[partner.id]
return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents through <a style='color:inherit' href="%s">our Customer Portal</a></small></span>""") % signup_url return _("""<span class='oe_mail_footer_access'><small>Access your messages and documents through <a style='color:inherit' href="%s">our Customer Portal</a></small></span>""") % signup_url
else: else:

View File

@ -35,14 +35,14 @@ class mail_message(osv.Model):
""" """
if uid == SUPERUSER_ID: if uid == SUPERUSER_ID:
return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order, return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
context=context, count=False, access_rights_uid=access_rights_uid) context=context, count=count, access_rights_uid=access_rights_uid)
group_ids = self.pool.get('res.users').browse(cr, uid, uid, context=context).groups_id group_ids = self.pool.get('res.users').browse(cr, uid, uid, context=context).groups_id
group_user_id = self.pool.get("ir.model.data").get_object_reference(cr, uid, 'base', 'group_user')[1] group_user_id = self.pool.get("ir.model.data").get_object_reference(cr, uid, 'base', 'group_user')[1]
if group_user_id not in [group.id for group in group_ids]: if group_user_id not in [group.id for group in group_ids]:
args = [('subtype_id', '!=', False)] + list(args) args = [('subtype_id', '!=', False)] + list(args)
return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order, return super(mail_message, self)._search(cr, uid, args, offset=offset, limit=limit, order=order,
context=context, count=False, access_rights_uid=access_rights_uid) context=context, count=count, access_rights_uid=access_rights_uid)
def check_access_rule(self, cr, uid, ids, operation, context=None): def check_access_rule(self, cr, uid, ids, operation, context=None):
""" Add Access rules of mail.message for non-employee user: """ Add Access rules of mail.message for non-employee user:

View File

@ -30,10 +30,3 @@ class portal(osv.osv):
_columns = { _columns = {
'is_portal': fields.boolean('Portal', help="If checked, this group is usable as a portal."), 'is_portal': fields.boolean('Portal', help="If checked, this group is usable as a portal."),
} }
class res_users(osv.Model):
_inherit = 'res.users'
def _signup_create_user(self, cr, uid, values, context=None):
values['share'] = True
return super(res_users, self)._signup_create_user(cr, uid, values, context=context)

View File

@ -6,9 +6,6 @@
<record id="base.group_portal" model="res.groups"> <record id="base.group_portal" model="res.groups">
<field name="is_portal" eval="True"/> <field name="is_portal" eval="True"/>
</record> </record>
<record id="auth_signup.default_template_user" model="res.users">
<field name="share" eval="True"/>
</record>
</data> </data>
</openerp> </openerp>

View File

@ -22,7 +22,6 @@
Mr Demo Portal</field> Mr Demo Portal</field>
<!-- Avoid auto-including this user in any default group --> <!-- Avoid auto-including this user in any default group -->
<field name="groups_id" eval="[(5,)]"/> <field name="groups_id" eval="[(5,)]"/>
<field name="share" eval="True" />
</record> </record>
<!-- Add the demo user to the portal (and therefore to the portal member group) --> <!-- Add the demo user to the portal (and therefore to the portal member group) -->

View File

@ -6,12 +6,18 @@
z-index: 0; z-index: 0;
} }
.openerp .oe_form .oe_form_embedded_html.view_portal_payment_options {
overflow: visible;
}
.openerp .payment_acquirers { .openerp .payment_acquirers {
margin: -40px 0 -32px -24px;
position: relative; position: relative;
padding: 10px 15px; padding: 10px 15px;
right: -125px; /* improved margin according bootstrap3 */ right: -125px; /* improved margin according bootstrap3 */
width: 650px;
margin-left: 80px;
background: #729FCF; background: #729FCF;
background-image: -webkit-gradient(linear, left top, left bottom, from(#729FCF), to(#3465A4)); background-image: -webkit-gradient(linear, left top, left bottom, from(#729FCF), to(#3465A4));
background-image: -webkit-linear-gradient(top, #729FCF, #3465A4); background-image: -webkit-linear-gradient(top, #729FCF, #3465A4);

View File

@ -134,7 +134,8 @@ class test_portal(TestMail):
'invite: subject of invitation email is incorrect') 'invite: subject of invitation email is incorrect')
self.assertIn('Administrator invited you to follow Discussion group document: Pigs', sent_email.get('body'), self.assertIn('Administrator invited you to follow Discussion group document: Pigs', sent_email.get('body'),
'invite: body of invitation email is incorrect') 'invite: body of invitation email is incorrect')
self.assertTrue(partner_carine.signup_url in sent_email.get('body'), invite_url = partner_carine._get_signup_url_for_action(model='mail.group', res_id=self.group_pigs_id)[partner_carine.id]
self.assertTrue(invite_url in sent_email.get('body'),
'invite: body of invitation email does not contain signup url') 'invite: body of invitation email does not contain signup url')
def test_20_notification_url(self): def test_20_notification_url(self):

View File

@ -209,7 +209,6 @@ class wizard_user(osv.osv_memory):
'login': extract_email(wizard_user.email), 'login': extract_email(wizard_user.email),
'partner_id': wizard_user.partner_id.id, 'partner_id': wizard_user.partner_id.id,
'groups_id': [(6, 0, [])], 'groups_id': [(6, 0, [])],
'share': True,
} }
user_id = res_users.create(cr, uid, values, context=create_context) user_id = res_users.create(cr, uid, values, context=create_context)
return res_users.browse(cr, uid, user_id, context) return res_users.browse(cr, uid, user_id, context)

View File

@ -39,7 +39,7 @@ class sale_order(osv.Model):
if this.state not in ('draft', 'cancel') and not this.invoiced: if this.state not in ('draft', 'cancel') and not this.invoiced:
result[this.id] = payment_acquirer.render_payment_block( result[this.id] = payment_acquirer.render_payment_block(
cr, uid, this.name, this.amount_total, this.pricelist_id.currency_id.id, cr, uid, this.name, this.amount_total, this.pricelist_id.currency_id.id,
partner_id=this.partner_id.id, context=context) partner_id=this.partner_id.id, company_id=this.company_id.id, context=context)
return result return result
def action_quotation_send(self, cr, uid, ids, context=None): def action_quotation_send(self, cr, uid, ids, context=None):
@ -90,7 +90,7 @@ class account_invoice(osv.Model):
if this.type == 'out_invoice' and this.state not in ('draft', 'done') and not this.reconciled: if this.type == 'out_invoice' and this.state not in ('draft', 'done') and not this.reconciled:
result[this.id] = payment_acquirer.render_payment_block( result[this.id] = payment_acquirer.render_payment_block(
cr, uid, this.number, this.residual, this.currency_id.id, cr, uid, this.number, this.residual, this.currency_id.id,
partner_id=this.partner_id.id, context=context) partner_id=this.partner_id.id, company_id=this.company_id.id, context=context)
return result return result
def action_invoice_sent(self, cr, uid, ids, context=None): def action_invoice_sent(self, cr, uid, ids, context=None):

View File

@ -9,7 +9,7 @@
<field name="inherit_id" ref="sale.view_order_form"/> <field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<notebook version="7.0" position="before"> <notebook version="7.0" position="before">
<field name="portal_payment_options" groups="portal_sale.group_payment_options"/> <field name="portal_payment_options" groups="portal_sale.group_payment_options" class="view_portal_payment_options"/>
</notebook> </notebook>
</field> </field>
</record> </record>
@ -19,7 +19,7 @@
<field name="inherit_id" ref="account.invoice_form"/> <field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<notebook version="7.0" position="before"> <notebook version="7.0" position="before">
<field name="portal_payment_options" groups="portal_sale.group_payment_options"/> <field name="portal_payment_options" groups="portal_sale.group_payment_options" class="view_portal_payment_options"/>
</notebook> </notebook>
</field> </field>
</record> </record>

View File

@ -11,7 +11,7 @@
<div> <div>
<field name="group_payment_options" class="oe_inline"/> <field name="group_payment_options" class="oe_inline"/>
<label for="group_payment_options"/> <label for="group_payment_options"/>
<button name='%(payment.acquirer_list)d' type="action" <button name='%(payment.action_payment_acquirer)d' type="action"
string="Configure payment acquiring methods" class="oe_link"/> string="Configure payment acquiring methods" class="oe_link"/>
</div> </div>
</xpath> </xpath>

View File

@ -63,7 +63,6 @@ Print product labels with barcode.
], ],
'test': [ 'test': [
'product_pricelist_demo.yml', 'product_pricelist_demo.yml',
'test/product_uom.yml',
'test/product_pricelist.yml', 'test/product_pricelist.yml',
], ],
'installable': True, 'installable': True,

View File

@ -18,12 +18,18 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
# #
############################################################################## ##############################################################################
from openerp import tools
import math
def rounding(f, r): def rounding(f, r):
# TODO for trunk: log deprecation warning
# _logger.warning("Deprecated rounding method, please use tools.float_round to round floats.")
return tools.float_round(f, precision_rounding=r)
# TODO for trunk: add rounding method parameter to tools.float_round and use this method as hook
def ceiling(f, r):
if not r: if not r:
return f return f
return round(f / r) * r return math.ceil(f / r) * r
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -21,8 +21,7 @@
import time import time
from _common import rounding from openerp import tools
from openerp.osv import fields, osv from openerp.osv import fields, osv
from openerp.tools.translate import _ from openerp.tools.translate import _
@ -116,11 +115,29 @@ class product_pricelist(osv.osv):
if name and operator == '=' and not args: if name and operator == '=' and not args:
# search on the name of the pricelist and its currency, opposite of name_get(), # search on the name of the pricelist and its currency, opposite of name_get(),
# Used by the magic context filter in the product search view. # Used by the magic context filter in the product search view.
query_args = {'name': name, 'limit': limit} query_args = {'name': name, 'limit': limit, 'lang': (context or {}).get('lang') or 'en_US'}
query = """SELECT p.id query = """SELECT p.id
FROM product_pricelist p JOIN FROM ((
res_currency c ON (p.currency_id = c.id) SELECT pr.id, pr.name
WHERE p.name || ' (' || c.name || ')' = %(name)s FROM product_pricelist pr JOIN
res_currency cur ON
(pr.currency_id = cur.id)
WHERE pr.name || ' (' || cur.name || ')' = %(name)s
)
UNION (
SELECT tr.res_id as id, tr.value as name
FROM ir_translation tr JOIN
product_pricelist pr ON (
pr.id = tr.res_id AND
tr.type = 'model' AND
tr.name = 'product.pricelist,name' AND
tr.lang = %(lang)s
) JOIN
res_currency cur ON
(pr.currency_id = cur.id)
WHERE tr.value || ' (' || cur.name || ')' = %(name)s
)
) p
ORDER BY p.name""" ORDER BY p.name"""
if limit: if limit:
query += " LIMIT %(limit)s" query += " LIMIT %(limit)s"
@ -180,7 +197,8 @@ class product_pricelist(osv.osv):
if ((v.date_start is False) or (v.date_start <= date)) and ((v.date_end is False) or (v.date_end >= date)): if ((v.date_start is False) or (v.date_start <= date)) and ((v.date_end is False) or (v.date_end >= date)):
version = v version = v
break break
if not version:
raise osv.except_osv(_('Warning!'), _("At least one pricelist has no active version !\nPlease create or activate one."))
categ_ids = {} categ_ids = {}
for p in products: for p in products:
categ = p.categ_id categ = p.categ_id
@ -269,7 +287,8 @@ class product_pricelist(osv.osv):
if price is not False: if price is not False:
price_limit = price price_limit = price
price = price * (1.0+(rule.price_discount or 0.0)) price = price * (1.0+(rule.price_discount or 0.0))
price = rounding(price, rule.price_round) #TOFIX: rounding with tools.float_rouding if rule.price_round:
price = tools.float_round(price, precision_rounding=rule.price_round)
price += (rule.price_surcharge or 0.0) price += (rule.price_surcharge or 0.0)
if rule.price_min_margin: if rule.price_min_margin:
price = max(price, price_limit+rule.price_min_margin) price = max(price, price_limit+rule.price_min_margin)

View File

@ -22,8 +22,7 @@
import math import math
import re import re
import time import time
from _common import ceiling
from _common import rounding
from openerp import SUPERUSER_ID from openerp import SUPERUSER_ID
from openerp import tools from openerp import tools
@ -181,7 +180,7 @@ class product_uom(osv.osv):
if to_unit: if to_unit:
amount = amount * to_unit.factor amount = amount * to_unit.factor
if round: if round:
amount = rounding(amount, to_unit.rounding) amount = ceiling(amount * to_unit.factor, to_unit.rounding)
return amount return amount
def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False): def _compute_price(self, cr, uid, from_uom_id, price, to_uom_id=False):
@ -600,12 +599,13 @@ class product_product(osv.osv):
cr, uid, pricelist, operator='=', context=context, limit=1) cr, uid, pricelist, operator='=', context=context, limit=1)
pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist pricelist = pricelist_ids[0][0] if pricelist_ids else pricelist
products = self.browse(cr, uid, ids, context=context) if isinstance(pricelist, (int, long)):
qtys = map(lambda x: (x, quantity, partner), products) products = self.browse(cr, uid, ids, context=context)
pl = plobj.browse(cr, uid, pricelist, context=context) qtys = map(lambda x: (x, quantity, partner), products)
price = plobj._price_get_multi(cr,uid, pl, qtys, context=context) pl = plobj.browse(cr, uid, pricelist, context=context)
for id in ids: price = plobj._price_get_multi(cr,uid, pl, qtys, context=context)
res[id] = price.get(id, 0.0) for id in ids:
res[id] = price.get(id, 0.0)
for id in ids: for id in ids:
res.setdefault(id, 0.0) res.setdefault(id, 0.0)
return res return res

View File

@ -217,7 +217,6 @@
<kanban> <kanban>
<field name="color"/> <field name="color"/>
<field name="type"/> <field name="type"/>
<field name="image_small"/>
<field name="list_price"/> <field name="list_price"/>
<templates> <templates>
<t t-name="kanban-box"> <t t-name="kanban-box">

View File

@ -1,26 +0,0 @@
-
In order to test conversation of UOM,
-
I convert Grams into TON with price.
-
!python {model: product.uom}: |
from_uom_id = ref("product_uom_gram")
to_uom_id = ref("product_uom_ton")
price = 2
qty = 1020000
price = self._compute_price(cr, uid, from_uom_id, price, to_uom_id)
qty = self._compute_qty(cr, uid, from_uom_id, qty, to_uom_id)
assert qty == 1.02, "Qty is not correspond."
assert price == 2000000.0, "Price is not correspond."
-
I convert Liters into Gallons with price.
-
!python {model: product.uom}: |
from_uom_id = ref("product_uom_litre")
to_uom_id = ref("product_uom_gal")
price = 2
qty = 30.28
price = self._compute_price(cr, uid, from_uom_id, price, to_uom_id)
qty = self._compute_qty(cr, uid, from_uom_id, qty, to_uom_id)
assert qty == 8, "Qty does not correspond."
assert round(price,2) == 7.57, "Price does not correspond."

View File

@ -0,0 +1,5 @@
from . import test_uom
fast_suite = [
test_uom,
]

View File

@ -0,0 +1,37 @@
from openerp.tests.common import TransactionCase
class TestUom(TransactionCase):
"""Tests for unit of measure conversion"""
def setUp(self):
super(TestUom, self).setUp()
self.product = self.registry('product.product')
self.uom = self.registry('product.uom')
self.imd = self.registry('ir.model.data')
def test_10_conversion(self):
cr, uid = self.cr, self.uid
gram_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_gram')[1]
tonne_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_ton')[1]
qty = self.uom._compute_qty(cr, uid, gram_id, 1020000, tonne_id)
self.assertEquals(qty, 1.02, "Converted quantity does not correspond.")
price = self.uom._compute_price(cr, uid, gram_id, 2, tonne_id)
self.assertEquals(price, 2000000.0, "Converted price does not correspond.")
def test_20_rounding(self):
cr, uid = self.cr, self.uid
unit_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_unit')[1]
categ_unit_id = self.imd.get_object_reference(cr, uid, 'product', 'product_uom_categ_unit')[1]
score_id = self.uom.create(cr, uid, {
'name': 'Score',
'factor_inv': 20,
'uom_type': 'bigger',
'rounding': 1.0,
'category_id': categ_unit_id
})
qty = self.uom._compute_qty(cr, uid, unit_id, 2, score_id)
self.assertEquals(qty, 1, "Converted quantity should be rounded up.")

View File

@ -0,0 +1,283 @@
# Amharic translation for openobject-addons
# Copyright (c) 2014 Rosetta Contributors and Canonical Ltd 2014
# This file is distributed under the same license as the openobject-addons package.
# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: openobject-addons\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2012-12-21 17:06+0000\n"
"PO-Revision-Date: 2014-03-18 08:01+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: Amharic <am@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-03-19 05:52+0000\n"
"X-Generator: Launchpad (build 16963)\n"
#. module: product_margin
#: view:product.product:0
#: field:product.product,turnover:0
msgid "Turnover"
msgstr "ተመላሽ"
#. module: product_margin
#: field:product.product,expected_margin_rate:0
msgid "Expected Margin (%)"
msgstr ""
#. module: product_margin
#: field:product.margin,from_date:0
msgid "From"
msgstr "ከ"
#. module: product_margin
#: help:product.product,total_cost:0
msgid ""
"Sum of Multiplication of Invoice price and quantity of Supplier Invoices "
msgstr ""
#. module: product_margin
#: field:product.margin,to_date:0
msgid "To"
msgstr "ለ"
#. module: product_margin
#: help:product.product,total_margin:0
msgid "Turnover - Standard price"
msgstr "የተመላሽ መደበኛ ዋጋ"
#. module: product_margin
#: field:product.product,total_margin_rate:0
msgid "Total Margin Rate(%)"
msgstr ""
#. module: product_margin
#: selection:product.margin,invoice_state:0
#: selection:product.product,invoice_state:0
msgid "Draft, Open and Paid"
msgstr "ያልተከፈለና የተከፈለ ደረሰኞች"
#. module: product_margin
#: code:addons/product_margin/wizard/product_margin.py:73
#: model:ir.actions.act_window,name:product_margin.product_margin_act_window
#: model:ir.ui.menu,name:product_margin.menu_action_product_margin
#: view:product.product:0
#, python-format
msgid "Product Margins"
msgstr "የእቃው አይነት በአንድ መጠን ሲጨምር"
#. module: product_margin
#: field:product.product,purchase_avg_price:0
#: field:product.product,sale_avg_price:0
msgid "Avg. Unit Price"
msgstr "የእቃዎች መካከለኛ ዋጋ"
#. module: product_margin
#: field:product.product,sale_num_invoiced:0
msgid "# Invoiced in Sale"
msgstr ""
#. module: product_margin
#: view:product.product:0
msgid "Catalog Price"
msgstr "ቅናሽ ዋጋ"
#. module: product_margin
#: selection:product.margin,invoice_state:0
#: selection:product.product,invoice_state:0
msgid "Paid"
msgstr "ተከፈል"
#. module: product_margin
#: view:product.product:0
#: field:product.product,sales_gap:0
msgid "Sales Gap"
msgstr "የሽያጭ ክፍተት"
#. module: product_margin
#: help:product.product,sales_gap:0
msgid "Expected Sale - Turn Over"
msgstr "ሊሸጥ የሚችል እቃ"
#. module: product_margin
#: field:product.product,sale_expected:0
msgid "Expected Sale"
msgstr "ሊሸጥ የሚችል እቃ"
#. module: product_margin
#: view:product.product:0
msgid "Standard Price"
msgstr "የእቃው መደበኛ ዋጋ"
#. module: product_margin
#: help:product.product,purchase_num_invoiced:0
msgid "Sum of Quantity in Supplier Invoices"
msgstr ""
#. module: product_margin
#: field:product.product,date_to:0
msgid "Margin Date To"
msgstr "የእቃው መጠን የጨመረበት ቀን"
#. module: product_margin
#: view:product.product:0
msgid "Analysis Criteria"
msgstr "የመመዘኛ መስፈርት"
#. module: product_margin
#: view:product.product:0
#: field:product.product,total_cost:0
msgid "Total Cost"
msgstr "አጠቃላይ ዋጋ"
#. module: product_margin
#: help:product.product,normal_cost:0
msgid "Sum of Multiplication of Cost price and quantity of Supplier Invoices"
msgstr ""
#. module: product_margin
#: field:product.product,expected_margin:0
msgid "Expected Margin"
msgstr "የሚጠበቅ ጭማሪ"
#. module: product_margin
#: view:product.product:0
msgid "#Purchased"
msgstr ""
#. module: product_margin
#: help:product.product,expected_margin_rate:0
msgid "Expected margin * 100 / Expected Sale"
msgstr ""
#. module: product_margin
#: help:product.product,sale_avg_price:0
msgid "Avg. Price in Customer Invoices."
msgstr "የመካከለኛ ዋጋ ለገዢዎች"
#. module: product_margin
#: help:product.product,purchase_avg_price:0
msgid "Avg. Price in Supplier Invoices "
msgstr "የመካከለኛ ዋጋ አቅራቢዎች "
#. module: product_margin
#: field:product.margin,invoice_state:0
#: field:product.product,invoice_state:0
msgid "Invoice State"
msgstr ""
#. module: product_margin
#: help:product.product,purchase_gap:0
msgid "Normal Cost - Total Cost"
msgstr ""
#. module: product_margin
#: help:product.product,sale_expected:0
msgid ""
"Sum of Multiplication of Sale Catalog price and quantity of Customer Invoices"
msgstr ""
#. module: product_margin
#: field:product.product,total_margin:0
msgid "Total Margin"
msgstr "የሁሉም ዋጋ በአንድ መጠን ሲጨምር"
#. module: product_margin
#: field:product.product,date_from:0
msgid "Margin Date From"
msgstr "እቃው ከጨመረበት ቀን ጀምሮ"
#. module: product_margin
#: help:product.product,turnover:0
msgid ""
"Sum of Multiplication of Invoice price and quantity of Customer Invoices"
msgstr ""
#. module: product_margin
#: field:product.product,normal_cost:0
msgid "Normal Cost"
msgstr "መደበኛ ዋጋ"
#. module: product_margin
#: view:product.product:0
msgid "Purchases"
msgstr "ግዢዎች"
#. module: product_margin
#: field:product.product,purchase_num_invoiced:0
msgid "# Invoiced in Purchase"
msgstr ""
#. module: product_margin
#: help:product.product,expected_margin:0
msgid "Expected Sale - Normal Cost"
msgstr ""
#. module: product_margin
#: view:product.margin:0
msgid "Properties categories"
msgstr "በአይነታቸው መከፍፈል"
#. module: product_margin
#: help:product.product,total_margin_rate:0
msgid "Total margin * 100 / Turnover"
msgstr ""
#. module: product_margin
#: view:product.margin:0
msgid "Open Margins"
msgstr ""
#. module: product_margin
#: selection:product.margin,invoice_state:0
#: selection:product.product,invoice_state:0
msgid "Open and Paid"
msgstr "የተከፈተና የትከፈል"
#. module: product_margin
#: view:product.product:0
msgid "Sales"
msgstr "ሽያጭ"
#. module: product_margin
#: model:ir.model,name:product_margin.model_product_product
msgid "Product"
msgstr "ውጤት"
#. module: product_margin
#: view:product.margin:0
msgid "General Information"
msgstr "አጠቃላይ መርጃ"
#. module: product_margin
#: field:product.product,purchase_gap:0
msgid "Purchase Gap"
msgstr "የግዢ ክፍተት"
#. module: product_margin
#: view:product.margin:0
msgid "Cancel"
msgstr "መሰረዝ"
#. module: product_margin
#: view:product.product:0
msgid "Margins"
msgstr "በአንድ መጠን ሲጨምር"
#. module: product_margin
#: help:product.product,sale_num_invoiced:0
msgid "Sum of Quantity in Customer Invoices"
msgstr ""
#. module: product_margin
#: view:product.margin:0
msgid "or"
msgstr "ወይም"
#. module: product_margin
#: model:ir.model,name:product_margin.model_product_margin
msgid "Product Margin"
msgstr "የእቃው መጨመር"

View File

@ -58,9 +58,9 @@ class product_product(osv.osv):
#Cost price is calculated afterwards as it is a property #Cost price is calculated afterwards as it is a property
sqlstr="""select sqlstr="""select
sum(l.price_unit * l.quantity)/sum(l.quantity * pu.factor / pu2.factor) as avg_unit_price, sum(l.price_unit * l.quantity)/sum(nullif(l.quantity * pu.factor / pu2.factor),0) as avg_unit_price,
sum(l.quantity * pu.factor / pu2.factor) as num_qty, sum(l.quantity * pu.factor / pu2.factor) as num_qty,
sum(l.quantity * (l.price_subtotal/l.quantity)) as total, sum(l.quantity * (l.price_subtotal/(nullif(l.quantity,0)))) as total,
sum(l.quantity * pu.factor * pt.list_price / pu2.factor) as sale_expected sum(l.quantity * pu.factor * pt.list_price / pu2.factor) as sale_expected
from account_invoice_line l from account_invoice_line l
left join account_invoice i on (l.invoice_id = i.id) left join account_invoice i on (l.invoice_id = i.id)

View File

@ -7,14 +7,14 @@ msgstr ""
"Project-Id-Version: OpenERP Server 6.0dev\n" "Project-Id-Version: OpenERP Server 6.0dev\n"
"Report-Msgid-Bugs-To: support@openerp.com\n" "Report-Msgid-Bugs-To: support@openerp.com\n"
"POT-Creation-Date: 2012-12-21 17:04+0000\n" "POT-Creation-Date: 2012-12-21 17:04+0000\n"
"PO-Revision-Date: 2013-08-13 04:04+0000\n" "PO-Revision-Date: 2014-03-15 06:42+0000\n"
"Last-Translator: Wei \"oldrev\" Li <oldrev@gmail.com>\n" "Last-Translator: Victor Yu <vivianyw@163.com>\n"
"Language-Team: \n" "Language-Team: \n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2014-03-04 06:19+0000\n" "X-Launchpad-Export-Date: 2014-03-16 05:42+0000\n"
"X-Generator: Launchpad (build 16948)\n" "X-Generator: Launchpad (build 16963)\n"
#. module: project #. module: project
#: view:project.project:0 #: view:project.project:0
@ -671,7 +671,7 @@ msgstr "新任务"
#. module: project #. module: project
#: field:project.config.settings,module_project_issue_sheet:0 #: field:project.config.settings,module_project_issue_sheet:0
msgid "Invoice working time on issues" msgid "Invoice working time on issues"
msgstr "" msgstr "依据问题工作时间开票"
#. module: project #. module: project
#: view:project.project:0 #: view:project.project:0
@ -828,7 +828,7 @@ msgstr "花费的时间"
#: view:project.project:0 #: view:project.project:0
#: view:project.task:0 #: view:project.task:0
msgid "í" msgid "í"
msgstr "" msgstr "í"
#. module: project #. module: project
#: field:account.analytic.account,company_uom_id:0 #: field:account.analytic.account,company_uom_id:0
@ -958,7 +958,7 @@ msgstr "本项目和相关项目的任务实际花费的小时数合计"
msgid "" msgid ""
"Follow this project to automatically track the events associated to tasks " "Follow this project to automatically track the events associated to tasks "
"and issues of this project." "and issues of this project."
msgstr "" msgstr "关注此项目的同时,追踪此项目的任务和问题关联的事件。"
#. module: project #. module: project
#: view:project.task:0 #: view:project.task:0
@ -1018,7 +1018,7 @@ msgstr "信息和通信历史记录"
msgid "" msgid ""
"To invoice or setup invoicing and renewal options, go to the related " "To invoice or setup invoicing and renewal options, go to the related "
"contract:" "contract:"
msgstr "" msgstr "开票或设定发票更新选项,到相关的合同:"
#. module: project #. module: project
#: field:project.task.delegate,state:0 #: field:project.task.delegate,state:0

View File

@ -745,7 +745,7 @@ class task(osv.osv):
_columns = { _columns = {
'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."), 'active': fields.function(_is_template, store=True, string='Not a Template Task', type='boolean', help="This field is computed automatically and have the same behavior than the boolean 'active' field: if the task is linked to a template or unactivated project, it will be hidden unless specifically asked."),
'name': fields.char('Task Summary', size=128, required=True, select=True), 'name': fields.char('Task Summary', track_visibility='onchange', size=128, required=True, select=True),
'description': fields.text('Description'), 'description': fields.text('Description'),
'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True), 'priority': fields.selection([('4','Very Low'), ('3','Low'), ('2','Medium'), ('1','Important'), ('0','Very important')], 'Priority', select=True),
'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."), 'sequence': fields.integer('Sequence', select=True, help="Gives the sequence order when displaying a list of tasks."),

View File

@ -48,8 +48,9 @@ class project_issue(osv.Model):
_mail_post_access = 'read' _mail_post_access = 'read'
_track = { _track = {
'stage_id': { 'stage_id': {
'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence == 1, # this is only an heuristics; depending on your particular stage configuration it may not match all 'new' stages
'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence != 1, 'project_issue.mt_issue_new': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence <= 1,
'project_issue.mt_issue_stage': lambda self, cr, uid, obj, ctx=None: obj.stage_id and obj.stage_id.sequence > 1,
}, },
'user_id': { 'user_id': {
'project_issue.mt_issue_assigned': lambda self, cr, uid, obj, ctx=None: obj.user_id and obj.user_id.id, 'project_issue.mt_issue_assigned': lambda self, cr, uid, obj, ctx=None: obj.user_id and obj.user_id.id,
@ -74,7 +75,7 @@ class project_issue(osv.Model):
def _get_default_stage_id(self, cr, uid, context=None): def _get_default_stage_id(self, cr, uid, context=None):
""" Gives default stage_id """ """ Gives default stage_id """
project_id = self._get_default_project_id(cr, uid, context=context) project_id = self._get_default_project_id(cr, uid, context=context)
return self.stage_find(cr, uid, [], project_id, [('sequence', '=', 1)], context=context) return self.stage_find(cr, uid, [], project_id, [('fold', '=', False)], context=context)
def _resolve_project_id_from_context(self, cr, uid, context=None): def _resolve_project_id_from_context(self, cr, uid, context=None):
""" Returns ID of project based on the value of 'default_project_id' """ Returns ID of project based on the value of 'default_project_id'

View File

@ -334,7 +334,6 @@ class purchase_order(osv.osv):
value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False}) value.update({'related_location_id': picktype.default_location_dest_id and picktype.default_location_dest_id.id or False})
return {'value': value} return {'value': value}
def onchange_partner_id(self, cr, uid, ids, partner_id): def onchange_partner_id(self, cr, uid, ids, partner_id):
partner = self.pool.get('res.partner') partner = self.pool.get('res.partner')
if not partner_id: if not partner_id:
@ -480,12 +479,7 @@ class purchase_order(osv.osv):
''' '''
assert len(ids) == 1, 'This option should only be used for a single id at a time' assert len(ids) == 1, 'This option should only be used for a single id at a time'
self.signal_send_rfq(cr, uid, ids) self.signal_send_rfq(cr, uid, ids)
datas = { return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
'model': 'purchase.order',
'ids': ids,
'form': self.read(cr, uid, ids[0], context=context),
}
return {'type': 'ir.actions.report.xml', 'report_name': 'purchase.quotation', 'datas': datas, 'nodestroy': True}
#TODO: implement messages system #TODO: implement messages system
def wkf_confirm_order(self, cr, uid, ids, context=None): def wkf_confirm_order(self, cr, uid, ids, context=None):
@ -832,7 +826,6 @@ class purchase_order(osv.osv):
@return: new purchase order id @return: new purchase order id
""" """
#TOFIX: merged order line should be unlink #TOFIX: merged order line should be unlink
def make_key(br, fields): def make_key(br, fields):
list_key = [] list_key = []

View File

@ -162,13 +162,14 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Purchase Order" version="7.0"> <form string="Purchase Order" version="7.0">
<header> <header>
<button name="bid_received" states="sent" string="Bid Received" class="oe_highlight"/> <button name="wkf_send_rfq" states="draft" string="Send by Email" type="object" context="{'send_rfq':True}" class="oe_highlight"/>
<button name="wkf_send_rfq" states="draft" string="Send RFQ by Email" type="object" context="{'send_rfq':True}" class="oe_highlight"/> <button name="wkf_send_rfq" states="sent" string="Send by Email" type="object" context="{'send_rfq':True}"/>
<button name="wkf_send_rfq" states="sent" string="Re-Send RFQ by Email" type="object" context="{'send_rfq':True}"/> <button name="print_quotation" string="Print" type="object" states="draft" class="oe_highlight" groups="base.group_user"/>
<button name="print_quotation" string="Print RFQ" type="object" states="draft" class="oe_highlight" groups="base.group_user"/> <button name="print_quotation" string="Print" type="object" states="sent" groups="base.group_user"/>
<button name="print_quotation" string="Re-Print RFQ" type="object" states="sent" groups="base.group_user"/> <button name="purchase_confirm" states="draft" string="Confirm Order"/>
<button name="purchase_confirm" states="draft" string="Confirm Order" id="draft_confirm"/> <button name="purchase_confirm" states="sent" string="Confirm Order" class="oe_highlight"/>
<button name="purchase_confirm" states="bid" string="Confirm Order" class="oe_highlight" id="bid_confirm"/> <button name="wkf_send_rfq" states="confirmed" string="Resend Purchase Order" type="object" class="oe_highlight"/>
<button name="action_cancel" states="approved,except_picking,except_invoice" string="Cancel Order" type="object" />
<button name="picking_ok" states="except_picking" string="Manually Corrected"/> <button name="picking_ok" states="except_picking" string="Manually Corrected"/>
<button name="invoice_ok" states="except_invoice" string="Manually Corrected"/> <button name="invoice_ok" states="except_invoice" string="Manually Corrected"/>
<button name="purchase_approve" states="confirmed" string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager"/> <button name="purchase_approve" states="confirmed" string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager"/>
@ -249,9 +250,9 @@
<page string="Deliveries &amp; Invoices"> <page string="Deliveries &amp; Invoices">
<group> <group>
<group> <group>
<field name="minimum_planned_date"/> <field name="minimum_planned_date"/>
<field name="location_id" groups="stock.group_locations"/> <field name="location_id" groups="stock.group_locations"/>
<field name="dest_address_id" string="Customer Address" on_change="onchange_dest_address_id(dest_address_id)" groups="stock.group_locations"/>
<field name="shipped" groups="base.group_no_one"/> <field name="shipped" groups="base.group_no_one"/>
</group> </group>
<group> <group>
@ -347,7 +348,7 @@
</record> </record>
<record id="purchase_rfq" model="ir.actions.act_window"> <record id="purchase_rfq" model="ir.actions.act_window">
<field name="name">Requests for Quotation</field> <field name="name">Quotations</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">purchase.order</field> <field name="res_model">purchase.order</field>
<field name="context">{}</field> <field name="context">{}</field>

View File

@ -19,459 +19,62 @@
# #
############################################################################## ##############################################################################
from openerp.osv.osv import except_osv from openerp.addons.web.http import Controller, route, request
from openerp.addons.web import http
from openerp.tools.translate import _
from openerp.addons.web.http import request
import openerp.tools.config as config
import time
import base64
import logging
import tempfile
import lxml.html
import subprocess
import simplejson import simplejson
try: import urlparse
import cStringIO as StringIO
except ImportError:
import StringIO
import psutil
import signal
import os
from distutils.version import LooseVersion
from werkzeug import exceptions from werkzeug import exceptions
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse
from werkzeug.datastructures import Headers
from reportlab.graphics.barcode import createBarcodeDrawing from reportlab.graphics.barcode import createBarcodeDrawing
_logger = logging.getLogger(__name__) class ReportController(Controller):
try:
from pyPdf import PdfFileWriter, PdfFileReader
except ImportError:
PdfFileWriter = PdfFileReader = None
class Report(http.Controller): #------------------------------------------------------
# Generic reports controller
@http.route(['/report/<reportname>/<docids>'], type='http', auth='user', website=True, multilang=True) #------------------------------------------------------
def report_html(self, reportname, docids, **kwargs): @route('/report/<reportname>/<docids>', type='http', auth='user', website=True, multilang=True)
"""This is the generic route for QWeb reports. It is used for reports def report_html(self, reportname, docids):
which do not need to preprocess the data (i.e. reports that just display
fields of a record).
It is given a ~fully qualified report name, for instance 'account.report_invoice'.
Based on it, we know the module concerned and the name of the template. With the
name of the template, we will make a search on the ir.actions.reports.xml table and
get the record associated to finally know the model this template refers to.
There is a way to declare the report (in module_report(s).xml) that you must respect:
id="action_report_model"
model="module.model" # To know which model the report refers to
string="Invoices"
report_type="qweb-pdf" # or qweb-html
name="module.template_name"
file="module.template_name"
If you don't want your report listed under the print button, just add
'menu=False'.
"""
ids = [int(i) for i in docids.split(',')]
ids = list(set(ids))
report = self._get_report_from_name(reportname)
report_obj = request.registry[report.model]
docs = report_obj.browse(request.cr, request.uid, ids, context=request.context)
docargs = {
'doc_ids': ids,
'doc_model': report.model,
'docs': docs,
}
return request.registry['report'].render(request.cr, request.uid, [], report.report_name,
docargs, context=request.context)
@http.route(['/report/pdf/<path:path>'], type='http', auth="user", website=True)
def report_pdf(self, path=None, landscape=False, **post):
"""Route converting any reports to pdf. It will get the html-rendered report, extract
header, page and footer in order to prepare minimal html pages that will be further passed
to wkhtmltopdf.
:param path: URL of the report (e.g. /report/account.report_invoice/1)
:returns: a response with 'application/pdf' headers and the pdf as content
"""
cr, uid, context = request.cr, request.uid, request.context cr, uid, context = request.cr, request.uid, request.context
docids = self._eval_params(docids)
return request.registry['report'].get_html(cr, uid, docids, reportname, context=context)
# Get the report we are working on. @route('/report/pdf/report/<reportname>/<docids>', type='http', auth="user", website=True)
# Pattern is /report/module.reportname(?a=1) def report_pdf(self, reportname, docids):
reportname_in_path = path.split('/')[1].split('?')[0] cr, uid, context = request.cr, request.uid, request.context
report = self._get_report_from_name(reportname_in_path) docids = self._eval_params(docids)
pdf = request.registry['report'].get_pdf(cr, uid, docids, reportname, context=context)
# Check attachment_use field. If set to true and an existing pdf is already saved, load pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
# this one now. If not, mark save it.
save_in_attachment = {}
if report.attachment_use is True:
# Get the record ids we are working on.
path_ids = [int(i) for i in path.split('/')[2].split('?')[0].split(',')]
save_in_attachment['model'] = report.model
save_in_attachment['loaded_documents'] = {}
for path_id in path_ids:
obj = request.registry[report.model].browse(cr, uid, path_id)
filename = eval(report.attachment, {'object': obj, 'time': time})
if filename is False: # May be false if, for instance, the record is in draft state
continue
else:
alreadyindb = [('datas_fname', '=', filename),
('res_model', '=', report.model),
('res_id', '=', path_id)]
attach_ids = request.registry['ir.attachment'].search(cr, uid, alreadyindb)
if attach_ids:
# Add the loaded pdf in the loaded_documents list
pdf = request.registry['ir.attachment'].browse(cr, uid, attach_ids[0]).datas
pdf = base64.decodestring(pdf)
save_in_attachment['loaded_documents'][path_id] = pdf
_logger.info('The PDF document %s was loaded from the database' % filename)
else:
# Mark current document to be saved
save_in_attachment[path_id] = filename
# Get the paperformat associated to the report. If there is not, get the one associated to
# the company.
if not report.paperformat_id:
user = request.registry['res.users'].browse(cr, uid, uid, context=context)
paperformat = user.company_id.paperformat_id
else:
paperformat = report.paperformat_id
# Get the html report.
html = self._get_url_content('/' + path, post)[0]
subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering
css = '' # Local css
headerhtml = []
contenthtml = []
footerhtml = []
base_url = request.registry['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
minimalhtml = """
<base href="{3}">
<!DOCTYPE html>
<html style="height: 0;">
<head>
<link href="/report/static/src/css/reset.min.css" rel="stylesheet"/>
<link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="/website/static/src/css/website.css" rel="stylesheet"/>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<style type='text/css'>{0}</style>
<script type='text/javascript'>{1}</script>
</head>
<body class="container" onload='subst()'>
{2}
</body>
</html>"""
# The retrieved html report must be simplified. We convert it into a xml tree
# via lxml in order to extract headers, footers and content.
try:
root = lxml.html.fromstring(html)
for node in root.xpath("//html/head/style"):
css += node.text
for node in root.xpath("//div[@class='header']"):
body = lxml.html.tostring(node)
header = minimalhtml.format(css, subst, body, base_url)
headerhtml.append(header)
for node in root.xpath("//div[@class='footer']"):
body = lxml.html.tostring(node)
footer = minimalhtml.format(css, subst, body, base_url)
footerhtml.append(footer)
for node in root.xpath("//div[@class='page']"):
# Previously, we marked some reports to be saved in attachment via their ids, so we
# must set a relation between report ids and report's content. We use the QWeb
# branding in order to do so: searching after a node having a data-oe-model
# attribute with the value of the current report model and read its oe-id attribute
oemodelnode = node.find(".//*[@data-oe-model='" + report.model + "']")
if oemodelnode is not None:
reportid = oemodelnode.get('data-oe-id', False)
if reportid is not False:
reportid = int(reportid)
else:
reportid = False
body = lxml.html.tostring(node)
reportcontent = minimalhtml.format(css, '', body, base_url)
contenthtml.append(tuple([reportid, reportcontent]))
except lxml.etree.XMLSyntaxError:
contenthtml = []
contenthtml.append(html)
save_in_attachment = {} # Don't save this potentially malformed document
# Get paperformat arguments set in the root html tag. They are prioritized over
# paperformat-record arguments.
specific_paperformat_args = {}
for attribute in root.items():
if attribute[0].startswith('data-report-'):
specific_paperformat_args[attribute[0]] = attribute[1]
# Execute wkhtmltopdf process.
pdf = self._generate_wkhtml_pdf(headerhtml, footerhtml, contenthtml, landscape,
paperformat, specific_paperformat_args, save_in_attachment)
return self._make_pdf_response(pdf)
def _get_url_content(self, url, post=None):
"""Resolve an internal webpage url and return its content with the help of
werkzeug.test.client.
:param url: string representing the url to resolve
:param post: a dict representing the query string
:returns: a tuple str(html), int(statuscode)
"""
# Rebuilding the query string.
if post:
url += '?'
url += '&'.join('%s=%s' % (k, v) for (k, v) in post.iteritems())
# We have to pass the current headers in order to see the report.
reqheaders = Headers(request.httprequest.headers)
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders,
follow_redirects=True)
content = response.data
try:
content = content.decode('utf-8')
except UnicodeDecodeError:
pass
return tuple([content, response.headers])
def _generate_wkhtml_pdf(self, headers, footers, bodies, landscape,
paperformat, spec_paperformat_args=None, save_in_attachment=None):
"""Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
document.
:param header: list of string containing the headers
:param footer: list of string containing the footers
:param bodies: list of string containing the reports
:param landscape: boolean to force the pdf to be rendered under a landscape format
:param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments
:param specific_paperformat_args: dict of prioritized paperformat arguments
:param save_in_attachment: dict of reports to save/load in/from the db
:returns: Content of the pdf as a string
"""
command = ['wkhtmltopdf']
tmp_dir = tempfile.gettempdir()
command_args = []
# Passing the cookie in order to resolve URL.
command_args.extend(['--cookie', 'session_id', request.httprequest.cookies['session_id']])
# Display arguments
if paperformat:
command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
if landscape and '--orientation' in command_args:
command_args_copy = list(command_args)
for index, elem in enumerate(command_args_copy):
if elem == '--orientation':
del command_args[index]
del command_args[index]
command_args.extend(['--orientation', 'landscape'])
elif landscape and not '--orientation' in command_args:
command_args.extend(['--orientation', 'landscape'])
pdfdocuments = []
# HTML to PDF thanks to WKhtmltopdf
for index, reporthtml in enumerate(bodies):
command_arg_local = []
pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.',
mode='w+b')
# Directly load the document if we have it
if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
pdfreport.seek(0)
pdfdocuments.append(pdfreport)
continue
# Header stuff
if headers:
head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
dir=tmp_dir, mode='w+')
head_file.write(headers[index])
head_file.seek(0)
command_arg_local.extend(['--header-html', head_file.name])
# Footer stuff
if footers:
foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
dir=tmp_dir, mode='w+')
foot_file.write(footers[index])
foot_file.seek(0)
command_arg_local.extend(['--footer-html', foot_file.name])
# Body stuff
content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
dir=tmp_dir, mode='w+')
content_file.write(reporthtml[1])
content_file.seek(0)
try:
# If the server is running with only one worker, increase it to two to be able
# to serve the http request from wkhtmltopdf.
if config['workers'] == 1:
ppid = psutil.Process(os.getpid()).ppid
os.kill(ppid, signal.SIGTTIN)
wkhtmltopdf = command + command_args + command_arg_local
wkhtmltopdf += [content_file.name] + [pdfreport.name]
process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = process.communicate()
if config['workers'] == 1:
os.kill(ppid, signal.SIGTTOU)
if process.returncode != 0:
raise except_osv(_('Report (PDF)'),
_('wkhtmltopdf failed with error code = %s. '
'Message: %s') % (str(process.returncode), err))
# Save the pdf in attachment if marked
if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
attachment = {
'name': save_in_attachment.get(reporthtml[0]),
'datas': base64.encodestring(pdfreport.read()),
'datas_fname': save_in_attachment.get(reporthtml[0]),
'res_model': save_in_attachment.get('model'),
'res_id': reporthtml[0],
}
request.registry['ir.attachment'].create(request.cr, request.uid, attachment)
_logger.info('The PDF document %s is now saved in the '
'database' % attachment['name'])
pdfreport.seek(0)
pdfdocuments.append(pdfreport)
if headers:
head_file.close()
if footers:
foot_file.close()
except:
raise
# Get and return the full pdf
if len(pdfdocuments) == 1:
content = pdfdocuments[0].read()
pdfdocuments[0].close()
else:
content = self._merge_pdf(pdfdocuments)
return content
def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
"""Build arguments understandable by wkhtmltopdf from an ir.actions.report.paperformat
record.
:paperformat: ir.actions.report.paperformat record associated to a document
:specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
:returns: list of string containing the wkhtmltopdf arguments
"""
command_args = []
if paperformat.format and paperformat.format != 'custom':
command_args.extend(['--page-size', paperformat.format])
if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom':
command_args.extend(['--page-width', str(paperformat.page_width) + 'in'])
command_args.extend(['--page-height', str(paperformat.page_height) + 'in'])
if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']:
command_args.extend(['--margin-top',
str(specific_paperformat_args['data-report-margin-top'])])
elif paperformat.margin_top:
command_args.extend(['--margin-top', str(paperformat.margin_top)])
if paperformat.margin_left:
command_args.extend(['--margin-left', str(paperformat.margin_left)])
if paperformat.margin_bottom:
command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
if paperformat.margin_right:
command_args.extend(['--margin-right', str(paperformat.margin_right)])
if paperformat.orientation:
command_args.extend(['--orientation', str(paperformat.orientation)])
if paperformat.header_spacing:
command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
if paperformat.header_line:
command_args.extend(['--header-line'])
if paperformat.dpi:
command_args.extend(['--dpi', str(paperformat.dpi)])
return command_args
def _get_report_from_name(self, report_name):
"""Get the first record of ir.actions.report.xml having the argument as value for
the field report_name.
"""
report_obj = request.registry['ir.actions.report.xml']
qwebtypes = ['qweb-pdf', 'qweb-html']
idreport = report_obj.search(request.cr, request.uid,
[('report_type', 'in', qwebtypes),
('report_name', '=', report_name)])
report = report_obj.browse(request.cr, request.uid, idreport[0],
context=request.context)
return report
def _make_pdf_response(self, pdf):
"""Make a request response for a PDF file with correct http headers.
:param pdf: content of a pdf in a string
:returns: request response for a pdf document
"""
pdfhttpheaders = [('Content-Type', 'application/pdf'),
('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders) return request.make_response(pdf, headers=pdfhttpheaders)
def _merge_pdf(self, documents): #------------------------------------------------------
"""Merge PDF files into one. # Particular reports controller
#------------------------------------------------------
@route('/report/<reportname>', type='http', auth='user', website=True, multilang=True)
def report_html_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context
report_obj = request.registry['report']
data = self._eval_params(data) # Sanitizing
return report_obj.get_html(cr, uid, [], reportname, data=data, context=context)
:param documents: list of pdf files @route('/report/pdf/report/<reportname>', type='http', auth='user', website=True, multilang=True)
:returns: string containing the merged pdf def report_pdf_particular(self, reportname, **data):
""" cr, uid, context = request.cr, request.uid, request.context
writer = PdfFileWriter() report_obj = request.registry['report']
for document in documents: data = self._eval_params(data) # Sanitizing
reader = PdfFileReader(file(document.name, "rb")) pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context)
for page in range(0, reader.getNumPages()): pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
writer.addPage(reader.getPage(page)) return request.make_response(pdf, headers=pdfhttpheaders)
document.close()
merged = StringIO.StringIO()
writer.write(merged)
merged.seek(0)
content = merged.read()
merged.close()
return content
@http.route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="user") #------------------------------------------------------
def barcode(self, type, value, width=300, height=50): # Misc. route utils
#------------------------------------------------------
@route(['/report/barcode', '/report/barcode/<type>/<path:value>'], type='http', auth="user")
def report_barcode(self, type, value, width=300, height=50):
"""Contoller able to render barcode images thanks to reportlab. """Contoller able to render barcode images thanks to reportlab.
Samples: Samples:
<img t-att-src="'/report/barcode/QR/%s' % o.name"/> <img t-att-src="'/report/barcode/QR/%s' % o.name"/>
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('QR', o.name, 200, 200)"/> <img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' %
('QR', o.name, 200, 200)"/>
:param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39', :param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39',
'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93', 'Extended93', 'FIM', 'I2of5', 'MSI', 'POSTNET', 'QR', 'Standard39', 'Standard93',
@ -488,52 +91,69 @@ class Report(http.Controller):
return request.make_response(barcode, headers=[('Content-Type', 'image/png')]) return request.make_response(barcode, headers=[('Content-Type', 'image/png')])
@http.route('/report/download/', type='http', auth="user") @route(['/report/download'], type='http', auth="user", website=True)
def report_attachment(self, data, token): def report_download(self, data, token):
"""This function is used by 'qwebactionmanager.js' in order to trigger the download of """This function is used by 'qwebactionmanager.js' in order to trigger the download of
a report of any type. a pdf report.
:param data: a javasscript array JSON.stringified containg report internal url ([0]) and :param data: a javascript array JSON.stringified containg report internal url ([0]) and
type [1] type [1]
:returns: Response with a filetoken cookie and an attachment header :returns: Response with a filetoken cookie and an attachment header
""" """
requestcontent = simplejson.loads(data) requestcontent = simplejson.loads(data)
url, type = requestcontent[0], requestcontent[1] url, type = requestcontent[0], requestcontent[1]
file, fileheaders = self._get_url_content(url)
if type == 'qweb-pdf': if type == 'qweb-pdf':
response = self._make_pdf_response(file) reportname = url.split('/report/pdf/report/')[1].split('?')[0].split('/')[0]
response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;')
elif type == 'controller': if '?' not in url:
response = request.make_response(file) # Generic report:
response.headers.add('Content-Disposition', fileheaders['Content-Disposition']) docids = url.split('/')[-1]
response.headers.add('Content-Type', fileheaders['Content-Type']) response = self.report_pdf(reportname, docids)
else:
# Particular report:
querystring = url.split('?')[1]
querystring = dict(urlparse.parse_qsl(querystring))
response = self.report_pdf_particular(reportname, **querystring)
response.headers.add('Content-Disposition', 'attachment; filename=%s.pdf;' % reportname)
response.set_cookie('fileToken', token)
return response
elif type =='controller':
from werkzeug.test import Client
from werkzeug.wrappers import BaseResponse
from werkzeug.datastructures import Headers
reqheaders = Headers(request.httprequest.headers)
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, follow_redirects=True)
response.set_cookie('fileToken', token)
return response
else: else:
return return
response.headers.add('Content-Length', len(file)) @route(['/report/check_wkhtmltopdf'], type='json', auth="user")
response.set_cookie('fileToken', token)
return response
@http.route('/report/check_wkhtmltopdf/', type='json', auth="user")
def check_wkhtmltopdf(self): def check_wkhtmltopdf(self):
"""Check the presence of wkhtmltopdf and return its version. If wkhtmltopdf return request.registry['report']._check_wkhtmltopdf()
cannot be found, return False.
def _eval_params(self, param):
"""Parse a dict generated by the webclient (javascript) into a python dict.
""" """
try: if isinstance(param, dict):
process = subprocess.Popen(['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) for key, value in param.iteritems():
out, err = process.communicate() if value.lower() == 'false':
if err: param[key] = False
raise elif value.lower() == 'true':
param[key] = True
version = out.splitlines()[1].strip() elif ',' in value:
version = version.split(' ')[1] param[key] = [int(i) for i in value.split(',')]
else:
if LooseVersion(version) < LooseVersion('0.12.0'): try:
_logger.warning('Upgrade WKHTMLTOPDF to (at least) 0.12.0') param[key] = int(value)
return 'upgrade' except (ValueError, TypeError):
pass
return True else:
except: if isinstance(param, (str, unicode)):
_logger.error('You need WKHTMLTOPDF to print a pdf version of this report.') param = [int(i) for i in param.split(',')]
return False if isinstance(param, list):
param = list(set(param))
if isinstance(param, int):
param = [param]
return param

View File

@ -19,54 +19,85 @@
# #
############################################################################## ##############################################################################
from openerp.addons.web.http import request
from openerp.osv import osv from openerp.osv import osv
from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _ from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, config
from openerp.osv.fields import float as float_field, function as function_field, datetime as datetime_field
import os
import time import time
import psutil
import signal
import base64
import logging
import tempfile
import lxml.html
import cStringIO
import subprocess
from datetime import datetime from datetime import datetime
from functools import partial
from werkzeug.datastructures import Headers from distutils.version import LooseVersion
from werkzeug.wrappers import BaseResponse try:
from werkzeug.test import Client from pyPdf import PdfFileWriter, PdfFileReader
except ImportError:
PdfFileWriter = PdfFileReader = None
def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT): _logger = logging.getLogger(__name__)
return len((datetime.now()).strftime(date_format))
class report(osv.Model): """Check the presence of wkhtmltopdf and return its version."""
wkhtmltopdf_state = 'install'
try:
process = subprocess.Popen(
['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
except OSError:
_logger.error('You need wkhtmltopdf to print a pdf version of the reports.')
else:
out, err = process.communicate()
version = out.splitlines()[1].strip()
version = version.split(' ')[1]
if LooseVersion(version) < LooseVersion('0.12.0'):
_logger.warning('Upgrade wkhtmltopdf to (at least) 0.12.0')
wkhtmltopdf_state = 'upgrade'
wkhtmltopdf_state = 'ok'
class Report(osv.Model):
_name = "report" _name = "report"
_description = "Report" _description = "Report"
public_user = None public_user = None
def get_digits(self, obj=None, f=None, dp=None): #--------------------------------------------------------------------------
# Extension of ir_ui_view.render with arguments frequently used in reports
#--------------------------------------------------------------------------
def _get_digits(self, cr, uid, obj=None, f=None, dp=None):
d = DEFAULT_DIGITS = 2 d = DEFAULT_DIGITS = 2
if dp: if dp:
decimal_precision_obj = self.pool['decimal.precision'] decimal_precision_obj = self.pool['decimal.precision']
ids = decimal_precision_obj.search(request.cr, request.uid, [('name', '=', dp)]) ids = decimal_precision_obj.search(cr, uid, [('name', '=', dp)])
if ids: if ids:
d = decimal_precision_obj.browse(request.cr, request.uid, ids)[0].digits d = decimal_precision_obj.browse(cr, uid, ids)[0].digits
elif obj and f: elif obj and f:
res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS))) res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
if isinstance(res_digits, tuple): if isinstance(res_digits, tuple):
d = res_digits[1] d = res_digits[1]
else: else:
d = res_digits(request.cr)[1] d = res_digits(cr)[1]
elif (hasattr(obj, '_field') and elif (hasattr(obj, '_field') and
isinstance(obj._field, (float_field, function_field)) and isinstance(obj._field, (float_field, function_field)) and
obj._field.digits): obj._field.digits):
d = obj._field.digits[1] or DEFAULT_DIGITS d = obj._field.digits[1] or DEFAULT_DIGITS
return d return d
def _get_lang_dict(self): def _get_lang_dict(self, cr, uid):
pool_lang = self.pool['res.lang'] pool_lang = self.pool['res.lang']
lang = self.localcontext.get('lang', 'en_US') or 'en_US' lang = self.localcontext.get('lang', 'en_US') or 'en_US'
lang_ids = pool_lang.search(request.cr, request.uid, [('code', '=', lang)])[0] lang_ids = pool_lang.search(cr, uid, [('code', '=', lang)])[0]
lang_obj = pool_lang.browse(request.cr, request.uid, lang_ids) lang_obj = pool_lang.browse(cr, uid, lang_ids)
lang_dict = { lang_dict = {
'lang_obj': lang_obj, 'lang_obj': lang_obj,
'date_format': lang_obj.date_format, 'date_format': lang_obj.date_format,
@ -76,7 +107,7 @@ class report(osv.Model):
self.default_lang[lang] = self.lang_dict.copy() self.default_lang[lang] = self.lang_dict.copy()
return True return True
def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False): def formatLang(self, value, digits=None, date=False, date_time=False, grouping=True, monetary=False, dp=False, currency_obj=False, cr=None, uid=None):
""" """
Assuming 'Account' decimal.precision=3: Assuming 'Account' decimal.precision=3:
formatLang(value) -> digits=2 (default) formatLang(value) -> digits=2 (default)
@ -84,17 +115,20 @@ class report(osv.Model):
formatLang(value, dp='Account') -> digits=3 formatLang(value, dp='Account') -> digits=3
formatLang(value, digits=5, dp='Account') -> digits=5 formatLang(value, digits=5, dp='Account') -> digits=5
""" """
def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
return len((datetime.now()).strftime(date_format))
if digits is None: if digits is None:
if dp: if dp:
digits = self.get_digits(dp=dp) digits = self._get_digits(cr, uid, dp=dp)
else: else:
digits = self.get_digits(value) digits = self._get_digits(cr, uid, value)
if isinstance(value, (str, unicode)) and not value: if isinstance(value, (str, unicode)) and not value:
return '' return ''
if not self.lang_dict_called: if not self.lang_dict_called:
self._get_lang_dict() self._get_lang_dict(cr, uid)
self.lang_dict_called = True self.lang_dict_called = True
if date or date_time: if date or date_time:
@ -117,9 +151,7 @@ class report(osv.Model):
date = datetime(*value.timetuple()[:6]) date = datetime(*value.timetuple()[:6])
if date_time: if date_time:
# Convert datetime values to the expected client/context timezone # Convert datetime values to the expected client/context timezone
date = datetime_field.context_timestamp(request.cr, request.uid, date = datetime_field.context_timestamp(cr, uid, timestamp=date, context=self.localcontext)
timestamp=date,
context=self.localcontext)
return date.strftime(date_format.encode('utf-8')) return date.strftime(date_format.encode('utf-8'))
res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary) res = self.lang_dict['lang_obj'].format('%.' + str(digits) + 'f', value, grouping=grouping, monetary=monetary)
@ -150,7 +182,7 @@ class report(osv.Model):
'tz': context.get('tz'), 'tz': context.get('tz'),
'uid': context.get('uid'), 'uid': context.get('uid'),
} }
self._get_lang_dict() self._get_lang_dict(cr, uid)
view_obj = self.pool['ir.ui.view'] view_obj = self.pool['ir.ui.view']
@ -177,51 +209,180 @@ class report(osv.Model):
qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx) qcontext['o'] = self.pool[model].browse(cr, uid, doc_id, context=ctx)
return view_obj.render(cr, uid, template, qcontext, context=ctx) return view_obj.render(cr, uid, template, qcontext, context=ctx)
current_user = self.pool['res.users'].browse(cr, uid, uid, context=context)
# Website independance code
website = False
res_company = current_user.company_id
try:
website = request.website
res_company = request.website.company_id
except:
pass
values.update({ values.update({
'time': time, 'time': time,
'user': current_user, 'formatLang': partial(self.formatLang, cr=cr, uid=uid),
'user_id': current_user.id, 'get_digits': self._get_digits,
'formatLang': self.formatLang,
'get_digits': self.get_digits,
'render_doc': render_doc, 'render_doc': render_doc,
'website': website, 'editable': True, # Will active inherit_branding
'res_company': res_company, 'res_company': self.pool['res.users'].browse(cr, uid, uid).company_id,
'website': False, # Will be overidden by ir.ui.view if the request has website enabled
}) })
return view_obj.render(cr, uid, template, values, context=context) return view_obj.render(cr, uid, template, values, context=context)
def get_pdf(self, report, record_id, context=None): #--------------------------------------------------------------------------
"""Used to return the content of a generated PDF. # Main reports methods
#--------------------------------------------------------------------------
:returns: pdf def get_html(self, cr, uid, ids, report_name, data=None, context=None):
"""This method generates and returns html version of a report.
""" """
url = '/report/pdf/report/' + report.report_file + '/' + str(record_id) # If the report is using a custom model to render its html, we must use it.
reqheaders = Headers(request.httprequest.headers) # Otherwise, fallback on the generic html rendering.
reqheaders.pop('Accept') try:
reqheaders.add('Accept', 'application/pdf') report_model_name = 'report.%s' % report_name
reqheaders.pop('Content-Type') particularreport_obj = self.pool[report_model_name]
reqheaders.add('Content-Type', 'text/plain') return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context)
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders, except KeyError:
follow_redirects=True) report = self._get_report_from_name(cr, uid, report_name)
return response.data report_obj = self.pool[report.model]
docs = report_obj.browse(cr, uid, ids, context=context)
docargs = {
'doc_ids': ids,
'doc_model': report.model,
'docs': docs,
}
return self.render(cr, uid, [], report.report_name, docargs, context=context)
def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, context=None):
"""This method generates and returns pdf version of a report.
"""
if context is None:
context = {}
if html is None:
html = self.get_html(cr, uid, ids, report_name, data=data, context=context)
html = html.decode('utf-8')
# Get the ir.actions.report.xml record we are working on.
report = self._get_report_from_name(cr, uid, report_name)
# Check attachment_use field. If set to true and an existing pdf is already saved, load
# this one now. Else, mark save it.
save_in_attachment = {}
if report.attachment_use is True:
save_in_attachment['model'] = report.model
save_in_attachment['loaded_documents'] = {}
for record_id in ids:
obj = self.pool[report.model].browse(cr, uid, record_id)
filename = eval(report.attachment, {'object': obj, 'time': time})
if filename is False: # May be false if, for instance, the record is in draft state
continue
else:
alreadyindb = [('datas_fname', '=', filename),
('res_model', '=', report.model),
('res_id', '=', record_id)]
attach_ids = self.pool['ir.attachment'].search(cr, uid, alreadyindb)
if attach_ids:
# Add the loaded pdf in the loaded_documents list
pdf = self.pool['ir.attachment'].browse(cr, uid, attach_ids[0]).datas
pdf = base64.decodestring(pdf)
save_in_attachment['loaded_documents'][record_id] = pdf
_logger.info('The PDF document %s was loaded from the database' % filename)
else:
# Mark current document to be saved
save_in_attachment[id] = filename
# Get the paperformat associated to the report, otherwise fallback on the company one.
if not report.paperformat_id:
user = self.pool['res.users'].browse(cr, uid, uid)
paperformat = user.company_id.paperformat_id
else:
paperformat = report.paperformat_id
# Preparing the minimal html pages
#subst = self._get_url_content('/report/static/src/js/subst.js')[0] # Used in age numbering
subst = "<script src='/report/static/src/js/subst.js'></script> "
css = '' # Will contain local css
headerhtml = []
contenthtml = []
footerhtml = []
base_url = self.pool['ir.config_parameter'].get_param(cr, uid, 'web.base.url')
minimalhtml = """
<base href="{base_url}">
<!DOCTYPE html>
<html style="height: 0;">
<head>
<link href="/report/static/src/css/reset.min.css" rel="stylesheet"/>
<link href="/web/static/lib/bootstrap/css/bootstrap.css" rel="stylesheet"/>
<link href="/website/static/src/css/website.css" rel="stylesheet"/>
<link href="/web/static/lib/fontawesome/css/font-awesome.css" rel="stylesheet"/>
<style type='text/css'>{css}</style>
{subst}
</head>
<body class="container" onload='subst()'>
{body}
</body>
</html>"""
# The retrieved html report must be simplified. We convert it into a xml tree
# via lxml in order to extract headers, footers and content.
try:
root = lxml.html.fromstring(html)
for node in root.xpath("//html/head/style"):
css += node.text
for node in root.xpath("//div[@class='header']"):
body = lxml.html.tostring(node)
header = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url)
headerhtml.append(header)
for node in root.xpath("//div[@class='footer']"):
body = lxml.html.tostring(node)
footer = minimalhtml.format(css=css, subst=subst, body=body, base_url=base_url)
footerhtml.append(footer)
for node in root.xpath("//div[@class='page']"):
# Previously, we marked some reports to be saved in attachment via their ids, so we
# must set a relation between report ids and report's content. We use the QWeb
# branding in order to do so: searching after a node having a data-oe-model
# attribute with the value of the current report model and read its oe-id attribute
oemodelnode = node.find(".//*[@data-oe-model='%s']" % report.model)
if oemodelnode is not None:
reportid = oemodelnode.get('data-oe-id')
if reportid:
reportid = int(reportid)
else:
reportid = False
body = lxml.html.tostring(node)
reportcontent = minimalhtml.format(css=css, subst='', body=body, base_url=base_url)
contenthtml.append(tuple([reportid, reportcontent]))
except lxml.etree.XMLSyntaxError:
contenthtml = []
contenthtml.append(html)
save_in_attachment = {} # Don't save this potentially malformed document
# Get paperformat arguments set in the root html tag. They are prioritized over
# paperformat-record arguments.
specific_paperformat_args = {}
for attribute in root.items():
if attribute[0].startswith('data-report-'):
specific_paperformat_args[attribute[0]] = attribute[1]
# Run wkhtmltopdf process
pdf = self._generate_wkhtml_pdf(
cr, uid, headerhtml, footerhtml, contenthtml, context.get('landscape'),
paperformat, specific_paperformat_args, save_in_attachment
)
return pdf
def get_action(self, cr, uid, ids, report_name, datas=None, context=None): def get_action(self, cr, uid, ids, report_name, datas=None, context=None):
"""Used to return an action of type ir.actions.report.xml. """Return an action of type ir.actions.report.xml.
:param report_name: Name of the template to generate an action for :param report_name: Name of the template to generate an action for
""" """
# TODO: return the action for the ids passed in args
if context is None: if context is None:
context = {} context = {}
@ -249,26 +410,203 @@ class report(osv.Model):
return action return action
def eval_params(self, dict_param): #--------------------------------------------------------------------------
"""Parse a dictionary generated by the webclient (javascript) into a dictionary # Report generation helpers
understandable by a wizard controller (python). #--------------------------------------------------------------------------
"""
for key, value in dict_param.iteritems():
if value.lower() == 'false':
dict_param[key] = False
elif value.lower() == 'true':
dict_param[key] = True
elif ',' in value:
dict_param[key] = [int(i) for i in value.split(',')]
elif '%2C' in value:
dict_param[key] = [int(i) for i in value.split('%2C')]
else:
try:
i = int(value)
dict_param[key] = i
except (ValueError, TypeError):
pass
data = {} def _check_wkhtmltopdf(self):
data['form'] = dict_param return wkhtmltopdf_state
return data
def _generate_wkhtml_pdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None):
"""Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
document.
:param header: list of string containing the headers
:param footer: list of string containing the footers
:param bodies: list of string containing the reports
:param landscape: boolean to force the pdf to be rendered under a landscape format
:param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments
:param specific_paperformat_args: dict of prioritized paperformat arguments
:param save_in_attachment: dict of reports to save/load in/from the db
:returns: Content of the pdf as a string
"""
command = ['wkhtmltopdf']
command_args = []
tmp_dir = tempfile.gettempdir()
# Passing the cookie to wkhtmltopdf in order to resolve URL.
try:
from openerp.addons.web.http import request
command_args.extend(['--cookie', 'session_id', request.session.sid])
except AttributeError:
pass
# Display arguments
if paperformat:
command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
if landscape and '--orientation' in command_args:
command_args_copy = list(command_args)
for index, elem in enumerate(command_args_copy):
if elem == '--orientation':
del command_args[index]
del command_args[index]
command_args.extend(['--orientation', 'landscape'])
elif landscape and not '--orientation' in command_args:
command_args.extend(['--orientation', 'landscape'])
pdfdocuments = []
# HTML to PDF thanks to WKhtmltopdf
for index, reporthtml in enumerate(bodies):
command_arg_local = []
pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.',
mode='w+b')
# Directly load the document if we have it
if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
pdfreport.flush()
pdfdocuments.append(pdfreport)
continue
# Header stuff
if headers:
head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
dir=tmp_dir, mode='w+')
head_file.write(headers[index])
head_file.flush()
command_arg_local.extend(['--header-html', head_file.name])
# Footer stuff
if footers:
foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
dir=tmp_dir, mode='w+')
foot_file.write(footers[index])
foot_file.flush()
command_arg_local.extend(['--footer-html', foot_file.name])
# Body stuff
content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
dir=tmp_dir, mode='w+')
content_file.write(reporthtml[1])
content_file.flush()
try:
# If the server is running with only one worker, ask to create a secund to be able
# to serve the http request of wkhtmltopdf subprocess.
if config['workers'] == 1:
ppid = psutil.Process(os.getpid()).ppid
os.kill(ppid, signal.SIGTTIN)
wkhtmltopdf = command + command_args + command_arg_local
wkhtmltopdf += [content_file.name] + [pdfreport.name]
process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = process.communicate()
if config['workers'] == 1:
os.kill(ppid, signal.SIGTTOU)
if process.returncode != 0:
raise osv.except_osv(_('Report (PDF)'),
_('wkhtmltopdf failed with error code = %s. '
'Message: %s') % (str(process.returncode), err))
# Save the pdf in attachment if marked
if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
attachment = {
'name': save_in_attachment.get(reporthtml[0]),
'datas': base64.encodestring(pdfreport.read()),
'datas_fname': save_in_attachment.get(reporthtml[0]),
'res_model': save_in_attachment.get('model'),
'res_id': reporthtml[0],
}
self.pool['ir.attachment'].create(cr, uid, attachment)
_logger.info('The PDF document %s is now saved in the '
'database' % attachment['name'])
pdfreport.flush()
pdfdocuments.append(pdfreport)
if headers:
head_file.close()
if footers:
foot_file.close()
except:
raise
# Get and return the full pdf
if len(pdfdocuments) == 1:
content = pdfdocuments[0].read()
pdfdocuments[0].close()
else:
content = self._merge_pdf(pdfdocuments)
return content
def _get_report_from_name(self, cr, uid, report_name):
"""Get the first record of ir.actions.report.xml having the ``report_name`` as value for
the field report_name.
"""
report_obj = self.pool['ir.actions.report.xml']
qwebtypes = ['qweb-pdf', 'qweb-html']
conditions = [('report_type', 'in', qwebtypes), ('report_name', '=', report_name)]
idreport = report_obj.search(cr, uid, conditions)[0]
return report_obj.browse(cr, uid, idreport)
def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
"""Build arguments understandable by wkhtmltopdf from a report.paperformat record.
:paperformat: report.paperformat record
:specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
:returns: list of string representing the wkhtmltopdf arguments
"""
command_args = []
if paperformat.format and paperformat.format != 'custom':
command_args.extend(['--page-size', paperformat.format])
if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom':
command_args.extend(['--page-width', str(paperformat.page_width) + 'in'])
command_args.extend(['--page-height', str(paperformat.page_height) + 'in'])
if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']:
command_args.extend(['--margin-top',
str(specific_paperformat_args['data-report-margin-top'])])
elif paperformat.margin_top:
command_args.extend(['--margin-top', str(paperformat.margin_top)])
if paperformat.margin_left:
command_args.extend(['--margin-left', str(paperformat.margin_left)])
if paperformat.margin_bottom:
command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
if paperformat.margin_right:
command_args.extend(['--margin-right', str(paperformat.margin_right)])
if paperformat.orientation:
command_args.extend(['--orientation', str(paperformat.orientation)])
if paperformat.header_spacing:
command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
if paperformat.header_line:
command_args.extend(['--header-line'])
if paperformat.dpi:
command_args.extend(['--dpi', str(paperformat.dpi)])
return command_args
def _merge_pdf(self, documents):
"""Merge PDF files into one.
:param documents: list of pdf files
:returns: string containing the merged pdf
"""
writer = PdfFileWriter()
for document in documents:
reader = PdfFileReader(file(document.name, "rb"))
for page in range(0, reader.getNumPages()):
writer.addPage(reader.getPage(page))
document.close()
merged = cStringIO.StringIO()
writer.write(merged)
merged.seek(0)
content = merged.read()
merged.close()
return content

View File

@ -1,4 +1,5 @@
openerp.report = function(instance) { openerp.report = function(instance) {
var wkhtmltopdf_state;
instance.web.ActionManager = instance.web.ActionManager.extend({ instance.web.ActionManager = instance.web.ActionManager.extend({
ir_actions_report_xml: function(action, options) { ir_actions_report_xml: function(action, options) {
@ -11,8 +12,7 @@ openerp.report = function(instance) {
// QWeb reports // QWeb reports
if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) { if ('report_type' in action && (action.report_type == 'qweb-html' || action.report_type == 'qweb-pdf' || action.report_type == 'controller')) {
var report_url = '';
var report_url = ''
switch (action.report_type) { switch (action.report_type) {
case 'qweb-html': case 'qweb-html':
report_url = '/report/' + action.report_name; report_url = '/report/' + action.report_name;
@ -49,41 +49,49 @@ openerp.report = function(instance) {
} }
if (action.report_type == 'qweb-html') { if (action.report_type == 'qweb-html') {
// Open the html report in a popup // Open the html report in a popup
window.open(report_url, '_blank', 'height=768,width=1024'); window.open(report_url, '_blank', 'height=900,width=1280');
instance.web.unblockUI(); instance.web.unblockUI();
return; return;
} else { } else {
// Trigger the download of the pdf/custom controller report // Trigger the download of the pdf/controller report
var c = openerp.webclient.crashmanager; var c = openerp.webclient.crashmanager;
var response = new Array() var response = new Array();
response[0] = report_url response[0] = report_url;
response[1] = action.report_type response[1] = action.report_type;
openerp.session.rpc('/report/check_wkhtmltopdf').then(function (presence) { if (action.report_type == 'qweb-pdf') {
// Fallback of qweb-pdf if wkhtmltopdf is not installed (wkhtmltopdf_state = wkhtmltopdf_state || openerp.session.rpc('/report/check_wkhtmltopdf')).then(function (presence) {
if (!presence && action.report_type == 'qweb-pdf') { // Fallback of qweb-pdf if wkhtmltopdf is not installed
self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \ if (presence == 'install' && action.report_type == 'qweb-pdf') {
system. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" _target="blank">\ self.do_notify(_t('Report'), _t('Unable to find Wkhtmltopdf on this \
wkhtmltopdf.org</a>'), true); system. The report will be shown in html.<br><br><a href="http://wkhtmltopdf.org/" target="_blank">\
window.open(report_url.substring(12), '_blank', 'height=768,width=1024'); wkhtmltopdf.org</a>'), true);
instance.web.unblockUI(); window.open(report_url.substring(12), '_blank', 'height=768,width=1024');
} instance.web.unblockUI();
else { } else {
if (presence == 'upgrade') { if (presence == 'upgrade') {
self.do_notify(_t('Report'), _t('You should upgrade your version of\ self.do_notify(_t('Report'), _t('You should upgrade your version of\
Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\ Wkhtmltopdf to at least 0.12.0 in order to get a correct display of headers and footers as well as\
support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \ support for table-breaking between pages.<br><br><a href="http://wkhtmltopdf.org/" \
target="_blank">wkhtmltopdf.org</a>'), true); target="_blank">wkhtmltopdf.org</a>'), true);
}
self.session.get_file({
url: '/report/download',
data: {data: JSON.stringify(response)},
complete: openerp.web.unblockUI,
error: c.rpc_error.bind(c)
});
} }
self.session.get_file({ });
url: '/report/download', } else {
data: {data: JSON.stringify(response)}, self.session.get_file({
complete: openerp.web.unblockUI, url: '/report/download',
error: c.rpc_error.bind(c) data: {data: JSON.stringify(response)},
}); complete: openerp.web.unblockUI,
} error: c.rpc_error.bind(c)
}); });
} }
}
} else { } else {
return self._super(action, options); return self._super(action, options);
} }

Some files were not shown because too many files have changed in this diff Show More