[MERGE] merged trunk up to revision 9251
bzr revid: qdp-launchpad@openerp.com-20140326152751-b6bo0fawrzmd71f0
This commit is contained in:
commit
e0b27b4ee8
|
@ -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:
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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})
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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 user’s ID for security checks,
|
|
||||||
@param ids: List of compute data’s 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:
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -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="['&', '|', ('case_default', '=', True), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"
|
domain="['&', '|', ('case_default', '=', True), ('section_ids', '=', section_id), '|', ('type', '=', type), ('type', '=', 'both')]"
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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/>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)]}">
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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','<=',0)]}">N </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','<=',0)]}">N </span>
|
||||||
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','>=',0)]}">S </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','>=',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','<=',0)]}">E </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','<=',0)]}">E </span>
|
||||||
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','>=',0)]}">W </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','>=',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','<=',0)]}">N </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','<=',0)]}">N </span>
|
||||||
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','>=',0)]}">S </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_latitude','>=',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','<=',0)]}">E </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','<=',0)]}">E </span>
|
||||||
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','>=',0)]}">W </span>
|
<span class="oe_grey oe_inline" attrs="{'invisible':[('partner_longitude','>=',0)]}">W </span>
|
||||||
<span class="oe_grey">) </span>
|
<span class="oe_grey">) </span>
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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'}"/>
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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 ""
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
@ -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 ""
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 "&"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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.'),
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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){
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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) -->
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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:
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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."
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
from . import test_uom
|
||||||
|
|
||||||
|
fast_suite = [
|
||||||
|
test_uom,
|
||||||
|
]
|
|
@ -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.")
|
|
@ -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 "የእቃው መጨመር"
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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."),
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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 = []
|
||||||
|
|
|
@ -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 & Invoices">
|
<page string="Deliveries & 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>
|
||||||
|
|
|
@ -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&value=%s&width=%s&height=%s' % ('QR', o.name, 200, 200)"/>
|
<img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue