[MERGE] from trunk

bzr revid: chm@openerp.com-20140326105539-9hf1x7u6lvh2hq2f
This commit is contained in:
chm@openerp.com 2014-03-26 11:55:39 +01:00
commit 99ac3ff81b
58 changed files with 2248 additions and 937 deletions

View File

@ -25,7 +25,7 @@
<separator/>
<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="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/>
<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"/>

View File

@ -19,6 +19,7 @@
#
##############################################################################
from openerp.osv import osv
from openerp.addons.web import http
from openerp.addons.web.http import request
from common_report_header import common_report_header
@ -29,14 +30,12 @@ except ImportError:
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 report_account_tax(self, **data):
def render_html(self, cr, uid, ids, data=None, context=None):
report_obj = request.registry['report']
self.cr, self.uid, self.pool = request.cr, request.uid, request.registry
data = report_obj.eval_params(data)
self.cr, self.uid, self.context = cr, uid, context
res = {}
self.period_ids = []
@ -54,23 +53,23 @@ class tax_report(http.Controller, common_report_header):
'based_on': self._get_basedon(data),
'period_from': self.get_start_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):
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
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:
res = self._add_codes(based_on, res, period_list, context=context)
else:
self.cr.execute ("select id from account_fiscalyear")
fy = self.cr.fetchall()
self.cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],))
periods = self.cr.fetchall()
cr.execute ("select id from account_fiscalyear")
fy = cr.fetchall()
cr.execute ("select id from account_period where fiscalyear_id = %s",(fy[0][0],))
periods = cr.fetchall()
for p in periods:
period_list.append(p[0])
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)
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
while ind_general < len(res_general):
res_general[ind_general]['type'] = 2
@ -101,14 +100,14 @@ class tax_report(http.Controller, common_report_header):
i+=1
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:
return []
res = []
obj_account = self.pool.get('account.account')
periods_ids = tuple(period_list)
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.credit) AS credit, \
COUNT(*) AS count, \
@ -132,7 +131,7 @@ class tax_report(http.Controller, common_report_header):
company_id, periods_ids, 'paid',))
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.credit) AS credit, \
COUNT(*) AS count, \
@ -149,23 +148,21 @@ class tax_report(http.Controller, common_report_header):
AND account.active \
GROUP BY account.id,account.name,account.code', ('draft', tax_code_id,
company_id, periods_ids,))
res = self.cr.dictfetchall()
res = cr.dictfetchall()
i = 0
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
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')
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 = []
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 += self._get_codes(based_on, company_id, code.id, level+1, context=context)
res += self._get_codes(based_on, company_id, code.id, level+1, cr=cr, uid=uid, context=context)
return res
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))
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):
result_accounts = []
ind=0
@ -206,7 +200,8 @@ class tax_report(http.Controller, common_report_header):
bcl_rup_ind = ind - 1
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': '',
'debit': 0,
'credit': 0,
@ -229,25 +224,23 @@ class tax_report(http.Controller, common_report_header):
return result_accounts
class tax_report_xls(http.Controller):
@http.route(['/report/account.report_vat_xls'], type='http', auth='user', website=True, multilang=True)
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 = {}
self.period_ids = []
period_obj = self.pool.get('account.period')
self.display_detail = data['form']['display_detail']
res['periods'] = ''
res['fiscalyear'] = data['form'].get('fiscalyear_id', False)
cr, uid = request.cr, request.uid
data = eval_params(data)
data = {'form': data}
if data['form'].get('period_from', False) and data['form'].get('period_to', False):
self.period_ids = period_obj.build_ctx_periods(self.cr, self.uid, data['form']['period_from'], data['form']['period_to'])
content = ''
lines = self._get_lines(self._get_basedon(data), company_id=data['form']['company_id'])
taxreport_obj.render_html(cr, uid, [], data=data)
lines = taxreport_obj._get_lines(taxreport_obj._get_basedon(data), company_id=data['form']['company_id'], cr=cr, uid=uid)
if lines:
xls = StringIO.StringIO()

View File

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

View File

@ -78,6 +78,9 @@ class OAuthLogin(openerp.addons.web.controllers.main.Home):
@http.route()
def web_login(self, *args, **kw):
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()
response = super(OAuthLogin, self).web_login(*args, **kw)

View File

@ -37,6 +37,9 @@ class AuthSignupHome(openerp.addons.web.controllers.main.Home):
ensure_db()
response = super(AuthSignupHome, self).web_login(*args, **kw)
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
@http.route('/web/signup', type='http', auth='public', website=True, multilang=True)

View File

@ -20,8 +20,8 @@
##############################################################################
from datetime import datetime, timedelta
import random
from urllib import urlencode
from urlparse import urljoin
import werkzeug
from openerp.addons.base.ir.ir_mail_server import MailDeliveryException
from openerp.osv import osv, fields
@ -53,7 +53,7 @@ class res_partner(osv.Model):
(not partner.signup_expiration or dt <= partner.signup_expiration)
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
the url state components (menu_id, id, view_type) """
if context is None:
@ -81,6 +81,8 @@ class res_partner(osv.Model):
continue # no signup token, no user, thus no signup url!
fragment = dict()
if action:
fragment['action'] = action
if view_type:
fragment['view_type'] = view_type
if menu_id:
@ -90,7 +92,10 @@ class res_partner(osv.Model):
if 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

View File

@ -65,8 +65,8 @@
Salesman create a mass convert wizard and convert all the leads.
-
!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"), 'no_force_assignation': False})
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)
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, 'force_assignation': True}, 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.

View File

@ -42,7 +42,7 @@ class crm_lead2opportunity_partner(osv.osv_memory):
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)}}
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
"""
@ -56,8 +56,11 @@ class crm_lead2opportunity_partner(osv.osv_memory):
partner_match_domain.append(('partner_id', '=', partner_id))
partner_match_domain = ['|'] * (len(partner_match_domain) - 1) + partner_match_domain
if not partner_match_domain:
return []
return lead_obj.search(cr, uid, partner_match_domain + final_stage_domain)
return []
domain = partner_match_domain
if not include_lost:
domain += final_stage_domain
return lead_obj.search(cr, uid, domain)
def default_get(self, cr, uid, fields, context=None):
"""
@ -75,7 +78,7 @@ class crm_lead2opportunity_partner(osv.osv_memory):
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
tomerge.extend(self._get_duplicated_leads(cr, uid, partner_id, email))
tomerge.extend(self._get_duplicated_leads(cr, uid, partner_id, email, include_lost=True, context=context))
tomerge = list(set(tomerge))
if 'action' in fields:
@ -197,8 +200,7 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
('each_exist_or_create', 'Use existing partner or create'),
('nothing', 'Do not link to a customer')
], 'Related Customer', required=True),
# Uncomment me in trunk
# 'force_assignation': fields.boolean('Force assignation', help='If unchecked, this will leave the salesman of duplicated opportunities'),
'force_assignation': fields.boolean('Force assignation', help='If unchecked, this will leave the salesman of duplicated opportunities'),
}
_defaults = {
@ -274,10 +276,7 @@ class crm_lead2opportunity_mass_convert(osv.osv_memory):
active_ids = active_ids.difference(merged_lead_ids)
active_ids = active_ids.union(remaining_lead_ids)
ctx['active_ids'] = list(active_ids)
# Remove me in trunk
ctx['no_force_assignation'] = ctx.get('no_force_assignation', True)
# Uncomment me in trunk
# ctx['no_force_assignation'] = not data.force_assignation
ctx['no_force_assignation'] = not data.force_assignation
return self.action_apply(cr, uid, ids, context=ctx)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -449,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 ?
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:
result, format = openerp.report.render_report(cr, uid, [res_id], report_service, {'model': template.model}, ctx)

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

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

View File

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

View File

@ -19,24 +19,22 @@
#
##############################################################################
from openerp.addons.web import http
from openerp.addons.web.http import request
from openerp.osv import osv
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 report_mrpbomstructure(self, docids):
ids = [int(i) for i in docids.split(',')]
ids = list(set(ids))
report_obj = request.registry['mrp.bom']
docs = report_obj.browse(request.cr, request.uid, ids, context=request.context)
def render_html(self, cr, uid, ids, data=None, context=None):
mrpbom_obj = self.pool['mrp.bom']
report_obj = self.pool['report']
docs = mrpbom_obj.browse(cr, uid, ids, context=context)
docargs = {
'docs': docs,
'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):
result = []

View File

@ -19,5 +19,11 @@
<field name="global" eval="True" />
<field name="domain_force">[('company_id', '=', user.company_id.id)]</field>
</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>
</openerp>

View File

@ -10,7 +10,7 @@
<separator string="Select your Point of Sale" colspan="4" attrs="{'invisible' : [('show_config', '=', False)]}" />
<group attrs="{'invisible' : [('show_config', '=', False)]}">
<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"/>
<field name="pos_state" invisible="1" />
</group>

View File

@ -39,7 +39,7 @@ class mail_mail(osv.Model):
if partner and not partner.user_ids:
contex_signup = dict(context, signup_valid=True)
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]
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:

View File

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

View File

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

View File

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

View File

@ -134,7 +134,8 @@ class test_portal(TestMail):
'invite: subject of invitation email is incorrect')
self.assertIn('Administrator invited you to follow Discussion group document: Pigs', sent_email.get('body'),
'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')
def test_20_notification_url(self):

View File

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

View File

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

View File

@ -745,7 +745,7 @@ class task(osv.osv):
_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."),
'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'),
'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."),

View File

@ -446,12 +446,7 @@ class purchase_order(osv.osv):
'''
assert len(ids) == 1, 'This option should only be used for a single id at a time'
self.signal_send_rfq(cr, uid, ids)
datas = {
'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}
return self.pool['report'].get_action(cr, uid, ids, 'purchase.report_purchasequotation', context=context)
#TODO: implement messages system
def wkf_confirm_order(self, cr, uid, ids, context=None):
@ -634,10 +629,9 @@ class purchase_order(osv.osv):
'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
'date': self.date_to_datetime(cr, uid, order.date_order, context),
'partner_id': order.dest_address_id.id or order.partner_id.id,
'partner_id': order.partner_id.id,
'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
'type': 'in',
'partner_id': order.dest_address_id.id or order.partner_id.id,
'purchase_id': order.id,
'company_id': order.company_id.id,
'move_lines' : [],

View File

@ -19,459 +19,62 @@
#
##############################################################################
from openerp.osv.osv import except_osv
from openerp.addons.web import http
from openerp.tools.translate import _
from openerp.addons.web.http import request
import openerp.tools.config as config
from openerp.addons.web.http import Controller, route, request
import time
import base64
import logging
import tempfile
import lxml.html
import subprocess
import simplejson
try:
import cStringIO as StringIO
except ImportError:
import StringIO
import psutil
import signal
import os
from distutils.version import LooseVersion
import urlparse
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
_logger = logging.getLogger(__name__)
try:
from pyPdf import PdfFileWriter, PdfFileReader
except ImportError:
PdfFileWriter = PdfFileReader = None
class ReportController(Controller):
class Report(http.Controller):
@http.route(['/report/<reportname>/<docids>'], type='http', auth='user', website=True, multilang=True)
def report_html(self, reportname, docids, **kwargs):
"""This is the generic route for QWeb reports. It is used for reports
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
"""
#------------------------------------------------------
# Generic reports controller
#------------------------------------------------------
@route('/report/<reportname>/<docids>', type='http', auth='user', website=True, multilang=True)
def report_html(self, reportname, docids):
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.
# Pattern is /report/module.reportname(?a=1)
reportname_in_path = path.split('/')[1].split('?')[0]
report = self._get_report_from_name(reportname_in_path)
# Check attachment_use field. If set to true and an existing pdf is already saved, load
# 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))]
@route('/report/pdf/report/<reportname>/<docids>', type='http', auth="user", website=True)
def report_pdf(self, reportname, docids):
cr, uid, context = request.cr, request.uid, request.context
docids = self._eval_params(docids)
pdf = request.registry['report'].get_pdf(cr, uid, docids, reportname, context=context)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
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
: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 = StringIO.StringIO()
writer.write(merged)
merged.seek(0)
content = merged.read()
merged.close()
return content
@route('/report/pdf/report/<reportname>', type='http', auth='user', website=True, multilang=True)
def report_pdf_particular(self, reportname, **data):
cr, uid, context = request.cr, request.uid, request.context
report_obj = request.registry['report']
data = self._eval_params(data) # Sanitizing
pdf = report_obj.get_pdf(cr, uid, [], reportname, data=data, context=context)
pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
return request.make_response(pdf, headers=pdfhttpheaders)
@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.
Samples:
<img t-att-src="'/report/barcode/QR/%s' % o.name"/>
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' % ('QR', o.name, 200, 200)"/>
<img t-att-src="'/report/barcode/?type=%s&amp;value=%s&amp;width=%s&amp;height=%s' %
('QR', o.name, 200, 200)"/>
:param type: Accepted types: 'Codabar', 'Code11', 'Code128', 'EAN13', 'EAN8', 'Extended39',
'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')])
@http.route('/report/download', type='http', auth="user")
def report_attachment(self, data, token):
@route(['/report/download'], type='http', auth="user", website=True)
def report_download(self, data, token):
"""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]
:returns: Response with a filetoken cookie and an attachment header
"""
requestcontent = simplejson.loads(data)
url, type = requestcontent[0], requestcontent[1]
file, fileheaders = self._get_url_content(url)
if type == 'qweb-pdf':
response = self._make_pdf_response(file)
response.headers.add('Content-Disposition', 'attachment; filename=report.pdf;')
elif type == 'controller':
response = request.make_response(file)
response.headers.add('Content-Disposition', fileheaders['Content-Disposition'])
response.headers.add('Content-Type', fileheaders['Content-Type'])
reportname = url.split('/report/pdf/report/')[1].split('?')[0].split('/')[0]
if '?' not in url:
# Generic report:
docids = url.split('/')[-1]
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:
return
response.headers.add('Content-Length', len(file))
response.set_cookie('fileToken', token)
return response
@http.route('/report/check_wkhtmltopdf', type='json', auth="user")
@route(['/report/check_wkhtmltopdf'], type='json', auth="user")
def check_wkhtmltopdf(self):
"""Check the presence of wkhtmltopdf and return its version. If wkhtmltopdf
cannot be found, return False.
return request.registry['report']._check_wkhtmltopdf()
def _eval_params(self, param):
"""Parse a dict generated by the webclient (javascript) into a python dict.
"""
try:
process = subprocess.Popen(['wkhtmltopdf', '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = process.communicate()
if err:
raise
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')
return 'upgrade'
return True
except:
_logger.error('You need WKHTMLTOPDF to print a pdf version of this report.')
return False
if isinstance(param, dict):
for key, value in param.iteritems():
if value.lower() == 'false':
param[key] = False
elif value.lower() == 'true':
param[key] = True
elif ',' in value:
param[key] = [int(i) for i in value.split(',')]
else:
try:
param[key] = int(value)
except (ValueError, TypeError):
pass
else:
if isinstance(param, (str, unicode)):
param = [int(i) for i in param.split(',')]
if isinstance(param, list):
param = list(set(param))
if isinstance(param, int):
param = [param]
return param

View File

@ -19,54 +19,85 @@
#
##############################################################################
from openerp.addons.web.http import request
from openerp.osv import osv
from openerp.osv.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 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 psutil
import signal
import base64
import logging
import tempfile
import lxml.html
import cStringIO
import subprocess
from datetime import datetime
from werkzeug.datastructures import Headers
from werkzeug.wrappers import BaseResponse
from werkzeug.test import Client
from functools import partial
from distutils.version import LooseVersion
try:
from pyPdf import PdfFileWriter, PdfFileReader
except ImportError:
PdfFileWriter = PdfFileReader = None
def get_date_length(date_format=DEFAULT_SERVER_DATE_FORMAT):
return len((datetime.now()).strftime(date_format))
_logger = logging.getLogger(__name__)
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"
_description = "Report"
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
if dp:
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:
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:
res_digits = getattr(obj._columns[f], 'digits', lambda x: ((16, DEFAULT_DIGITS)))
if isinstance(res_digits, tuple):
d = res_digits[1]
else:
d = res_digits(request.cr)[1]
d = res_digits(cr)[1]
elif (hasattr(obj, '_field') and
isinstance(obj._field, (float_field, function_field)) and
obj._field.digits):
d = obj._field.digits[1] or DEFAULT_DIGITS
return d
def _get_lang_dict(self):
def _get_lang_dict(self, cr, uid):
pool_lang = self.pool['res.lang']
lang = self.localcontext.get('lang', 'en_US') or 'en_US'
lang_ids = pool_lang.search(request.cr, request.uid, [('code', '=', lang)])[0]
lang_obj = pool_lang.browse(request.cr, request.uid, lang_ids)
lang_ids = pool_lang.search(cr, uid, [('code', '=', lang)])[0]
lang_obj = pool_lang.browse(cr, uid, lang_ids)
lang_dict = {
'lang_obj': lang_obj,
'date_format': lang_obj.date_format,
@ -76,7 +107,7 @@ class report(osv.Model):
self.default_lang[lang] = self.lang_dict.copy()
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:
formatLang(value) -> digits=2 (default)
@ -84,17 +115,20 @@ class report(osv.Model):
formatLang(value, dp='Account') -> digits=3
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 dp:
digits = self.get_digits(dp=dp)
digits = self._get_digits(cr, uid, dp=dp)
else:
digits = self.get_digits(value)
digits = self._get_digits(cr, uid, value)
if isinstance(value, (str, unicode)) and not value:
return ''
if not self.lang_dict_called:
self._get_lang_dict()
self._get_lang_dict(cr, uid)
self.lang_dict_called = True
if date or date_time:
@ -117,9 +151,7 @@ class report(osv.Model):
date = datetime(*value.timetuple()[:6])
if date_time:
# Convert datetime values to the expected client/context timezone
date = datetime_field.context_timestamp(request.cr, request.uid,
timestamp=date,
context=self.localcontext)
date = datetime_field.context_timestamp(cr, uid, timestamp=date, context=self.localcontext)
return date.strftime(date_format.encode('utf-8'))
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'),
'uid': context.get('uid'),
}
self._get_lang_dict()
self._get_lang_dict(cr, uid)
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)
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({
'time': time,
'user': current_user,
'user_id': current_user.id,
'formatLang': self.formatLang,
'get_digits': self.get_digits,
'formatLang': partial(self.formatLang, cr=cr, uid=uid),
'get_digits': self._get_digits,
'render_doc': render_doc,
'website': website,
'res_company': res_company,
'editable': True, # Will active inherit_branding
'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)
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)
reqheaders = Headers(request.httprequest.headers)
reqheaders.pop('Accept')
reqheaders.add('Accept', 'application/pdf')
reqheaders.pop('Content-Type')
reqheaders.add('Content-Type', 'text/plain')
response = Client(request.httprequest.app, BaseResponse).get(url, headers=reqheaders,
follow_redirects=True)
return response.data
# If the report is using a custom model to render its html, we must use it.
# Otherwise, fallback on the generic html rendering.
try:
report_model_name = 'report.%s' % report_name
particularreport_obj = self.pool[report_model_name]
return particularreport_obj.render_html(cr, uid, ids, data={'form': data}, context=context)
except KeyError:
report = self._get_report_from_name(cr, uid, report_name)
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):
"""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
"""
# TODO: return the action for the ids passed in args
if context is None:
context = {}
@ -249,26 +410,203 @@ class report(osv.Model):
return action
def eval_params(self, dict_param):
"""Parse a dictionary generated by the webclient (javascript) into a dictionary
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
#--------------------------------------------------------------------------
# Report generation helpers
#--------------------------------------------------------------------------
data = {}
data['form'] = dict_param
return data
def _check_wkhtmltopdf(self):
return wkhtmltopdf_state
def _generate_wkhtml_pdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None):
"""Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf
document.
:param header: list of string containing the headers
:param footer: list of string containing the footers
:param bodies: list of string containing the reports
:param landscape: boolean to force the pdf to be rendered under a landscape format
:param paperformat: ir.actions.report.paperformat to generate the wkhtmltopf arguments
:param specific_paperformat_args: dict of prioritized paperformat arguments
:param save_in_attachment: dict of reports to save/load in/from the db
:returns: Content of the pdf as a string
"""
command = ['wkhtmltopdf']
command_args = []
tmp_dir = tempfile.gettempdir()
# Passing the cookie to wkhtmltopdf in order to resolve URL.
try:
from openerp.addons.web.http import request
command_args.extend(['--cookie', 'session_id', request.session.sid])
except AttributeError:
pass
# Display arguments
if paperformat:
command_args.extend(self._build_wkhtmltopdf_args(paperformat, spec_paperformat_args))
if landscape and '--orientation' in command_args:
command_args_copy = list(command_args)
for index, elem in enumerate(command_args_copy):
if elem == '--orientation':
del command_args[index]
del command_args[index]
command_args.extend(['--orientation', 'landscape'])
elif landscape and not '--orientation' in command_args:
command_args.extend(['--orientation', 'landscape'])
pdfdocuments = []
# HTML to PDF thanks to WKhtmltopdf
for index, reporthtml in enumerate(bodies):
command_arg_local = []
pdfreport = tempfile.NamedTemporaryFile(suffix='.pdf', prefix='report.tmp.',
mode='w+b')
# Directly load the document if we have it
if save_in_attachment and save_in_attachment['loaded_documents'].get(reporthtml[0]):
pdfreport.write(save_in_attachment['loaded_documents'].get(reporthtml[0]))
pdfreport.flush()
pdfdocuments.append(pdfreport)
continue
# Header stuff
if headers:
head_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.header.tmp.',
dir=tmp_dir, mode='w+')
head_file.write(headers[index])
head_file.flush()
command_arg_local.extend(['--header-html', head_file.name])
# Footer stuff
if footers:
foot_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.footer.tmp.',
dir=tmp_dir, mode='w+')
foot_file.write(footers[index])
foot_file.flush()
command_arg_local.extend(['--footer-html', foot_file.name])
# Body stuff
content_file = tempfile.NamedTemporaryFile(suffix='.html', prefix='report.body.tmp.',
dir=tmp_dir, mode='w+')
content_file.write(reporthtml[1])
content_file.flush()
try:
# If the server is running with only one worker, ask to create a secund to be able
# to serve the http request of wkhtmltopdf subprocess.
if config['workers'] == 1:
ppid = psutil.Process(os.getpid()).ppid
os.kill(ppid, signal.SIGTTIN)
wkhtmltopdf = command + command_args + command_arg_local
wkhtmltopdf += [content_file.name] + [pdfreport.name]
process = subprocess.Popen(wkhtmltopdf, stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
out, err = process.communicate()
if config['workers'] == 1:
os.kill(ppid, signal.SIGTTOU)
if process.returncode != 0:
raise osv.except_osv(_('Report (PDF)'),
_('wkhtmltopdf failed with error code = %s. '
'Message: %s') % (str(process.returncode), err))
# Save the pdf in attachment if marked
if reporthtml[0] is not False and save_in_attachment.get(reporthtml[0]):
attachment = {
'name': save_in_attachment.get(reporthtml[0]),
'datas': base64.encodestring(pdfreport.read()),
'datas_fname': save_in_attachment.get(reporthtml[0]),
'res_model': save_in_attachment.get('model'),
'res_id': reporthtml[0],
}
self.pool['ir.attachment'].create(cr, uid, attachment)
_logger.info('The PDF document %s is now saved in the '
'database' % attachment['name'])
pdfreport.flush()
pdfdocuments.append(pdfreport)
if headers:
head_file.close()
if footers:
foot_file.close()
except:
raise
# Get and return the full pdf
if len(pdfdocuments) == 1:
content = pdfdocuments[0].read()
pdfdocuments[0].close()
else:
content = self._merge_pdf(pdfdocuments)
return content
def _get_report_from_name(self, cr, uid, report_name):
"""Get the first record of ir.actions.report.xml having the ``report_name`` as value for
the field report_name.
"""
report_obj = self.pool['ir.actions.report.xml']
qwebtypes = ['qweb-pdf', 'qweb-html']
conditions = [('report_type', 'in', qwebtypes), ('report_name', '=', report_name)]
idreport = report_obj.search(cr, uid, conditions)[0]
return report_obj.browse(cr, uid, idreport)
def _build_wkhtmltopdf_args(self, paperformat, specific_paperformat_args=None):
"""Build arguments understandable by wkhtmltopdf from a report.paperformat record.
:paperformat: report.paperformat record
:specific_paperformat_args: a dict containing prioritized wkhtmltopdf arguments
:returns: list of string representing the wkhtmltopdf arguments
"""
command_args = []
if paperformat.format and paperformat.format != 'custom':
command_args.extend(['--page-size', paperformat.format])
if paperformat.page_height and paperformat.page_width and paperformat.format == 'custom':
command_args.extend(['--page-width', str(paperformat.page_width) + 'in'])
command_args.extend(['--page-height', str(paperformat.page_height) + 'in'])
if specific_paperformat_args and specific_paperformat_args['data-report-margin-top']:
command_args.extend(['--margin-top',
str(specific_paperformat_args['data-report-margin-top'])])
elif paperformat.margin_top:
command_args.extend(['--margin-top', str(paperformat.margin_top)])
if paperformat.margin_left:
command_args.extend(['--margin-left', str(paperformat.margin_left)])
if paperformat.margin_bottom:
command_args.extend(['--margin-bottom', str(paperformat.margin_bottom)])
if paperformat.margin_right:
command_args.extend(['--margin-right', str(paperformat.margin_right)])
if paperformat.orientation:
command_args.extend(['--orientation', str(paperformat.orientation)])
if paperformat.header_spacing:
command_args.extend(['--header-spacing', str(paperformat.header_spacing)])
if paperformat.header_line:
command_args.extend(['--header-line'])
if paperformat.dpi:
command_args.extend(['--dpi', str(paperformat.dpi)])
return command_args
def _merge_pdf(self, documents):
"""Merge PDF files into one.
:param documents: list of pdf files
:returns: string containing the merged pdf
"""
writer = PdfFileWriter()
for document in documents:
reader = PdfFileReader(file(document.name, "rb"))
for page in range(0, reader.getNumPages()):
writer.addPage(reader.getPage(page))
document.close()
merged = cStringIO.StringIO()
writer.write(merged)
merged.seek(0)
content = merged.read()
merged.close()
return content

View File

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

View File

@ -1,26 +1,36 @@
# -*- coding: utf-8 -*-
import logging
import logging
import openerp
import urllib2
_logger = logging.getLogger(__name__)
class TestReports(openerp.tests.HttpCase):
@openerp.tests.common.at_install(False)
@openerp.tests.common.post_install(True)
class TestReports(openerp.tests.TransactionCase):
def test_reports(self):
return # commented out until post_install tests are working
registry, cr, uid = self.registry, self.cr, self.uid
r_model = registry('ir.actions.report.xml')
domain = [('report_type','like','qweb')]
domain = [('report_type', 'like', 'qweb')]
for r in r_model.browse(cr, uid, r_model.search(cr, uid, domain)):
report_model = 'report.%s' % r.report_name
particular_model = registry('ir.model').search(cr, uid, [('model', '=', report_model)])
# Only test the generic reports here
if particular_model:
continue
_logger.info("testing report %s", r.report_name)
report_model = registry(r.model)
report_model_ids = report_model.search(cr, uid, [], limit=10)
if not report_model_ids:
_logger.info("no record found skipping report %s", r.report_name)
continue
if not r.multi:
report_model_ids = report_model_ids[:1]
url = "/report/%s/%s" % (r.report_name, ','.join(str(i) for i in report_model_ids))
_logger.info("testing report %s", url)
# TODO sle: uncomment this
#content = self.url_open(url)
# Test report generation
registry('report').get_html(cr, uid, report_model_ids, r.report_name)

View File

@ -4,9 +4,7 @@
<template id="html_container">
&lt;!DOCTYPE html&gt;
<html t-att-lang="lang and lang.replace('_', '-')"
t-att-data-website-id="website.id if editable else None"
t-att-data-editable="'1' if editable else None"
t-att-data-translatable="'1' if translatable else None"
t-att-data-view-xmlid="xmlid if editable else None"
t-att-data-main-object="repr(main_object) if editable else None">
<head>

View File

@ -85,6 +85,7 @@
class="oe_link"
string="Search associated QWeb views"
name="associated_view"
attrs="{'invisible':[('report_type', 'not in', ['qweb-pdf', 'qweb-html'])]}"
/>
</xpath>
</data>

View File

@ -1,3 +1,4 @@
<!DOCTYPE html>
<html>
<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type"/>

View File

@ -112,6 +112,7 @@ def webkit_report_extender(report_name):
return fct
return fct1
class WebKitParser(report_sxw):
"""Custom class that use webkit to render HTML reports
Code partially taken from report openoffice. Thanks guys :)
@ -173,7 +174,7 @@ class WebKitParser(report_sxw):
),
'w'
)
head_file.write(header.encode('utf-8'))
head_file.write(self._sanitize_html(header.encode('utf-8')))
head_file.close()
file_to_del.append(head_file.name)
command.extend(['--header-html', head_file.name])
@ -184,7 +185,7 @@ class WebKitParser(report_sxw):
),
'w'
)
foot_file.write(footer.encode('utf-8'))
foot_file.write(self._sanitize_html(footer.encode('utf-8')))
foot_file.close()
file_to_del.append(foot_file.name)
command.extend(['--footer-html', foot_file.name])
@ -205,7 +206,7 @@ class WebKitParser(report_sxw):
for html in html_list :
html_file = file(os.path.join(tmp_dir, str(time.time()) + str(count) +'.body.html'), 'w')
count += 1
html_file.write(html.encode('utf-8'))
html_file.write(self._sanitize_html(html.encode('utf-8')))
html_file.close()
file_to_del.append(html_file.name)
command.append(html_file.name)
@ -366,7 +367,6 @@ class WebKitParser(report_sxw):
pdf = self.generate_pdf(bin, report_xml, head, foot, htmls)
return (pdf, 'pdf')
def create(self, cursor, uid, ids, data, context=None):
"""We override the create function in order to handle generator
Code taken from report openoffice. Thanks guys :) """
@ -387,11 +387,18 @@ class WebKitParser(report_sxw):
report_xml.report_sxw = None
else:
return super(WebKitParser, self).create(cursor, uid, ids, data, context)
if report_xml.report_type != 'webkit' :
if report_xml.report_type != 'webkit':
return super(WebKitParser, self).create(cursor, uid, ids, data, context)
result = self.create_source_pdf(cursor, uid, ids, data, report_xml, context)
if not result:
return (False,False)
return result
def _sanitize_html(self, html):
"""wkhtmltopdf expects the html page to declare a doctype.
"""
if html and html[:9].upper() != "<!DOCTYPE":
html = "<!DOCTYPE html>\n" + html
return html
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -55,11 +55,12 @@ class sale_make_invoice(osv.osv_memory):
raise osv.except_osv(_('Warning!'), _("You shouldn't manually invoice the following sale order %s") % (sale_order.name))
order_obj.action_invoice_create(cr, uid, context.get(('active_ids'), []), data['grouped'], date_invoice=data['invoice_date'])
for o in order_obj.browse(cr, uid, context.get(('active_ids'), []), context=context):
orders = order_obj.browse(cr, uid, context.get(('active_ids'), []), context=context)
for o in orders:
for i in o.invoice_ids:
newinv.append(i.id)
# Dummy call to workflow, will not create another invoice but bind the new invoice to the subflow
order_obj.signal_manual_invoice(cr, uid, [o.id for o in orders if o.order_policy == 'manual'])
result = mod_obj.get_object_reference(cr, uid, 'account', 'action_invoice_tree1')
id = result and result[1] or False
result = act_obj.read(cr, uid, [id], context=context)[0]

