[MERGE] forward port of branch saas-6 up to 7a768a4

Due to `sale` rewrite (94716a3f14),
the commit 503820acb6 has been partially
ignored (in sale.order.line) and will be rewritten later using new-api.
This commit is contained in:
Christophe Simonis
2015-08-28 15:07:16 +02:00
55 changed files with 483 additions and 209 deletions
+8
View File
@@ -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"
+1 -1
View File
@@ -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
@@ -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");
});
},
+2 -2
View File
@@ -39,11 +39,11 @@
<field name="tax_ids" widget="one2many_list" nolabel="1">
<tree string="Tax Mapping" editable="bottom">
<field name="tax_src_id" domain="[('type_tax_use', '!=', None)]"/>
<field name="tax_dest_id" domain="[('type_tax_use', '!=', None)]"/>
<field name="tax_dest_id" domain="[('type_tax_use', '!=', None), ('price_include', '=', False)]"/>
</tree>
<form string="Tax Mapping">
<field name="tax_src_id" domain="[('type_tax_use', '!=', None)]"/>
<field name="tax_dest_id" domain="[('type_tax_use', '!=', None)]"/>
<field name="tax_dest_id" domain="[('type_tax_use', '!=', None), ('price_include', '=', False)]"/>
</form>
</field>
</group>
@@ -39,7 +39,9 @@
<t t-call="web.login_layout">
<form class="oe_signup_form" role="form" method="post" t-if="not message">
<t t-call="auth_signup.fields"/>
<t t-call="auth_signup.fields">
<t t-set="only_passwords" t-value="bool(token)"/>
</t>
<p class="alert alert-danger" t-if="error">
<t t-esc="error"/>
+3
View File
@@ -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):
+2 -2
View File
@@ -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()
+29 -67
View File
@@ -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.<br/><a href='/hw_proxy/status'>Ok</a>"
@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.<br/><a href="/hw_proxy/status">Ok</a>'
+1 -1
View File
@@ -1 +1 @@
__all__ = ["constants","escpos","exceptions","printer","supported_devices"]
__all__ = ["constants","escpos","exceptions","printer"]
@@ -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' },
]
+10 -1
View File
@@ -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 = """
<p>
To upgrade the posbox, click on the upgrade button. The upgrade will take a few minutes. <b>Do not reboot</b> the PosBox during the upgrade.
</p>
<p>
Latest patch:
</p>
<pre>
"""
upgrade_template += subprocess.check_output("git --work-tree=/home/pi/odoo/ --git-dir=/home/pi/odoo/.git log -1", shell=True).replace("\n", "<br/>")
upgrade_template += """
</pre>
<div class='centering'>
<a href='#' id='upgrade'>Upgrade</a>
</div>
@@ -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'
+1 -1
View File
@@ -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
})
+1 -1
View File
@@ -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,
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
}
@@ -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
+13 -7
View File
@@ -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):
+34 -2
View File
@@ -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
+1
View File
@@ -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
@@ -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")
+4 -3
View File
@@ -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.
+6 -4
View File
@@ -184,7 +184,7 @@
<group>
<group>
<field name="product_id"
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}"
attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}"
/>
<field name="invoice_status" invisible="1"/>
@@ -193,7 +193,7 @@
<label for="product_uom_qty" string="Ordered Quantity"/>
<div>
<field
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'uom_qty_change':True}"
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'uom_qty_change':True, 'company_id': parent.company_id}"
name="product_uom_qty" class="oe_inline"/>
<field name="product_uom" groups="product.group_uom" class="oe_inline oe_no_button"
attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}"/>
@@ -234,12 +234,13 @@
<field name="sequence" widget="handle"/>
<field name="product_id"
attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}"
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}"
/>
<field name="name"/>
<field name="product_uom_qty"
string="Ordered Qty"
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}"/>
context="{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}"
/>
<field name="qty_delivered" invisible="not context.get('show_sale')"
attrs="{'readonly': [('qty_delivered_updateable', '=', False)]}"/>
<field name="qty_invoiced"
@@ -247,6 +248,7 @@
<field name="qty_to_invoice" invisible="1"/>
<field name="product_uom"
attrs="{'readonly': [('state', 'in', ('sale','done', 'cancel'))]}"
context="{'company_id': parent.company_id}"
groups="product.group_uom" options='{"no_open": True}'/>
<field name="price_unit"
attrs="{'readonly': [('qty_invoiced', '&gt;', 0)]}"/>
+1
View File
@@ -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
@@ -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")
+1 -1
View File
@@ -27,7 +27,7 @@
</xpath>
<xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="before">
<field name="product_tmpl_id" invisible="1"/>
<field name="product_packaging" context="{'default_product_tmpl_id': product_tmpl_id, 'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging" />
<field name="product_packaging" context="{'default_product_tmpl_id': product_tmpl_id, 'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging" />
</xpath>
<xpath expr="//field[@name='order_line']/form/group/group/field[@name='price_unit']" position="before">
<field name="route_id" groups="sale_stock.group_route_so_lines"/>
+3 -9
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@@ -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
+8 -1
View File
@@ -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 = '<img class="%s" src="%s" style="%s"/>' % (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 = '<img class="%s" src="%s" style="%s"%s/>' % \
(classes, src, options.get('style', ''), ' alt="%s"' % alt if alt else '')
return ir_qweb.HTMLSafe(img)
local_url_re = re.compile(r'^/(?P<module>[^]]+)/static/(?P<rest>.+)$')
@@ -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 || '');
+9 -1
View File
@@ -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()
],
+92 -37
View File
@@ -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<node.childNodes.length; k++) {
var cur = node.childNodes[k];
@@ -385,14 +388,20 @@ dom.removeSpace = function (node, begin, so, end, eo) {
if (node.childNodes.length > 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("<br/>");
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($('<div/>').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 = $("<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(/</g, "&lt;");
var text = $('<div />').text(clipboardData.getData("text/plain").toString()).html();
dom.pasteText(r.sc, r.so, text);
event.preventDefault();
return false;
+12 -11
View File
@@ -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'));
@@ -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,
@@ -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) {
+24 -4
View File
@@ -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':'<t>hello world</t>',
'arch':'<t><p><h1>hello world</h1></p></t>',
'type':'qweb'
})
view = self.registry('ir.ui.view').browse(self.cr, self.uid, view_id)
replacement = 'hello world &amp; &lt;angle brackets&gt;!'
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 = '<script>1 && "hello & world"</script>'
view.save(replacement, xpath='/t/p/h1')
self.assertIn(
replacement.replace('&', '&amp;'),
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 &amp;amp; &amp;lt;b&amp;gt;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('&', '&amp;'),
'text node characters wrongly unescaped when rendering'
)
def test_save_only_embedded(self):
Company = self.registry('res.company')
@@ -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']]);
+2 -1
View File
@@ -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:
+2 -2
View File
@@ -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'])]
@@ -37,7 +37,7 @@
<xpath expr="//field[@name='project_id']" position="before">
<label for="template_id"/>
<div>
<field name="template_id" on_change="onchange_template_id(template_id, partner_id, fiscal_position_id, pricelist_id)"/>
<field name="template_id" context="{'company_id': company_id}" on_change="onchange_template_id(template_id, partner_id, fiscal_position_id, pricelist_id)"/>
<button name="open_quotation" type="object"
class="oe_link o_open_quotation" string="View Online Quotation"
attrs="{'invisible': [('template_id', '=', False)]}"/>
+1
View File
@@ -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]
+2 -2
View File
@@ -72,7 +72,7 @@
</div>
<div class="oe_product_image">
<a itemprop="url" t-att-href="keep('/shop/product/%s' % slug(product), page=(pager['page']['num'] if pager['page']['num']>1 else None))">
<span itemprop="image" t-field="product.image" t-field-options='{"widget": "image" #{None if product_image_big else ", \"resize\": \"300x300\""}}'/>
<span itemprop="image" t-field="product.image" t-field-options='{"widget": "image" #{None if product_image_big else ", \"resize\": \"300x300\""}}' t-att-alt="product.name"/>
</a>
</div>
<t t-if="show_publish">
@@ -340,7 +340,7 @@
</div>
<div class="row">
<div class="col-sm-7 col-md-7 col-lg-7">
<span itemprop="image" t-field="product.image" t-field-options='{"widget": "image", "class": "product_detail_img"}'/>
<span itemprop="image" t-field="product.image" t-field-options='{"widget": "image", "class": "product_detail_img", "alt-field": "name"}'/>
</div>
<div class="col-sm-5 col-md-5 col-lg-4 col-lg-offset-1" id="product_details">
<h1 itemprop="name" t-field="product.name">Product Name</h1>
+18 -2
View File
@@ -626,9 +626,9 @@ instead of a single view its ``arch`` field is composed of any number of
<field name="model">idea.category</field>
<field name="inherit_id" ref="id_category_list"/>
<field name="arch" type="xml">
<!-- find field description inside tree, and add the field
<!-- find field description and add the field
idea_ids after it -->
<xpath expr="/tree/field[@name='description']" position="after">
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" string="Number of ideas"/>
</xpath>
</field>
@@ -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
<xpath expr="//field[@name='description']" position="after">
<field name="idea_ids" />
</xpath>
<field name="description" position="after">
<field name="idea_ids" />
</field>
.. exercise:: Alter existing content
* Using model inheritance, modify the existing *Partner* model to add an
+2 -3
View File
@@ -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
+2 -1
View File
@@ -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:
+7 -14
View File
@@ -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:
+3 -2
View File
@@ -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
+4
View File
@@ -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: