diff --git a/addons/account/models/account.py b/addons/account/models/account.py index b98390b8d04..8cd3943d9a1 100644 --- a/addons/account/models/account.py +++ b/addons/account/models/account.py @@ -661,6 +661,14 @@ class AccountTax(models.Model): recs = self.browse(cr, uid, ids, context=context) return recs.compute_all(price_unit, currency, quantity, product, partner) + @api.model + def _fix_tax_included_price(self, price, prod_taxes, line_taxes): + """Subtract tax amount from price when corresponding "price included" taxes do not apply""" + # FIXME get currency in param? + incl_tax = prod_taxes.filtered(lambda tax: tax.id not in line_taxes and tax.price_include) + if incl_tax: + return incl_tax.compute_all(price)['total_excluded'] + return price class AccountOperationTemplate(models.Model): _name = "account.operation.template" diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 34932b7c1a3..e84961a2657 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -559,7 +559,7 @@ class AccountInvoice(models.Model): if self.currency_id != company_currency: currency = self.currency_id.with_context(date=self.date_invoice or fields.Date.context_today(self)) line['currency_id'] = currency.id - line['amount_currency'] = line['price'] + line['amount_currency'] = currency.round(line['price']) line['price'] = currency.compute(line['price'], company_currency) else: line['currency_id'] = False diff --git a/addons/account/static/src/js/account_reconciliation_widgets.js b/addons/account/static/src/js/account_reconciliation_widgets.js index b3ad4ee874d..b705b3ddb00 100644 --- a/addons/account/static/src/js/account_reconciliation_widgets.js +++ b/addons/account/static/src/js/account_reconciliation_widgets.js @@ -1832,9 +1832,11 @@ var bankStatementReconciliationLine = abstractReconciliationLine.extend({ changePartnerClickHandler: function() { var self = this; + var partner_name = self.st_line.partner_name; $.when(self.changePartner(false)).then(function(){ self.$(".change_partner_container").show(); self.$(".partner_name").hide(); + self.$(".change_partner_container").find("input").attr("placeholder", partner_name); self.change_partner_field.$dropdown.trigger("click"); }); }, diff --git a/addons/account/views/partner_view.xml b/addons/account/views/partner_view.xml index fc257fa2433..8f590996464 100644 --- a/addons/account/views/partner_view.xml +++ b/addons/account/views/partner_view.xml @@ -39,11 +39,11 @@ - + - + diff --git a/addons/auth_signup/views/auth_signup_login.xml b/addons/auth_signup/views/auth_signup_login.xml index 8e63c143cbd..fb201c98d81 100644 --- a/addons/auth_signup/views/auth_signup_login.xml +++ b/addons/auth_signup/views/auth_signup_login.xml @@ -39,7 +39,9 @@ - + + + diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 68c5db17ab2..0e711541477 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -363,6 +363,9 @@ class gamification_challenge(osv.Model): Create goals that haven't been created yet (eg: if added users) Recompute the current value for each goal related""" + goal_obj = self.pool['gamification.goal'] + goal_ids = goal_obj.search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context) + goal_obj.unlink(cr, uid, goal_ids, context=context) return self._update_all(cr, uid, ids=ids, context=context) def action_report_progress(self, cr, uid, ids, context=None): diff --git a/addons/google_drive/google_drive.py b/addons/google_drive/google_drive.py index f5f90b10d6e..dd723c4f10d 100644 --- a/addons/google_drive/google_drive.py +++ b/addons/google_drive/google_drive.py @@ -62,7 +62,7 @@ class config(osv.Model): client_secret=google_drive_client_secret, grant_type="refresh_token", scope=scope or 'https://www.googleapis.com/auth/drive')) - headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"} + headers = {"Content-type": "application/x-www-form-urlencoded"} try: req = urllib2.Request('https://accounts.google.com/o/oauth2/token', data, headers) content = urllib2.urlopen(req, timeout=TIMEOUT).read() @@ -82,7 +82,7 @@ class config(osv.Model): access_token = self.get_access_token(cr, uid, context=context) # Copy template in to drive with help of new access token request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, access_token) - headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"} + headers = {"Content-type": "application/x-www-form-urlencoded"} try: req = urllib2.Request(request_url, None, headers) parents = urllib2.urlopen(req, timeout=TIMEOUT).read() diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index 083e5cd5557..0b0f58027ab 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -52,60 +52,37 @@ class EscposDriver(Thread): self.lock = Lock() self.status = {'status':'connecting', 'messages':[]} - def supported_devices(self): - if not os.path.isfile('escpos_devices.pickle'): - return supported_devices.device_list - else: - try: - f = open('escpos_devices.pickle','r') - return pickle.load(f) - f.close() - except Exception as e: - self.set_status('error',str(e)) - return supported_devices.device_list - - def add_supported_device(self,device_string): - r = re.compile('[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}'); - match = r.search(device_string) - if match: - match = match.group().split(':') - vendor = int(match[0],16) - product = int(match[1],16) - name = device_string.split('ID') - if len(name) >= 2: - name = name[1] - else: - name = name[0] - _logger.info('ESC/POS: adding support for device: '+match[0]+':'+match[1]+' '+name) - - device_list = supported_devices.device_list[:] - if os.path.isfile('escpos_devices.pickle'): - try: - f = open('escpos_devices.pickle','r') - device_list = pickle.load(f) - f.close() - except Exception as e: - self.set_status('error',str(e)) - device_list.append({ - 'vendor': vendor, - 'product': product, - 'name': name, - }) - - try: - f = open('escpos_devices.pickle','w+') - f.seek(0) - pickle.dump(device_list,f) - f.close() - except Exception as e: - self.set_status('error',str(e)) - def connected_usb_devices(self): connected = [] - - for device in self.supported_devices(): - if usb.core.find(idVendor=device['vendor'], idProduct=device['product']) != None: - connected.append(device) + + # printers can either define bDeviceClass=7, or they can define one of + # their interfaces with bInterfaceClass=7. This class checks for both. + class FindUsbClass(object): + def __init__(self, usb_class): + self._class = usb_class + def __call__(self, device): + # first, let's check the device + if device.bDeviceClass == self._class: + return True + # transverse all devices and look through their interfaces to + # find a matching class + for cfg in device: + intf = usb.util.find_descriptor(cfg, bInterfaceClass=self._class) + + if intf is not None: + return True + + return False + + printers = usb.core.find(find_all=True, custom_match=FindUsbClass(7)) + + for printer in printers: + connected.append({ + 'vendor': printer.idVendor, + 'product': printer.idProduct, + 'name': usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct) + }) + return connected def lockedstart(self): @@ -377,18 +354,3 @@ class EscposProxy(hw_proxy.Proxy): _logger.info('ESC/POS: PRINT XML RECEIPT') driver.push_task('xml_receipt',receipt) - @http.route('/hw_proxy/escpos/add_supported_device', type='http', auth='none', cors='*') - def add_supported_device(self, device_string): - _logger.info('ESC/POS: ADDED NEW DEVICE:'+device_string) - driver.add_supported_device(device_string) - return "The device:\n"+device_string+"\n has been added to the list of supported devices.Ok" - - @http.route('/hw_proxy/escpos/reset_supported_devices', type='http', auth='none', cors='*') - def reset_supported_devices(self): - try: - os.remove('escpos_devices.pickle') - except Exception as e: - pass - return 'The list of supported devices has been reset to factory defaults.Ok' - - diff --git a/addons/hw_escpos/escpos/__init__.py b/addons/hw_escpos/escpos/__init__.py index 3fdeddee28b..22a5af61029 100644 --- a/addons/hw_escpos/escpos/__init__.py +++ b/addons/hw_escpos/escpos/__init__.py @@ -1 +1 @@ -__all__ = ["constants","escpos","exceptions","printer","supported_devices"] +__all__ = ["constants","escpos","exceptions","printer"] diff --git a/addons/hw_escpos/escpos/supported_devices.py b/addons/hw_escpos/escpos/supported_devices.py deleted file mode 100644 index ad086b5516b..00000000000 --- a/addons/hw_escpos/escpos/supported_devices.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/python - -# This is a list of esc/pos compatible usb printers. The vendor and product ids can be found by -# typing lsusb in a linux terminal, this will give you the ids in the form ID VENDOR:PRODUCT - -device_list = [ - { 'vendor' : 0x04b8, 'product' : 0x0e03, 'name' : 'Epson TM-T20' }, - { 'vendor' : 0x04b8, 'product' : 0x0202, 'name' : 'Epson TM-T70' }, - { 'vendor' : 0x04b8, 'product' : 0x0e15, 'name' : 'Epson TM-T20II' }, -] diff --git a/addons/hw_posbox_upgrade/controllers/main.py b/addons/hw_posbox_upgrade/controllers/main.py index 5ed958d020e..42811820e74 100644 --- a/addons/hw_posbox_upgrade/controllers/main.py +++ b/addons/hw_posbox_upgrade/controllers/main.py @@ -6,6 +6,7 @@ import time import openerp import openerp.addons.hw_proxy.controllers.main as hw_proxy import threading +import subprocess from openerp import http from openerp.http import request from openerp.tools.translate import _ @@ -74,6 +75,14 @@ upgrade_template = """ To upgrade the posbox, click on the upgrade button. The upgrade will take a few minutes. Do not reboot the PosBox during the upgrade. + + Latest patch: + + +""" +upgrade_template += subprocess.check_output("git --work-tree=/home/pi/odoo/ --git-dir=/home/pi/odoo/.git log -1", shell=True).replace("\n", "") +upgrade_template += """ + Upgrade @@ -95,7 +104,7 @@ class PosboxUpgrader(hw_proxy.Proxy): def perform_upgrade(self): self.upgrading.acquire() - os.system('/home/pi/posbox_update.sh') + os.system('/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/posbox_update.sh') self.upgrading.release() return 'SUCCESS' diff --git a/addons/point_of_sale/point_of_sale.py b/addons/point_of_sale/point_of_sale.py index cf8c9b33b9c..228d136d2c8 100644 --- a/addons/point_of_sale/point_of_sale.py +++ b/addons/point_of_sale/point_of_sale.py @@ -521,7 +521,7 @@ class pos_session(osv.osv): }) for journal in pos_config.journal_ids] values.update({ - 'name': self.pool['ir.sequence'].next_by_code(cr, uid, 'pos.session'), + 'name': self.pool['ir.sequence'].next_by_code(cr, uid, 'pos.session', context=context), 'statement_ids': statements, 'config_id': config_id }) diff --git a/addons/point_of_sale/report/pos_details.py b/addons/point_of_sale/report/pos_details.py index e9100e6785e..4625ddbd766 100644 --- a/addons/point_of_sale/report/pos_details.py +++ b/addons/point_of_sale/report/pos_details.py @@ -29,7 +29,7 @@ class pos_details(report_sxw.rml_parse): user_ids = form['user_ids'] or self._get_all_users() company_id = user_obj.browse(self.cr, self.uid, self.uid).company_id.id pos_ids = pos_obj.search(self.cr, self.uid, [('date_order','>=',form['date_start'] + ' 00:00:00'),('date_order','<=',form['date_end'] + ' 23:59:59'),('user_id','in',user_ids),('state','in',['done','paid','invoiced']),('company_id','=',company_id)]) - for pos in pos_obj.browse(self.cr, self.uid, pos_ids): + for pos in pos_obj.browse(self.cr, self.uid, pos_ids, context=self.localcontext): for pol in pos.lines: result = { 'code': pol.product_id.default_code, diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/led_status.sh b/addons/point_of_sale/tools/posbox/configuration/led_status.sh similarity index 100% rename from addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/led_status.sh rename to addons/point_of_sale/tools/posbox/configuration/led_status.sh diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/odoo.conf b/addons/point_of_sale/tools/posbox/configuration/odoo.conf similarity index 100% rename from addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/odoo.conf rename to addons/point_of_sale/tools/posbox/configuration/odoo.conf diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/posbox_update.sh b/addons/point_of_sale/tools/posbox/configuration/posbox_update.sh similarity index 100% rename from addons/point_of_sale/tools/posbox/overwrite_before_init/home/pi/posbox_update.sh rename to addons/point_of_sale/tools/posbox/configuration/posbox_update.sh diff --git a/addons/point_of_sale/tools/posbox/overwrite_after_init/etc/setup_ramdisks.sh b/addons/point_of_sale/tools/posbox/configuration/setup_ramdisks.sh similarity index 100% rename from addons/point_of_sale/tools/posbox/overwrite_after_init/etc/setup_ramdisks.sh rename to addons/point_of_sale/tools/posbox/configuration/setup_ramdisks.sh 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 index 6380e949834..4d32107fac8 100755 --- 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 @@ -5,6 +5,6 @@ # Call all S??* scripts in /etc/rcS.d/ in numerical/alphabetical order # -/etc/setup_ramdisks.sh +/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/setup_ramdisks.sh exec /etc/init.d/rc S 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 index 065b6a05f77..dab16e3b415 100755 --- 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 @@ -20,6 +20,6 @@ fi mkdir -p /var/run/odoo chown pi:pi /var/run/odoo -/home/pi/led_status.sh & +/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/led_status.sh & exit 0 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 index 9bdaa86c584..f9bc0400abc 100755 --- 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 @@ -17,7 +17,7 @@ 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 +CONFIG=/home/pi/odoo/addons/point_of_sale/tools/posbox/configuration/odoo.conf LOGFILE=/var/log/odoo/odoo-server.log PIDFILE=/var/run/${NAME}.pid USER=pi 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 index 781b599d51f..6838f7fa78c 100755 --- 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 @@ -12,7 +12,7 @@ __base="$(basename ${__file} .sh)" 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" +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 avahi-daemon" INSTALLED_PKGS_TO_DELETE="" set +o errexit for CURRENT_PKG in $(echo $PKGS_TO_DELETE); do @@ -56,6 +56,12 @@ sudo -u postgres createuser -s pi mkdir /var/log/odoo chown pi:pi /var/log/odoo +# logrotate is very picky when it comes to file permissions +chown -R root:root /etc/logrotate.d/ +chmod -R 644 /etc/logrotate.d/ +chown root:root /etc/logrotate.conf +chmod 644 /etc/logrotate.conf + echo "* * * * * rm /var/run/odoo/sessions/*" | crontab - update-rc.d odoo defaults diff --git a/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.d/rsyslog b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.d/rsyslog new file mode 100644 index 00000000000..3c4ad13016d --- /dev/null +++ b/addons/point_of_sale/tools/posbox/overwrite_before_init/etc/logrotate.d/rsyslog @@ -0,0 +1,37 @@ +/var/log/syslog +{ + rotate 3 + daily + missingok + notifempty + delaycompress + compress + postrotate + invoke-rc.d rsyslog rotate > /dev/null + endscript +} + +/var/log/mail.info +/var/log/mail.warn +/var/log/mail.err +/var/log/mail.log +/var/log/daemon.log +/var/log/kern.log +/var/log/auth.log +/var/log/user.log +/var/log/lpr.log +/var/log/cron.log +/var/log/debug +/var/log/messages +{ + rotate 3 + daily + missingok + notifempty + compress + delaycompress + sharedscripts + postrotate + invoke-rc.d rsyslog rotate > /dev/null + endscript +} diff --git a/addons/point_of_sale/tools/posbox/posbox_create_image.sh b/addons/point_of_sale/tools/posbox/posbox_create_image.sh index 9e65410f8fe..5b860c65043 100755 --- a/addons/point_of_sale/tools/posbox/posbox_create_image.sh +++ b/addons/point_of_sale/tools/posbox/posbox_create_image.sh @@ -35,6 +35,7 @@ git config core.sparsecheckout true echo "addons/web addons/web_kanban addons/hw_* +addons/point_of_sale/tools/posbox/configuration openerp/ odoo.py" | tee --append .git/info/sparse-checkout > /dev/null git read-tree -mu HEAD diff --git a/addons/product/product.py b/addons/product/product.py index adb238969f7..64ac47de584 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -772,18 +772,24 @@ class product_template(osv.osv): if not name or any(term[0] == 'id' for term in (args or [])): return super(product_template, self).name_search( cr, user, name=name, args=args, operator=operator, context=context, limit=limit) - + template_ids = set() product_product = self.pool['product.product'] - results = product_product.name_search( - cr, user, name, args, operator=operator, context=context, limit=limit) + results = product_product.name_search(cr, user, name, args, operator=operator, context=context, limit=limit) product_ids = [p[0] for p in results] - template_ids = [p.product_tmpl_id.id - for p in product_product.browse( - cr, user, product_ids, context=context)] + for p in product_product.browse(cr, user, product_ids, context=context): + template_ids.add(p.product_tmpl_id.id) + while (results and len(template_ids) < limit): + domain = [('product_tmpl_id', 'not in', list(template_ids))] + results = product_product.name_search( + cr, user, name, args+domain, operator=operator, context=context, limit=limit) + product_ids = [p[0] for p in results] + for p in product_product.browse(cr, user, product_ids, context=context): + template_ids.add(p.product_tmpl_id.id) + # re-apply product.template order + name_get return super(product_template, self).name_search( - cr, user, '', args=[('id', 'in', template_ids)], + cr, user, '', args=[('id', 'in', list(template_ids))], operator='ilike', context=context, limit=limit) class product_product(osv.osv): diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index dc2e5b1f08d..fbc1714fac5 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -18,6 +18,7 @@ class purchase_order(osv.osv): def _amount_all(self, cr, uid, ids, field_name, arg, context=None): res = {} cur_obj = self.pool.get('res.currency') + line_obj = self.pool['purchase.order.line'] for order in self.browse(cr, uid, ids, context=context): res[order.id] = { 'amount_untaxed': 0.0, @@ -27,8 +28,14 @@ class purchase_order(osv.osv): val = val1 = 0.0 cur = order.pricelist_id.currency_id for line in order.order_line: + line_price = line_obj._calc_line_base_price(cr, uid, line, + context=context) + line_qty = line_obj._calc_line_quantity(cr, uid, line, + context=context) + taxes = line.taxes_id.compute_all(line_price, cur, line_qty, + line.product_id, order.partner_id) val1 += line.price_subtotal - for c in line.taxes_id.compute_all(line.price_unit, cur, line.product_qty, product=line.product_id, partner=order.partner_id)['taxes']: + for c in taxes['taxes']: val += c.get('amount', 0.0) res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val) res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1) @@ -1020,11 +1027,35 @@ class purchase_order(osv.osv): class purchase_order_line(osv.osv): + def _calc_line_base_price(self, cr, uid, line, context=None): + """Return the base price of the line to be used for tax calculation. + + This function can be extended by other modules to modify this base + price (adding a discount, for example). + """ + return line.price_unit + + def _calc_line_quantity(self, cr, uid, line, context=None): + """Return the base quantity of the line to be used for the subtotal. + + This function can be extended by other modules to modify this base + quantity (adding for example offers 3x2 and so on). + """ + return line.product_qty + def _amount_line(self, cr, uid, ids, prop, arg, context=None): res = {} for line in self.browse(cr, uid, ids, context=context): + line_price = self._calc_line_base_price(cr, uid, line, + context=context) + line_qty = self._calc_line_quantity(cr, uid, line, + context=context) cur = line.order_id.pricelist_id.currency_id - res[line.id] = line.taxes_id.compute_all(line.price_unit, cur, line.product_qty, product=line.product_id, partner=line.order_id.partner_id)['total_excluded'] + taxes = line.taxes_id.compute_all(line_price, cur, + line_qty, line.product_id, + line.order_id.partner_id) + + res[line.id] = taxes['total_excluded'] return res def _get_uom_id(self, cr, uid, context=None): @@ -1220,6 +1251,7 @@ class purchase_order_line(osv.osv): taxes = account_tax.browse(cr, uid, map(lambda x: x.id, product.supplier_taxes_id)) fpos = fiscal_position_id and account_fiscal_position.browse(cr, uid, fiscal_position_id, context=context) or False taxes_ids = account_fiscal_position.map_tax(cr, uid, fpos, taxes) + price = self.pool['account.tax']._fix_tax_included_price(cr, uid, price, product.supplier_taxes_id, taxes_ids) res['value'].update({'price_unit': price, 'taxes_id': taxes_ids}) return res diff --git a/addons/purchase/tests/__init__.py b/addons/purchase/tests/__init__.py index afe3f5e13f5..15ef7dc32e9 100644 --- a/addons/purchase/tests/__init__.py +++ b/addons/purchase/tests/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from . import test_purchase_to_invoice +from . import test_onchange_product_id diff --git a/addons/purchase/tests/test_onchange_product_id.py b/addons/purchase/tests/test_onchange_product_id.py new file mode 100644 index 00000000000..10ed1f01f81 --- /dev/null +++ b/addons/purchase/tests/test_onchange_product_id.py @@ -0,0 +1,45 @@ +from openerp.tests.common import TransactionCase + +class TestOnchangeProductId(TransactionCase): + """Test that when an included tax is mapped by a fiscal position, the included tax must be + subtracted to the price of the product. + """ + + def setUp(self): + super(TestOnchangeProductId, self).setUp() + self.fiscal_position_model = self.registry('account.fiscal.position') + self.fiscal_position_tax_model = self.registry('account.fiscal.position.tax') + self.tax_model = self.registry('account.tax') + self.pricelist_model = self.registry('product.pricelist') + self.res_partner_model = self.registry('res.partner') + self.product_model = self.registry('product.product') + self.product_uom_model = self.registry('product.uom') + self.so_line_model = self.registry('purchase.order.line') + + def test_onchange_product_id(self): + cr, uid = self.cr, self.uid + uom_id = self.product_uom_model.search(cr, uid, [('name', '=', 'Unit(s)')])[0] + pricelist = self.pricelist_model.search(cr, uid, [('name', '=', 'Public Pricelist')])[0] + partner_id = self.res_partner_model.create(cr, uid, dict(name="George")) + tax_include_id = self.tax_model.create(cr, uid, dict(name="Include tax", + amount_type='percent', + amount='21.0', + type_tax_use='purchase', + price_include=True)) + tax_exclude_id = self.tax_model.create(cr, uid, dict(name="Exclude tax", + amount_type='percent', + type_tax_use='purchase', + amount='0.00')) + product_id = self.product_model.create(cr, uid, dict(name="Voiture", + list_price='121', + supplier_taxes_id=[(6, 0, [tax_include_id])])) + fp_id = self.fiscal_position_model.create(cr, uid, dict(name="fiscal position", + sequence=1)) + self.fiscal_position_tax_model.create(cr, uid, dict(position_id=fp_id, + tax_src_id=tax_include_id, + tax_dest_id=tax_exclude_id)) + # import pudb; pudb.set_trace() + res = self.so_line_model.onchange_product_id(cr, uid, [], pricelist, product_id, 1.0, uom_id, partner_id, + fiscal_position_id=fp_id) + + self.assertEquals(100, res['value']['price_unit'], "The included tax must be subtracted to the price") diff --git a/addons/report/models/report.py b/addons/report/models/report.py index c86ed372260..467e79be826 100644 --- a/addons/report/models/report.py +++ b/addons/report/models/report.py @@ -224,7 +224,8 @@ class Report(osv.Model): # Run wkhtmltopdf process return self._run_wkhtmltopdf( cr, uid, headerhtml, footerhtml, contenthtml, context.get('landscape'), - paperformat, specific_paperformat_args, save_in_attachment + paperformat, specific_paperformat_args, save_in_attachment, + context.get('set_viewport_size') ) @api.v8 @@ -316,7 +317,7 @@ class Report(osv.Model): def _check_wkhtmltopdf(self): return wkhtmltopdf_state - def _run_wkhtmltopdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment={}): + def _run_wkhtmltopdf(self, cr, uid, headers, footers, bodies, landscape, paperformat, spec_paperformat_args=None, save_in_attachment=None, set_viewport_size=False): """Execute wkhtmltopdf as a subprocess in order to convert html given in input into a pdf document. @@ -330,7 +331,7 @@ class Report(osv.Model): :returns: Content of the pdf as a string """ command_args = [] - if wkhtmltopdf_state == "ok": + if set_viewport_size: command_args.extend(['--viewport-size', landscape and '1024x1280' or '1280x1024']) # Passing the cookie to wkhtmltopdf in order to resolve internal links. diff --git a/addons/sale/sale_view.xml b/addons/sale/sale_view.xml index b60853aa918..613745b4f8d 100644 --- a/addons/sale/sale_view.xml +++ b/addons/sale/sale_view.xml @@ -184,7 +184,7 @@ @@ -193,7 +193,7 @@ @@ -234,12 +234,13 @@ + context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}" + /> diff --git a/addons/sale/tests/__init__.py b/addons/sale/tests/__init__.py index 29f17446e86..563d5a51a7f 100644 --- a/addons/sale/tests/__init__.py +++ b/addons/sale/tests/__init__.py @@ -2,3 +2,4 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import test_sale_to_invoice import test_sale_order +from . import test_product_id_change diff --git a/addons/sale/tests/test_product_id_change.py b/addons/sale/tests/test_product_id_change.py new file mode 100644 index 00000000000..e6cb0267a17 --- /dev/null +++ b/addons/sale/tests/test_product_id_change.py @@ -0,0 +1,47 @@ +import unittest2 +from openerp.tests.common import TransactionCase + +class TestProductIdChange(TransactionCase): + """Test that when an included tax is mapped by a fiscal position, the included tax must be + subtracted to the price of the product. + """ + + def setUp(self): + super(TestProductIdChange, self).setUp() + self.fiscal_position_model = self.registry('account.fiscal.position') + self.fiscal_position_tax_model = self.registry('account.fiscal.position.tax') + self.tax_model = self.registry('account.tax') + self.pricelist_model = self.registry('product.pricelist') + self.res_partner_model = self.registry('res.partner') + self.product_tmpl_model = self.registry('product.template') + self.product_model = self.registry('product.product') + self.so_line_model = self.registry('sale.order.line') + + @unittest2.skip(""" + Due to `sale` rewrite (94716a3f14d929574c46ef165e4364d12a5903ed), + the commit 503820acb6d82a85c0c49ac26f7e7f8cd73851dd has been partially ignored + during the forward-port to master and need to be rewritten using new-api. + """) + def test_product_id_change(self): + cr, uid = self.cr, self.uid + pricelist = self.pricelist_model.search(cr, uid, [('name', '=', 'Public Pricelist')])[0] + partner_id = self.res_partner_model.create(cr, uid, dict(name="George")) + tax_include_id = self.tax_model.create(cr, uid, dict(name="Include tax", + type='percent', + amount='0.21', + price_include=True)) + tax_exclude_id = self.tax_model.create(cr, uid, dict(name="Exclude tax", + type='percent', + amount='0.00')) + product_tmpl_id = self.product_tmpl_model.create(cr, uid, dict(name="Voiture", + list_price='121', + taxes_id=[(6, 0, [tax_include_id])])) + product_id = self.product_model.create(cr, uid, dict(product_tmpl_id=product_tmpl_id)) + fp_id = self.fiscal_position_model.create(cr, uid, dict(name="fiscal position", + sequence=1)) + fp_tax_id = self.fiscal_position_tax_model.create(cr, uid, dict(position_id=fp_id, + tax_src_id=tax_include_id, + tax_dest_id=tax_exclude_id)) + res = self.so_line_model.product_id_change(cr, uid, [], pricelist, product_id, partner_id=partner_id, + fiscal_position=fp_id) + self.assertEquals(100, res['value']['price_unit'], "The included tax must be subtracted to the price") diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml index f67aa533dbc..b1dd752746f 100644 --- a/addons/sale_stock/sale_stock_view.xml +++ b/addons/sale_stock/sale_stock_view.xml @@ -27,7 +27,7 @@ - + diff --git a/addons/survey/survey.py b/addons/survey/survey.py index 16a0592dde8..6722bb6c2b5 100644 --- a/addons/survey/survey.py +++ b/addons/survey/survey.py @@ -845,7 +845,7 @@ class survey_user_input(osv.Model): 'survey_id': fields.many2one('survey.survey', 'Survey', required=True, readonly=1, ondelete='restrict'), 'date_create': fields.datetime('Creation Date', required=True, - readonly=1), + readonly=1, copy=False), 'deadline': fields.datetime("Deadline", help="Date by which the person can open the survey and submit answers", oldname="date_deadline"), @@ -858,7 +858,7 @@ class survey_user_input(osv.Model): 'Status', readonly=True), 'test_entry': fields.boolean('Test entry', readonly=1), - 'token': fields.char("Identification token", readonly=1, required=1), + 'token': fields.char("Identification token", readonly=1, required=1, copy=False), # Optional Identification data 'partner_id': fields.many2one('res.partner', 'Partner', readonly=1), @@ -869,7 +869,7 @@ class survey_user_input(osv.Model): 'Last displayed page'), # The answers ! 'user_input_line_ids': fields.one2many('survey.user_input_line', - 'user_input_id', 'Answers'), + 'user_input_id', 'Answers', copy=True), # URLs used to display the answers 'result_url': fields.related('survey_id', 'result_url', type='char', @@ -892,9 +892,6 @@ class survey_user_input(osv.Model): ('deadline_in_the_past', 'CHECK (deadline >= date_create)', 'The deadline cannot be in the past') ] - def copy_data(self, cr, uid, id, default=None, context=None): - raise UserError(_('You cannot duplicate this element!')) - def do_clean_emptys(self, cr, uid, automatic=False, context=None): ''' Remove empty user inputs that have been created manually (used as a cronjob declared in data/survey_cron.xml) ''' @@ -1023,9 +1020,6 @@ class survey_user_input_line(osv.Model): vals.update({'quizz_mark': self.__get_mark(cr, uid, value_suggested)}) return super(survey_user_input_line, self).write(cr, uid, ids, vals, context=context) - def copy_data(self, cr, uid, id, default=None, context=None): - raise UserError(_('You cannot duplicate this element!')) - def save_lines(self, cr, uid, user_input_id, question, post, answer_tag, context=None): ''' Save answers to questions, depending on question type diff --git a/addons/theme_default/static/description/icon.png b/addons/theme_default/static/description/icon.png new file mode 100644 index 00000000000..26fbfb95ce8 Binary files /dev/null and b/addons/theme_default/static/description/icon.png differ diff --git a/addons/web/static/src/js/views/list_view_editable.js b/addons/web/static/src/js/views/list_view_editable.js index d63151e880e..b0085a69f63 100644 --- a/addons/web/static/src/js/views/list_view_editable.js +++ b/addons/web/static/src/js/views/list_view_editable.js @@ -454,7 +454,7 @@ ListView.include(/** @lends instance.web.ListView# */{ var $cell = $(cell); field.set_dimensions($cell.outerHeight(), $cell.outerWidth()); - field.$el.position({ + field.$el.css({top: 0, left: 0}).position({ my: 'left top', at: 'left top', of: $cell diff --git a/addons/web_editor/models/ir_qweb.py b/addons/web_editor/models/ir_qweb.py index 503f95a0d38..06e83eb2c5c 100644 --- a/addons/web_editor/models/ir_qweb.py +++ b/addons/web_editor/models/ir_qweb.py @@ -341,7 +341,14 @@ class Image(orm.AbstractModel): max_size = '' if max_size is None else '/%s' % max_size src = '/web/image/%s/%s/%s%s?unique=%s' % (record._name, record.id, field_name, max_size, sha) - img = '' % (classes, src, options.get('style', '')) + alt = None + if options.get('alt-field') and getattr(record, options['alt-field'], None): + alt = record[options['alt-field']] + elif options.get('alt'): + alt = options['alt'] + + img = '' % \ + (classes, src, options.get('style', ''), ' alt="%s"' % alt if alt else '') return ir_qweb.HTMLSafe(img) local_url_re = re.compile(r'^/(?P[^]]+)/static/(?P.+)$') diff --git a/addons/web_editor/static/src/js/backend.js b/addons/web_editor/static/src/js/backend.js index c58d70ff389..52ef2017a14 100644 --- a/addons/web_editor/static/src/js/backend.js +++ b/addons/web_editor/static/src/js/backend.js @@ -90,6 +90,10 @@ var FieldTextHtmlSimple = widget.extend({ } return value; }, + focus: function() { + var input = !this.get("effective_readonly") && this.$textarea; + return input ? input.focus() : false; + }, render_value: function() { var value = this.get('value'); this.$textarea.val(value || ''); diff --git a/addons/web_editor/static/src/js/rte.js b/addons/web_editor/static/src/js/rte.js index c8d5655cbd6..5b9f52d0c89 100644 --- a/addons/web_editor/static/src/js/rte.js +++ b/addons/web_editor/static/src/js/rte.js @@ -426,13 +426,21 @@ var RTE = Widget.extend({ if (this.__saved[key]) return true; this.__saved[key] = true; } + // escape text nodes for xml saving + var escaped_el = $el.clone(); + escaped_el.find('*').addBack().not('script,style').contents().each(function(){ + if(this.nodeType == 3) { + this.nodeValue = _.escape(this.nodeValue); + } + }); + var markup = escaped_el.prop('outerHTML'); return ajax.jsonRpc('/web/dataset/call', 'call', { model: 'ir.ui.view', method: 'save', args: [ $el.data('oe-id'), - $el.prop('outerHTML'), + markup, $el.data('oe-xpath') || null, context || base.get_context() ], diff --git a/addons/web_editor/static/src/js/summernote.js b/addons/web_editor/static/src/js/summernote.js index 235bd6c3500..96462501825 100644 --- a/addons/web_editor/static/src/js/summernote.js +++ b/addons/web_editor/static/src/js/summernote.js @@ -24,6 +24,7 @@ var options = $.summernote.options; dom.hasContentAfter = function (node) { var next; + if(dom.isEditable(node)) return; while (node.nextSibling) { next = node.nextSibling; if (next.tagName || dom.isVisibleText(next) || dom.isBR(next)) return next; @@ -32,6 +33,7 @@ dom.hasContentAfter = function (node) { }; dom.hasContentBefore = function (node) { var prev; + if(dom.isEditable(node)) return; while (node.previousSibling) { prev = node.previousSibling; if (prev.tagName || dom.isVisibleText(prev) || dom.isBR(prev)) return prev; @@ -40,12 +42,12 @@ dom.hasContentBefore = function (node) { }; dom.ancestorHaveNextSibling = function (node, pred) { pred = pred || dom.hasContentAfter; - while (!node.nextSibling || !pred(node)) { node = node.parentNode; } + while (!dom.isEditable(node) && (!node.nextSibling || !pred(node))) { node = node.parentNode; } return node; }; dom.ancestorHavePreviousSibling = function (node, pred) { pred = pred || dom.hasContentBefore; - while (!node.previousSibling || !pred(node)) { node = node.parentNode; } + while (!dom.isEditable(node) && (!node.previousSibling || !pred(node))) { node = node.parentNode; } return node; }; dom.nextElementSibling = function (node) { @@ -369,6 +371,7 @@ dom.removeSpace = function (node, begin, so, end, eo) { (function __remove_space (node) { if (!node) return; + var t_begin, t_end; for (var k=0; k 1 && dom.isText(cur) && !dom.isVisibleText(cur)) { removed = true; if (cur === begin) { - so = 0; - begin = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur))); + t_begin = dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur)); + if(t_begin) { + so = 0; + begin = dom.lastChild(t_begin); + } } if (cur === end) { - eo = 1; - end = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(cur))); - if (dom.isText(end)) { - eo = end.textContent.length; + t_end = dom.hasContentAfter(dom.ancestorHaveNextSibling(cur)); + if(t_end) { + eo = 1; + end = dom.firstChild(t_end); + if (dom.isText(end)) { + eo = end.textContent.length; + } } } cur.parentNode.removeChild(cur); @@ -429,7 +438,7 @@ dom.removeSpace = function (node, begin, so, end, eo) { sc: begin, ec: end, so: !dom.isBR(begin) && so > 0 ? so : 0, - eo: eo + eo: dom.isBR(end) ? 0 : eo }; }; dom.node = function (node) { @@ -448,47 +457,61 @@ dom.pasteText = function (textNode, offset, text, isOnlyText) { data.sc = node.insertBefore(document.createTextNode(" "), data.sc); } else if (node.firstChild && !dom.isBR(dom.firstChild(node))) { data.sc = node.insertBefore(document.createTextNode(" "), dom.firstChild(node)); + } else if (dom.isBR(node)) { + data.sc = node.parentNode.insertBefore(document.createTextNode(" "), node); } else { data.sc = node.appendChild(document.createTextNode(" ")); } data.so = 0; } - data.sc.splitText(data.so); var first = data.sc; - var last = data.sc.nextSibling; + var try_to_clean = [first, first.splitText(data.so)]; isOnlyText = isOnlyText || !text.match('\n'); - + + var sc, so, ec, eo; + if (!isOnlyText) { + var paste_after = dom.isBR(node) ? node.parentElement : node; + // tag to close and open - var tag = node.tagName.toLowerCase(); + var tag = paste_after.tagName.toLowerCase(); if(dom.pasteTextApply.indexOf(tag) === -1) { text = text.split('\n').join(""); + paste_after = data.sc; } else { text = "<"+tag+">"+text.split('\n').join(""+tag+"><"+tag+">")+""+tag+">"; + try_to_clean.push(paste_after); } - var $text = $(text); + var nodes = _.toArray($('').html(text).prop('childNodes')); + sc = nodes[0]; + so = 0; + ec = dom.lastChild(nodes[nodes.length - 1]); + eo = dom.nodeLength(ec); // split parent node and insert text - if(dom.pasteTextClose.indexOf(tag) !== -1) { - var $next = $(node).clone().empty(); - $next.append( last ); - $(node).after( $next ); - $(node).after( $text ); + if(!dom.isText(paste_after) && dom.pasteTextClose.indexOf(tag) !== -1) { + try_to_clean.push(dom.splitTree(paste_after, {node: first, offset: data.so})); + $(paste_after).after( nodes ); } else { - $(data.sc).after( $text ); + $(paste_after).after( nodes ); } } else { + sc = ec = first; + so = dom.nodeLength(first); + eo = so + text.length; first.appendData( text ); } - // clean the dom content - data = dom.merge(node.parentNode.parentNode, last, 0, last, 0, null, true); - data = dom.removeSpace(node.parentNode.parentNode, data.sc, data.so, data.ec, data.eo); - // move caret - range.create(data.sc, data.so, data.ec, data.eo).select(); + range.create(sc, so, ec, eo).clean().select(); + + _(try_to_clean).each(function(node){ + _(dom.listDescendant(node).reverse().concat(node)).each(function(subnode){ + if(dom.isEmpty(subnode) || (dom.isText(subnode) && !dom.isVisibleText(subnode))) $(subnode).remove(); + }); + }); }; dom.removeBetween = function (sc, so, ec, eo, towrite) { if (ec.tagName) { @@ -508,7 +531,7 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) { } } if (!eo && sc !== ec) { - ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(ec))); + ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(ec)) || ec); eo = ec.textContent.length; } @@ -555,8 +578,13 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) { } } - sc = before ? dom.lastChild(before) : dom.firstChild(after); - so = sc.textContent.length; + if (dom.listAncestor(after).length <= dom.listAncestor(before).length) { + sc = dom.lastChild(before || ancestor); + so = dom.nodeLength(sc); + } else { + sc = dom.firstChild(after); + so = 0; + } if (before) { var text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0'); @@ -565,7 +593,7 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) { } if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) { var br = $("")[0]; - node.appendChild(br); + node.appendChild(sc); sc = br; so = 0; } @@ -829,13 +857,13 @@ range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) { if ($.contains(start, end)) { if (keep_end) { - sc = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end))); + sc = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || sc); so = sc.textContent.length; } else if (!eo) { - ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end))); + ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || ec); eo = ec.textContent.length; } else { - ec = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(end))); + ec = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(end)) || ec); eo = 0; } } else { @@ -864,6 +892,7 @@ range.WrappedRange.prototype.deleteContents = function (towrite) { }; range.WrappedRange.prototype.clean = function (mergeFilter, all) { var node = dom.node(this.sc === this.ec ? this.sc : this.commonAncestor()); + node = node || $(this.sc).closest('[contenteditable]')[0]; if (node.childNodes.length <=1) { return this; } @@ -941,7 +970,7 @@ $.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) { var r = range.create(); var outdent = outdent || false; - if (r.isCollapsed()) { + if (r && r.isCollapsed()) { if (r.isOnCell() && r.isOnCellFirst()) { var td = dom.ancestor(r.sc, dom.isCell); if (!outdent && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode)) { @@ -1104,7 +1133,9 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) { $editable.data('NoteHistory').recordUndo($editable, "visible"); var r = range.create(); - if (r && !r.isCollapsed()) { + if (!r) return; + + if (!r.isCollapsed()) { r = r.deleteContents(true); r.select(); } @@ -1208,7 +1239,7 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) { } while (dom.isImg(node)) { parent = node.parentNode; - index = dom.makeOffsetPath(parent, node)[0]; + index = dom.position(node); if (index>0) { var next = node.previousSibling; range.create(next,next.textContent.length).select(); @@ -1289,6 +1320,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) { $editable.data('NoteHistory').recordUndo($editable, "backspace"); var r = range.create(); + if (!r) return; if (!r.isContentEditable()) { event.preventDefault(); return false; @@ -1313,10 +1345,15 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) { var node = dom.node(target); var data = dom.merge(node, target, offset, target, offset, null, true); data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo); - r = range.create(data.sc, data.so); + r = dom.isVoid(data.sc) ? range.createFromNode(data.sc) : range.create(data.sc, data.so); r.select(); target = r.sc; offset = r.so; + if (target.tagName && target.childNodes[offset]) { + target = target.childNodes[offset]; + offset = 0; + node = dom.node(target); + } while (node.parentNode && !dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;} @@ -1779,6 +1816,22 @@ eventHandler.toolbar.button.updateRecentColor = function (elBtn, sEvent, sValue) return false; }; +$(document).on('click keyup', function () { + var $popover = $((range.create()||{}).sc).closest('[contenteditable]'); + var popover_history = ($popover.data()||{}).NoteHistory; + if(!popover_history || popover_history == history) return; + var editor = $popover.parent('.note-editor'); + $('button[data-event="undo"]', editor).attr('disabled', !popover_history.hasUndo()); + $('button[data-event="redo"]', editor).attr('disabled', !popover_history.hasRedo()); +}); + +eventHandler.editor.undo = function ($popover) { + if(!$popover.attr('disabled')) $popover.data('NoteHistory').undo(); +}; +eventHandler.editor.redo = function ($popover) { + if(!$popover.attr('disabled')) $popover.data('NoteHistory').redo(); +}; + options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62]; $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) { var rng = range.create(); @@ -2183,9 +2236,11 @@ function summernote_paste (event) { if (!r.isCollapsed()) { r = r.deleteContents(); r.select(); + event.preventDefault(); + return false; } - var text = clipboardData.getData("text/plain").replace(/').text(clipboardData.getData("text/plain").toString()).html(); dom.pasteText(r.sc, r.so, text); event.preventDefault(); return false; diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js index 5e852a1c5ef..56ada20729a 100644 --- a/addons/web_editor/static/src/js/widgets.js +++ b/addons/web_editor/static/src/js/widgets.js @@ -129,10 +129,12 @@ var MediaDialog = Dialog.extend({ this.imageDialog.appendTo(this.$("#editor-media-image")); this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options)); this.documentDialog.appendTo(this.$("#editor-media-document")); - this.iconDialog = new fontIconsDialog(this, this.media, this.options); - this.iconDialog.appendTo(this.$("#editor-media-icon")); - this.videoDialog = new VideoDialog(this, this.media, this.options); - this.videoDialog.appendTo(this.$("#editor-media-video")); + if (!this.only_images) { + this.iconDialog = new fontIconsDialog(this, this.media, this.options); + this.iconDialog.appendTo(this.$("#editor-media-icon")); + this.videoDialog = new VideoDialog(this, this.media, this.options); + this.videoDialog.appendTo(this.$("#editor-media-video")); + } this.active = this.imageDialog; @@ -175,10 +177,11 @@ var MediaDialog = Dialog.extend({ if (this.active !== this.documentDialog) { this.documentDialog.clear(); } - if (this.active !== this.iconDialog) { + // if not mode only_images + if (this.iconDialog && this.active !== this.iconDialog) { this.iconDialog.clear(); } - if (this.active !== this.videoDialog) { + if (this.videoDialog && this.active !== this.videoDialog) { this.videoDialog.clear(); } } else { @@ -574,7 +577,7 @@ var getCssSelectors = function(filter) { } return cacheCssSelectors[filter] = css; }; -function computeFonts() { +var computeFonts = _.once(function() { _.each(fontIcons, function (data) { data.cssData = getCssSelectors(data.parser); data.alias = []; @@ -583,7 +586,7 @@ function computeFonts() { return css[2]; }); }); -} +}); rte.Class.include({ init: function (EditorBar) { @@ -632,9 +635,7 @@ var fontIconsDialog = Widget.extend({ this._super(); this.parent = parent; this.media = media; - if (_.find(fontIcons, function (data) {return !data.icons;})) { - computeFonts(); - } + computeFonts(); }, start: function () { return this._super().then(this.proxy('load_data')); diff --git a/addons/web_kanban/static/src/js/kanban_widgets.js b/addons/web_kanban/static/src/js/kanban_widgets.js index 4920b423a4f..3b2dbada66b 100644 --- a/addons/web_kanban/static/src/js/kanban_widgets.js +++ b/addons/web_kanban/static/src/js/kanban_widgets.js @@ -46,6 +46,18 @@ var AbstractField = Widget.extend(FieldInterface, { }, }); +var FormatChar = AbstractField.extend({ + tagName: 'span', + init: function(parent, field, $node) { + this._super.apply(this, arguments); + this.format_descriptor = _.extend({}, this.field, {'widget': this.$node.attr('widget')}); + }, + renderElement: function() { + this.$el.text(instance.web.format_value(this.field.raw_value, this.format_descriptor)); + } +}); + + var KanbanPriority = AbstractField.extend({ init: function(parent, field, $node) { this._super.apply(this, arguments); @@ -233,7 +245,9 @@ fields_registry .add('priority', KanbanPriority) .add('kanban_state_selection', KanbanSelection) .add("attachment_image", KanbanAttachmentImage) - .add('progress', KanbanProgressBar); + .add('progress', KanbanProgressBar) + .add('float_time', FormatChar) + ; return { AbstractField: AbstractField, diff --git a/addons/website/static/src/js/website.theme.js b/addons/website/static/src/js/website.theme.js index 7b10fd7ea13..d44e2278fb1 100644 --- a/addons/website/static/src/js/website.theme.js +++ b/addons/website/static/src/js/website.theme.js @@ -29,10 +29,10 @@ var Theme = Widget.extend({ template: 'website.theme_customize', events: { 'change input[data-xmlid],input[data-enable],input[data-disable]': 'change_selection', - 'click label:has(input[data-xmlid],input[data-enable],input[data-disable])': function (event) { + 'mousedown label:has(input[data-xmlid],input[data-enable],input[data-disable])': function (event) { var self = this; this.time_select = _.defer(function () { - self.on_select($(event.target).find('input')); + self.on_select($(event.target).find('input'), event); }); }, 'click .close': 'close', @@ -198,7 +198,7 @@ var Theme = Widget.extend({ if (this.$el.hasClass("loading")) return; // prevent to change selection when css is loading - var $option = $(event.target), + var $option = $(event.target).is('input') ? $(event.target) : $("input", event.target), $options = $option, checked = $option.prop("checked"); @@ -252,13 +252,13 @@ var Theme = Widget.extend({ clearTimeout(this.timer); if (this.flag) { this.timer = _.defer(function () { - if (!init_mode) self.on_select($options); + if (!init_mode) self.on_select($options, event); self.update_style(self.get_xml_ids($enable), self.get_xml_ids($disable), self.reload); self.reload = false; }); } else { this.timer = _.defer(function () { - if (!init_mode) self.on_select($options); + if (!init_mode) self.on_select($options, event); self.reload = false; }); } @@ -266,9 +266,8 @@ var Theme = Widget.extend({ /* Method call when the user change the selection or click on an input * @values: all changed inputs */ - on_select: function ($inputs) { + on_select: function ($inputs, event) { clearTimeout(this.time_select); - console.log($inputs.get()); }, click: function (event) { if (!$(event.target).closest("#theme_customize_modal > *").length) { diff --git a/addons/website/tests/test_views.py b/addons/website/tests/test_views.py index c8e7efb9044..b63ac75997d 100644 --- a/addons/website/tests/test_views.py +++ b/addons/website/tests/test_views.py @@ -175,14 +175,34 @@ class TestViewSaving(common.TransactionCase): ) def test_save_escaped_text(self): + """ Test saving html special chars in text nodes """ view_id = self.registry('ir.ui.view').create(self.cr, self.uid, { - 'arch':'hello world', + 'arch':'hello world', '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') + # script and style text nodes should not escaped client side + replacement = '' + view.save(replacement, xpath='/t/p/h1') + self.assertIn( + replacement.replace('&', '&'), + view.arch, + 'inline script should be escaped server side' + ) + self.assertIn( + replacement, + view.render(), + 'inline script should not be escaped when rendering' + ) + # common text nodes should be be escaped client side + replacement = 'world & <b>cie' + view.save(replacement, xpath='/t/p') + self.assertIn(replacement, view.arch, 'common text node should not be escaped server side') + self.assertIn( + replacement, + view.render().replace('&', '&'), + 'text node characters wrongly unescaped when rendering' + ) def test_save_only_embedded(self): Company = self.registry('res.company') diff --git a/addons/website_forum/static/src/js/website_forum.js b/addons/website_forum/static/src/js/website_forum.js index a0be5e8cef9..563ef1e0177 100644 --- a/addons/website_forum/static/src/js/website_forum.js +++ b/addons/website_forum/static/src/js/website_forum.js @@ -262,7 +262,8 @@ if(!$('.website_forum').length) { ['style', ['style']], ['font', ['bold', 'italic', 'underline', 'clear']], ['para', ['ul', 'ol', 'paragraph']], - ['table', ['table']] + ['table', ['table']], + ['history', ['undo', 'redo']], ]; if (parseInt($("#karma").val()) >= editor_karma) { toolbar.push(['insert', ['link', 'picture']]); diff --git a/addons/website_mail/controllers/main.py b/addons/website_mail/controllers/main.py index 3efcbe34d38..4468c49d7f3 100644 --- a/addons/website_mail/controllers/main.py +++ b/addons/website_mail/controllers/main.py @@ -92,7 +92,8 @@ class WebsiteMail(http.Controller): partner_ids = _object._find_partner_from_emails( cr, SUPERUSER_ID, _id, [email], context=context, check_followers=True) if not partner_ids or not partner_ids[0]: - partner_ids = [partner_obj.create(cr, SUPERUSER_ID, {'name': email, 'email': email}, context=context)] + name = email.split('@')[0] + partner_ids = [partner_obj.create(cr, SUPERUSER_ID, {'name': name, 'email': email}, context=context)] # add or remove follower if _message_is_follower: diff --git a/addons/website_quote/controllers/main.py b/addons/website_quote/controllers/main.py index c88209948df..4f375216d23 100644 --- a/addons/website_quote/controllers/main.py +++ b/addons/website_quote/controllers/main.py @@ -36,7 +36,7 @@ class sale_quote(http.Controller): days = (datetime.datetime.strptime(order.validity_date, '%Y-%m-%d') - datetime.datetime.now()).days + 1 if pdf: report_obj = request.registry['report'] - pdf = report_obj.get_pdf(request.cr, SUPERUSER_ID, [order_id], 'website_quote.report_quote', data=None, context=request.context) + pdf = report_obj.get_pdf(request.cr, SUPERUSER_ID, [order_id], 'website_quote.report_quote', data=None, context=dict(request.context, set_viewport_size=True)) pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))] return request.make_response(pdf, headers=pdfhttpheaders) user = request.registry['res.users'].browse(request.cr, SUPERUSER_ID, request.uid, context=request.context) @@ -141,7 +141,7 @@ class sale_quote(http.Controller): res = request.registry.get('sale.order.line').product_id_change(request.cr, SUPERUSER_ID, order_id, False, option.product_id.id, option.quantity, option.uom_id.id, option.quantity, option.uom_id.id, option.name, order.partner_id.id, False, True, time.strftime('%Y-%m-%d'), - False, order.fiscal_position_id.id, True, request.context) + False, order.fiscal_position_id.id, True, dict(request.context or {}, company_id=order.company_id.id)) vals = res.get('value', {}) if 'tax_id' in vals: vals['tax_id'] = [(6, 0, vals['tax_id'])] diff --git a/addons/website_quote/views/website_quotation_backend.xml b/addons/website_quote/views/website_quotation_backend.xml index 0cd417cea88..64d984510c4 100644 --- a/addons/website_quote/views/website_quotation_backend.xml +++ b/addons/website_quote/views/website_quotation_backend.xml @@ -37,7 +37,7 @@ - + diff --git a/addons/website_sale/controllers/main.py b/addons/website_sale/controllers/main.py index f01f20c2613..134a759f4c5 100644 --- a/addons/website_sale/controllers/main.py +++ b/addons/website_sale/controllers/main.py @@ -264,6 +264,7 @@ class website_sale(http.Controller): if category: category = category_obj.browse(cr, uid, int(category), context=context) + category = category if category.exists() else False attrib_list = request.httprequest.args.getlist('attrib') attrib_values = [map(int,v.split("-")) for v in attrib_list if v] diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 960a9b9162d..6b9f5eaa38a 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -72,7 +72,7 @@ - + @@ -340,7 +340,7 @@ - + Product Name diff --git a/doc/howtos/backend.rst b/doc/howtos/backend.rst index 77b8c1ed176..dbfd95e6eec 100644 --- a/doc/howtos/backend.rst +++ b/doc/howtos/backend.rst @@ -626,9 +626,9 @@ instead of a single view its ``arch`` field is composed of any number of idea.category - - + @@ -652,6 +652,22 @@ instead of a single view its ``arch`` field is composed of any number of alters the attributes of the matched element using special ``attribute`` elements in the ``xpath``'s body +.. tip:: + + When matching a single element, the ``position`` attribute can be set directly + on the element to be found. Both inheritances below will give the same result. + + .. code-block:: xml + + + + + + + + + + .. exercise:: Alter existing content * Using model inheritance, modify the existing *Partner* model to add an diff --git a/openerp/addons/base/ir/ir_attachment.py b/openerp/addons/base/ir/ir_attachment.py index d4249145c6b..e21efbbc354 100644 --- a/openerp/addons/base/ir/ir_attachment.py +++ b/openerp/addons/base/ir/ir_attachment.py @@ -237,9 +237,8 @@ class ir_attachment(osv.osv): """ compute the checksum for the given datas :param bin_data : datas in its binary form """ - if bin_data: - return hashlib.sha1(bin_data).hexdigest() - return False + # an empty file has a checksum too (for caching) + return hashlib.sha1(bin_data or '').hexdigest() def _compute_mimetype(self, values): """ compute the mimetype of the given values diff --git a/openerp/addons/base/ir/ir_http.py b/openerp/addons/base/ir/ir_http.py index a30f0da1850..a41362e74e5 100644 --- a/openerp/addons/base/ir/ir_http.py +++ b/openerp/addons/base/ir/ir_http.py @@ -106,6 +106,7 @@ class ir_http(osv.AbstractModel): wdate = attach[0]['__last_update'] datas = attach[0]['datas'] or '' name = attach[0]['name'] + checksum = attach[0]['checksum'] or hashlib.sha1(datas).hexdigest() if (not datas and name != request.httprequest.path and name.startswith(('http://', 'https://', '/'))): @@ -119,7 +120,7 @@ class ir_http(osv.AbstractModel): # just in case we have a timestamp without microseconds response.last_modified = datetime.datetime.strptime(wdate, server_format) - response.set_etag(attach[0]['checksum']) + response.set_etag(checksum) response.make_conditional(request.httprequest) if response.status_code == 304: diff --git a/openerp/addons/base/ir/ir_sequence.py b/openerp/addons/base/ir/ir_sequence.py index 3dfdad37161..098d0a2c8d3 100644 --- a/openerp/addons/base/ir/ir_sequence.py +++ b/openerp/addons/base/ir/ir_sequence.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. import logging +import pytz import time from datetime import datetime, timedelta @@ -194,22 +195,14 @@ class ir_sequence(models.Model): def _interpolation_dict(): if self.env.context.get('ir_sequence_date'): - t = time.strptime(self.env.context.get('ir_sequence_date'), '%Y-%m-%d') + t = datetime.strptime(self.env.context.get('ir_sequence_date'), '%Y-%m-%d') else: - t = time.localtime() # Actually, the server is always in UTC. - return { - 'year': time.strftime('%Y', t), - 'month': time.strftime('%m', t), - 'day': time.strftime('%d', t), - 'y': time.strftime('%y', t), - 'doy': time.strftime('%j', t), - 'woy': time.strftime('%W', t), - 'weekday': time.strftime('%w', t), - 'h24': time.strftime('%H', t), - 'h12': time.strftime('%I', t), - 'min': time.strftime('%M', t), - 'sec': time.strftime('%S', t), + t = datetime.now(pytz.timezone(self.env.context.get('tz') or 'UTC')) + sequences = { + 'year': '%Y', 'month': '%m', 'day': '%d', 'y': '%y', 'doy': '%j', 'woy': '%W', + 'weekday': '%w', 'h24': '%H', 'h12': '%I', 'min': '%M', 'sec': '%S' } + return {key: t.strftime(sequence) for key, sequence in sequences.iteritems()} d = _interpolation_dict() try: diff --git a/openerp/addons/base/res/res_currency.py b/openerp/addons/base/res/res_currency.py index 717accf2eb9..b5256847e5c 100644 --- a/openerp/addons/base/res/res_currency.py +++ b/openerp/addons/base/res/res_currency.py @@ -9,6 +9,7 @@ from openerp import api, fields as fields2 from openerp import tools from openerp.osv import fields, osv from openerp.tools import float_round, float_is_zero, float_compare +import simplejson as json CURRENCY_DISPLAY_PATTERN = re.compile(r'(\w+)\s*(?:\((.*)\))?') @@ -252,9 +253,9 @@ class res_currency(osv.osv): symbol = row['symbol'] or row['name'] format_number_str = "openerp.web.format_value(arguments[0], {type: 'float', digits: [69," + str(row['decimal_places']) + "]}, 0.00)" if row['position'] == 'after': - return_str = "return " + format_number_str + " + '\\xA0" + symbol + "';" + return_str = "return " + format_number_str + " + '\\xA0' + " + json.dumps(symbol) + ";" else: - return_str = "return '" + symbol + "\\xA0' + " + format_number_str + ";" + return_str = "return " + json.dumps(symbol) + " + '\\xA0' + " + format_number_str + ";" function += "if (arguments[1] === " + str(row['id']) + ") { " + return_str + " }" if (row['id'] == company_currency_id): company_currency_format = return_str diff --git a/openerp/tools/mail.py b/openerp/tools/mail.py index 697a0301a06..3c22262e14c 100644 --- a/openerp/tools/mail.py +++ b/openerp/tools/mail.py @@ -461,6 +461,10 @@ def html2plaintext(html, body_id=None, encoding='utf-8'): ## download here: http://www.peterbe.com/plog/html2plaintext html = ustr(html) + + if not html: + return '' + tree = etree.fromstring(html, parser=etree.HTMLParser()) if body_id is not None:
diff --git a/addons/gamification/models/challenge.py b/addons/gamification/models/challenge.py index 68c5db17ab2..0e711541477 100644 --- a/addons/gamification/models/challenge.py +++ b/addons/gamification/models/challenge.py @@ -363,6 +363,9 @@ class gamification_challenge(osv.Model): Create goals that haven't been created yet (eg: if added users) Recompute the current value for each goal related""" + goal_obj = self.pool['gamification.goal'] + goal_ids = goal_obj.search(cr, uid, [('challenge_id', 'in', ids), ('state', '=', 'inprogress')], context=context) + goal_obj.unlink(cr, uid, goal_ids, context=context) return self._update_all(cr, uid, ids=ids, context=context) def action_report_progress(self, cr, uid, ids, context=None): diff --git a/addons/google_drive/google_drive.py b/addons/google_drive/google_drive.py index f5f90b10d6e..dd723c4f10d 100644 --- a/addons/google_drive/google_drive.py +++ b/addons/google_drive/google_drive.py @@ -62,7 +62,7 @@ class config(osv.Model): client_secret=google_drive_client_secret, grant_type="refresh_token", scope=scope or 'https://www.googleapis.com/auth/drive')) - headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"} + headers = {"Content-type": "application/x-www-form-urlencoded"} try: req = urllib2.Request('https://accounts.google.com/o/oauth2/token', data, headers) content = urllib2.urlopen(req, timeout=TIMEOUT).read() @@ -82,7 +82,7 @@ class config(osv.Model): access_token = self.get_access_token(cr, uid, context=context) # Copy template in to drive with help of new access token request_url = "https://www.googleapis.com/drive/v2/files/%s?fields=parents/id&access_token=%s" % (template_id, access_token) - headers = {"Content-type": "application/x-www-form-urlencoded", "Accept-Encoding": "gzip, deflate"} + headers = {"Content-type": "application/x-www-form-urlencoded"} try: req = urllib2.Request(request_url, None, headers) parents = urllib2.urlopen(req, timeout=TIMEOUT).read() diff --git a/addons/hw_escpos/controllers/main.py b/addons/hw_escpos/controllers/main.py index 083e5cd5557..0b0f58027ab 100644 --- a/addons/hw_escpos/controllers/main.py +++ b/addons/hw_escpos/controllers/main.py @@ -52,60 +52,37 @@ class EscposDriver(Thread): self.lock = Lock() self.status = {'status':'connecting', 'messages':[]} - def supported_devices(self): - if not os.path.isfile('escpos_devices.pickle'): - return supported_devices.device_list - else: - try: - f = open('escpos_devices.pickle','r') - return pickle.load(f) - f.close() - except Exception as e: - self.set_status('error',str(e)) - return supported_devices.device_list - - def add_supported_device(self,device_string): - r = re.compile('[0-9A-Fa-f]{4}:[0-9A-Fa-f]{4}'); - match = r.search(device_string) - if match: - match = match.group().split(':') - vendor = int(match[0],16) - product = int(match[1],16) - name = device_string.split('ID') - if len(name) >= 2: - name = name[1] - else: - name = name[0] - _logger.info('ESC/POS: adding support for device: '+match[0]+':'+match[1]+' '+name) - - device_list = supported_devices.device_list[:] - if os.path.isfile('escpos_devices.pickle'): - try: - f = open('escpos_devices.pickle','r') - device_list = pickle.load(f) - f.close() - except Exception as e: - self.set_status('error',str(e)) - device_list.append({ - 'vendor': vendor, - 'product': product, - 'name': name, - }) - - try: - f = open('escpos_devices.pickle','w+') - f.seek(0) - pickle.dump(device_list,f) - f.close() - except Exception as e: - self.set_status('error',str(e)) - def connected_usb_devices(self): connected = [] - - for device in self.supported_devices(): - if usb.core.find(idVendor=device['vendor'], idProduct=device['product']) != None: - connected.append(device) + + # printers can either define bDeviceClass=7, or they can define one of + # their interfaces with bInterfaceClass=7. This class checks for both. + class FindUsbClass(object): + def __init__(self, usb_class): + self._class = usb_class + def __call__(self, device): + # first, let's check the device + if device.bDeviceClass == self._class: + return True + # transverse all devices and look through their interfaces to + # find a matching class + for cfg in device: + intf = usb.util.find_descriptor(cfg, bInterfaceClass=self._class) + + if intf is not None: + return True + + return False + + printers = usb.core.find(find_all=True, custom_match=FindUsbClass(7)) + + for printer in printers: + connected.append({ + 'vendor': printer.idVendor, + 'product': printer.idProduct, + 'name': usb.util.get_string(printer, 256, printer.iManufacturer) + " " + usb.util.get_string(printer, 256, printer.iProduct) + }) + return connected def lockedstart(self): @@ -377,18 +354,3 @@ class EscposProxy(hw_proxy.Proxy): _logger.info('ESC/POS: PRINT XML RECEIPT') driver.push_task('xml_receipt',receipt) - @http.route('/hw_proxy/escpos/add_supported_device', type='http', auth='none', cors='*') - def add_supported_device(self, device_string): - _logger.info('ESC/POS: ADDED NEW DEVICE:'+device_string) - driver.add_supported_device(device_string) - return "The device:\n"+device_string+"\n has been added to the list of supported devices.Ok" - - @http.route('/hw_proxy/escpos/reset_supported_devices', type='http', auth='none', cors='*') - def reset_supported_devices(self): - try: - os.remove('escpos_devices.pickle') - except Exception as e: - pass - return 'The list of supported devices has been reset to factory defaults.Ok' - - diff --git a/addons/hw_escpos/escpos/__init__.py b/addons/hw_escpos/escpos/__init__.py index 3fdeddee28b..22a5af61029 100644 --- a/addons/hw_escpos/escpos/__init__.py +++ b/addons/hw_escpos/escpos/__init__.py @@ -1 +1 @@ -__all__ = ["constants","escpos","exceptions","printer","supported_devices"] +__all__ = ["constants","escpos","exceptions","printer"] diff --git a/addons/hw_escpos/escpos/supported_devices.py b/addons/hw_escpos/escpos/supported_devices.py deleted file mode 100644 index ad086b5516b..00000000000 --- a/addons/hw_escpos/escpos/supported_devices.py +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/python - -# This is a list of esc/pos compatible usb printers. The vendor and product ids can be found by -# typing lsusb in a linux terminal, this will give you the ids in the form ID VENDOR:PRODUCT - -device_list = [ - { 'vendor' : 0x04b8, 'product' : 0x0e03, 'name' : 'Epson TM-T20' }, - { 'vendor' : 0x04b8, 'product' : 0x0202, 'name' : 'Epson TM-T70' }, - { 'vendor' : 0x04b8, 'product' : 0x0e15, 'name' : 'Epson TM-T20II' }, -] diff --git a/addons/hw_posbox_upgrade/controllers/main.py b/addons/hw_posbox_upgrade/controllers/main.py index 5ed958d020e..42811820e74 100644 --- a/addons/hw_posbox_upgrade/controllers/main.py +++ b/addons/hw_posbox_upgrade/controllers/main.py @@ -6,6 +6,7 @@ import time import openerp import openerp.addons.hw_proxy.controllers.main as hw_proxy import threading +import subprocess from openerp import http from openerp.http import request from openerp.tools.translate import _ @@ -74,6 +75,14 @@ upgrade_template = """
To upgrade the posbox, click on the upgrade button. The upgrade will take a few minutes. Do not reboot the PosBox during the upgrade.
+ Latest patch: +
+""" +upgrade_template += subprocess.check_output("git --work-tree=/home/pi/odoo/ --git-dir=/home/pi/odoo/.git log -1", shell=True).replace("\n", "") +upgrade_template += """ +