View File

@ -38,9 +38,25 @@ class res_groups(osv.osv):
class res_users(osv.osv):
_name = 'res.users'
_inherit = 'res.users'
def _is_share(self, cr, uid, ids, name, args, context=None):
res = {}
for user in self.browse(cr, uid, ids, context=context):
res[user.id] = not self.has_group(cr, user.id, 'base.group_user')
return res
def _get_users_from_group(self, cr, uid, ids, context=None):
result = set()
for group in self.pool['res.groups'].browse(cr, uid, ids, context=context):
result.update(user.id for user in group.users)
return list(result)
_columns = {
'share': fields.boolean('Share User', readonly=True,
help="External user with limited access, created only for the purpose of sharing data.")
'share': fields.function(_is_share, string='Share User', type='boolean',
store={
'res.users': (lambda self, cr, uid, ids, c={}: ids, None, 50),
'res.groups': (_get_users_from_group, None, 50),
}, help="External user with limited access, created only for the purpose of sharing data."),
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -3,7 +3,6 @@
<data noupdate="1">
<record id="base.public_user" model="res.users">
<field eval="[(4, ref('group_share_user'))]" name="groups_id"/>
<field name="share" eval="True" />
</record>
</data>
</openerp>

View File

@ -258,7 +258,6 @@ class share_wizard(osv.TransientModel):
'name': new_user,
'email': new_user,
'groups_id': [(6,0,[group_id])],
'share': True,
'company_id': current_user.company_id.id,
'company_ids': [(6, 0, [current_user.company_id.id])],
}, context)
@ -276,7 +275,6 @@ class share_wizard(osv.TransientModel):
'password': new_pass,
'name': new_login,
'groups_id': [(6,0,[group_id])],
'share': True,
'company_id': current_user.company_id.id,
'company_ids': [(6, 0, [current_user.company_id.id])],
}, context)

