diff options
| author | Christophe Simonis <chs@odoo.com> | 2015-08-14 18:35:33 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2015-08-15 16:57:35 +0200 |
| commit | 73e652818b812670b7d6ce3da727de1809da531c (patch) | |
| tree | fcc9f2ae92e5d1318dbddf423d330114cff68896 | |
| parent | 9979939cd160a234270426f644c3bb2fdda8b543 (diff) | |
| parent | 8d95f6dcdf6e451f32c247bcabd5ce024a158ebe (diff) | |
[MERGE] forward port of branch saas-6 up to 8d95f6d
75 files changed, 926 insertions, 242 deletions
diff --git a/addons/account_analytic_plans/account_analytic_plans_view.xml b/addons/account_analytic_plans/account_analytic_plans_view.xml index 1870b894849..d6110b6faa2 100644 --- a/addons/account_analytic_plans/account_analytic_plans_view.xml +++ b/addons/account_analytic_plans/account_analytic_plans_view.xml @@ -70,7 +70,7 @@ <group col="4"> <field name="name"/> <field name="code"/> - <field name="plan_id"/> + <field name="plan_id" required="1"/> <field name="journal_id"/> </group> <field name="account_ids"> diff --git a/addons/auth_oauth/auth_oauth_data.xml b/addons/auth_oauth/auth_oauth_data.xml index ff5187f18f1..3b71b56be1f 100644 --- a/addons/auth_oauth/auth_oauth_data.xml +++ b/addons/auth_oauth/auth_oauth_data.xml @@ -14,8 +14,8 @@ <record id="provider_facebook" model="auth.oauth.provider"> <field name="name">Facebook Graph</field> <field name="auth_endpoint">https://www.facebook.com/dialog/oauth</field> - <field name="scope"></field> - <field name="validation_endpoint">https://graph.facebook.com/me/permissions</field> + <field name="scope">public_profile,email</field> + <field name="validation_endpoint">https://graph.facebook.com/me</field> <field name="data_endpoint"></field> <field name="css_class">fa fa-facebook-square</field> <field name="body">Log in with facebook</field> diff --git a/addons/auth_oauth/res_users.py b/addons/auth_oauth/res_users.py index f2c6be389f1..cada7d51a26 100644 --- a/addons/auth_oauth/res_users.py +++ b/addons/auth_oauth/res_users.py @@ -101,7 +101,12 @@ class res_users(osv.Model): validation = self._auth_oauth_validate(cr, uid, provider, access_token) # required check if not validation.get('user_id'): - raise openerp.exceptions.AccessDenied() + # Workaround: facebook does not send 'user_id' in Open Graph Api + if validation.get('id'): + validation['user_id'] = validation['id'] + else: + raise openerp.exceptions.AccessDenied() + # retrieve and sign in user login = self._auth_oauth_signin(cr, uid, provider, validation, params, context=context) if not login: diff --git a/addons/hr_evaluation/hr_evaluation_view.xml b/addons/hr_evaluation/hr_evaluation_view.xml index a80241a15dd..c737cc29039 100644 --- a/addons/hr_evaluation/hr_evaluation_view.xml +++ b/addons/hr_evaluation/hr_evaluation_view.xml @@ -342,7 +342,6 @@ <filter string="New Mail" name="message_unread" domain="[('message_unread','=',True)]"/> <group expand="0" string="Group By"> <filter string="Interviewer" domain="[]" context="{'group_by':'user_id'}"/> - <filter string="Survey" domain="[]" context="{'group_by':'survey_id'}"/> <filter string="Status" name="group_state" domain="[]" context="{'group_by':'state'}"/> </group> </search> diff --git a/addons/hr_holidays/hr_holidays_view.xml b/addons/hr_holidays/hr_holidays_view.xml index 4272b471656..43d861d026f 100644 --- a/addons/hr_holidays/hr_holidays_view.xml +++ b/addons/hr_holidays/hr_holidays_view.xml @@ -1,3 +1,4 @@ +<?xml version='1.0' encoding='UTF-8' ?> <openerp> <data> @@ -24,6 +25,8 @@ <filter domain="[('state','in',('confirm','validate1'))]" string="To Approve" name="approve"/> <filter string="Approved Leaves" domain="[('state', '=', 'validate')]" name="validated"/> <separator/> + <filter name="active_types" string="Active Types" domain="[('holiday_status_id.active', '=', True)]" help="Filters only on allocations and requests that belong to an holiday type that is 'active' (active field is True)"/> + <separator/> <filter string="New Mail" name="message_unread" domain="[('message_unread','=',True)]"/> <separator/> <filter string="My Department Leaves" domain="[('department_id.manager_id.user_id', '=', uid)]" help="My Department Leaves"/> @@ -31,9 +34,17 @@ <separator/> <filter string="To Report in Payslip" name="gray" domain="[('payslip_status', '=', False)]" groups="base.group_hr_manager"/> <separator/> - <filter name="year" string="Year" domain="[('holiday_status_id.active','=',True)]" help="Filters only on allocations and requests that belong to an holiday type that is 'active' (active field is True)"/> <separator/> <filter string="To Do" name="todo" domain="[('payslip_status', '=', False), ('holiday_status_id.active', '=', True)]"/> + <separator/> + <filter name="year" string="Year" + domain="['|','&',('date_to', '<=', time.strftime('%%Y-12-31')), + ('date_to', '>=', time.strftime('%%Y-01-01')), + '&',('date_from', '<=', time.strftime('%%Y-12-31')), + ('date_from', '>=', time.strftime('%%Y-01-01'))]" + help="Current Year Leaves"/> + <separator/> + <filter string="My Requests" name="my_leaves" domain="[('user_id', '=', uid)]" help="My Leave Requests"/> <field name="employee_id"/> <field name="department_id"/> <field name="holiday_status_id"/> diff --git a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py index 4be9e3a1de3..7dffc8c042b 100644 --- a/addons/hr_timesheet_invoice/hr_timesheet_invoice.py +++ b/addons/hr_timesheet_invoice/hr_timesheet_invoice.py @@ -210,6 +210,7 @@ class account_analytic_line(osv.osv): def invoice_cost_create(self, cr, uid, ids, data=None, context=None): invoice_obj = self.pool.get('account.invoice') invoice_line_obj = self.pool.get('account.invoice.line') + analytic_line_obj = self.pool.get('account.analytic.line') invoices = [] if context is None: context = {} @@ -279,13 +280,15 @@ class account_analytic_line(osv.osv): analytic_line.to_invoice.id, analytic_line.account_id, analytic_line.journal_id.type) + # We want to retrieve the data in the partner language for the invoice creation + analytic_line = analytic_line_obj.browse(cr, uid , [line.id for line in analytic_line], context=invoice_context) invoice_lines_grouping.setdefault(key, []).append(analytic_line) # finally creates the invoice line for (product_id, uom, user_id, factor_id, account, journal_type), lines_to_invoice in invoice_lines_grouping.items(): curr_invoice_line = self._prepare_cost_invoice_line(cr, uid, last_invoice, product_id, uom, user_id, factor_id, account, lines_to_invoice, - journal_type, data, context=context) + journal_type, data, context=invoice_context) invoice_line_obj.create(cr, uid, curr_invoice_line, context=context) self.write(cr, uid, [l.id for l in analytic_line_ids], {'invoice_id': last_invoice}, context=context) diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index c57238bd659..083e5cd5557 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -40,6 +40,10 @@ from openerp.tools.translate import _ _logger = logging.getLogger(__name__) +# workaround https://bugs.launchpad.net/openobject-server/+bug/947231 +# related to http://bugs.python.org/issue7980 +from datetime import datetime +datetime.strptime('2012-01-01', '%Y-%m-%d') class EscposDriver(Thread): def __init__(self): @@ -146,7 +150,7 @@ class EscposDriver(Thread): _logger.warning('ESC/POS Device Disconnected: '+message) def run(self): - + printer = None if not escpos: _logger.error('ESC/POS cannot initialize, please verify system dependencies.') return @@ -192,7 +196,7 @@ class EscposDriver(Thread): errmsg = str(e) + '\n' + '-'*60+'\n' + traceback.format_exc() + '-'*60 + '\n' _logger.error(errmsg); finally: - if error: + if error: self.queue.put((timestamp, task, data)) if printer: printer.close() diff --git a/addons/hw_posbox_homepage/controllers/main.py b/addons/hw_posbox_homepage/controllers/main.py index 25e33c409fa..a021986bee7 100644 --- a/addons/hw_posbox_homepage/controllers/main.py +++ b/addons/hw_posbox_homepage/controllers/main.py @@ -36,16 +36,16 @@ index_template = """ </p> <p> For more information on how to setup the Point of Sale with - the PosBox, please refer to <a href='/hw_proxy/static/doc/manual.pdf'>the manual</a> + the PosBox, please refer to <a href='/hw_proxy/static/doc/manual.pdf'>the manual</a>. </p> <p> To see the status of the connected hardware, please refer - to the <a href='/hw_proxy/status'>hardware status page</a> + to the <a href='/hw_proxy/status'>hardware status page</a>. </p> <p> - The PosBox software installed on this posbox is <b>version 6</b>, + The PosBox software installed on this posbox is <b>version 13</b>, the posbox version number is independent from Odoo. You can upgrade - the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a> + the software on the <a href='/hw_proxy/upgrade/'>upgrade page</a>. </p> <p>For any other question, please contact the Odoo support at <a href='mailto:support@odoo.com'>support@odoo.com</a> </p> diff --git a/addons/hw_posbox_upgrade/controllers/main.py b/addons/hw_posbox_upgrade/controllers/main.py index 0947fbc8564..5ed958d020e 100644 --- a/addons/hw_posbox_upgrade/controllers/main.py +++ b/addons/hw_posbox_upgrade/controllers/main.py @@ -29,17 +29,8 @@ upgrade_template = """ $.ajax({ url:'/hw_proxy/perform_upgrade/' }).then(function(status){ - $('#upgrade').html('Upgrade Successful<br \\>Click to Restart the PosBox'); + $('#upgrade').html('Upgrade successful, restarting the posbox...'); $('#upgrade').off('click'); - $('#upgrade').click(function(){ - $.ajax({ url:'/hw_proxy/perform_restart' }) - $('#upgrade').text('Restarting'); - $('#upgrade').off('click'); - setTimeout(function(){ - window.location = '/' - },30*1000); - }); - },function(){ $('#upgrade').text('Upgrade Failed'); }); @@ -73,9 +64,11 @@ upgrade_template = """ <body> <h1>PosBox Software Upgrade</h1> <p> - This tool will help you perform an upgrade of the PosBox's software. + This tool will help you perform an upgrade of the PosBox's software over the + internet. + <p></p> However the preferred method to upgrade the posbox is to flash the sd-card with - the <a href='http://nightly.openerp.com/trunk/posbox/'>latest image</a>. The upgrade + the <a href='http://nightly.odoo.com/trunk/posbox/'>latest image</a>. The upgrade procedure is explained into to the <a href='/hw_proxy/static/doc/manual.pdf'>PosBox manual</a> </p> <p> @@ -93,7 +86,6 @@ class PosboxUpgrader(hw_proxy.Proxy): def __init__(self): super(PosboxUpgrader,self).__init__() self.upgrading = threading.Lock() - self.last_upgrade = 0 @http.route('/hw_proxy/upgrade', type='http', auth='none', ) def upgrade(self): @@ -102,25 +94,8 @@ class PosboxUpgrader(hw_proxy.Proxy): @http.route('/hw_proxy/perform_upgrade', type='http', auth='none') def perform_upgrade(self): self.upgrading.acquire() - if time.time() - self.last_upgrade < 30: - self.upgrading.release() - return 'UPTODATE' - else: - os.system('/bin/bash /home/pi/openerp/update.sh') - self.last_upgrade = time.time() - self.upgrading.release() - return 'SUCCESS' - - @http.route('/hw_proxy/perform_restart', type='http', auth='none') - def perform_restart(self): - self.upgrading.acquire() - if time.time() - self.last_upgrade < 30: - self.upgrading.release() - return 'RESTARTED' - else: - os.system('/bin/bash /home/pi/openerp/restart.sh') - self.last_upgrade = time.time() - self.upgrading.release() - return 'SUCCESS' + os.system('/home/pi/posbox_update.sh') + self.upgrading.release() + return 'SUCCESS' diff --git a/addons/hw_proxy/controllers/main.py b/addons/hw_proxy/controllers/main.py index 337874cf67a..eb95527efcd 100644 --- a/addons/hw_proxy/controllers/main.py +++ b/addons/hw_proxy/controllers/main.py @@ -17,6 +17,14 @@ _logger = logging.getLogger(__name__) from openerp import http from openerp.http import request +# Those are the builtin raspberry pi USB modules, they should +# not appear in the list of connected devices. +BANNED_DEVICES = set([ + "0424:9514", # Standard Microsystem Corp. Builtin Ethernet module + "1d6b:0002", # Linux Foundation 2.0 root hub + "0424:ec00", # Standard Microsystem Corp. Other Builtin Ethernet module +]) + # drivers modules must add to drivers an object with a get_status() method # so that 'status' can return the status of all active drivers @@ -88,10 +96,18 @@ class Proxy(http.Controller): <p>The list of connected USB devices as seen by the posbox</p> """ devices = commands.getoutput("lsusb").split('\n') + count = 0 resp += "<div class='devices'>\n" for device in devices: device_name = device[device.find('ID')+2:] - resp+= "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n" + device_id = device_name.split()[0] + if not (device_id in BANNED_DEVICES): + resp+= "<div class='device' data-device='"+device+"'>"+device_name+"</div>\n" + count += 1 + + if count == 0: + resp += "<div class='device'>No USB Device Found</div>" + resp += "</div>\n" resp += """ <h2>Add New Printer</h2> diff --git a/addons/hw_scale/controllers/main.py b/addons/hw_scale/controllers/main.py index 44a031f5566..2f9e0271dd5 100644 --- a/addons/hw_scale/controllers/main.py +++ b/addons/hw_scale/controllers/main.py @@ -44,6 +44,11 @@ class Scale(Thread): if status == self.status['status']: if message != None and message != self.status['messages'][-1]: self.status['messages'].append(message) + + if status == 'error' and message: + _logger.error('Scale Error: '+message) + elif status == 'disconnected' and message: + _logger.warning('Disconnected Scale: '+message) else: self.status['status'] = status if message: @@ -51,13 +56,16 @@ class Scale(Thread): else: self.status['messages'] = [] - if status == 'error' and message: - _logger.error('Scale Error: '+message) - elif status == 'disconnected' and message: - _logger.info('Disconnected Scale: %s', message) + if status == 'error' and message: + _logger.error('Scale Error: '+message) + elif status == 'disconnected' and message: + _logger.info('Disconnected Scale: %s', message) def get_device(self): try: + if not os.path.exists(self.input_dir): + self.set_status('disconnected','Scale Not Found') + return None devices = [ device for device in listdir(self.input_dir)] scales = [ device for device in devices if ('mettler' in device.lower()) or ('toledo' in device.lower()) ] if len(scales) > 0: @@ -69,8 +77,8 @@ class Scale(Thread): stopbits = serial.STOPBITS_ONE, parity = serial.PARITY_EVEN, #xonxoff = serial.XON, - timeout = 0.01, - writeTimeout= 0.01) + timeout = 0.02, + writeTimeout= 0.02) else: self.set_status('disconnected','Scale Not Found') return None @@ -95,7 +103,7 @@ class Scale(Thread): if self.device: try: self.device.write('W') - time.sleep(0.1) + time.sleep(0.2) answer = [] while True: @@ -171,7 +179,7 @@ class Scale(Thread): while True: if self.device: self.read_weight() - time.sleep(0.05) + time.sleep(0.15) else: with self.scalelock: self.device = self.get_device() diff --git a/addons/hw_scanner/controllers/main.py b/addons/hw_scanner/controllers/main.py index 6e53c37006f..8e5e8f6af18 100644 --- a/addons/hw_scanner/controllers/main.py +++ b/addons/hw_scanner/controllers/main.py @@ -111,6 +111,8 @@ class Scanner(Thread): def get_device(self): try: + if not evdev: + return None devices = [ device for device in listdir(self.input_dir)] keyboards = [ device for device in devices if ('kbd' in device) and ('keyboard' not in device.lower())] scanners = [ device for device in devices if ('barcode' in device.lower()) or ('scanner' in device.lower())] @@ -133,6 +135,7 @@ class Scanner(Thread): been returned before. This is necessary to catch barcodes scanned while the POS is busy reading another barcode """ + self.lockedstart() while True: @@ -164,10 +167,13 @@ class Scanner(Thread): try: device.ungrab() except Exception as e: + device = None self.set_status('error',str(e)) else: time.sleep(5) # wait until a suitable device is plugged device = self.get_device() + if not device: + continue try: device.grab() diff --git a/addons/l10n_cn/account_chart_small_business_template.xml b/addons/l10n_cn/account_chart_small_business_template.xml index 43e0431d20c..0d6abf5c222 100644 --- a/addons/l10n_cn/account_chart_small_business_template.xml +++ b/addons/l10n_cn/account_chart_small_business_template.xml @@ -4,7 +4,7 @@ <!-- Copyright (C) 2012-2012 南京盈通 ccdos@intoerp.com <small business chart> - + 会计科目表模板( 小企业会计准则2011) 科目表依据: @@ -13,19 +13,24 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html --> <data noupdate="True"> - + <!-- 科目表模板--> <record id="cn_small_business_root" model="account.account.template"> - <field name="name">会计科目</field> + <field name="name">2011会计科目</field> <field name="code">0</field> <field name="user_type_id" ref="user_type_all"/> </record> <!-- 资产类 --> + <record id="cn_small_business_asset" model="account.account.template"> + <field name="name">资产类</field> + <field name="code">1</field> + <field name="user_type_id" ref="user_type_all"/> + </record> <record id="small_business_chart1001" model="account.account.template"> <field name="code">1001</field> <field eval="False" name="reconcile"/> - <field name="name">现金</field> + <field name="name">库存现金</field> <field name="user_type_id" ref="user_type_bs1"/> </record> <record id="small_business_chart1002" model="account.account.template"> @@ -64,7 +69,7 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="code">1123</field> <field eval="True" name="reconcile"/> <field name="name">预付账款</field> - <field name="user_type_id" ref="user_type_bs6"/> + <field name="user_type_id" ref="user_type_bs8"/> </record> <record id="small_business_chart1131" model="account.account.template"> <field name="code">1131</field> @@ -187,21 +192,21 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="code">1606</field> <field eval="False" name="reconcile"/> <field name="name">固定资产清理</field> - <field name="user_type_id" ref="user_type_bs39"/> + <field name="user_type_id" ref="user_type_bs46"/> </record> <record id="small_business_chart1621" model="account.account.template"> <field name="code">1621</field> <field eval="False" name="reconcile"/> <field name="name">生产性生物资产</field> - <field name="user_type_id" ref="user_type_bs39"/> + <field name="user_type_id" ref="user_type_bs47"/> </record> - <record id="small_business_chart1602" model="account.account.template"> + <record id="small_business_chart1622" model="account.account.template"> <field name="code">1622</field> <field eval="False" name="reconcile"/> <field name="name">生产性生物资产累计折旧</field> - <field name="user_type_id" ref="user_type_bs40"/> + <field name="user_type_id" ref="user_type_bs47"/> </record> <record id="small_business_chart1701" model="account.account.template"> @@ -230,11 +235,16 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="code">1901</field> <field eval="False" name="reconcile"/> <field name="name">待处理财产损溢</field> - <field name="user_type_id" ref="user_type_bs44"/> + <field name="user_type_id" ref="user_type_bs53"/> </record> - - + + <!-- 二、负债类 --> + <record id="cn_small_business_liability" model="account.account.template"> + <field name="name">负债类</field> + <field name="code">2</field> + <field name="user_type_id" ref="user_type_all"/> + </record> <record id="small_business_chart2001" model="account.account.template"> <field name="code">2001</field> <field eval="False" name="reconcile"/> @@ -277,27 +287,27 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="code">2231</field> <field eval="False" name="reconcile"/> <field name="name">应付利息</field> - <field name="user_type_id" ref="user_type_bs74"/> + <field name="user_type_id" ref="user_type_bs174"/> </record> <record id="small_business_chart2232" model="account.account.template"> <field name="code">2232</field> <field eval="False" name="reconcile"/> <field name="name">应付利润</field> - <field name="user_type_id" ref="user_type_bs74"/> + <field name="user_type_id" ref="user_type_bs175"/> </record> <record id="small_business_chart2241" model="account.account.template"> <field name="code">2241</field> <field eval="True" name="reconcile"/> <field name="name">其他应付款</field> - <field name="user_type_id" ref="user_type_bs70"/> + <field name="user_type_id" ref="user_type_bs73"/> </record> <record id="small_business_chart2401" model="account.account.template"> <field name="code">2401</field> <field eval="False" name="reconcile"/> <field name="name">递延收益</field> - <field name="user_type_id" ref="user_type_all"/> + <field name="user_type_id" ref="user_type_bs104"/> </record> <record id="small_business_chart2501" model="account.account.template"> <field name="code">2501</field> @@ -315,6 +325,11 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <!-- 所有者权益类 --> + <record id="cn_small_business_right" model="account.account.template"> + <field name="name">所有者权益类</field> + <field name="code">3</field> + <field name="user_type_id" ref="user_type_all"/> + </record> <record id="small_business_chart3001" model="account.account.template"> <field name="code">3001</field> <field eval="False" name="reconcile"/> @@ -337,7 +352,7 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="code">3103</field> <field eval="False" name="reconcile"/> <field name="name">本年利润</field> - <field name="user_type_id" ref="user_type_bs121"/> + <field name="user_type_id" ref="user_type_bs120"/> </record> <record id="small_business_chart3104" model="account.account.template"> <field name="code">3104</field> @@ -347,42 +362,52 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html </record> <!-- 成本类 --> + <record id="cn_small_business_cost" model="account.account.template"> + <field name="name">成本类</field> + <field name="code">4</field> + <field name="user_type_id" ref="user_type_all"/> + </record> <record id="small_business_chart4001" model="account.account.template"> <field name="code">4001</field> <field eval="False" name="reconcile"/> <field name="name">生产成本</field> - <field name="user_type_id" ref="user_type_all"/> + <field name="user_type_id" ref="user_type_cost2"/> </record> <record id="small_business_chart4101" model="account.account.template"> <field name="code">4101</field> <field eval="False" name="reconcile"/> <field name="name">制造费用</field> - <field name="user_type_id" ref="user_type_all"/> + <field name="user_type_id" ref="user_type_cost2"/> </record> <record id="small_business_chart4301" model="account.account.template"> <field name="code">4301</field> <field eval="False" name="reconcile"/> <field name="name">研发支出</field> - <field name="user_type_id" ref="user_type_all"/> + <field name="user_type_id" ref="user_type_cost3"/> </record> <record id="small_business_chart4401" model="account.account.template"> <field name="code">4401</field> <field eval="False" name="reconcile"/> <field name="name">工程施工</field> - <field name="user_type_id" ref="user_type_all"/> + <field name="user_type_id" ref="user_type_cost2"/> </record> <record id="small_business_chart4403" model="account.account.template"> <field name="code">4403</field> <field eval="False" name="reconcile"/> <field name="name">机械作业</field> - <field name="user_type_id" ref="user_type_all"/> + <field name="user_type_id" ref="user_type_cost2"/> </record> - <!-- 损益类 --> + <!-- 损益类 --> + <record id="cn_small_business_gal" model="account.account.template"> + <field name="name">损益类</field> + <field name="code">5</field> + <field name="user_type_id" ref="user_type_all"/> + </record> <record id="small_business_chart5001" model="account.account.template"> <field name="code">5001</field> <field eval="False" name="reconcile"/> @@ -393,7 +418,7 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <field name="code">5051</field> <field eval="False" name="reconcile"/> <field name="name">其他业务收入</field> - <field name="user_type_id" ref="user_type_pl23"/> + <field name="user_type_id" ref="user_type_pl1"/> </record> <record id="small_business_chart5111" model="account.account.template"> @@ -456,7 +481,7 @@ http://kjs.mof.gov.cn/zhengwuxinxi/zhengcefabu/201111/t20111107_605525.html <record id="small_business_chart5801" model="account.account.template"> <field name="code">5801</field> <field eval="False" name="reconcile"/> - <field name="name">所得税</field> + <field name="name">所得税费用</field> <field name="user_type_id" ref="user_type_pl29"/> </record> diff --git a/addons/l10n_cn/account_chart_template.xml b/addons/l10n_cn/account_chart_template.xml index b061bd311ee..eeac49a3bdd 100644 --- a/addons/l10n_cn/account_chart_template.xml +++ b/addons/l10n_cn/account_chart_template.xml @@ -21,7 +21,7 @@ <record id="chart1001" model="account.account.template"> <field name="code">1001</field> <field eval="False" name="reconcile"/> - <field name="name">现金</field> + <field name="name">库存现金</field> <field name="user_type_id" ref="user_type_bs1"/> </record> <record id="chart1002" model="account.account.template"> @@ -432,6 +432,12 @@ <field name="name">应付利息</field> <field name="user_type_id" ref="user_type_bs74"/> </record> + <record id="chart2241" model="account.account.template"> + <field name="code">2241</field> + <field eval="True" name="reconcile"/> + <field name="name">其他应付款</field> + <field name="user_typed_id" ref="user_type_bs73"/> + </record> <record id="chart2401" model="account.account.template"> <field name="code">2401</field> <field eval="True" name="reconcile"/> diff --git a/addons/l10n_cn/account_chart_type.xml b/addons/l10n_cn/account_chart_type.xml index 49900d84f7b..ae694220c73 100644 --- a/addons/l10n_cn/account_chart_type.xml +++ b/addons/l10n_cn/account_chart_type.xml @@ -15,7 +15,7 @@ </record> <!-- 利润表科目 --> <record id="user_type_pl1" model="account.account.type"> - <field name="name">一、主营业务收入</field> + <field name="name">一、营业收入</field> <field name="report_type">income</field> </record> <record id="user_type_pl21" model="account.account.type"> @@ -23,7 +23,7 @@ <field name="report_type">income</field> </record> <record id="user_type_pl3" model="account.account.type"> - <field name="name">减:主营业务成本</field> + <field name="name">减:营业成本</field> <field name="report_type">expense</field> </record> <record id="user_type_pl5" model="account.account.type"> @@ -35,7 +35,7 @@ <field name="report_type">income</field> </record> <record id="user_type_pl11" model="account.account.type"> - <field name="name">减:营业费用</field> + <field name="name"> 销售费用</field> <field name="report_type">expense</field> </record> <record id="user_type_pl13" model="account.account.type"> @@ -87,6 +87,10 @@ <field name="name">应收账款</field> <field name="report_type">asset</field> </record> + <record id="user_type_bs8" model="account.account.type"> + <field name="name">预付账款</field> + <field name="report_type">asset</field> + </record> <record id="user_type_bs7" model="account.account.type"> <field name="name">其他应收款</field> <field name="report_type">asset</field> @@ -135,6 +139,10 @@ <field name="name">固定资产清理</field> <field name="report_type">asset</field> </record> + <record id="user_type_bs47" model="account.account.type"> + <field name="name">生产性生物资产</field> + <field name="report_type">asset</field> + </record> <record id="user_type_bs51" model="account.account.type"> <field name="name">无形资产</field> <field name="report_type">asset</field> @@ -143,6 +151,10 @@ <field name="name">长期待摊费用</field> <field name="report_type">asset</field> </record> + <record id="user_type_bs53" model="account.account.type"> + <field name="name">待处理财产损溢-无结转</field> + <field name="report_type">asset</field> + </record> <record id="user_type_bs61" model="account.account.type"> <field name="name">递延税款借项</field> <field name="report_type">asset</field> @@ -159,12 +171,25 @@ <field name="name">应付账款</field> <field name="report_type">liability</field> </record> + <record id="user_type_bs73" model="account.account.type"> + <field name="name">其他应付款</field> + <field name="report_type">liability</field> + </record> <record id="user_type_bs71" model="account.account.type"> <field name="name">预收账款</field> <field name="report_type">liability</field> </record> <record id="user_type_bs72" model="account.account.type"> - <field name="name">应付工资</field> + <field name="name">应付职工薪酬</field> + <field name="report_type">liability</field> + </record> + <record id="user_type_bs174" model="account.account.type"> + <field name="name">应付利息</field> + <field name="report_type">liability</field> + </record> + <record id="user_type_bs175" model="account.account.type"> + <field name="name">应付利润</field> + <field name="report_type">liability</field> </record> <record id="user_type_bs74" model="account.account.type"> @@ -191,6 +216,11 @@ <field name="name">长期应付款</field> <field name="report_type">liability</field> </record> + <record id="user_type_bs104" model="account.account.type"> + <field name="name">递延收益</field> + <field name="report_type">liability</field> + </record> + <record id="user_type_bs115" model="account.account.type"> <field name="name">实收资本</field> <field name="report_type">liability</field> @@ -203,9 +233,26 @@ <field name="name">盈余公积</field> <field name="report_type">liability</field> </record> + <record id="user_type_bs120" model="account.account.type"> + <field name="name">本年利润</field> + + <field name="report_type">liability</field> + </record> <record id="user_type_bs121" model="account.account.type"> <field name="name">未分配利润</field> <field name="report_type">liability</field> </record> + <record id="user_type_cost1" model="account.account.type"> + <field name="name">成本类-余额结转</field> + <field name="report_type">none</field> + </record> + <record id="user_type_cost2" model="account.account.type"> + <field name="name">成本-无结转</field> + <field name="report_type">none</field> + </record> + <record id="user_type_cost3" model="account.account.type"> + <field name="name">开发支出</field> + <field name="report_type">none</field> + </record> </data> </openerp> diff --git a/addons/mass_mailing/models/mail_mail.py b/addons/mass_mailing/models/mail_mail.py index 8ac5dea8113..0e8e94c94c2 100644 --- a/addons/mass_mailing/models/mail_mail.py +++ b/addons/mass_mailing/models/mail_mail.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import urllib import urlparse import re +import werkzeug.urls from openerp import tools from openerp import SUPERUSER_ID @@ -41,7 +41,7 @@ class MailMail(osv.Model): track_url = urlparse.urljoin( base_url, 'mail/track/%(mail_id)s/blank.gif?%(params)s' % { 'mail_id': mail.id, - 'params': urllib.urlencode({'db': cr.dbname}) + 'params': werkzeug.url_encode({'db': cr.dbname}) } ) return '<img src="%s" alt=""/>' % track_url @@ -51,7 +51,7 @@ class MailMail(osv.Model): url = urlparse.urljoin( base_url, 'mail/mailing/%(mailing_id)s/unsubscribe?%(params)s' % { 'mailing_id': mail.mailing_id.id, - 'params': urllib.urlencode({'db': cr.dbname, 'res_id': mail.res_id, 'email': email_to}) + 'params': werkzeug.url_encode({'db': cr.dbname, 'res_id': mail.res_id, 'email': email_to}) } ) return url diff --git a/addons/mass_mailing/models/mass_mailing.py b/addons/mass_mailing/models/mass_mailing.py index 443abcb123c..89ac9cb0244 100644 --- a/addons/mass_mailing/models/mass_mailing.py +++ b/addons/mass_mailing/models/mass_mailing.py @@ -52,8 +52,8 @@ class MassMailingList(osv.Model): } def _get_default_popup_content(self, cr, uid, context=None): - return """<div class="o_popup_modal_header text-center"> - <h3 class="o_popup_modal_title mt8">Odoo Presents</h3> + return """<div class="modal-header text-center"> + <h3 class="modal-title mt8">Odoo Presents</h3> </div> <div class="o_popup_message"> <font>7</font> diff --git a/addons/mass_mailing/views/editor_field_html.xml b/addons/mass_mailing/views/editor_field_html.xml index de7c8316062..4e28ab8647a 100644 --- a/addons/mass_mailing/views/editor_field_html.xml +++ b/addons/mass_mailing/views/editor_field_html.xml @@ -69,8 +69,8 @@ <div class="o_editable" t-att-data-oe-model="model" t-att-data-oe-field="field" t-att-data-oe-id="res_id" data-oe-type="html"> <!-- Can be removed once default values are ok for the editor --> <t t-raw="content"> - <div class="o_popup_modal_header text-center"> - <h3 class="o_popup_modal_title mt8">Odoo Presents</h3> + <div class="modal-header text-center"> + <h3 class="modal-title mt8">Odoo Presents</h3> </div> <div class="o_popup_message"> <font>7</font> diff --git a/addons/mrp_repair/wizard/make_invoice.py b/addons/mrp_repair/wizard/make_invoice.py index 4abfcb9c2d8..72241c4fa72 100644 --- a/addons/mrp_repair/wizard/make_invoice.py +++ b/addons/mrp_repair/wizard/make_invoice.py @@ -36,7 +36,7 @@ class make_invoice(osv.osv_memory): form_res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form') form_id = form_res and form_res[1] or False tree_res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_tree') - tree_id = tree_res and tree_res[1] or Fals + tree_id = tree_res and tree_res[1] or False return { 'domain': [('id','in', newinv.values())], diff --git a/addons/point_of_sale/tools/posbox/.gitignore b/addons/point_of_sale/tools/posbox/.gitignore new file mode 100644 index 00000000000..b26eafa34d1 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/.gitignore @@ -0,0 +1,3 @@ +kernel-qemu +posbox.img +raspbian.img
\ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/fstab b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/fstab new file mode 100644 index 00000000000..28898fc8ea4 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/fstab @@ -0,0 +1,4 @@ +proc /proc proc defaults 0 0 +/dev/mmcblk0p1 /boot vfat defaults 0 2 +/dev/mmcblk0p2 / ext4 defaults,noatime,ro 0 1 +# a swapfile is not a swap partition, so no using swapon|off from here on, use dphys-swapfile swap[on|off] for that diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/init.d/rcS b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/init.d/rcS new file mode 100755 index 00000000000..6380e949834 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/init.d/rcS @@ -0,0 +1,10 @@ +#! /bin/sh +# +# rcS +# +# Call all S??* scripts in /etc/rcS.d/ in numerical/alphabetical order +# + +/etc/setup_ramdisks.sh + +exec /etc/init.d/rc S diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/network/interfaces b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/network/interfaces new file mode 100644 index 00000000000..95b6f07524a --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/network/interfaces @@ -0,0 +1,3 @@ +# needed for postgresql +auto lo +iface lo inet loopback
\ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/rc.local b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/rc.local new file mode 100755 index 00000000000..065b6a05f77 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/rc.local @@ -0,0 +1,25 @@ +#!/bin/sh -e +# +# rc.local +# +# This script is executed at the end of each multiuser runlevel. +# Make sure that the script will "exit 0" on success or any other +# value on error. +# +# In order to enable or disable this script just change the execution +# bits. +# +# By default this script does nothing. + +# Print the IP address +_IP=$(hostname -I) || true +if [ "$_IP" ]; then + printf "My IP address is %s\n" "$_IP" +fi + +mkdir -p /var/run/odoo +chown pi:pi /var/run/odoo + +/home/pi/led_status.sh & + +exit 0 diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/setup_ramdisks.sh b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/setup_ramdisks.sh new file mode 100755 index 00000000000..47e11d1bdd3 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/setup_ramdisks.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail +# set -o xtrace + +create_ramdisk () { + ORIGINAL="${1}" + RAMDISK="${ORIGINAL}_ram" + SIZE="${2}" + echo "Creating ramdisk for ${1} of size ${SIZE}..." + + mount -t tmpfs -o size="${SIZE}" tmpfs "${RAMDISK}" + rsync -a --exclude="swap" --exclude="apt" --exclude="dpkg" "${ORIGINAL}/" "${RAMDISK}/" + mount --bind "${RAMDISK}" "${ORIGINAL}" +} + +# check /proc/cmdline + +# bind mount / so that we can get to the real /var and /etc +mount --bind / /root_bypass_ramdisks + +echo "Creating ramdisks..." +create_ramdisk "/var" "128M" +create_ramdisk "/etc" "16M" diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo new file mode 100755 index 00000000000..9bdaa86c584 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init.d/odoo @@ -0,0 +1,72 @@ +#!/bin/bash +### BEGIN INIT INFO +# Provides: odoo.py +# Required-Start: $remote_fs $syslog postgresql +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start odoo daemon at boot time +# Description: Enable service provided by daemon. +# X-Interactive: true +### END INIT INFO +## more info: http://wiki.debian.org/LSBInitScripts + +. /lib/lsb/init-functions + +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin +DAEMON=/home/pi/odoo/odoo.py +NAME=odoo +DESC=odoo +CONFIG=/home/pi/odoo.conf +LOGFILE=/var/log/odoo/odoo-server.log +PIDFILE=/var/run/${NAME}.pid +USER=pi + +test -x $DAEMON || exit 0 +set -e + +function _start() { + start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos +} + +function _stop() { + start-stop-daemon --stop --quiet --pidfile $PIDFILE --oknodo --retry 3 + rm -f $PIDFILE +} + +function _status() { + start-stop-daemon --status --quiet --pidfile $PIDFILE + return $? +} + + +case "$1" in + start) + echo -n "Starting $DESC: " + _start + echo "ok" + ;; + stop) + echo -n "Stopping $DESC: " + _stop + echo "ok" + ;; + restart|force-reload) + echo -n "Restarting $DESC: " + _stop + sleep 1 + _start + echo "ok" + ;; + status) + echo -n "Status of $DESC: " + _status && echo "running" || echo "stopped" + ;; + *) + N=/etc/init.d/$NAME + echo "Usage: $N {start|stop|restart|force-reload|status}" >&2 + exit 1 + ;; +esac + +exit 0 diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh new file mode 100755 index 00000000000..68b64fb94ca --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/init_posbox_image.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail +# set -o xtrace + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__file="${__dir}/$(basename "${BASH_SOURCE[0]}")" +__base="$(basename ${__file} .sh)" + +# Recommends: antiword, graphviz, ghostscript, postgresql, python-gevent, poppler-utils +export DEBIAN_FRONTEND=noninteractive + +# GUI-related packages +PKGS_TO_DELETE="xserver-xorg-video-fbdev xserver-xorg xinit gstreamer1.0-x gstreamer1.0-omx gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-alsa gstreamer1.0-libav epiphany-browser lxde lxtask menu-xdg gksu xserver-xorg-video-fbturbo xpdf gtk2-engines alsa-utils netsurf-gtk zenity desktop-base lxpolkit weston omxplayer raspberrypi-artwork lightdm gnome-themes-standard-data gnome-icon-theme qt50-snapshot qt50-quick-particle-examples idle python-pygame python-tk idle3 python-serial python-picamera debian-reference-en dillo x2x scratch nuscratch raspberrypi-ui-mods timidity smartsim penguinspuzzle pistore sonic-pi python-pifacecommon python-pifacedigitalio oracle-java8-jdk minecraft-pi python-minecraftpi wolfram-engine raspi-config libgl1-mesa-dri libicu48 pypy-upstream lxde-icon-theme python3" +INSTALLED_PKGS_TO_DELETE="" +set +o errexit +for CURRENT_PKG in $(echo $PKGS_TO_DELETE); do + $(dpkg --status $CURRENT_PKG &> /dev/null) + if [[ $? -eq 0 ]]; then + INSTALLED_PKGS_TO_DELETE="$INSTALLED_PKGS_TO_DELETE $CURRENT_PKG" + fi +done +set -o errexit + +apt-get -y remove --purge ${INSTALLED_PKGS_TO_DELETE} + +# Remove automatically installed dependency packages +apt-get -y autoremove + +apt-get update +apt-get -y dist-upgrade + +PKGS_TO_INSTALL="adduser postgresql-client python python-dateutil python-decorator python-docutils python-feedparser python-imaging python-jinja2 python-ldap python-libxslt1 python-lxml python-mako python-mock python-openid python-passlib python-psutil python-psycopg2 python-pybabel python-pychart python-pydot python-pyparsing python-pypdf python-reportlab python-requests python-simplejson python-tz python-unittest2 python-vatnumber python-vobject python-werkzeug python-xlwt python-yaml postgresql python-gevent python-serial python-pip python-dev localepurge vim mc mg" + +apt-get -y install ${PKGS_TO_INSTALL} + +apt-get clean +localepurge +rm -rf /usr/share/doc || true +rm -rf /home/pi/python_games || true + +# python-usb in wheezy is too old +# the latest pyusb from pip does not work either, usb.core.find() never returns +# this may be fixed with libusb>2:1.0.11-1, but that's the most recent one in raspbian +# so we install the latest pyusb that works with this libusb +pip install pyusb==1.0.0b1 +pip install qrcode +pip install evdev + +usermod -a -G lp pi + +sudo -u postgres createuser -s pi +mkdir /var/log/odoo +chown pi:pi /var/log/odoo + +update-rc.d odoo defaults + +# create dirs for ramdisks +create_ramdisk_dir () { + mkdir "${1}_ram" +} + +create_ramdisk_dir "/var" +create_ramdisk_dir "/etc" +mkdir /root_bypass_ramdisks + +reboot diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/ld.so.preload b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/ld.so.preload new file mode 100644 index 00000000000..cfa850b8356 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/ld.so.preload @@ -0,0 +1 @@ +#/usr/lib/arm-linux-gnueabihf/libcofi_rpi.so diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.conf b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.conf new file mode 100644 index 00000000000..1ac409b252e --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.conf @@ -0,0 +1,32 @@ +# see "man logrotate" for details +# rotate log files daily +daily + +# keep 2 days worth of backlogs +rotate 2 + +# create new (empty) log files after rotating old ones +create + +# uncomment this if you want your log files compressed +#compress + +# packages drop log rotation information into this directory +include /etc/logrotate.d + +# no packages own wtmp, or btmp -- we'll rotate them here +/var/log/wtmp { + missingok + monthly + create 0664 root utmp + rotate 1 +} + +/var/log/btmp { + missingok + monthly + create 0660 root utmp + rotate 1 +} + +# system-specific logs may be configured here diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.d/odoo b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.d/odoo new file mode 100644 index 00000000000..060ec54d012 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.d/odoo @@ -0,0 +1,5 @@ +/var/log/odoo/*.log { + copytruncate + missingok + notifempty +} diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/rc.local b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/rc.local new file mode 100755 index 00000000000..c55290c3978 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/rc.local @@ -0,0 +1,22 @@ +#!/bin/sh -e +# +# rc.local +# +# This script is executed at the end of each multiuser runlevel. +# Make sure that the script will "exit 0" on success or any other +# value on error. +# +# In order to enable or disable this script just change the execution +# bits. +# +# By default this script does nothing. + +# Print the IP address +_IP=$(hostname -I) || true +if [ "$_IP" ]; then + printf "My IP address is %s\n" "$_IP" +fi + +/etc/init_posbox_image.sh + +exit 0 diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/90-qemu.rules b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/90-qemu.rules new file mode 100644 index 00000000000..fadedd206fe --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/udev/rules.d/90-qemu.rules @@ -0,0 +1,3 @@ +KERNEL=="sda", SYMLINK+="mmcblk0" +KERNEL=="sda?", SYMLINK+="mmcblk0p%n" +KERNEL=="sda2", SYMLINK+="root"
\ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/led_status.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/led_status.sh new file mode 100755 index 00000000000..8d272d7de2b --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/led_status.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set_brightness() { + echo "${1}" > /sys/class/leds/led0/brightness +} + +check_status_loop() { + while true ; do + if wget --quiet localhost:8069/hw_proxy/hello -O /dev/null ; then + set_brightness 255 + else + set_brightness 0 + fi + sleep 5 + done +} + +echo none > /sys/class/leds/led0/trigger +check_status_loop diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/odoo.conf b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/odoo.conf new file mode 100644 index 00000000000..1bc0a0d3ba5 --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/odoo.conf @@ -0,0 +1,6 @@ +[options] +data_dir = /var/run/odoo +log_level = error +logfile = /var/log/odoo/odoo-server.log +pidfile = /var/run/odoo/odoo.pid +server_wide_modules = web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos
\ No newline at end of file diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/posbox_update.sh b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/posbox_update.sh new file mode 100755 index 00000000000..e994a60461f --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/posbox_update.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +sudo mount -o remount,rw / +sudo git --work-tree=/home/pi/odoo/ --git-dir=/home/pi/odoo/.git pull +sudo mount -o remount,ro / +(sleep 5 && sudo reboot) & diff --git a/addons/point_of_sale/tools/posbox/posbox_create_image.sh b/addons/point_of_sale/tools/posbox/posbox_create_image.sh new file mode 100755 index 00000000000..9e65410f8fe --- /dev/null +++ b/addons/point_of_sale/tools/posbox/posbox_create_image.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -o errexit +set -o nounset +set -o pipefail +# set -o xtrace + +if [[ $EUID -ne 0 ]]; then + echo "This script must be run as root" + exit 1 +fi + +file_exists() { + [[ -f $1 ]]; +} + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +__file="${__dir}/$(basename "${BASH_SOURCE[0]}")" +__base="$(basename ${__file} .sh)" + +MOUNT_POINT="${__dir}/root_mount" +OVERWRITE_FILES_BEFORE_INIT_DIR="${__dir}/overwrite_before_init" +OVERWRITE_FILES_AFTER_INIT_DIR="${__dir}/overwrite_after_init" + +if [ ! -f kernel-qemu ] || ! file_exists *raspbian*.img ; then + ./posbox_download_images.sh +fi + +cp -a *raspbian*.img posbox.img + +CLONE_DIR="${OVERWRITE_FILES_BEFORE_INIT_DIR}/home/pi/odoo" +mkdir "${CLONE_DIR}" +git clone -b 8.0 --no-checkout --depth 1 https://github.com/odoo/odoo.git "${CLONE_DIR}" +cd "${CLONE_DIR}" +git config core.sparsecheckout true +echo "addons/web +addons/web_kanban +addons/hw_* +openerp/ +odoo.py" | tee --append .git/info/sparse-checkout > /dev/null +git read-tree -mu HEAD +cd "${__dir}" + +# rc.local +LOOP_MAPPER_PATH=$(kpartx -av posbox.img | tail -n 1 | cut -d ' ' -f 3) +LOOP_MAPPER_PATH="/dev/mapper/${LOOP_MAPPER_PATH}" +mkdir "${MOUNT_POINT}" +mount "${LOOP_MAPPER_PATH}" "${MOUNT_POINT}" + +# 'overlay' the overwrite directory onto the mounted image filesystem +cp -a "${OVERWRITE_FILES_BEFORE_INIT_DIR}"/* "${MOUNT_POINT}" + +# get rid of the git clone +rm -rf "${CLONE_DIR}" + +# get rid of the mount, we have to remount it anyway because we have +# to "refresh" the filesystem after qemu modified it +sleep 2 +umount "${MOUNT_POINT}" + +# from http://paulscott.co.za/blog/full-raspberry-pi-raspbian-emulation-with-qemu/ +# ssh pi@localhost -p10022 +QEMU_OPTS=(-kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb -no-reboot -serial stdio -append 'root=/dev/sda2 panic=1 rootfstype=ext4 rw' -hda posbox.img -net user,hostfwd=tcp::10022-:22,hostfwd=tcp::18069-:8069 -net nic) +if [ -z ${DISPLAY:-} ] ; then + QEMU_OPTS+=(-nographic) +fi +qemu-system-arm "${QEMU_OPTS[@]}" + +mount "${LOOP_MAPPER_PATH}" "${MOUNT_POINT}" +cp -av "${OVERWRITE_FILES_AFTER_INIT_DIR}"/* "${MOUNT_POINT}" + +# cleanup +sleep 2 +umount "${MOUNT_POINT}" +rm -r "${MOUNT_POINT}" + +echo "Running zerofree..." +zerofree -v "${LOOP_MAPPER_PATH}" || true + +kpartx -d posbox.img diff --git a/addons/point_of_sale/tools/posbox/posbox_download_images.sh b/addons/point_of_sale/tools/posbox/posbox_download_images.sh new file mode 100755 index 00000000000..a138e44bfab --- /dev/null +++ b/addons/point_of_sale/tools/posbox/posbox_download_images.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +wget 'http://downloads.raspberrypi.org/raspbian_latest' -O raspbian.img.zip +unzip raspbian.img.zip +wget 'https://github.com/dhruvvyas90/qemu-rpi-kernel/raw/master/kernel-qemu' diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 3085f221c09..243822b9715 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -1399,13 +1399,15 @@ class procurement_order(osv.osv): pricelist_id = partner.property_product_pricelist_purchase.id prices_qty = [] + qty = {} + for procurement in procurements: seller_qty = procurement.product_id.seller_qty if procurement.location_id.usage != 'customer' else 0.0 uom_id = procurement.product_id.uom_po_id.id - qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id) + qty[procurement.product_id.id] = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id) if seller_qty: - qty = max(qty, seller_qty) - prices_qty += [(procurement.product_id, qty, partner)] + qty[procurement.product_id.id] = max(qty[procurement.product_id.id], seller_qty) + prices_qty += [(procurement.product_id, qty[procurement.product_id.id], partner)] prices = pricelist_obj.price_get_multi(cr, uid, [pricelist_id], prices_qty) #Passing partner_id to context for purchase order line integrity of Line name @@ -1430,7 +1432,7 @@ class procurement_order(osv.osv): values = { 'name': name, - 'product_qty': qty, + 'product_qty': qty[procurement.product_id.id], 'product_id': procurement.product_id.id, 'product_uom': procurement.product_id.uom_po_id.id, 'price_unit': price or 0.0, diff --git a/addons/purchase/stock.py b/addons/purchase/stock.py index eee573c8e53..3397a99f37d 100644 --- a/addons/purchase/stock.py +++ b/addons/purchase/stock.py @@ -126,7 +126,7 @@ class stock_move(osv.osv): pricelist_obj = self.pool.get("product.pricelist") pricelist = partner.property_product_pricelist_purchase.id price = pricelist_obj.price_get(cr, uid, [pricelist], - move.product_id.id, move.product_uom_qty, partner, { + move.product_id.id, move.product_uom_qty, partner.id, { 'uom': move.product_uom.id, 'date': move.date, })[pricelist] diff --git a/addons/purchase_requisition/purchase_requisition.py b/addons/purchase_requisition/purchase_requisition.py index 87f16bb4060..976ab5ea9c6 100644 --- a/addons/purchase_requisition/purchase_requisition.py +++ b/addons/purchase_requisition/purchase_requisition.py @@ -379,28 +379,36 @@ class procurement_order(osv.osv): 'requisition_id': fields.many2one('purchase.requisition', 'Latest Requisition') } - def _run(self, cr, uid, procurement, context=None): + def make_po(self, cr, uid, ids, context=None): requisition_obj = self.pool.get('purchase.requisition') warehouse_obj = self.pool.get('stock.warehouse') - if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition: - warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id)], context=context) - requisition_id = requisition_obj.create(cr, uid, { - 'origin': procurement.origin, - 'date_end': procurement.date_planned, - 'warehouse_id': warehouse_id and warehouse_id[0] or False, - 'company_id': procurement.company_id.id, - 'procurement_id': procurement.id, - 'picking_type_id': procurement.rule_id.picking_type_id.id, - 'line_ids': [(0, 0, { - 'product_id': procurement.product_id.id, - 'product_uom_id': procurement.product_uom.id, - 'product_qty': procurement.product_qty - - })], - }) - self.message_post(cr, uid, [procurement.id], body=_("Purchase Requisition created"), context=context) - return self.write(cr, uid, [procurement.id], {'requisition_id': requisition_id}, context=context) - return super(procurement_order, self)._run(cr, uid, procurement, context=context) + req_ids = [] + res = {} + for procurement in self.browse(cr, uid, ids, context=context): + res[procurement.id] = False + if procurement.product_id.purchase_requisition: + warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id)], context=context) + requisition_id = requisition_obj.create(cr, uid, { + 'origin': procurement.origin, + 'date_end': procurement.date_planned, + 'warehouse_id': warehouse_id and warehouse_id[0] or False, + 'company_id': procurement.company_id.id, + 'procurement_id': procurement.id, + 'picking_type_id': procurement.rule_id.picking_type_id.id, + 'line_ids': [(0, 0, { + 'product_id': procurement.product_id.id, + 'product_uom_id': procurement.product_uom.id, + 'product_qty': procurement.product_qty + })], + }) + self.message_post(cr, uid, [procurement.id], body=_("Purchase Requisition created"), context=context) + self.write(cr, uid, [procurement.id], {'requisition_id': requisition_id}, context=context) + req_ids += [procurement.id] + res[procurement.id] = True + set_others = set(ids) - set(req_ids) + if set_others: + res.update(super(procurement_order, self).make_po(cr, uid, list(set_others), context=context)) + return res def _check(self, cr, uid, procurement, context=None): if procurement.rule_id and procurement.rule_id.action == 'buy' and procurement.product_id.purchase_requisition: diff --git a/addons/report/models/report.py b/addons/report/models/report.py index 97e5e28fbc3..c86ed372260 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -331,7 +331,7 @@ class Report(osv.Model): """ command_args = [] if wkhtmltopdf_state == "ok": - command_args.extend(['--viewport-size', '1024x768']) + command_args.extend(['--viewport-size', landscape and '1024x1280' or '1280x1024']) # Passing the cookie to wkhtmltopdf in order to resolve internal links. try: @@ -354,7 +354,7 @@ class Report(osv.Model): del command_args[index] del command_args[index] command_args.extend(['--orientation', 'landscape']) - elif landscape and not '--orientation' in command_args: + elif landscape and '--orientation' not in command_args: command_args.extend(['--orientation', 'landscape']) # Execute WKhtmltopdf @@ -403,7 +403,7 @@ class Report(osv.Model): if process.returncode not in [0, 1]: raise UserError(_('Wkhtmltopdf failed (error code: %s). ' - 'Message: %s') % (str(process.returncode), err)) + '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]): @@ -418,8 +418,7 @@ class Report(osv.Model): try: self.pool['ir.attachment'].create(cr, uid, attachment) except AccessError: - _logger.info("Cannot save PDF report %r as attachment", - attachment['name']) + _logger.info("Cannot save PDF report %r as attachment", attachment['name']) else: _logger.info('The PDF document %s is now saved in the database', attachment['name']) diff --git a/addons/sale/wizard/sale_make_invoice_advance.py b/addons/sale/wizard/sale_make_invoice_advance.py index 4910d5e8c01..add69e5839a 100644 --- a/addons/sale/wizard/sale_make_invoice_advance.py +++ b/addons/sale/wizard/sale_make_invoice_advance.py @@ -128,7 +128,7 @@ class sale_advance_payment_inv(osv.osv_memory): 'partner_id': sale.partner_invoice_id.id, 'invoice_line_ids': [(0, 0, inv_line_values)], 'currency_id': sale.pricelist_id.currency_id.id, - 'comment': '', + 'comment': sale.note, 'payment_term_id': sale.payment_term_id.id, 'fiscal_position_id': sale.fiscal_position_id.id or sale.partner_id.property_account_position_id.id, 'team_id': sale.team_id.id, diff --git a/addons/sale_contract/models/sale_contract.py b/addons/sale_contract/models/sale_contract.py index 6fa1ddf9114..7fcd9f51bc5 100644 --- a/addons/sale_contract/models/sale_contract.py +++ b/addons/sale_contract/models/sale_contract.py @@ -280,7 +280,7 @@ class account_analytic_account(osv.osv): inv_ids = [] for account in self.browse(cr, uid, ids, context=context): res[account.id] = 0.0 - line_ids = lines_obj.search(cr, uid, [('account_id','=', account.id), ('invoice_id','!=',False), ('to_invoice','!=', False), ('journal_id.type', '=', 'general'), ('invoice_id.type', 'in', ['out_invoice', 'out_refund'])], context=context) + line_ids = lines_obj.search(cr, uid, [('account_id','=', account.id), ('invoice_id','!=',False), ('invoice_id.state', 'not in', ['draft', 'cancel']), ('to_invoice','!=', False), ('journal_id.type', '=', 'general'), ('invoice_id.type', 'in', ['out_invoice', 'out_refund'])], context=context) for line in lines_obj.browse(cr, uid, line_ids, context=context): if line.invoice_id not in inv_ids: inv_ids.append(line.invoice_id) diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml index 553a6bfec02..3f72d3f1c10 100644 --- a/addons/sale_stock/sale_stock_view.xml +++ b/addons/sale_stock/sale_stock_view.xml @@ -45,6 +45,7 @@ parent.partner_id, False, True, parent.date_order, product_packaging, parent.fiscal_position_id, False, parent.warehouse_id, context) </attribute> + <attribute name="context">{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':False}</attribute> </xpath> <xpath expr="//field[@name='order_line']/tree//field[@name='product_id']" position="attributes"> <attribute name="on_change">product_id_change_with_wh( diff --git a/addons/stock/stock.py b/addons/stock/stock.py index 15d06f2d3af..480835dffb6 100644 --- a/addons/stock/stock.py +++ b/addons/stock/stock.py @@ -1061,7 +1061,8 @@ class stock_picking(models.Model): move_obj = self.pool.get("stock.move") move_obj.write(cr, uid, backorder_move_ids, {'picking_id': backorder_id}, context=context) - self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) + if not picking.date_done: + self.write(cr, uid, [picking.id], {'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)}, context=context) self.action_confirm(cr, uid, [backorder_id], context=context) self.action_assign(cr, uid, [backorder_id], context=context) return backorder_id @@ -1726,12 +1727,14 @@ class stock_move(osv.osv): settings_obj = self.pool.get('stock.config.settings') uom_obj = self.pool.get('product.uom') res = dict.fromkeys(ids, '') + precision = self.pool['decimal.precision'].precision_get(cr, uid, 'Product Unit of Measure') for move in self.browse(cr, uid, ids, context=context): if move.state in ('draft', 'done', 'cancel') or move.location_id.usage != 'internal': res[move.id] = '' # 'not applicable' or 'n/a' could work too continue total_available = min(move.product_qty, move.reserved_availability + move.availability) - total_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, total_available, move.product_uom, context=context) + total_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, total_available, move.product_uom, round=False, context=context) + total_available = float_round(total_available, precision_digits=precision) info = str(total_available) #look in the settings if we need to display the UoM name or not config_ids = settings_obj.search(cr, uid, [], limit=1, order='id DESC', context=context) @@ -1742,7 +1745,8 @@ class stock_move(osv.osv): if move.reserved_availability: if move.reserved_availability != total_available: #some of the available quantity is assigned and some are available but not reserved - reserved_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, move.reserved_availability, move.product_uom, context=context) + reserved_available = uom_obj._compute_qty_obj(cr, uid, move.product_id.uom_id, move.reserved_availability, move.product_uom, round=False, context=context) + reserved_available = float_round(reserved_available, precision_digits=precision) info += _(' (%s reserved)') % str(reserved_available) else: #all available quantity is assigned @@ -3076,7 +3080,7 @@ class stock_warehouse(osv.osv): 'crossdock_route_id': fields.many2one('stock.location.route', 'Crossdock Route'), 'reception_route_id': fields.many2one('stock.location.route', 'Receipt Route'), 'delivery_route_id': fields.many2one('stock.location.route', 'Delivery Route'), - 'resupply_from_wh': fields.boolean('Resupply From Other Warehouses'), + 'resupply_from_wh': fields.boolean('Resupply From Other Warehouses', help='Unused field'), 'resupply_wh_ids': fields.many2many('stock.warehouse', 'stock_wh_resupply_table', 'supplied_wh_id', 'supplier_wh_id', 'Resupply Warehouses'), 'resupply_route_ids': fields.one2many('stock.location.route', 'supplied_wh_id', 'Resupply Routes', help="Routes will be created for these resupply warehouses and you can select them on products and product categories"), @@ -3136,7 +3140,7 @@ class stock_warehouse(osv.osv): pull_obj.create(cr, uid, vals=pull_rule, context=context) #if the warehouse is also set as default resupply method, assign this route automatically to the warehouse if default_resupply_wh and default_resupply_wh.id == wh.id: - self.write(cr, uid, [warehouse.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context) + self.write(cr, uid, [warehouse.id, wh.id], {'route_ids': [(4, inter_wh_route_id)]}, context=context) _defaults = { 'company_id': lambda self, cr, uid, c: self.pool.get('res.company')._company_default_get(cr, uid, 'stock.inventory', context=c), diff --git a/addons/stock/tests/__init__.py b/addons/stock/tests/__init__.py index 4c20f7a769a..c372f3c8fb5 100644 --- a/addons/stock/tests/__init__.py +++ b/addons/stock/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_stock_flow from . import test_owner_available +from . import test_resupply diff --git a/addons/stock/tests/test_resupply.py b/addons/stock/tests/test_resupply.py new file mode 100644 index 00000000000..79ecbf456b7 --- /dev/null +++ b/addons/stock/tests/test_resupply.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +from openerp.addons.stock.tests.common import TestStockCommon +from openerp.tools import mute_logger, float_round + + +class TestResupply(TestStockCommon): + def setUp(self): + super(TestResupply, self).setUp() + self.Warehouse = self.env['stock.warehouse'] + # create 2 WH, BIG and SMALL + # SMALL resupplies from BIG + self.bigwh = self.Warehouse.create({'name': 'BIG', 'code': 'B'}) + self.smallwh = self.Warehouse.create({'name': 'SMALL', 'code': 'S', + 'default_resupply_wh_id': self.bigwh.id, + 'resupply_wh_ids': [(6, 0, [self.bigwh.id])], + }) + # minimum stock rule for Product A on SMALL + Orderpoint = self.env['stock.warehouse.orderpoint'] + Orderpoint.create({'warehouse_id': self.smallwh.id, + 'location_id': self.smallwh.lot_stock_id.id, + 'product_id': self.productA.id, + 'product_min_qty': 100, + 'product_max_qty': 200, + 'product_uom': self.uom_unit.id, + }) + # create some stock on BIG + Wiz = self.env['stock.change.product.qty'] + wiz = Wiz.create({'product_id': self.productA.id, + 'new_quantity': 1000, + 'location_id': self.bigwh.lot_stock_id.id, + }) + wiz.change_product_qty() + + def test_resupply_from_wh(self): + sched = self.env['procurement.order'] + sched.run_scheduler() + # we generated 2 procurements for product A: one on small wh and the + # other one on the transit location + procs = sched.search([('product_id', '=', self.productA.id)]) + self.assertEqual(len(procs), 2) + proc1 = sched.search([('product_id', '=', self.productA.id), + ('warehouse_id', '=', self.smallwh.id)]) + self.assertEqual(proc1.state, 'running') + proc2 = sched.search([('product_id', '=', self.productA.id), + ('warehouse_id', '=', self.bigwh.id)]) + self.assertEqual(proc2.location_id.usage, 'transit') + self.assertNotEqual(proc2.state, 'exception') + proc2.run() + self.assertEqual(proc2.state, 'running') + self.assertTrue(proc2.rule_id) diff --git a/addons/stock/views/report_stockpicking_operations.xml b/addons/stock/views/report_stockpicking_operations.xml index 7350302fffd..641d26199ba 100644 --- a/addons/stock/views/report_stockpicking_operations.xml +++ b/addons/stock/views/report_stockpicking_operations.xml @@ -107,7 +107,7 @@ <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', pack_operation.lot_id.name, 600, 100)" style="width:300px;height:50px"/> </span> <span t-if="pack_operation.product_id and not pack_operation.lot_id and pack_operation.product_id.barcode"> - <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Barcode', pack_operation.product_id.barcode, 600, 100)" style="width:300px;height:50px"/> + <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('EAN13', pack_operation.product_id.barcode, 600, 100)" style="width:300px;height:50px"/> </span> <span t-if="pack_operation.package_id and not pack_operation.product_id"> <img t-att-src="'/report/barcode/?type=%s&value=%s&width=%s&height=%s' % ('Code128', pack_operation.package_id.name, 600, 100)" style="width:300px;height:50px"/> diff --git a/addons/stock_account/stock.py b/addons/stock_account/stock.py index 6e3e81be447..e4903326670 100644 --- a/addons/stock_account/stock.py +++ b/addons/stock_account/stock.py @@ -214,7 +214,7 @@ class stock_picking(osv.osv): @return: object of the partner to invoice """ return picking.partner_id and picking.partner_id.id - + def action_invoice_create(self, cr, uid, ids, journal_id, group=False, type='out_invoice', context=None): """ Creates invoice based on the invoice state selected for picking. @param journal_id: Id of journal diff --git a/addons/survey/security/survey_security.xml b/addons/survey/security/survey_security.xml index 07989d79181..afaf592686b 100644 --- a/addons/survey/security/survey_security.xml +++ b/addons/survey/security/survey_security.xml @@ -52,7 +52,7 @@ <record id="survey_input_users_access" model="ir.rule"> <field name="name">Access to user_input for regular users</field> <field name="model_id" ref="survey.model_survey_user_input"/> - <field name="domain_force">[('create_uid', '=', user.id)]</field> + <field name="domain_force">['|', ('create_uid', '=', user.id), ('partner_id', '=', user.partner_id.id)]</field> <field name="groups" eval="[(4, ref('base.group_survey_user'))]"/> <field eval="0" name="perm_unlink"/> <field eval="0" name="perm_write"/> diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 40733f97309..16a0592dde8 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -3,6 +3,7 @@ from openerp.osv import fields, osv from openerp.tools.translate import _ +from openerp import SUPERUSER_ID from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DF from openerp.addons.website.models.website import slug from urlparse import urljoin @@ -1140,7 +1141,7 @@ class survey_user_input_line(osv.Model): ('question_id', '=', question.id)], context=context) if old_uil: - self.unlink(cr, uid, old_uil, context=context) + self.unlink(cr, SUPERUSER_ID, old_uil, context=context) if answer_tag in post and post[answer_tag].strip() != '': vals.update({'answer_type': 'suggestion', 'value_suggested': post[answer_tag]}) @@ -1171,7 +1172,7 @@ class survey_user_input_line(osv.Model): ('question_id', '=', question.id)], context=context) if old_uil: - self.unlink(cr, uid, old_uil, context=context) + self.unlink(cr, SUPERUSER_ID, old_uil, context=context) ca = dict_keys_startswith(post, answer_tag) comment_answer = ca.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() @@ -1202,7 +1203,7 @@ class survey_user_input_line(osv.Model): ('question_id', '=', question.id)], context=context) if old_uil: - self.unlink(cr, uid, old_uil, context=context) + self.unlink(cr, SUPERUSER_ID, old_uil, context=context) no_answers = True ca = dict_keys_startswith(post, answer_tag) diff --git a/addons/web/static/src/css/ie.css b/addons/web/static/src/css/ie.css index f628ed1213d..a547120033e 100644 --- a/addons/web/static/src/css/ie.css +++ b/addons/web/static/src/css/ie.css @@ -24,9 +24,6 @@ } /** web_kanban */ -:root .openerp .oe_kanban_view .oe_kanban_group_title_vertical { - writing-mode: lr-tb; -} .openerp .oe_kanban_column_cards { min-height: 80px; } diff --git a/addons/web/static/src/js/control_panel.js b/addons/web/static/src/js/control_panel.js index b2d603b53bf..5e2cc36a6a1 100644 --- a/addons/web/static/src/js/control_panel.js +++ b/addons/web/static/src/js/control_panel.js @@ -189,7 +189,7 @@ var ControlPanel = Widget.extend({ function make_breadcrumb (bc, is_last) { var $bc = $('<li>') - .append(is_last ? bc.title : $('<a>').html(bc.title)) + .append(is_last ? _.escape(bc.title) : $('<a>').text(bc.title)) .toggleClass('active', is_last); if (!is_last) { $bc.click(function () { diff --git a/addons/web_editor/models/ir_ui_view.py b/addons/web_editor/models/ir_ui_view.py index dd7501c44f9..d0d18b5ee41 100644 --- a/addons/web_editor/models/ir_ui_view.py +++ b/addons/web_editor/models/ir_ui_view.py @@ -5,6 +5,7 @@ from openerp.exceptions import AccessError from openerp.osv import osv from lxml import etree, html from openerp import api +from openerp.tools import html_escape class view(osv.osv): @@ -73,6 +74,14 @@ class view(osv.osv): # ensure there's only one match [root] = arch.xpath(section_xpath) + # html text need to be escaped for xml storage + def escape_node(node): + node.text = node.text and html_escape(node.text) + node.tail = node.tail and html_escape(node.tail) + escape_node(replacement) + for descendant in replacement.iterdescendants(): + escape_node(descendant) + root.text = replacement.text root.tail = replacement.tail # replace all children diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js index 5b6b6d593e9..467f0042090 100644 --- a/addons/web_editor/static/src/js/rte.summernote.js +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -214,6 +214,8 @@ eventHandler.popover.button.update = function ($container, oStyle) { $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length); $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin")); + $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow")); + $container.find('.btn-group:has(button[data-event="imageShape"])').removeClass("hidden"); } else { diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index 4a56edd3c53..c304ec1b04a 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -186,7 +186,7 @@ class Website(openerp.addons.web.controllers.main.Home): request.registry['website.menu'].create( request.cr, request.uid, { 'name': path, - 'url': "/page/" + xml_id, + 'url': "/page/" + xml_id[8:], 'parent_id': request.website.menu_id.id, 'website_id': request.website.id, }, context=request.context) diff --git a/addons/website/data/data.xml b/addons/website/data/data.xml index 8ba468c4b45..f30068b72e9 100644 --- a/addons/website/data/data.xml +++ b/addons/website/data/data.xml @@ -24,7 +24,7 @@ <record id="menu_contactus" model="website.menu"> <field name="name">Contact us</field> - <field name="url">/page/website.contactus</field> + <field name="url">/page/contactus</field> <field name="parent_id" ref="website.main_menu"/> <field name="sequence" type="int">60</field> <field name="website_id" ref="default_website"/> @@ -79,7 +79,7 @@ </record> <record id="action_website_homepage" model="ir.actions.act_url"> <field name="name">Website Homepage</field> - <field name="url">/page/website.homepage</field> + <field name="url">/page/homepage</field> <field name="target">self</field> </record> diff --git a/addons/website/static/src/js/website.js b/addons/website/static/src/js/website.js index 6d656aa3936..f049593e8d7 100644 --- a/addons/website/static/src/js/website.js +++ b/addons/website/static/src/js/website.js @@ -113,8 +113,7 @@ var prompt = function (options, _qweb) { field.focus(); dialog.on('click', '.btn-primary', function () { def.resolve(field.val(), field, dialog); - dialog.remove(); - $('.modal-backdrop').remove(); + dialog.modal('hide').remove(); }); }); dialog.on('hidden.bs.modal', function () { @@ -208,7 +207,7 @@ base.ready().then(function () { var redirect = { lang: self.data('lang'), url: encodeURIComponent(self.attr('href').replace(/[&?]edit_translations[^&?]+/, '')), - hash: location.hash + hash: encodeURIComponent(location.hash) }; location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect); }); diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js index 86d0717d979..9cc40247b89 100644 --- a/addons/website/static/src/js/website.snippets.editor.js +++ b/addons/website/static/src/js/website.snippets.editor.js @@ -81,17 +81,24 @@ options.registry.slider = options.Class.extend({ this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>'); // clone the best candidate from template to use new features - var $snippets = this.buildingBlock.$snippets.find('.oe_snippet_body.carousel'); - var point = 0; - var selection; - var className = _.compact(this.$target.attr("class").split(" ")); - $snippets.each(function () { - var len = _.intersection(_.compact(this.className.split(" ")), className).length; - if (len > point) { - point = len; - selection = this; - } - }); + var $snippets = this.BuildingBlock.$snippets; + //since saas-6, all snippets must start by s_ + var selection = this.$target.closest('[class*="s_"'); + if (_.isUndefined(selection)) { + var point = 0; + var className = _.compact(this.$target.attr("class").split(" ")); + $snippets.find('.oe_snippet_body').each(function () { + var len = _.intersection(_.compact(this.className.split(" ")), className).length; + if (len > point) { + point = len; + selection = this; + } + }); + } + else { + var s_class = selection.attr('class').split(' ').filter(function(o) { return _.str.startsWith(o, "s_") })[0] + selection = $snippets.find("." + s_class); + } var $clone = $(selection).find('.item:first').clone(); // insert diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index 1adb11712ec..c8e7efb9044 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -174,6 +174,16 @@ class TestViewSaving(common.TransactionCase): ) ) + def test_save_escaped_text(self): + view_id = self.registry('ir.ui.view').create(self.cr, self.uid, { + 'arch':'<t>hello world</t>', + 'type':'qweb' + }) + view = self.registry('ir.ui.view').browse(self.cr, self.uid, view_id) + replacement = 'hello world & <angle brackets>!' + view.save(replacement, xpath='/t') + self.assertEqual(view.render(), replacement, 'html special characters wrongly escaped') + def test_save_only_embedded(self): Company = self.registry('res.company') company_id = 1 diff --git a/addons/website/views/website_templates.xml b/addons/website/views/website_templates.xml index dacd1454bf0..ad5b53ac608 100644 --- a/addons/website/views/website_templates.xml +++ b/addons/website/views/website_templates.xml @@ -59,16 +59,15 @@ <template id="submenu" name="Submenu"> <li t-if="not submenu.child_id" t-att-class=" - ((submenu.url and submenu.url != '/' and request.httprequest.path.startswith(submenu.url)) or - request.httprequest.path == submenu.url) and 'active' + 'active' if submenu.url and submenu.url != '/' and request.httprequest.path == submenu.url.replace('/page/website.', '/page/') else None "> <a t-att-href="(website.menu_id.child_id[0] == submenu) and '/' or submenu.url" t-ignore="true" t-att-target="'_blank' if submenu.new_window else None"> <span t-field="submenu.name"/> </a> </li> <li t-if="submenu.child_id" t-attf-class="dropdown #{ - ((submenu.url and submenu.url != '/' and [1 for submenu in submenu.child_id if request.httprequest.path.startswith(submenu.url)]) or - request.httprequest.path == submenu.url) and 'active' + (submenu.url and submenu.url != '/' and any([request.httprequest.path == child.url.replace('/page/website.', '/page/') for child in submenu.child_id]) or + request.httprequest.path == submenu.url.replace('/page/website.', '/page/')) and 'active' }"> <a class="dropdown-toggle" data-toggle="dropdown" href="#"> <span t-field="submenu.name"/> <span class="caret" t-ignore="true"></span> diff --git a/addons/website_crm/data/website_crm_data.xml b/addons/website_crm/data/website_crm_data.xml index ada2e73e71d..8d6c916fffa 100644 --- a/addons/website_crm/data/website_crm_data.xml +++ b/addons/website_crm/data/website_crm_data.xml @@ -5,7 +5,7 @@ <record id="action_open_website" model="ir.actions.act_url"> <field name="name">Website Contact Form</field> <field name="target">self</field> - <field name="url">/page/website.contactus</field> + <field name="url">/page/contactus</field> </record> <record id="base.open_menu" model="ir.actions.todo"> <field name="action_id" ref="action_open_website"/> diff --git a/addons/website_hr/data/website_hr_data.xml b/addons/website_hr/data/website_hr_data.xml index 161b875556a..5afe68add8f 100644 --- a/addons/website_hr/data/website_hr_data.xml +++ b/addons/website_hr/data/website_hr_data.xml @@ -5,7 +5,7 @@ <record id="action_open_website" model="ir.actions.act_url"> <field name="name">Website About</field> <field name="target">self</field> - <field name="url">/page/website.aboutus</field> + <field name="url">/page/aboutus</field> </record> <record id="base.open_menu" model="ir.actions.todo"> <field name="action_id" ref="action_open_website"/> diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index 50e49230500..d35ef34f3c1 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -554,7 +554,7 @@ class website_sale(http.Controller): partner_lang = request.lang if request.lang in [lang.code for lang in request.website.language_ids] else None - billing_info = {} + billing_info = {'customer': True} if partner_lang: billing_info['lang'] = partner_lang billing_info.update(self.checkout_parse('billing', checkout, True)) diff --git a/addons/website_sale/static/src/js/website_sale.js b/addons/website_sale/static/src/js/website_sale.js index 4c160e0018b..67f7a47be35 100644 --- a/addons/website_sale/static/src/js/website_sale.js +++ b/addons/website_sale/static/src/js/website_sale.js @@ -173,7 +173,7 @@ $('.oe_website_sale').each(function () { return; } if (!data.quantity) { - location.reload(); + location.reload(true); return; } var $q = $(".my_cart_quantity"); diff --git a/doc/cla/corporate/hbee.md b/doc/cla/corporate/hbee.md new file mode 100644 index 00000000000..aaebe686d00 --- /dev/null +++ b/doc/cla/corporate/hbee.md @@ -0,0 +1,21 @@ +Lithuania, Macedonia, 2015-07-31 + +HBEE agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Paulius Sladkevičius paulius@hbee.eu https://github.com/komsas + +List of contributors: + +Darko Nikolovski darko@hbee.eu https://github.com/DarkoNikolovski +Ignas Karpiejus ignas@hbee.eu https://github.com/ignask +Karolis Kalantojus karolis@hbee.eu https://github.com/kkarolis +Kiril Vangelovski kiril@hbee.eu https://github.com/kirca +Naglis Jonaitis naglis@hbee.eu https://github.com/naglis +Paulius Sladkevičius paulius@hbee.eu https://github.com/komsas +Tomas Račys tomas@hbee.eu https://github.com/taezaz diff --git a/doc/cla/corporate/rossa.md b/doc/cla/corporate/rossa.md new file mode 100644 index 00000000000..5f274745bd5 --- /dev/null +++ b/doc/cla/corporate/rossa.md @@ -0,0 +1,14 @@ +Paraguay, 2015-07-09 + +Rossa S.A. agrees to the terms of the Odoo Corporate Contributor License Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this declaration. + +Signed, + +Rossa S.A. emaildarossa@gmail.com https://github.com/rossasa + +List of contributors: + +Marcelo Pickler marcelo@rossa.com.py https://github.com/loxamir +Alan R. S. Pieske alan@rossa.com.py https://github.com/alanpy diff --git a/doc/cla/corporate/willow-it.md b/doc/cla/corporate/willow-it.md new file mode 100644 index 00000000000..9e3480e6cb4 --- /dev/null +++ b/doc/cla/corporate/willow-it.md @@ -0,0 +1,18 @@ +Australia, 2015-07-20 + +Willow IT agrees to the terms of the Odoo Corporate Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Richard deMeester richard@willowit.com.au https://github.com/richard-willowit + +List of contributors: + +Richard deMeester richard@willowit.com.au https://github.com/richard-willowit +Douglas Parker doug@willowit.com.au https://github.com/doug-willowit +Bhavya Malik bhavya@willowit.com.au https://github.com/bhavya-willowit +Jon Wilson jon@willowit.com.au https://github.com/jon-willowit diff --git a/doc/cla/individual/cysnake4713.md b/doc/cla/individual/cysnake4713.md new file mode 100644 index 00000000000..911c1b318e9 --- /dev/null +++ b/doc/cla/individual/cysnake4713.md @@ -0,0 +1,11 @@ +China, 2015-07-09 + +I hereby agree to the terms of the Odoo Individual Contributor License +Agreement v1.0. + +I declare that I am authorized and able to make this agreement and sign this +declaration. + +Signed, + +Matt Cai 蔡旸 cysnake4713@gmail.com https://github.com/cysnake4713 diff --git a/openerp/addons/base/ir/ir_ui_view.py b/openerp/addons/base/ir/ir_ui_view.py index 2d5678a0a37..97b5fbe17fe 100644 --- a/openerp/addons/base/ir/ir_ui_view.py +++ b/openerp/addons/base/ir/ir_ui_view.py @@ -746,7 +746,7 @@ class view(osv.osv): orm.transfer_field_to_modifiers(field, modifiers) elif node.tag in ('form', 'tree'): - result = Model.view_header_get(cr, user, False, node.tag, context) + result = Model.view_header_get(cr, user, False, node.tag, context=context) if result: node.set('string', result) in_tree_view = node.tag == 'tree' @@ -845,17 +845,17 @@ class view(osv.osv): if node.tag == 'diagram': if node.getchildren()[0].tag == 'node': node_model = self.pool[node.getchildren()[0].get('object')] - node_fields = node_model.fields_get(cr, user, None, context) + node_fields = node_model.fields_get(cr, user, None, context=context) fields.update(node_fields) if not node.get("create") and \ not node_model.check_access_rights(cr, user, 'create', raise_exception=False) or \ not context.get("create", True): node.set("create", 'false') if node.getchildren()[1].tag == 'arrow': - arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context) + arrow_fields = self.pool[node.getchildren()[1].get('object')].fields_get(cr, user, None, context=context) fields.update(arrow_fields) else: - fields = Model.fields_get(cr, user, None, context) + fields = Model.fields_get(cr, user, None, context=context) node = self.add_on_change(cr, user, model, node) fields_def = self.postprocess(cr, user, model, node, view_id, False, fields, context=context) @@ -1006,7 +1006,7 @@ class view(osv.osv): def get_trans(text): if not text or not text.strip(): return None - text = h.unescape(text.strip()) + text = text.strip() if len(text) < 2 or (text.startswith('<!') and text.endswith('>')): return None return translate_func(text) diff --git a/openerp/addons/base/res/res_users.py b/openerp/addons/base/res/res_users.py index ce9c4cdd2a1..abd0afdf5bd 100644 --- a/openerp/addons/base/res/res_users.py +++ b/openerp/addons/base/res/res_users.py @@ -126,12 +126,13 @@ class res_users(osv.osv): avatar, ... The user model is now dedicated to technical data. """ __admin_ids = {} - _uid_cache = {} + __uid_cache = {} _inherits = { 'res.partner': 'partner_id', } _name = "res.users" _description = 'Users' + _order = 'name, login' def _set_new_password(self, cr, uid, id, name, value, args, context=None): if value is False: @@ -311,6 +312,24 @@ class res_users(osv.osv): return result + def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True): + if uid != SUPERUSER_ID: + groupby_fields = set([groupby] if isinstance(groupby, basestring) else groupby) + if groupby_fields.intersection(USER_PRIVATE_FIELDS): + raise openerp.exceptions.AccessError('Invalid groupby') + return super(res_users, self).read_group( + cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy) + + def _search(self, cr, user, args, offset=0, limit=None, order=None, context=None, count=False, access_rights_uid=None): + if user != SUPERUSER_ID and args: + domain_terms = [term for term in args if isinstance(term, (tuple, list))] + domain_fields = set(left for (left, op, right) in domain_terms) + if domain_fields.intersection(USER_PRIVATE_FIELDS): + raise openerp.exceptions.AccessError('Invalid search criterion') + return super(res_users, self)._search( + cr, user, args, offset=offset, limit=limit, order=order, context=context, count=count, + access_rights_uid=access_rights_uid) + def create(self, cr, uid, vals, context=None): user_id = super(res_users, self).create(cr, uid, vals, context=context) user = self.browse(cr, uid, user_id, context=context) @@ -353,10 +372,10 @@ class res_users(osv.osv): clear = partial(self.pool['ir.rule'].clear_cache, cr) map(clear, ids) db = cr.dbname - if db in self._uid_cache: + if db in self.__uid_cache: for id in ids: - if id in self._uid_cache[db]: - del self._uid_cache[db][id] + if id in self.__uid_cache[db]: + del self.__uid_cache[db][id] self.context_get.clear_cache(self) self.has_group.clear_cache(self) return res @@ -365,10 +384,10 @@ class res_users(osv.osv): if 1 in ids: raise UserError(_('You can not remove the admin user as it is used internally for resources created by Odoo (updates, module installation, ...)')) db = cr.dbname - if db in self._uid_cache: + if db in self.__uid_cache: for id in ids: - if id in self._uid_cache[db]: - del self._uid_cache[db][id] + if id in self.__uid_cache[db]: + del self.__uid_cache[db][id] return super(res_users, self).unlink(cr, uid, ids, context=context) def name_search(self, cr, user, name='', args=None, operator='ilike', context=None, limit=100): @@ -504,15 +523,12 @@ class res_users(osv.osv): if not passwd: # empty passwords disallowed for obvious security reasons raise openerp.exceptions.AccessDenied() - if self._uid_cache.get(db, {}).get(uid) == passwd: + if self.__uid_cache.setdefault(db, {}).get(uid) == passwd: return cr = self.pool.cursor() try: self.check_credentials(cr, uid, passwd) - if self._uid_cache.has_key(db): - self._uid_cache[db][uid] = passwd - else: - self._uid_cache[db] = {uid:passwd} + self.__uid_cache[db][uid] = passwd finally: cr.close() diff --git a/openerp/addons/base/tests/test_search.py b/openerp/addons/base/tests/test_search.py index df8b317cb52..bd9d5089356 100644 --- a/openerp/addons/base/tests/test_search.py +++ b/openerp/addons/base/tests/test_search.py @@ -106,6 +106,21 @@ class test_search(common.TransactionCase): test_user_ids = filter(lambda x: x in expected_ids, user_ids) self.assertEqual(test_user_ids, expected_ids, 'search on res_users did not provide expected ids or expected order') + def test_11_indirect_inherits_m2order(self): + registry, cr, uid = self.registry, self.cr, self.uid + Cron = registry('ir.cron') + Users = registry('res.users') + + user_ids = {} + cron_ids = {} + for u in 'BAC': + user_ids[u] = Users.create(cr, uid, {'name': u, 'login': u}) + cron_ids[u] = Cron.create(cr, uid, {'name': u, 'user_id': user_ids[u]}) + + ids = Cron.search(cr, uid, [('id', 'in', cron_ids.values())], order='user_id') + expected_ids = [cron_ids[l] for l in 'ABC'] + self.assertEqual(ids, expected_ids) + if __name__ == '__main__': unittest2.main() diff --git a/openerp/models.py b/openerp/models.py index 73fbc47aae7..a57e5588564 100644 --- a/openerp/models.py +++ b/openerp/models.py @@ -1881,7 +1881,7 @@ class BaseModel(object): gb_function = split[1] if len(split) == 2 else None temporal = field_type in ('date', 'datetime') tz_convert = field_type == 'datetime' and self._context.get('tz') in pytz.all_timezones - qualified_field = self._inherits_join_calc(split[0], query) + qualified_field = self._inherits_join_calc(self._table, split[0], query) if temporal: display_formats = { # Careful with week/year formats: @@ -2041,7 +2041,7 @@ class BaseModel(object): field_formatter = lambda f: ( self._fields[f].group_operator or 'sum', - self._inherits_join_calc(cr, uid, f, query, context=context), + self._inherits_join_calc(cr, uid, self._table, f, query, context=context), f, ) select_terms = ["%s(%s) AS %s" % field_formatter(f) for f in aggregated_fields] @@ -2118,17 +2118,18 @@ class BaseModel(object): return parent_alias @api.model - def _inherits_join_calc(self, field, query): + def _inherits_join_calc(self, alias, field, query): """ Adds missing table select and join clause(s) to ``query`` for reaching the field coming from an '_inherits' parent table (no duplicates). + :param alias: name of the initial SQL alias :param field: name of inherited field to reach :param query: query object on which the JOIN should be added :return: qualified name of field, to be used in SELECT clause """ # INVARIANT: alias is the SQL alias of model._table in query - model, alias = self, self._table + model = self while field in model._inherit_fields and field not in model._columns: # retrieve the parent model where field is inherited from parent_model_name = model._inherit_fields[field][0] @@ -3263,7 +3264,7 @@ class BaseModel(object): # the query may involve several tables: we need fully-qualified names def qualify(field): col = field.name - res = self._inherits_join_calc(field.name, query) + res = self._inherits_join_calc(self._table, field.name, query) if field.type == 'binary' and (context.get('bin_size') or context.get('bin_size_' + col)): # PG 9.2 introduces conflicting pg_size_pretty(numeric) -> need ::cast res = 'pg_size_pretty(length(%s)::bigint)' % res @@ -4522,7 +4523,7 @@ class BaseModel(object): for inherited_model in self._inherits: rule_where_clause, rule_where_clause_params, rule_tables = rule_obj.domain_get(cr, uid, inherited_model, mode, context=context) apply_rule(rule_where_clause, rule_where_clause_params, rule_tables, - parent_model=inherited_model) + parent_model=inherited_model) @api.model def _generate_translated_field(self, table_alias, field, query): @@ -4546,7 +4547,7 @@ class BaseModel(object): return '"%s"."%s"' % (table_alias, field) @api.model - def _generate_m2o_order_by(self, order_field, query): + def _generate_m2o_order_by(self, alias, order_field, query, reverse_direction): """ Add possibly missing JOIN to ``query`` and generate the ORDER BY clause for m2o fields, either native m2o fields or function/related fields that are stored, including @@ -4556,10 +4557,10 @@ class BaseModel(object): """ if order_field not in self._columns and order_field in self._inherit_fields: # also add missing joins for reaching the table containing the m2o field - qualified_field = self._inherits_join_calc(order_field, query) + qualified_field = self._inherits_join_calc(alias, order_field, query) order_field_column = self._inherit_fields[order_field][2] else: - qualified_field = '"%s"."%s"' % (self._table, order_field) + qualified_field = '"%s"."%s"' % (alias, order_field) order_field_column = self._columns[order_field] assert order_field_column._type == 'many2one', 'Invalid field passed to _generate_m2o_order_by()' @@ -4570,24 +4571,68 @@ class BaseModel(object): return # figure out the applicable order_by for the m2o - dest_model = self.pool[order_field_column._obj] + dest_model = self.env[order_field_column._obj] m2o_order = dest_model._order if not regex_order.match(m2o_order): # _order is complex, can't use it here, so we default to _rec_name m2o_order = dest_model._rec_name - else: - # extract the field names, to be able to qualify them and add desc/asc - m2o_order_list = [] - for order_part in m2o_order.split(","): - m2o_order_list.append(order_part.strip().split(" ", 1)[0].strip()) - m2o_order = m2o_order_list # Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here # as we don't want to exclude results that have NULL values for the m2o src_table, src_field = qualified_field.replace('"', '').split('.', 1) dst_alias, dst_alias_statement = query.add_join((src_table, dest_model._table, src_field, 'id', src_field), implicit=False, outer=True) - qualify = lambda field: '"%s"."%s"' % (dst_alias, field) - return map(qualify, m2o_order) if isinstance(m2o_order, list) else qualify(m2o_order) + return dest_model._generate_order_by_inner(dst_alias, m2o_order, query, + reverse_direction=reverse_direction) + + @api.model + def _generate_order_by_inner(self, alias, order_spec, query, reverse_direction=False): + order_by_elements = [] + self._check_qorder(order_spec) + for order_part in order_spec.split(','): + order_split = order_part.strip().split(' ') + order_field = order_split[0].strip() + order_direction = order_split[1].strip().upper() if len(order_split) == 2 else '' + if reverse_direction: + order_direction = 'ASC' if order_direction == 'DESC' else 'DESC' + do_reverse = order_direction == 'DESC' + order_column = None + inner_clauses = [] + add_dir = False + if order_field == 'id': + order_by_elements.append('"%s"."%s" %s' % (alias, order_field, order_direction)) + elif order_field in self._columns: + order_column = self._columns[order_field] + if order_column._classic_read: + if order_column.translate and not callable(order_column.translate): + inner_clauses = [self._generate_translated_field(alias, order_field, query)] + else: + inner_clauses = ['"%s"."%s"' % (alias, order_field)] + add_dir = True + elif order_column._type == 'many2one': + inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse) + else: + continue # ignore non-readable or "non-joinable" fields + elif order_field in self._inherit_fields: + parent_obj = self.pool[self._inherit_fields[order_field][3]] + order_column = parent_obj._columns[order_field] + if order_column._classic_read: + inner_clauses = [self._inherits_join_calc(alias, order_field, query)] + add_dir = True + elif order_column._type == 'many2one': + inner_clauses = self._generate_m2o_order_by(alias, order_field, query, do_reverse) + else: + continue # ignore non-readable or "non-joinable" fields + else: + raise ValueError(_("Sorting field %s not found on model %s") % (order_field, self._name)) + if order_column and order_column._type == 'boolean': + inner_clauses = ["COALESCE(%s, false)" % inner_clauses[0]] + + for clause in inner_clauses: + if add_dir: + order_by_elements.append("%s %s" % (clause, order_direction)) + else: + order_by_elements.append(clause) + return order_by_elements @api.model def _generate_order_by(self, order_spec, query): @@ -4600,46 +4645,7 @@ class BaseModel(object): order_by_clause = '' order_spec = order_spec or self._order if order_spec: - order_by_elements = [] - self._check_qorder(order_spec) - for order_part in order_spec.split(','): - order_split = order_part.strip().split(' ') - order_field = order_split[0].strip() - order_direction = order_split[1].strip() if len(order_split) == 2 else '' - order_column = None - inner_clause = None - if order_field == 'id': - order_by_elements.append('"%s"."%s" %s' % (self._table, order_field, order_direction)) - elif order_field in self._columns: - order_column = self._columns[order_field] - if order_column._classic_read: - if order_column.translate and not callable(order_column.translate): - inner_clause = self._generate_translated_field(self._table, order_field, query) - else: - inner_clause = '"%s"."%s"' % (self._table, order_field) - elif order_column._type == 'many2one': - inner_clause = self._generate_m2o_order_by(order_field, query) - else: - continue # ignore non-readable or "non-joinable" fields - elif order_field in self._inherit_fields: - parent_obj = self.pool[self._inherit_fields[order_field][3]] - order_column = parent_obj._columns[order_field] - if order_column._classic_read: - inner_clause = self._inherits_join_calc(order_field, query) - elif order_column._type == 'many2one': - inner_clause = self._generate_m2o_order_by(order_field, query) - else: - continue # ignore non-readable or "non-joinable" fields - else: - raise ValueError(_("Sorting field %s not found on model %s") % ( order_field, self._name)) - if order_column and order_column._type == 'boolean': - inner_clause = "COALESCE(%s, false)" % inner_clause - if inner_clause: - if isinstance(inner_clause, list): - for clause in inner_clause: - order_by_elements.append("%s %s" % (clause, order_direction)) - else: - order_by_elements.append("%s %s" % (inner_clause, order_direction)) + order_by_elements = self._generate_order_by_inner(self._table, order_spec, query) if order_by_elements: order_by_clause = ",".join(order_by_elements) diff --git a/openerp/osv/fields.py b/openerp/osv/fields.py index 04707788109..5640e97e7de 100644 --- a/openerp/osv/fields.py +++ b/openerp/osv/fields.py @@ -979,12 +979,11 @@ class many2many(_column): wquery = obj._where_calc(cr, user, domain, context=context) obj._apply_ir_rules(cr, user, wquery, 'read', context=context) + order_by = obj._generate_order_by(cr, user, None, wquery, context=context) from_c, where_c, where_params = wquery.get_sql() if where_c: where_c = ' AND ' + where_c - order_by = ' ORDER BY "%s".%s' %(obj._table, obj._order.split(',')[0]) - limit_str = '' if self._limit is not None: limit_str = ' LIMIT %d' % self._limit diff --git a/openerp/tests/common.py b/openerp/tests/common.py index 17678cdbe1f..c7ac812d91f 100644 --- a/openerp/tests/common.py +++ b/openerp/tests/common.py @@ -344,6 +344,7 @@ class HttpCase(TransactionCase): phantom.terminate() phantom.wait() self._wait_remaining_requests() + # we ignore phantomjs return code as we kill it as soon as we have ok _logger.info("phantom_run execution finished") def _wait_remaining_requests(self): |