View File

@ -33,6 +33,10 @@ class stock_change_product_qty(osv.osv_memory):
'prodlot_id': fields.many2one('stock.production.lot', 'Serial Number', domain="[('product_id','=',product_id)]"),
'location_id': fields.many2one('stock.location', 'Location', required=True, domain="[('usage', '=', 'internal')]"),
}
_defaults = {
'new_quantity': 1,
'product_id': lambda self, cr, uid, ctx: ctx and ctx.get('active_id', False) or False
}
def fields_view_get(self, cr, uid, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
if context is None: context = {}
@ -54,20 +58,22 @@ class stock_change_product_qty(osv.osv_memory):
@param context: A standard dictionary
@return: A dictionary which of fields with values.
"""
product_id = context and context.get('active_id', False) or False
res = super(stock_change_product_qty, self).default_get(cr, uid, fields, context=context)
if 'new_quantity' in fields:
res.update({'new_quantity': 1})
if 'product_id' in fields:
res.update({'product_id': product_id})
if 'location_id' in fields:
try:
model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
except (orm.except_orm, ValueError):
location_id = False
res.update({'location_id': location_id})
location_id = res.get('location_id', False)
if not location_id:
try:
model, location_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', 'stock_location_stock')
except (orm.except_orm, ValueError):
pass
if location_id:
try:
self.pool.get('stock.location').check_access_rule(cr, uid, [location_id], 'read', context=context)
except (orm.except_orm, ValueError):
pass
res['location_id'] = location_id
return res
def change_product_qty(self, cr, uid, ids, context=None):

View File

@ -67,6 +67,7 @@
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'form_view_ref': False}</field>
</record>
<record id="view_split_in_lots" model="ir.ui.view">
@ -124,6 +125,7 @@
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{'form_view_ref': False}</field>
</record>
</data>

View File

@ -37,7 +37,7 @@ e.g. To have an invoice generated automatically periodically:
above. Specify the interval information and partner to be invoice.
""",
'author': 'OpenERP SA',
'depends': [],
'depends': ['base'],
'data': ['security/subcription_security.xml', 'security/ir.model.access.csv', 'subscription_view.xml'],
'demo': ['subscription_demo.xml',],
'installable': True,

View File

@ -128,23 +128,14 @@ Thanks,''') % (name, self.pool.get('ir.config_parameter').get_param(cr, uid, 'we
for use in exist_user:
new_user.append(use.id)
for id in survey_ref.browse(cr, uid, survey_ids):
report = self.create_report(cr, uid, [id.id], 'report.survey.form', id.title)
file = open(get_module_resource('survey', 'report') + id.title +".pdf")
file_data = ""
while 1:
line = file.readline()
file_data += line
if not line:
break
file.close()
attachments[id.title +".pdf"] = file_data
os.remove(get_module_resource('survey', 'report') + id.title +".pdf")
result, format = openerp.report.render_report(cr, uid, [id.id], 'survey.form', {}, {})
attachments[id.title +".pdf"] = result
for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids):
if not partner.email:
skipped+= 1
continue
user = user_ref.search(cr, uid, [('login', "=", partner.email)])
user = user_ref.search(cr, uid, [('partner_id', "=", partner.id)])
if user:
if user[0] not in new_user:
new_user.append(user[0])
@ -185,6 +176,8 @@ Thanks,''') % (name, self.pool.get('ir.config_parameter').get_param(cr, uid, 'we
if ans:
res_data = {'name': partner.name or _('Unknown'),
'login': partner.email,
'email': partner.email,
'partner_id': partner.id,
'password': passwd,
'address_id': partner.id,
'groups_id': [[6, 0, [group_id]]],

View File

@ -91,7 +91,12 @@ class ir_http(orm.AbstractModel):
assert path is not None
except Exception:
return self._handle_exception(werkzeug.exceptions.NotFound())
if path != request.httprequest.path:
generated_path = werkzeug.url_unquote_plus(path)
current_path = werkzeug.url_unquote_plus(request.httprequest.path)
if generated_path != current_path:
if request.lang != request.website.default_lang_code:
path = '/' + request.lang + path
return werkzeug.utils.redirect(path)
def _handle_exception(self, exception=None, code=500):

View File

@ -381,42 +381,8 @@ class RelativeDatetime(orm.AbstractModel):
class Contact(orm.AbstractModel):
_name = 'website.qweb.field.contact'
_inherit = ['website.qweb.field', 'website.qweb.field.many2one']
_inherit = ['ir.qweb.field.contact', 'website.qweb.field.many2one']
def from_html(self, cr, uid, model, column, element, context=None):
# FIXME: this behavior is really weird, what if the user wanted to edit the name of the related thingy? Should m2os really be editable without a widget?
divs = element.xpath(".//div")
for div in divs:
if div != divs[0]:
div.getparent().remove(div)
return super(Contact, self).from_html(cr, uid, model, column, element, context=context)
def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
opf = options.get('fields') or ["name", "address", "phone", "mobile", "fax", "email"]
if not getattr(record, field_name):
return None
id = getattr(record, field_name).id
field_browse = self.pool[column._obj].browse(cr, openerp.SUPERUSER_ID, id, context={"show_address": True})
value = werkzeug.utils.escape( field_browse.name_get()[0][1] )
val = {
'name': value.split("\n")[0],
'address': werkzeug.utils.escape("\n".join(value.split("\n")[1:])),
'phone': field_browse.phone,
'mobile': field_browse.mobile,
'fax': field_browse.fax,
'city': field_browse.city,
'country_id': field_browse.country_id and field_browse.country_id.name_get()[0][1],
'email': field_browse.email,
'fields': opf,
'options': options
}
html = self.pool["ir.ui.view"].render(cr, uid, "website.contact", val, engine='website.qweb', context=context).decode('utf8')
return ir_qweb.HTMLSafe(html)
def html_to_text(element):
""" Converts HTML content with HTML-specified line breaks (br, p, div, ...)

View File

@ -168,20 +168,9 @@ class view(osv.osv):
arch_no_whitespace = etree.fromstring(
etree.tostring(arch, encoding='utf-8'),
parser=etree.XMLParser(encoding='utf-8', remove_blank_text=True))
arch_pretty_indent_2 = etree.tostring(
return etree.tostring(
arch_no_whitespace, encoding='unicode', pretty_print=True)
# pretty_print uses a fixed indent level of 2, we want an indent of 4,
# double up leading spaces.
def repl(m):
indent = len(m.group(0)) / 2
return u' ' * 4 * indent
# FIXME: If py2.7 only, can use re.M in sub and don't have to do replacement line by line
return u'\n'.join(
re.sub(ur'^((?: )+)', repl, line)
for line in arch_pretty_indent_2.split(u'\n')
)
def save(self, cr, uid, res_id, value, xpath=None, context=None):
""" Update a view section. The view section may embed fields to write

View File

@ -1789,6 +1789,9 @@
if (node.nodeName === 'BR' && node.getAttribute('type') === '_moz') {
// <br type="_moz"> appears when focusing RTE in FF, ignore
continue;
} else if (node.nodeName === 'DIV' && $(node).hasClass('oe_drop_zone')) {
// ignore dropzone inserted by snippets
continue
}
}

View File

@ -1,6 +1,136 @@
(function () {
'use strict';
/* Building block / Snippet Editor
The building blocks appear in the edit bar website. These prebuilt html block
allowing the designer to easily generate content on a page (drag and drop).
Options allow snippets to add customizations part html code according to their
selector (jQuery) and javascript object.
How to create content?
Designers can add their own html block in the "snippets" (/website/views/snippets.xml).
The block must be added in one of four menus (structure, content, feature or effect).
Structure:
<div>
<div class="oe_snippet_thumbnail">
<img class="oe_snippet_thumbnail_img" src="...image src..."/>
<span class="oe_snippet_thumbnail_title">...Block Name...</span>
</div>
<div class="oe_snippet_body">
...
<!--
The block with class 'oe_snippet_body' is inserted in the page.
This class is removed when the block is dropped.
The block can be made of any html tag and content. -->
</div>
</div>
How to create options?
Designers can add their own html block in the "snippet_options" (/website/views/snippets.xml).
Structure:
<div data-snippet-option-id='...' <!-- Required: javascript object id (but javascript
for this option object is not required) -->
data-selector="..." <!-- Required: jQuery selector.
Apply options on all The part of html who
match with this jQuery selector.
E.g.: If the selector is div, all div will be selected
and can be highlighted and assigned an editor. -->
data-selector-siblings="..." <!-- Optional: jQuery selector.
The html part can be insert or move beside
the selected html block -->
data-selector-children="..." <!-- Optional: jQuery selector.
The html part can be insert or move inside
the selected html block -->
data-selector-vertical-children='...'> <!-- Optional: jQuery selector.
The html part can be insert or move inside
the selected html block. The drop zone is
displayed vertically -->
...
<li><a href="#">...</a></li> <!-- Optional: html li list.
List of menu items displayed in customize
menu. If the li tag have 'data-class', the
class is automaticcally added or removed to
the html content when the user select this item. -->
...
<li class="dropdown-submenu" <!-- Optional: html li list exemple. !-->
data-required="true"> <!-- Optional: if only one item can be selected
and can't be unselect. !-->
<a tabindex="-1" href="#">...</a> <!-- bootstrap dropdown button !-->
<ul class="dropdown-menu">
<li data-value="text_only"><a>...</a></li> <!-- by default data-value is apply
like a class to html block !-->
</ul>
</li>
</div>
How to create a javascript object for an options?
openerp.website.snippet.options["...option-id..."] = website.snippet.Option.extend({
// start is called when the user click into a block or when the user drop a block
// into the page (just after the init method).
// start is usually used to bind event.
//
// this.$target: block html inserted inside the page
// this.$el: html li list of this options
// this.$overlay: html editor overlay who content resize bar, customize menu...
start: function () {},
// onFocus is called when the user click inside the block inserted in page
// and when the user drop on block into the page
onFocus : function () {},
// onBlur is called when the user click outside the block inserted in page, if
// the block is focused
onBlur : function () {},
// on_clone is called when the snippet is duplicate
// @variables: $clone is allready inserted is the page
on_clone: function ($clone) {},
// on_remove is called when the snippet is removed (dom is removing after this tigger)
on_remove: function () {},
// drop_and_build_snippet is called just after that a thumbnail is drag and dropped
// into a drop zone. The content is already inserted in the page.
drop_and_build_snippet: function () {},
// select is called when a user select an item in the li list of options
// By default, if the li item have a data-value attribute, the data-vlue it's apply
// like a class to the html block (this.$target)
// @variables: next_previous = {$next, $prev}
// $next = next item selected or false
// $prev = previous item selected or false
select: function (event, next_previous) {}
// preview is called when a user is on mouse over or mouse out of an item
// variables: next_previous = {$next, $prev}
// $next = next item selected or false
// $prev = previous item selected or false
preview: function (event, next_previous) {}
// clean_for_save
// clean_for_save is called just before to save the vue
// Sometime it's important to remove or add some datas (contentEditable, added
// classes to a running animation...)
clean_for_save: function () {}
});
// 'snippet-dropped' is triggered on '#oe_snippets' whith $target as attribute when a snippet is dropped
// 'snippet-activated' is triggered on '#oe_snippets' (and on snippet) when a snippet is activated
*/
var website = openerp.website;
website.add_template_file('/website/static/src/xml/website.snippets.xml');
@ -19,23 +149,11 @@
edit: function () {
var self = this;
$("[data-oe-model] *, [data-oe-type=html] *").off('click');
website.snippet.stop_animation();
window.snippets = this.snippets = new website.snippet.BuildingBlock(this);
this.snippets.appendTo(this.$el);
this.on('rte:ready', this, function () {
self.snippets.$button.removeClass("hidden");
website.snippet.start_animation();
$(website.snippet.readyAnimation).each(function() {
var animation = $(this).data("snippet-view");
if (animation) {
animation.$target.on('focus', '*', function(){
animation.stop();
});
animation.$target.on('blur', '*', function(){
animation.start();
});
}
});
website.snippet.stop_animation();
});
return this._super.apply(this, arguments);
@ -56,9 +174,6 @@
}
});
// 'snippet-dropped' is triggered on '#oe_snippets' whith $target as attribute when a snippet is dropped
// 'snippet-activated' is triggered on '#oe_snippets' (and on snippet) when a snippet is activated
if (!website.snippet) website.snippet = {};
website.snippet.templateOptions = {};
website.snippet.globalSelector = "";
@ -397,18 +512,17 @@
$("#oe_snippets").trigger('snippet-dropped', $target);
website.snippet.start_animation(true, $target);
// drop_and_build_snippet
self.create_overlay($target);
if ($target.data("snippet-editor")) {
$target.data("snippet-editor").drop_and_build_snippet($target);
$target.data("snippet-editor").drop_and_build_snippet();
}
for (var k in website.snippet.templateOptions) {
$target.find(website.snippet.templateOptions[k].selector).each(function () {
var $snippet = $(this);
self.create_overlay($snippet);
if ($snippet.data("snippet-editor")) {
$snippet.data("snippet-editor").drop_and_build_snippet($snippet);
$snippet.data("snippet-editor").drop_and_build_snippet();
}
});
}
@ -665,80 +779,12 @@
self.set_active();
self.$target.trigger("snippet-style-change", [self, np]);
},0);
this.select(event, {'$next': $next, '$prev': $prev});
this.select({'$next': $next, '$prev': $prev});
} else {
setTimeout(function () {
self.$target.trigger("snippet-style-preview", [self, np]);
},0);
this.preview(event, np);
}
},
// start is call just after the init
start: function () {
},
/* onFocus
* This method is called when the user click inside the snippet in the dom
*/
onFocus : function () {
},
/* onFocus
* This method is called when the user click outside the snippet in the dom, after a focus
*/
onBlur : function () {
},
/* on_clone
* This method is called when the snippet is cloned ($clone is allready inserted)
*/
on_clone: function ($clone) {
},
/* on_remove
* This method is called when the snippet is removed (dom is removing after this call)
*/
on_remove: function () {
},
/*
* drop_and_build_snippet
* This method is called just after that a thumbnail is drag and dropped into a drop zone
* (after the insertion of this.$body, if this.$body exists)
*/
drop_and_build_snippet: function ($target) {
},
/* select
* called when a user select an item
* li must have data-value attribute
* variables: np = {$next, $prev}
* $next is false if they are no next item selected
* $prev is false if they are no previous item selected
*/
select: function (event, np) {
var self = this;
// add or remove html class
if (np.$prev) {
this.$target.removeClass(np.$prev.data('value' || ""));
}
if (np.$next) {
this.$target.addClass(np.$next.data('value') || "");
}
},
/* preview
* called when a user is on mouse over or mouse out of an item
* variables: np = {$next, $prev}
* $next is false if they are no next item selected
* $prev is false if they are no previous item selected
*/
preview: function (event, np) {
var self = this;
// add or remove html class
if (np.$prev) {
this.$target.removeClass(np.$prev.data('value') || "");
}
if (np.$next) {
this.$target.addClass(np.$next.data('value') || "");
this.preview(np);
}
},
/* set_active
@ -757,9 +803,48 @@
.addClass("active");
this.$el.find('li:has(li[data-value].active)').addClass("active");
},
/* clean_for_save
* function called just before save vue
*/
start: function () {
},
onFocus : function () {
},
onBlur : function () {
},
on_clone: function ($clone) {
},
on_remove: function () {
},
drop_and_build_snippet: function () {
},
select: function (np) {
var self = this;
// add or remove html class
if (np.$prev && this.required) {
this.$target.removeClass(np.$prev.data('value' || ""));
}
if (np.$next) {
this.$target.addClass(np.$next.data('value') || "");
}
},
preview: function (np) {
var self = this;
// add or remove html class
if (np.$prev && this.required) {
this.$target.removeClass(np.$prev.data('value') || "");
}
if (np.$next) {
this.$target.addClass(np.$next.data('value') || "");
}
},
clean_for_save: function () {
}
});
@ -776,9 +861,9 @@
var src = this._get_bg();
this.$el.find("li[data-value].active.oe_custom_bg").data("src", src);
},
select: function(event, np) {
select: function(np) {
var self = this;
this._super(event, np);
this._super(np);
if (np.$next) {
if (np.$next.hasClass("oe_custom_bg")) {
var editor = new website.editor.ImageDialog();
@ -803,8 +888,8 @@
this.$target.removeClass(np.$prev.data("value"));
}
},
preview: function (event, np) {
this._super(event, np);
preview: function (np) {
this._super(np);
if (np.$next) {
this._set_bg(np.$next.data("src"));
}
@ -872,6 +957,7 @@
start : function () {
var self = this;
this._super();
this.$target.carousel({interval: false});
this.id = this.$target.attr("id");
this.$inner = this.$target.find('.carousel-inner');
this.$indicators = this.$target.find('.carousel-indicators');
@ -1539,9 +1625,9 @@
* This method is called just after that a thumbnail is drag and dropped into a drop zone
* (after the insertion of this.$body, if this.$body exists)
*/
drop_and_build_snippet: function ($target) {
drop_and_build_snippet: function () {
for (var i in this.styles){
this.styles[i].drop_and_build_snippet($target);
this.styles[i].drop_and_build_snippet();
}
},

View File

@ -923,7 +923,7 @@
</div>
<div data-snippet-option-id='carousel-style'
data-selector="div[data-snippet-id='carousel']">
data-selector=".carousel:not(.quotecarousel)">
<li class="dropdown-submenu" data-required="true">
<a tabindex="-1" href="#">Layout</a>
<ul class="dropdown-menu">

View File

@ -757,23 +757,5 @@ Sitemap: <t t-esc="url_root"/>sitemap.xml
</t>
</template>
<template id="contact">
<address t-ignore="true" class="mb0" itemscope="itemscope" itemtype="http://schema.org/Organization">
<div t-att-class="'name' not in fields and 'css_non_editable_mode_hidden'"><span itemprop="name" t-esc="name"/></div>
<div itemprop="address" itemscope="itemscope" itemtype="http://schema.org/PostalAddress">
<div t-if="address and 'address' in fields" class='css_editable_mode_hidden'>
<i t-if="not options.get('no_marker')" class='fa fa-map-marker'/> <span itemprop="streetAddress" t-raw="address.replace('\n', options.get('no_tag_br') and ', ' or ('&lt;br/&gt;%s' % ('' if options.get('no_marker') else '&amp;nbsp; &amp;nbsp; ')))"/>
</div>
<div t-if="city and 'city' in fields" class='css_editable_mode_hidden'>
<i t-if="not options.get('no_marker')" class='fa fa-map-marker'/> <span itemprop="addressLocality" t-raw="city"/>, <span itemprop="addressCountry" t-raw="country_id"/>
</div>
<div t-if="phone and 'phone' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-phone'/> <span itemprop="telephone" t-esc="phone"/></div>
<div t-if="mobile and 'mobile' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-mobile-phone'/> <span itemprop="telephone" t-esc="mobile"/></div>
<div t-if="fax and 'fax' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-file-text-o'/> <span itemprop="faxNumber" t-esc="fax"/></div>
<div t-if="email and 'email' in fields" class='css_editable_mode_hidden'><i t-if="not options.get('no_marker')" class='fa fa-envelope'/> <span itemprop="email" t-esc="email"/></div>
</div>
</address>
</template>
</data>
</openerp>

View File

@ -1,7 +1,7 @@
<section class="oe_container">
<div class="oe_row oe_spaced">
<div class="oe_span12">
<h2 class="oe_slogan">Awesome Open Source Blogging Plateform</h2>
<h2 class="oe_slogan">Awesome Open Source Blogging Platform</h2>
<h3 class="oe_slogan">Write, Design, Promote, Engage</h3>
</div>
<div class="oe_span6">
@ -15,7 +15,7 @@
<div class="oe_span6">
<p class='oe_mt32'>
Express yourself with the OpenERP enterprise grade blogging
plateform. Write beautiful blog posts, engage with visitors,
platform. Write beautiful blog posts, engage with visitors,
translate content and moderate social streams.
</p><p>
Get your blog posts efficiently referenced in Google and

View File

@ -171,6 +171,7 @@ class event_sponsors(osv.osv):
'partner_id': fields.many2one('res.partner', 'Sponsor/Customer', required=True),
'url': fields.text('Sponsor Website'),
'sequence': fields.related('sponsor_type_id', 'sequence', string='Sequence', store=True),
'image_medium': fields.related('partner_id', 'image_medium', string='Logo')
}
def has_access_to_partner(self, cr, uid, ids, context=None):

View File

@ -15,10 +15,10 @@
<h2 class="text-center mb32">Our Sponsors</h2>
</section>
<div class="row">
<div t-attf-class="col-md-#{(len(event.sponsor_ids) > 6) and 2 or (12/ len(event.sponsor_ids))} text-center" t-foreach="event.sponsor_ids" t-as="sponsor">
<div t-attf-class="col-md-#{(len(event.sponsor_ids) > 6) and 2 or (12/ len(event.sponsor_ids))} text-center mb16" t-foreach="event.sponsor_ids" t-as="sponsor">
<t t-if="sponsor.url">
<a t-att-href="sponsor.url" style="position: relative; display: inline-block;">
<span t-field="sponsor.partner_id.image"
<span t-field="sponsor.image_medium"
t-field-options='{"widget": "image", "class": "shadow"}'/>
<div class="ribbon-wrapper">
<div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.name}"/>
@ -27,7 +27,7 @@
</t>
<t t-if="not sponsor.url">
<span style="position: relative; display: inline-block;">
<span t-field="sponsor.partner_id.image"
<span t-field="sponsor.image_medium"
t-field-options='{"widget": "image", "class": "shadow"}'/>
<div class="ribbon-wrapper">
<div t-field="sponsor.sponsor_type_id" t-attf-class="ribbon ribbon_#{sponsor.sponsor_type_id.name}"/>

View File

@ -26,43 +26,67 @@ from openerp.addons.web.http import request
class WebsiteMail(http.Controller):
def _find_or_create_partner(self, email, context=None):
# TDE TODO: FIXME: use mail_thread method
partner_obj = request.registry['res.partner']
user_obj = request.registry['res.users']
partner_ids = []
if email and email != u'false': # post contains stringified booleans
partner_ids = partner_obj.search(request.cr, SUPERUSER_ID, [("email", "=", email)], context=request.context)
if not partner_ids:
partner_ids = [partner_obj.name_create(request.cr, SUPERUSER_ID, email, request.context)[0]]
else:
partner_ids = [user_obj.browse(request.cr, request.uid, request.uid, request.context).partner_id.id]
return partner_ids
@http.route(['/website_mail/follow'], type='json', auth="public", website=True)
def website_message_subscribe(self, id=0, object=None, message_is_follower="on", email=False, **post):
cr, uid, context = request.cr, request.uid, request.context
partner_obj = request.registry['res.partner']
user_obj = request.registry['res.users']
website = request.registry['website']
_id = int(id)
_message_is_follower = message_is_follower == 'on'
_object = request.registry[object]
partner_ids = self._find_or_create_partner(email, request.context)
if _message_is_follower:
_object.check_access_rule(request.cr, request.uid, [_id], 'read', request.context)
_object.message_unsubscribe(request.cr, SUPERUSER_ID, [_id], partner_ids, context=request.context)
# search partner_id
public_id = website.get_public_user(cr, uid, context)
if uid != public_id:
partner_ids = [user_obj.browse(cr, uid, uid, context).partner_id.id]
else:
_object.check_access_rule(request.cr, request.uid, [_id], 'read', request.context)
_object.message_subscribe(request.cr, SUPERUSER_ID, [_id], partner_ids, context=request.context)
obj = _object.browse(request.cr, request.uid, _id)
follower_ids = [p.id for p in obj.message_follower_ids]
# mail_thread method
partner_ids = _object._find_partner_from_emails(
cr, SUPERUSER_ID, _id, [email], context=context, check_followers=True)
if not partner_ids or not partner_ids[0]:
partner_ids = [partner_obj.create(cr, SUPERUSER_ID, {'name': email, 'email': email}, context=context)]
return partner_ids[0] in follower_ids and 1 or 0
# add or remove follower
if _message_is_follower:
_object.check_access_rule(cr, uid, [_id], 'read', context)
_object.message_unsubscribe(cr, SUPERUSER_ID, [_id], partner_ids, context=context)
return False
else:
_object.check_access_rule(cr, uid, [_id], 'read', context)
# add partner to session
request.session['partner_id'] = partner_ids[0]
_object.message_subscribe(cr, SUPERUSER_ID, [_id], partner_ids, context=context)
return True
@http.route(['/website_mail/is_follower'], type='json', auth="public", website=True)
def call(self, model, id, **post):
email = request.registry['res.users'].browse(request.cr, request.uid, request.uid, request.context).partner_id.email
value = request.registry.get(model).read(request.cr, request.uid, [id], ['message_is_follower'], request.context)
cr, uid, context = request.cr, request.uid, request.context
partner_obj = request.registry.get('res.partner')
users_obj = request.registry.get('res.users')
obj = request.registry.get(model)
website = request.registry['website']
partner_id = None
public_id = website.get_public_user(cr, uid, context)
if uid != public_id:
partner_id = users_obj.browse(cr, SUPERUSER_ID, uid, context).partner_id
elif request.session.get('partner_id'):
partner_id = partner_obj.browse(cr, SUPERUSER_ID, request.session.get('partner_id'), context)
email = ""
is_follower = False
if partner_id:
email = partner_id and partner_id.email
is_follower = partner_id.id in [
fol.id for fol in obj.browse(cr, SUPERUSER_ID, id, context).message_follower_ids]
return {
'email': email,
'is_follower': value and value[0]['message_is_follower'] or False
'is_user': uid != public_id,
'email': email,
'is_follower': is_follower
}

View File

@ -1,5 +1,4 @@
.js_follow[data-follow='on'] .js_follow_btn ,
.js_follow[data-follow='on'] .js_follow_email,
.js_follow[data-follow='off'] .js_unfollow_btn {
display: none;
}

View File

@ -9,13 +9,14 @@
var self = this;
// set value and display button
self.$target.find("input").removeClass("hidden");
openerp.jsonRpc('/website_mail/is_follower', 'call', {
model: this.$target.data('object'),
id: +this.$target.data('id'),
}).always(function (data) {
self.$target.find('input.js_follow_email')
.val(data.email ? data.email : "")
.attr("disabled", data.email.length ? "disabled" : false);
.attr("disabled", data.is_follower && data.email.length ? "disabled" : false);
self.$target.attr("data-follow", data.is_follower ? 'on' : 'off');
self.$target.removeClass("hidden");
});
@ -47,8 +48,10 @@
'email': $email.length ? $email.val() : false,
}).then(function (follow) {
if (follow) {
self.$target.find(" > *").toggleClass("hidden");
self.$target.find(".js_follow_email, .input-group-btn").addClass("hidden");
self.$target.find(".alert").removeClass("hidden");
}
self.$target.find('input.js_follow_email').attr("disabled", follow ? "disabled" : false);
self.$target.attr("data-follow", follow ? 'on' : 'off');
});
},

View File

@ -509,8 +509,8 @@
class="js_follow_email form-control"
placeholder="your email..."/>
<span class="input-group-btn">
<a href="#" class="btn btn-default js_unfollow_btn">unsubscribe</a>
<a href="#" class="btn btn-primary js_follow_btn">subscribe</a>
<a href="#" class="btn btn-default js_unfollow_btn">Unsubscribe</a>
<a href="#" class="btn btn-primary js_follow_btn">Subscribe</a>
</span>
<div class="alert alert-success hidden">thanks for your subscription!</div>
</div>

View File

@ -59,7 +59,7 @@ class MailGroup(http.Controller):
domain.append(('parent_id','=',False))
thread_count = thread_obj.search_count(cr, uid, domain, context=context)
pager = request.website.pager(
url='/groups/%s/%s' % (group.id, mode),
url='/groups/%s/%s/' % (group.id, mode),
total=thread_count,
page=page,
step=self._thread_per_page,

View File

@ -45,13 +45,13 @@
<t t-call="website.layout">
<section class="container">
<div class="row mt8">
<div class="col-md-5">
<ol class="breadcrumb mb8">
<div class="col-md-5 mt16">
<ol class="breadcrumb">
<li><a href="/groups">Mailing Lists</a></li>
<li class="active" t-esc="group.name"/>
</ol>
</div>
<div class="col-md-5">
<div class="col-md-5 pull-right">
<t t-call="website.pager"/>
</div>
</div>

View File

@ -2,6 +2,11 @@
<openerp>
<data>
<template id="editor_head" inherit_id="report.html_container" name="Editor" groups="base.group_website_publisher">
<xpath expr="//html" position="attributes">
<attribute name="t-att-data-website-id">website.id if editable and website else None</attribute>
<attribute name="t-att-data-translatable">'1' if translatable else None</attribute>
</xpath>
<xpath expr="//body" position="attributes">
<attribute name="style">padding-top: 51px;</attribute>
</xpath>
@ -44,12 +49,15 @@
<script t-if="not translatable" type="text/javascript" src="/website/static/lib/ace/ace.js"></script>
<script type="text/javascript" src="/website/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script>
<script type="text/javascript" src="/web/static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js"></script>
<script type="text/javascript" src="/website/static/lib/bootstrap-tour/bootstrap-tour.js"></script>
<!-- mutation observers shim backed by mutation events (8 < IE < 11, Safari < 6, FF < 14, Chrome < 17) -->
<script type="text/javascript" src="/website/static/lib//jquery.mjs.nestedSortable/jquery.mjs.nestedSortable.js"></script>
<script type="text/javascript" src="/website/static/lib/MutationObservers/test/sidetable.js"></script>
<script type="text/javascript" src='/website/static/lib/nearest/jquery.nearest.js'></script>
<script type="text/javascript" src="/website/static/lib/MutationObservers/MutationObserver.js"></script>
<script type="text/javascript" src="/website/static/lib/jquery.placeholder/jquery.placeholder.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.editor.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.editor.newpage.js" groups="base.group_website_designer"></script>
<script type="text/javascript" src="/website/static/src/js/website.menu.js" groups="base.group_website_designer"></script>
@ -63,7 +71,7 @@
</xpath>
<xpath expr='//body[@class="container"]/div[@id="wrapwrap"]' position="before">
<t t-set="languages" t-value="website.get_languages()"/>
<t t-set="languages" t-value="website.get_languages() if website else list()"/>
<ul class="list-inline js_language_selector mt16" t-if="(len(languages) &gt; 1 or editable)">
<li t-foreach="languages" t-as="lg">
<a t-att-href="url_for('', lang=lg[0]) + '?' + keep_query()"

View File

@ -226,7 +226,8 @@ class Ecommerce(http.Controller):
def shop(self, category=None, page=0, filters='', search='', **post):
cr, uid, context = request.cr, request.uid, request.context
product_obj = request.registry.get('product.template')
domain = request.registry.get('website').ecommerce_get_product_domain()
base_domain = request.registry.get('website').ecommerce_get_product_domain()
domain = list(base_domain)
if search:
domain += ['|',
('name', 'ilike', search),
@ -265,9 +266,15 @@ class Ecommerce(http.Controller):
pass
category_obj = request.registry.get('product.public.category')
category_ids = category_obj.search(cr, uid, [], context=context)
category_ids = [product['public_categ_id'][0] for product in product_obj.read_group(cr, uid, base_domain, ['public_categ_id'], ['public_categ_id'], context=context) if product['public_categ_id']]
categories = category_obj.browse(cr, uid, category_ids, context=context)
categs = filter(lambda x: not x.parent_id, categories)
all_categories = set(categories)
for cat in categories:
parent = cat.parent_id
while parent:
all_categories.add(parent)
parent = parent.parent_id
categories = list(all_categories)
values = {
'products': products,
@ -282,7 +289,8 @@ class Ecommerce(http.Controller):
'pager': pager,
'styles': styles,
'category': category,
'categories': categs,
'categories': filter(lambda x: not x.parent_id, categories),
'all_categories': categories,
'Ecommerce': self, # TODO fp: Should be removed
'style_in_product': lambda style, product: style.id in [s.id for s in product.website_style_ids],
}

View File

@ -33,12 +33,14 @@
<template id="categories_recursive" name="Category list">
<li t-att-class="int(categ) == int(category or 0) and 'active' or ''">
<t t-if="categ in all_categories">
<a t-attf-href="/shop/category/#{ slug(categ) }" t-field="categ.name"></a>
<ul t-if="categ.child_id" class="nav nav-pills nav-stacked nav-hierarchy">
<t t-foreach="categ.child_id" t-as="categ">
<t t-call="website_sale.categories_recursive"/>
</t>
</ul>
</t>
</li>
</template>
@ -730,7 +732,7 @@
<div class="col-md-8 oe_mycart">
<h3 class="page-header mt16">Billing Information
<small groups="base.group_public"> or
<a class='btn btn-primary' t-if="not partner" t-attf-href="/web?redirect=#{ request.httprequest.url }">Sign in</a>
<a class='btn btn-primary' t-if="not partner" t-attf-href="/web/login?redirect=#{ request.httprequest.url }">Sign in</a>
</small>
</h3>
<div class="row">