[MERGE] forward port of branch saas-8 up to 99b6cd8

This commit is contained in:
Christophe Simonis
2016-01-07 18:21:13 +01:00
93 changed files with 945 additions and 256 deletions
+10
View File
@@ -177,6 +177,16 @@ class AccountAccount(models.Model):
def mark_as_reconciled(self):
return self.write({'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
@api.multi
def action_open_reconcile(self):
# Open reconciliation view for this account
action_context = {'show_mode_selector': False, 'account_ids': [self.id,]}
return {
'type': 'ir.actions.client',
'tag': 'manual_reconciliation_view',
'context': action_context,
}
class AccountJournal(models.Model):
_name = "account.journal"
+1 -1
View File
@@ -1162,7 +1162,7 @@ class AccountInvoiceLine(models.Model):
if not self.product_id:
fpos = self.invoice_id.fiscal_position_id
self.invoice_line_tax_ids = fpos.map_tax(self.account_id.tax_ids).ids
else:
elif not self.price_unit:
self._set_taxes()
@api.onchange('uom_id')
+22 -3
View File
@@ -213,6 +213,10 @@ class AccountMove(models.Model):
return [x.id for x in reversed_moves]
return True
@api.multi
def open_reconcile_view(self):
return self.line_ids.open_reconcile_view()
class AccountMoveLine(models.Model):
_name = "account.move.line"
@@ -421,6 +425,7 @@ class AccountMoveLine(models.Model):
# Note : this short-circuiting is better for performances, but also required
# since postgresql doesn't implement empty list (so 'AND id in ()' is useless)
return []
res_ids = res_ids and tuple(res_ids)
assert res_type in ('partner', 'account')
assert account_type in ('payable', 'receivable', None)
@@ -447,21 +452,22 @@ class AccountMoveLine(models.Model):
{3}
{4}
{5}
AND l.company_id = {6}
AND EXISTS (
SELECT NULL
FROM account_move_line l
WHERE l.account_id = a.id
{6}
{7}
AND l.amount_residual > 0
)
AND EXISTS (
SELECT NULL
FROM account_move_line l
WHERE l.account_id = a.id
{6}
{7}
AND l.amount_residual < 0
)
GROUP BY {7} a.id, a.name, a.code, {res_alias}.last_time_entries_checked
GROUP BY {8} a.id, a.name, a.code, {res_alias}.last_time_entries_checked
ORDER BY {res_alias}.last_time_entries_checked
) as s
WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked)
@@ -472,6 +478,7 @@ class AccountMoveLine(models.Model):
is_partner and ' ' or "AND at.type <> 'payable' AND at.type <> 'receivable'",
account_type and "AND at.type = %(account_type)s" or '',
res_ids and 'AND ' + res_alias + '.id in %(res_ids)s' or '',
self.env.user.company_id.id,
is_partner and 'AND l.partner_id = p.id' or ' ',
is_partner and 'l.partner_id, p.id,' or ' ',
res_alias=res_alias
@@ -1141,6 +1148,18 @@ class AccountMoveLine(models.Model):
tables, where_clause, where_clause_params = query.get_sql()
return tables, where_clause, where_clause_params
@api.multi
def open_reconcile_view(self):
model, action_id = self.pool['ir.model.data'].get_object_reference(self._cr, self._uid, 'account', "action_account_moves_all_a")
action = self.pool[model].read(self._cr, self._uid, action_id, context=self._context)
ids = []
for aml in self:
if aml.account_id.reconcile:
ids.extend([r.debit_move_id.id for r in aml.matched_debit_ids] if aml.credit > 0 else [r.credit_move_id.id for r in aml.matched_credit_ids])
ids.append(aml.id)
action['domain'] = [('id', 'in', ids)]
return action
class AccountPartialReconcile(models.Model):
_name = "account.partial.reconcile"
+1 -1
View File
@@ -749,7 +749,7 @@ class WizardMultiChartsAccounts(models.TransientModel):
# Create the current year earning account (outside of the CoA)
self.env['account.account'].create({
'code': '9999',
'code': '999999',
'name': _('Undistributed Profits/Losses'),
'user_type_id': self.env.ref("account.data_unaffected_earnings").id,
'company_id': company.id,})
+2 -1
View File
@@ -306,7 +306,8 @@ class ResPartner(models.Model):
# generate where clause to include multicompany rules
where_query = account_invoice_report._where_calc([
('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('company_id', '=', self.env.user.company_id.id)
('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('company_id', '=', self.env.user.company_id.id),
('type', 'in', ('out_invoice', 'out_refund'))
])
account_invoice_report._apply_ir_rules(where_query, 'read')
from_clause, where_clause, where_clause_params = where_query.get_sql()
+1
View File
@@ -12,6 +12,7 @@ from . import test_chart_of_account
from . import test_fiscal_position
from . import test_manual_reconciliation
from . import test_payment
from . import test_product_id_change
from . import test_reconciliation
from . import test_search
from . import test_tax
@@ -0,0 +1,91 @@
from openerp.addons.account.tests.account_test_classes import AccountingTestCase
import time
class TestProductIdChange(AccountingTestCase):
"""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.invoice_model = self.env['account.invoice']
self.fiscal_position_model = self.env['account.fiscal.position']
self.fiscal_position_tax_model = self.env['account.fiscal.position.tax']
self.tax_model = self.env['account.tax']
self.pricelist_model = self.env['product.pricelist']
self.res_partner_model = self.env['res.partner']
self.product_tmpl_model = self.env['product.template']
self.product_model = self.env['product.product']
self.invoice_line_model = self.env['account.invoice.line']
self.account_receivable = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_receivable').id)], limit=1)
self.account_revenue = self.env['account.account'].search([('user_type_id', '=', self.env.ref('account.data_account_type_revenue').id)], limit=1)
def test_product_id_change(self):
partner = self.res_partner_model.create(dict(name="George"))
tax_include_sale = self.tax_model.create(dict(name="Include tax",
type_tax_use='sale',
amount='21.00',
price_include=True))
tax_include_purchase = self.tax_model.create(dict(name="Include tax",
type_tax_use='purchase',
amount='21.00',
price_include=True))
tax_exclude_sale = self.tax_model.create(dict(name="Exclude tax",
type_tax_use='sale',
amount='0.00'))
tax_exclude_purchase = self.tax_model.create(dict(name="Exclude tax",
type_tax_use='purchase',
amount='0.00'))
product_tmpl = self.product_tmpl_model.create(dict(name="Voiture",
list_price='121',
standard_price='121',
taxes_id=[(6, 0, [tax_include_sale.id])],
supplier_taxes_id=[(6, 0, [tax_include_purchase.id])]))
product = self.product_model.create(dict(product_tmpl_id=product_tmpl.id))
fp = self.fiscal_position_model.create(dict(name="fiscal position", sequence=1))
fp_tax_sale = self.fiscal_position_tax_model.create(dict(position_id=fp.id,
tax_src_id=tax_include_sale.id,
tax_dest_id=tax_exclude_sale.id))
fp_tax_purchase = self.fiscal_position_tax_model.create(dict(position_id=fp.id,
tax_src_id=tax_include_purchase.id,
tax_dest_id=tax_exclude_purchase.id))
out_invoice = self.invoice_model.create({
'partner_id': partner.id,
'reference_type': 'none',
'name': 'invoice to client',
'account_id': self.account_receivable.id,
'type': 'out_invoice',
'date_invoice': time.strftime('%Y') + '-06-26',
'fiscal_position_id': fp.id,
})
out_line = self.invoice_line_model.create({
'product_id': product.id,
'quantity': 1,
'price_unit': 121.0,
'invoice_id': out_invoice.id,
'name': 'something out',
'account_id': self.account_revenue.id,
})
in_invoice = self.invoice_model.create({
'partner_id': partner.id,
'reference_type': 'none',
'name': 'invoice to supplier',
'account_id': self.account_receivable.id,
'type': 'in_invoice',
'date_invoice': time.strftime('%Y') + '-06-26',
'fiscal_position_id': fp.id,
})
in_line = self.invoice_line_model.create({
'product_id': product.id,
'quantity': 1,
'price_unit': 121.0,
'invoice_id': in_invoice.id,
'name': 'something in',
'account_id': self.account_revenue.id,
})
out_line._onchange_product_id()
self.assertEquals(100, out_line.price_unit, "The included tax must be subtracted to the price")
in_line._onchange_product_id()
self.assertEquals(100, in_line.price_unit, "The included tax must be subtracted to the price")
+25 -2
View File
@@ -21,7 +21,11 @@
<field name="company_id" options="{'no_create': True}" groups="base.group_multi_company"/>
<field name="currency_id" options="{'no_create': True}" groups="base.group_multi_currency"/>
<field name="internal_type" invisible="1" readonly="1"/>
<field name="reconcile"/>
<label for="reconcile"/>
<div>
<field name="reconcile"/>
<button name="action_open_reconcile" class="oe_link" type="object" string=" -> Reconcile" attrs="{'invisible': [('reconcile', '=', False)]}"/>
</div>
<field name="deprecated"/>
</group>
<group>
@@ -1021,7 +1025,18 @@
<field name="amount_currency"/>
</group>
<group string="Reconciliation">
<field name="reconciled"/>
<label for="reconciled"/>
<div>
<field name="reconciled"/>
<field name="matched_debit_ids" invisible="1"/>
<field name="matched_credit_ids" invisible="1"/>
<button name="open_reconcile_view"
class="oe_link"
type="object"
string="-> View reconciled entries"
attrs="{'invisible': [('matched_debit_ids', '=', []),('matched_credit_ids', '=', [])]}">
</button>
</div>
</group>
<group string="States">
<field name="blocked"/>
@@ -1238,6 +1253,14 @@
<field name="state" widget="statusbar"/>
</header>
<sheet>
<div class="oe_button_box">
<button name="open_reconcile_view"
class="oe_stat_button"
icon="fa-bars"
type="object"
string="View reconciled entries">
</button>
</div>
<label for="name" class="oe_edit_only" attrs="{'invisible':[('name','=','/')]}"/>
<h1>
<field name="name" readonly="True" attrs="{'invisible':[('name','=','/')]}"/>
+5 -1
View File
@@ -34,7 +34,11 @@ class AuthSignupHome(Home):
self.do_signup(qcontext)
return super(AuthSignupHome, self).web_login(*args, **kw)
except (SignupError, AssertionError), e:
qcontext['error'] = _(e.message)
if request.env["res.users"].sudo().search([("login", "=", qcontext.get("login"))]):
qcontext["error"] = _("Another user is already registered using this email address.")
else:
_logger.error(e.message)
qcontext['error'] = _("Could not create a new account.")
return request.render('auth_signup.signup', qcontext)
+19 -57
View File
@@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0\n"
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-09-14 10:27+0000\n"
"PO-Revision-Date: 2015-09-14 10:27+0000\n"
"POT-Creation-Date: 2016-01-06 07:13+0000\n"
"PO-Revision-Date: 2016-01-06 07:13+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -75,13 +75,19 @@ msgid "Allow external users to sign up"
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/controllers/main.py:60
#: code:addons/auth_signup/controllers/main.py:64
#, python-format
msgid "An email has been sent with credentials to reset your password"
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/controllers/main.py:108
#: code:addons/auth_signup/controllers/main.py:40
#, python-format
msgid "Another user is already registered using this email address."
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/controllers/main.py:115
#, python-format
msgid "Authentication Failed."
msgstr ""
@@ -114,7 +120,13 @@ msgid "Connected"
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/controllers/main.py:62
#: code:addons/auth_signup/controllers/main.py:43
#, python-format
msgid "Could not create a new account."
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/controllers/main.py:66
#, python-format
msgid "Could not reset your password"
msgstr ""
@@ -155,18 +167,13 @@ msgstr ""
msgid "Password reset"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_private
msgid "Private Profile"
msgstr ""
#. module: auth_signup
#: model:ir.ui.view,arch_db:auth_signup.login
msgid "Reset Password"
msgstr ""
#. module: auth_signup
#: code:addons/auth_signup/res_users.py:250
#: code:addons/auth_signup/res_users.py:253
#, python-format
msgid "Reset password: invalid username or email"
msgstr ""
@@ -227,11 +234,6 @@ msgstr ""
msgid "Template user for new users created through signup"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,help:auth_signup.field_res_users_website_url
msgid "The full URL to access the document through the website."
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,help:auth_signup.field_base_config_settings_auth_signup_reset_password
msgid "This allows users to trigger a password reset from the Login page."
@@ -242,41 +244,6 @@ msgstr ""
msgid "Users"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_published
msgid "Visible in Website"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_description
msgid "Website Partner Full Description"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_short_description
msgid "Website Partner Short Description"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_url
msgid "Website URL"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_meta_description
msgid "Website meta description"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_meta_keywords
msgid "Website meta keywords"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_website_meta_title
msgid "Website meta title"
msgstr ""
#. module: auth_signup
#: model:ir.ui.view,arch_db:auth_signup.fields
#: model:ir.ui.view,arch_db:auth_signup.reset_password
@@ -298,8 +265,3 @@ msgstr ""
msgid "e.g. John Doe"
msgstr ""
#. module: auth_signup
#: model:ir.model.fields,field_description:auth_signup.field_res_users_self
msgid "unknown"
msgstr ""
+1 -1
View File
@@ -1459,7 +1459,7 @@ class calendar_event(osv.Model):
for arg in args:
new_arg = arg
if arg[0] in ('start_date', 'start_datetime', 'start',) and arg[1] == ">=":
if arg[0] in ('stop_date', 'stop_datetime', 'stop',) and arg[1] == ">=":
if context.get('virtual_id', True):
new_args += ['|', '&', ('recurrency', '=', 1), ('final_date', arg[1], arg[2])]
elif arg[0] == "id":
+1
View File
@@ -87,6 +87,7 @@
<group>
<!-- Preload all the partner's information -->
<field name="partner_id" string="Customer"
on_change="on_change_partner_id(partner_id)"
options='{"create_name_field": "name"}'
context="{'default_name': contact_name, 'default_street': street, 'default_city': city, 'default_state_id': state_id, 'default_zip': zip, 'default_country_id': country_id, 'default_function': function, 'default_phone': phone, 'default_mobile': mobile, 'default_fax': fax, 'default_email': email_from, 'default_user_id': user_id, 'default_team_id': team_id}" groups="base.group_no_one"/>
<field name="partner_name" string="Company Name"/>
@@ -121,6 +121,7 @@ class crm_lead2opportunity_partner(osv.osv_memory):
context = {}
lead_obj = self.pool['crm.lead']
partner_obj = self.pool['res.partner']
w = self.browse(cr, uid, ids, context=context)[0]
opp_ids = [o.id for o in w.opportunity_ids]
@@ -144,6 +145,9 @@ class crm_lead2opportunity_partner(osv.osv_memory):
lead_ids = context.get('active_ids', [])
vals.update({'lead_ids': lead_ids, 'user_ids': [w.user_id.id]})
self._convert_opportunity(cr, uid, ids, vals, context=context)
for lead in lead_obj.browse(cr, uid, lead_ids, context=context):
if lead.partner_id and lead.partner_id.user_id != lead.user_id:
partner_obj.write(cr, uid, [lead.partner_id.id], {'user_id': lead.user_id.id}, context=context)
return self.pool.get('crm.lead').redirect_opportunity_view(cr, uid, lead_ids[0], context=context)
+22 -1
View File
@@ -3,7 +3,7 @@
import logging
from openerp import api, fields, models, _
from openerp.exceptions import UserError
from openerp.exceptions import UserError, ValidationError
_logger = logging.getLogger(__name__)
@@ -52,6 +52,25 @@ class DeliveryCarrier(models.Model):
fixed_price = fields.Float(compute='_compute_fixed_price', inverse='_set_product_fixed_price', store=True, string='Fixed Price',help="Keep empty if the pricing depends on the advanced pricing per destination")
shipping_enabled = fields.Boolean(string="Shipping enabled", default=True, help="Uncheck this box to disable package shipping while validating Delivery Orders")
@api.multi
def name_get(self):
display_delivery = self.env.context.get('display_delivery', False)
order_id = self.env.context.get('order_id', False)
if display_delivery and order_id:
order = self.env['sale.order'].browse(order_id)
currency = order.pricelist_id.currency_id.name or ''
res = []
for carrier_id in self.ids:
try:
r = self.read([carrier_id], ['name', 'price'])[0]
res.append((r['id'], r['name'] + ' (' + (str(r['price'])) + ' ' + currency + ')'))
except ValidationError:
r = self.read([carrier_id], ['name'])[0]
res.append((r['id'], r['name']))
else:
res = super(DeliveryCarrier, self).name_get()
return res
@api.depends('product_id.list_price', 'product_id.product_tmpl_id.list_price')
def _compute_fixed_price(self):
for carrier in self:
@@ -234,6 +253,8 @@ class DeliveryCarrier(models.Model):
quantity += qty
total = (order.amount_total or 0.0) - total_delivery
total = order.currency_id.with_context(date=order.date_order).compute(total, order.company_id.currency_id)
return self.get_price_from_picking(total, weight, volume, quantity)
def get_price_from_picking(self, total, weight, volume, quantity):
+2 -1
View File
@@ -171,7 +171,8 @@
</group>
</xpath>
<div name="button_box" position="inside">
<button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking" attrs="{'invisible': ['|',('carrier_tracking_ref','=',False),('delivery_type','in', ['fixed', 'base_on_rule'])]}" />
<button type="object" name="open_website_url" class="oe_stat_button" icon='fa-truck' string="Tracking"
attrs="{'invisible': ['|','|',('carrier_tracking_ref','=',False),('carrier_id', '=', False),('delivery_type','=','grid')]}" />
</div>
<xpath expr="/form/header/button[last()]" position="after">
<button name="send_to_shipper" string="Send to Shipper" type="object" attrs="{'invisible':['|','|','|',('carrier_tracking_ref','!=',False),('delivery_type','in', ['fixed', 'base_on_rule']),('delivery_type','=',False),('state','not in',('done'))]}"/>
+2 -2
View File
@@ -30,8 +30,8 @@ class sale_order_line(osv.osv):
help="Choose an event ticket and it will automatically create a registration for this event ticket."),
# those 2 fields are used for dynamic domains and filled by onchange
# TDE: really necessary ? ...
'event_type_id': fields.related('product_id', 'event_type_id', type='many2one', relation="event.type", string="Event Type"),
'event_ok': fields.related('product_id', 'event_ok', string='event_ok', type='boolean'),
'event_type_id': fields.related('product_id', 'event_type_id', type='many2one', relation="event.type", string="Event Type", readonly=True),
'event_ok': fields.related('product_id', 'event_ok', string='event_ok', type='boolean', readonly=True),
}
def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
+5 -1
View File
@@ -6,13 +6,17 @@
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<xpath expr="//field[@name='order_line']//form//field[@name='product_id']" position="after">
<field name="event_id" domain="['|', ('event_type_id','=', False),('event_type_id', '=', event_type_id),('date_end','&gt;=',time.strftime('%Y-%m-%d 00:00:00'))]" attrs="{'invisible': [('event_ok', '=', False)],'required': [('event_ok', '!=', False)]}"/>
<field name="event_ticket_id" domain="[('event_id', '=', event_id), ('seats_available', '>', 0)]" attrs="{'invisible': [('event_id', '=', False)], 'required': [('event_id', '!=', False)]}"
on_change="onchange_event_ticket_id(event_ticket_id, context)"/>
<field name="event_type_id" invisible="1"/>
<field name="event_ok" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']//tree//field[@name='product_id']" position="after">
<field name="event_type_id" invisible="1"/>
<field name="event_ok" invisible="1"/>
</xpath>
</field>
</record>
+2 -2
View File
@@ -232,12 +232,12 @@ class google_calendar(osv.AbstractModel):
"start": {
type: start_date,
vstype: None,
'timeZone': context.get('tz', 'UTC'),
'timeZone': context.get('tz') or 'UTC',
},
"end": {
type: final_date,
vstype: None,
'timeZone': context.get('tz', 'UTC'),
'timeZone': context.get('tz') or 'UTC',
},
"attendees": attendee_list,
"reminders": {
+1
View File
@@ -2,4 +2,5 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import models
import report
import wizard
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import hr_expense_report
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from openerp.osv import osv
from openerp.tools.misc import formatLang
from openerp.tools.translate import _
from openerp.report import report_sxw
from openerp.exceptions import UserError
class report_expense(report_sxw.rml_parse):
def set_context(self, objects, data, ids, report_type=None):
res = super(report_expense, self).set_context(objects, data, ids, report_type=report_type)
hr_expense_obj = self.pool.get('hr.expense')
context = {'lang': self.pool.get('res.users').browse(self.cr, self.uid, self.uid).lang}
state_field = hr_expense_obj.fields_get(self.cr, self.uid, 'state', context=context)['state']['selection']
state_dict = {}
for state_tuple in state_field:
state_dict[state_tuple[0]] = state_tuple[1]
expenses_info = {}
expenses = hr_expense_obj.search(self.cr, self.uid, [('id', 'in', ids)], order="employee_id, currency_id, state, date")
for expense in hr_expense_obj.browse(self.cr, self.uid, expenses):
key = expense.employee_id.name + '-' + expense.currency_id.name + '-' + expense.state
if expenses_info.get(key):
expenses_info[key]['lines'] += expense
expenses_info[key]['total_amount'] += expense.total_amount
else:
expenses_info[key] = {
'employee_name': expense.employee_id.name,
'total_amount': expense.total_amount,
'lines': expense,
'currency': expense.currency_id,
'validator_name': expense.employee_id.parent_id.name,
'notes': [],
'notes_index': {},
'state': state_dict[expense.state],
}
if expense.description:
index = len(expenses_info[key]['notes']) + 1
expenses_info[key]['notes'].append({'description': expense.description, 'index':index})
expenses_info[key]['notes_index'][expense.id] = index
# Qweb for-each do not work on dict, so we send a list and we sort it by the name of the employee
# that way if we have two sheet for the same employee they will follow in the report
self.localcontext.update({
'get_expenses': lambda : [v for k,v in sorted(expenses_info.items())],
})
return res
class report_hr_expense(osv.AbstractModel):
_name = 'report.hr_expense.report_expense'
_inherit = 'report.abstract_report'
_template = 'hr_expense.report_expense'
_wrapped_report_class = report_expense
+56 -41
View File
@@ -2,7 +2,7 @@
<odoo>
<template id="report_expense">
<t t-call="report.html_container">
<t t-foreach="docs" t-as="o">
<t t-foreach="get_expenses()" t-as="o">
<t t-call="report.external_layout">
<div class="page">
<h2>HR Expenses</h2>
@@ -10,25 +10,26 @@
<div class="row mt32 mb32">
<div class="col-xs-3">
<strong>Employee:</strong>
<p t-field="o.employee_id.name"/>
<p t-esc="o['employee_name']"/>
</div>
<div class="col-xs-3">
<strong>Date:</strong>
<p t-field="o.date"/>
<strong>Manager:</strong>
<p t-esc="o['validator_name']"/>
</div>
<div class="col-xs-3">
<strong>Description:</strong>
<p t-field="o.name"/>
<strong>Status:</strong>
<p t-esc="o['state']"/>
</div>
<div class="col-xs-3">
<strong>Validated By:</strong>
<p t-field="o.employee_id.parent_id.name"/>
<strong>Currency:</strong>
<p t-field="o['currency'].name"/>
</div>
</div>
<table class="table table-condensed">
<thead>
<tr>
<th>Date</th>
<th>Name</th>
<th>Unit Price</th>
<th class="text-center">Qty</th>
@@ -38,35 +39,46 @@
</tr>
</thead>
<tbody>
<tr>
<td>
<span t-field="o.name"/>
<span t-field="o.analytic_account_id.name"/>
</td>
<td>
<span t-field="o.unit_amount"/>
</td>
<td class="text-center">
<span t-field="o.quantity"/>
</td>
<td groups="product.group_uom">
<span t-field="o.product_uom_id"/>
</td>
<td>
<t t-foreach="o.tax_ids" t-as="tax">
<t t-if="tax.description">
<span t-field="tax.description"/>
</t>
<t t-if="not tax.description">
<span t-field="tax.name"/>
</t>
</t>
</td>
<td class="text-right">
<span t-field="o.total_amount"
t-field-options='{"widget": "monetary", "display_currency":"o.currency_id"}'/>
</td>
</tr>
<t t-foreach="o['lines']" t-as="line">
<tr>
<td>
<span t-field='line.date'/>
<t t-if="line.description">
<sup><span t-esc="o['notes_index'][line.id]"/></sup>
</t>
</td>
<td>
<span t-field="line.name"/>
<t t-if="line.analytic_account_id.name">
(<span t-field="line.analytic_account_id.name"/>)
</t>
</td>
<td>
<span t-field="line.unit_amount"
t-field-options='{"widget": "monetary", "display_currency":"line.currency_id"}'/>
</td>
<td class="text-center">
<span t-field="line.quantity"/>
</td>
<td groups="product.group_uom">
<span t-field="line.product_uom_id"/>
</td>
<td>
<t t-foreach="line.tax_ids" t-as="tax">
<t t-if="tax.description">
<span t-field="tax.description"/>
</t>
<t t-if="not tax.description">
<span t-field="tax.name"/>
</t>
</t>
</td>
<td class="text-right">
<span t-field="line.total_amount"
t-field-options='{"widget": "monetary", "display_currency":"line.currency_id"}'/>
</td>
</tr>
</t>
</tbody>
</table>
@@ -76,15 +88,18 @@
<tr class="border-black">
<td><strong>Total</strong></td>
<td class="text-right">
<span t-field="o.total_amount"
t-field-options='{"widget": "monetary", "display_currency": "o.currency_id"}'/>
<span t-esc="formatLang(o['total_amount'], currency_obj=o['currency'])"/>
</td>
</tr>
</table>
</div>
</div>
<p t-field="o.description"/>
<t t-foreach="o['notes']" t-as="note">
<span t-esc="note['index']"/>. <span t-esc="note['description']"/>
<br/>
</t>
<br/>
<br/>
<p>Certified honest and conform,<br/>(Date and signature).<br/><br/></p>
<p>This document must be dated and signed for reimbursement.</p>
</div>
@@ -22,7 +22,7 @@ class payslip_details_report(report_sxw.rml_parse):
if not rule_categories:
return []
if rule_categories[0].parent_id:
rule_categories.insert(0, rule_categories[0].parent_id)
rule_categories = rule_categories[0].parent_id | rule_categories
get_recursive_parent(rule_categories)
return rule_categories
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import controllers
+24
View File
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Blackbox Hardware Driver',
'version': '1.0',
'category': 'Hardware Drivers',
'sequence': 6,
'summary': 'Hardware Driver for Belgian Fiscal Data Modules',
'website': 'https://www.odoo.com/page/point-of-sale',
'description': """
Fiscal Data Module Hardware Driver
==================================
This module allows a Point Of Sale client to communicate with a
connected Belgian Fiscal Data Module.
""",
'author': 'OpenERP SA',
'depends': ['hw_proxy'],
'external_dependencies': {'python': ['serial']},
'test': [
],
'installable': True,
'auto_install': False,
}
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import main
+162
View File
@@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import serial
from os import listdir
from threading import Thread, Lock
from openerp import http
import openerp.addons.hw_proxy.controllers.main as hw_proxy
_logger = logging.getLogger(__name__)
class Blackbox(Thread):
def __init__(self):
Thread.__init__(self)
self.blackbox_lock = Lock()
self.set_status('connecting')
self.device_path = self._find_device_path_by_probing()
def set_status(self, status, messages=[]):
self.status = {
'status': status,
'messages': messages
}
def get_status(self):
return self.status
# There is no real way to find a serial device, all you can really
# find is the name of the serial to usb interface, which in the
# case of the blackbox is not defined because it doesn't always
# come with it's own interface (eg. Retail Cleancash SC-B). So, in
# order to differentiate between other devices like this, what
# we'll do is probe every serial device with an FDM status
# request. The first device to give an answer that makes sense
# wins.
def _find_device_path_by_probing(self):
path = "/dev/serial/by-id/"
probe_message = self._wrap_low_level_message_around("S000")
try:
devices = listdir(path)
except OSError:
_logger.warning(path + " doesn't exist")
else:
for device in listdir(path):
path_to_device = path + device
_logger.debug("Probing " + device)
if self._send_to_blackbox(probe_message, 21, path_to_device, just_wait_for_ack=True):
_logger.info(device + " will be used as the blackbox")
self.set_status("connected", [device])
return path_to_device
_logger.warning("Blackbox could not be found")
self.set_status("error", ["Couldn't find the Fiscal Data Module"])
return ""
def _lrc(self, msg):
lrc = 0
for character in msg:
byte = ord(character)
lrc = (lrc + byte) & 0xFF
lrc = ((lrc ^ 0xFF) + 1) & 0xFF
return lrc
def _wrap_low_level_message_around(self, high_level_message):
bcc = self._lrc(high_level_message)
high_level_message_bytes = map(ord, high_level_message)
low_level_message = bytearray()
low_level_message.append(0x02)
low_level_message.extend(high_level_message_bytes)
low_level_message.append(0x03)
low_level_message.append(bcc)
return low_level_message
def _send_and_wait_for_ack(self, packet, serial):
ack = 0
MAX_RETRIES = 1
while ack != 0x06 and int(chr(packet[4])) < MAX_RETRIES:
serial.write(packet)
ack = serial.read(1)
# This violates the principle that we do high level
# client-side and low level posbox-side but the retry
# counter is always in a fixed position in the high level
# message so it's safe to do it. Also it would be a pain
# to have to throw this all the way back to js just so it
# can increment the retry counter and then try again.
packet = packet[:4] + str(int(packet[4]) + 1) + packet[5:]
if ack:
ack = ord(ack)
else:
_logger.warning("did not get ACK, retrying...")
ack = 0
if ack == 0x06:
return True
else:
_logger.error("retried " + str(MAX_RETRIES) + " times without receiving ACK, is blackbox properly connected?")
return False
def _send_to_blackbox(self, packet, response_size, device_path, just_wait_for_ack=False):
if not device_path:
return ""
ser = serial.Serial(port=device_path,
baudrate=19200,
timeout=3)
MAX_NACKS = 1
got_response = False
sent_nacks = 0
if self._send_and_wait_for_ack(packet, ser):
if just_wait_for_ack:
return True
while not got_response and sent_nacks < MAX_NACKS:
stx = ser.read(1)
response = ser.read(response_size)
etx = ser.read(1)
bcc = ser.read(1)
if stx == chr(0x02) and etx == chr(0x03) and bcc and self._lrc(response) == ord(bcc):
got_response = True
ser.write(chr(0x06))
else:
_logger.warning("received ACK but not a valid response, sending NACK...")
sent_nacks += 1
ser.write(chr(0x15))
if not got_response:
_logger.error("sent " + str(MAX_NACKS) + " NACKS without receiving response, giving up.")
return ""
ser.close()
return response
else:
ser.close()
return ""
blackbox_thread = Blackbox()
hw_proxy.drivers['fiscal_data_module'] = blackbox_thread
class BlackboxDriver(hw_proxy.Proxy):
@http.route('/hw_proxy/request_blackbox/', type='json', auth='none', cors='*')
def request_blackbox(self, high_level_message, response_size):
to_send = blackbox_thread._wrap_low_level_message_around(high_level_message)
with blackbox_thread.blackbox_lock:
response = blackbox_thread._send_to_blackbox(to_send, response_size, blackbox_thread.device_path)
return response
+2 -1
View File
@@ -30,7 +30,8 @@ class LivechatController(http.Controller):
username = kwargs.get("username", _("Visitor"))
channel = request.env['im_livechat.channel'].sudo().browse(channel_id)
info = request.env['im_livechat.channel'].get_livechat_info(channel.id, username=username)
return request.render('im_livechat.loader', {'info': info, 'web_session_required': True}) if info['available'] else False
js = request.render('im_livechat.loader', {'info': info, 'web_session_required': True})
return request.make_response(js, headers=[('Content-Type', 'application/javascript')])
@http.route('/im_livechat/init', type='json', auth="public")
def livechat_init(self, channel_id):
@@ -195,7 +195,7 @@ class ImLivechatChannel(models.Model):
@api.model
def get_livechat_info(self, channel_id, username='Visitor'):
info = {}
info['available'] = len(self.browse(channel_id).get_available_users())
info['available'] = len(self.browse(channel_id).get_available_users()) > 0
if info['available']:
info['server_url'] = self.env['ir.config_parameter'].get_param('web.base.url')
info['options'] = self.sudo().get_channel_infos(channel_id)
@@ -143,7 +143,6 @@
<t t-set="info" t-value="request.env['im_livechat.channel'].get_livechat_info(channel)"/>
</t>
<t t-if="info['available']" t-translation="off">
document.addEventListener("DOMContentLoaded", function(event) {
<t t-if="web_session_required">
odoo.define('web.session', function (require) {
@@ -154,6 +153,7 @@
</t>
odoo.define('im_livechat.livesupport', function (require) {
<t t-if="info['available']" t-translation="off">
var im_livechat = require('im_livechat.im_livechat');
var button = new im_livechat.LivechatButton(
$('body'),
@@ -162,9 +162,9 @@
);
button.appendTo($('body'));
window.livechat_button = button;
</t>
});
});
</t>
</template>
+1 -1
View File
@@ -198,7 +198,7 @@ class account_invoice(osv.osv):
required=True, readonly=True),
}
_constraints = [
(_check_communication, 'Invalid BBA Structured Communication !', ['Communication']),
(_check_communication, 'Invalid BBA Structured Communication !', ['reference', 'reference_type']),
]
account_invoice()
+1 -1
View File
@@ -167,7 +167,7 @@ class MailMail(models.Model):
if partner:
email_to = [formataddr((partner.name, partner.email))]
else:
email_to = tools.email_split(self.email_to)
email_to = tools.email_split_and_format(self.email_to)
return email_to
@api.multi
+1 -1
View File
@@ -38,7 +38,7 @@ def format_tz(pool, cr, uid, dt, tz=False, format=False, context=None):
format_date = lang_params.get("date_format", '%B-%d-%Y')
format_time = lang_params.get("time_format", '%I-%M %p')
fdate = ts.strftime(format_date)
fdate = ts.strftime(format_date).decode('utf-8')
ftime = ts.strftime(format_time)
return "%s %s%s" % (fdate, ftime, (' (%s)' % tz) if tz else '')
+1
View File
@@ -39,6 +39,7 @@ class TestMail(common.SavepointCase):
cls.env['ir.mail_server']._patch_method('build_email', build_email)
cls.env['ir.mail_server']._patch_method('send_email', send_email)
# User groups
user_group_employee = cls.env.ref('base.group_user')
user_group_portal = cls.env.ref('base.group_portal')
+3 -1
View File
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
from email.utils import formataddr
from .common import TestMail
from openerp.tools import mute_logger
import socket
@@ -311,7 +313,7 @@ class TestMailgateway(TestMail):
'message_process: incoming email on Partners alias should send a bounce email')
self.assertIn('New Frogs', self._mails[0].get('subject'),
'message_process: bounce email on Partners alias should contain the original subject')
self.assertIn('test.sylvie.lelitre@agrolait.com', self._mails[0].get('email_to'),
self.assertIn(formataddr(('Sylvie Lelitre', 'test.sylvie.lelitre@agrolait.com')), self._mails[0].get('email_to'),
'message_process: bounce email on Partners alias should have original email sender as recipient')
@mute_logger('openerp.addons.mail.models.mail_thread', 'openerp.models', 'openerp.addons.mail.models.mail_mail')
@@ -96,6 +96,7 @@
context="{
'default_composition_mode': 'mass_mail',
'default_partner_to': '${object.id or \'\'}',
'default_active_domain': [['id', 'in', context.get('active_ids', [])]],
}"/>
</data>
</openerp>
+14 -20
View File
@@ -4,10 +4,10 @@
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0\n"
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2015-10-09 09:20+0000\n"
"PO-Revision-Date: 2015-10-09 09:20+0000\n"
"POT-Creation-Date: 2016-01-06 07:17+0000\n"
"PO-Revision-Date: 2016-01-06 07:17+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
@@ -640,10 +640,14 @@ msgstr ""
#. module: mass_mailing
#: model:ir.ui.view,arch_db:mass_mailing.email_designer_snippets
#: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1
msgid "Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br/>"
msgstr ""
#. module: mass_mailing
#: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1
msgid "Great stories have personality. Consider telling a great story that provides personality. Writing a story with personality for potential clients will assist with making a relationship connection. This shows up in small quirks like word choices or phrases. Write from your point of view, not from someone else's experience.<br>"
msgstr ""
#. module: mass_mailing
#: model:ir.ui.view,arch_db:mass_mailing.view_mail_mail_statistics_search
#: model:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_campaign_search
@@ -660,7 +664,7 @@ msgstr ""
#. module: mass_mailing
#: model:ir.ui.view,arch_db:mass_mailing.FieldTextHtmlInline
msgid "Help"
msgid "Here you can design your own template from scratch by selecting your favorite theme."
msgstr ""
#. module: mass_mailing
@@ -1023,11 +1027,6 @@ msgstr ""
msgid "Messages"
msgstr ""
#. module: mass_mailing
#: model:ir.model.fields,help:mass_mailing.field_mail_mass_mailing_contact_message_ids
msgid "Messages and communication history"
msgstr ""
#. module: mass_mailing
#: model:ir.ui.view,arch_db:mass_mailing.email_designer_snippets
#: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1
@@ -1551,6 +1550,11 @@ msgstr ""
msgid "This mass mailing is scheduled to"
msgstr ""
#. module: mass_mailing
#: model:ir.ui.view,arch_db:mass_mailing.view_mail_mass_mailing_form
msgid "This will send the email to all recipients. Do you still want to proceed ?"
msgstr ""
#. module: mass_mailing
#: model:ir.model.fields,field_description:mass_mailing.field_mail_mass_mailing_campaign_total
#: model:ir.model.fields,field_description:mass_mailing.field_mail_mass_mailing_total
@@ -1614,11 +1618,6 @@ msgstr ""
msgid "We would like you to know that your order has shipped! To track your order or make any changes please click the \"my order\" button below."
msgstr ""
#. module: mass_mailing
#: model:ir.model.fields,field_description:mass_mailing.field_mail_mass_mailing_contact_website_message_ids
msgid "Website Messages"
msgstr ""
#. module: mass_mailing
#: model:ir.model.fields,field_description:mass_mailing.field_mass_mailing_config_settings_group_website_popup_on_exit
msgid "Website Pop-up"
@@ -1634,11 +1633,6 @@ msgstr ""
msgid "Website Popup Redirect URL"
msgstr ""
#. module: mass_mailing
#: model:ir.model.fields,help:mass_mailing.field_mail_mass_mailing_contact_website_message_ids
msgid "Website communication history"
msgstr ""
#. module: mass_mailing
#: model:mail.mass_mailing,body_html:mass_mailing.mass_mail_1
msgid "You get a 20% discount for signing up!"
-1
View File
@@ -3,7 +3,6 @@
import re
import urlparse
import re
import werkzeug.urls
from openerp import tools
@@ -683,6 +683,8 @@ class MassMailing(osv.Model):
value['mailing_domain'] = "[('list_id', 'in', %s), ('opt_out', '=', False)]" % list(mailing_list_ids)
else:
value['mailing_domain'] = "[('list_id', '=', False)]"
elif 'opt_out' in self.pool[mailing_model]._fields:
value['mailing_domain'] = "[('opt_out', '=', False)]"
else:
value['mailing_domain'] = []
value['body_html'] = "on_change_model_and_list"
+2 -1
View File
@@ -379,7 +379,8 @@
<form string="Mass Mailing">
<header style="min-height:31px;">
<button name="action_test_mailing" type="object" class="oe_highlight" string="Test Mailing"/>
<button name="put_in_queue" type="object" attrs="{'invisible': ['|', ('state', 'in', ('in_queue', 'done'))]}" class="oe_highlight" string="Send to All"/>
<button name="put_in_queue" type="object" attrs="{'invisible': ['|', ('state', 'in', ('in_queue', 'done'))]}" class="oe_highlight" string="Send to All"
confirm="This will send the email to all recipients. Do you still want to proceed ?"/>
<button name="cancel_mass_mailing" type="object" attrs="{'invisible': [('state', '!=', 'in_queue')]}" class="oe_highlight" string="Cancel"/>
<button name="retry_failed_mail" type="object" attrs="{'invisible': ['|', ('state', '!=', 'done'), ('failed', '=', 0)]}" class="oe_highlight" string="Retry"/>
+26 -7
View File
@@ -1,6 +1,7 @@
# -*- coding: utf-'8' "-*-"
from hashlib import sha1
import logging
import urllib
import urlparse
from openerp.addons.payment.models.payment_acquirer import ValidationError
@@ -12,6 +13,16 @@ from openerp.tools.translate import _
_logger = logging.getLogger(__name__)
def normalize_keys_upper(data):
"""Set all keys of a dictionnary to uppercase
Buckaroo parameters names are case insensitive
convert everything to upper case to be able to easily detected the presence
of a parameter by checking the uppercase key only
"""
return dict((key.upper(), val) for key, val in data.items())
class AcquirerBuckaroo(osv.Model):
_inherit = 'payment.acquirer'
@@ -61,17 +72,22 @@ class AcquirerBuckaroo(osv.Model):
values = dict(values or {})
if inout == 'out':
if 'BRQ_SIGNATURE' in values:
del values['BRQ_SIGNATURE']
items = sorted((k.upper(), v) for k, v in values.items())
sign = ''.join('%s=%s' % (k, v) for k, v in items)
for key in values.keys():
# case insensitive keys
if key.upper() == 'BRQ_SIGNATURE':
del values[key]
break
items = sorted(values.items(), key=lambda (x, y): x.lower())
sign = ''.join('%s=%s' % (k, urllib.unquote_plus(v)) for k, v in items)
else:
sign = ''.join('%s=%s' % (k,get_value(k)) for k in keys)
#Add the pre-shared secret key at the end of the signature
sign = sign + acquirer.brq_secretkey
if isinstance(sign, str):
# TODO: remove me? should not be used
sign = urlparse.parse_qsl(sign)
shasign = sha1(sign).hexdigest()
shasign = sha1(sign.encode('utf-8')).hexdigest()
return shasign
@@ -117,6 +133,8 @@ class TxBuckaroo(osv.Model):
def _buckaroo_form_get_tx_from_data(self, cr, uid, data, context=None):
""" Given a data dict coming from buckaroo, verify it and find the related
transaction record. """
origin_data = dict(data)
data = normalize_keys_upper(data)
reference, pay_id, shasign = data.get('BRQ_INVOICENUMBER'), data.get('BRQ_PAYMENT'), data.get('BRQ_SIGNATURE')
if not reference or not pay_id or not shasign:
error_msg = _('Buckaroo: received data with missing reference (%s) or pay_id (%s) or shasign (%s)') % (reference, pay_id, shasign)
@@ -135,7 +153,7 @@ class TxBuckaroo(osv.Model):
tx = self.pool['payment.transaction'].browse(cr, uid, tx_ids[0], context=context)
#verify shasign
shasign_check = self.pool['payment.acquirer']._buckaroo_generate_digital_sign(tx.acquirer_id, 'out' ,data)
shasign_check = self.pool['payment.acquirer']._buckaroo_generate_digital_sign(tx.acquirer_id, 'out', origin_data)
if shasign_check.upper() != shasign.upper():
error_msg = _('Buckaroo: invalid shasign, received %s, computed %s, for data %s') % (shasign, shasign_check, data)
_logger.info(error_msg)
@@ -145,7 +163,7 @@ class TxBuckaroo(osv.Model):
def _buckaroo_form_get_invalid_parameters(self, cr, uid, tx, data, context=None):
invalid_parameters = []
data = normalize_keys_upper(data)
if tx.acquirer_reference and data.get('BRQ_TRANSACTIONS') != tx.acquirer_reference:
invalid_parameters.append(('Transaction Id', data.get('BRQ_TRANSACTIONS'), tx.acquirer_reference))
# check what is buyed
@@ -157,6 +175,7 @@ class TxBuckaroo(osv.Model):
return invalid_parameters
def _buckaroo_form_validate(self, cr, uid, tx, data, context=None):
data = normalize_keys_upper(data)
status_code = int(data.get('BRQ_STATUSCODE','0'))
if status_code in self._buckaroo_valid_tx_status:
tx.write({
@@ -122,7 +122,7 @@ class BuckarooForm(BuckarooCommon):
'BRQ_CURRENCY': u'EUR',
'BRQ_CUSTOMER_NAME': u'Jan de Tester',
'BRQ_INVOICENUMBER': u'SO004',
'BRQ_PAYMENT': u'573311D081B04069BD6336001611DBD4',
'brq_payment': u'573311D081B04069BD6336001611DBD4',
'BRQ_PAYMENT_METHOD': u'paypal',
'BRQ_SERVICE_PAYPAL_PAYERCOUNTRY': u'NL',
'BRQ_SERVICE_PAYPAL_PAYEREMAIL': u'fhe@openerp.com',
@@ -130,7 +130,7 @@ class BuckarooForm(BuckarooCommon):
'BRQ_SERVICE_PAYPAL_PAYERLASTNAME': u'Tester',
'BRQ_SERVICE_PAYPAL_PAYERMIDDLENAME': u'de',
'BRQ_SERVICE_PAYPAL_PAYERSTATUS': u'verified',
'BRQ_SIGNATURE': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91',
'Brq_signature': u'175d82dd53a02bad393fee32cb1eafa3b6fbbd91',
'BRQ_STATUSCODE': u'190',
'BRQ_STATUSCODE_DETAIL': u'S001',
'BRQ_STATUSMESSAGE': u'Transaction successfully processed',
+2 -2
View File
@@ -645,7 +645,7 @@ class pos_order(osv.osv):
_order = "id desc"
def _amount_line_tax(self, cr, uid, line, fiscal_position_id, context=None):
taxes = line.product_id.taxes_id.filtered(lambda t: t.company_id.id == line.order_id.company_id.id)
taxes = line.tax_ids.filtered(lambda t: t.company_id.id == line.order_id.company_id.id)
if fiscal_position_id:
taxes = fiscal_position_id.map_tax(taxes)
price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
@@ -1399,7 +1399,7 @@ class pos_order_line(osv.osv):
account_tax_obj = self.pool.get('account.tax')
for line in self.browse(cr, uid, ids, context=context):
cur = line.order_id.pricelist_id.currency_id
taxes = [ tax for tax in line.product_id.taxes_id if tax.company_id.id == line.order_id.company_id.id ]
taxes = [ tax for tax in line.tax_ids if tax.company_id.id == line.order_id.company_id.id ]
fiscal_position_id = line.order_id.fiscal_position_id
if fiscal_position_id:
taxes = fiscal_position_id.map_tax(taxes)
+7 -4
View File
@@ -8,6 +8,7 @@ odoo.define('point_of_sale.gui', function (require) {
var core = require('web.core');
var Model = require('web.DataModel');
var formats = require('web.formats');
var _t = core._t;
@@ -411,14 +412,16 @@ var Gui = core.Class.extend({
numpad_input: function(buffer, input, options) {
var newbuf = buffer.slice(0);
options = options || {};
var newbuf_float = formats.parse_value(newbuf, {type: "float"}, 0);
var decimal_point = _t.database.parameters.decimal_point;
if (input === '.') {
if (input === decimal_point) {
if (options.firstinput) {
newbuf = "0.";
}else if (!newbuf.length || newbuf === '-') {
newbuf += "0.";
} else if (newbuf.indexOf('.') < 0){
newbuf = newbuf + '.';
} else if (newbuf.indexOf(decimal_point) < 0){
newbuf = newbuf + decimal_point;
}
} else if (input === 'CLEAR') {
newbuf = "";
@@ -435,7 +438,7 @@ var Gui = core.Class.extend({
newbuf = '-' + newbuf;
}
} else if (input[0] === '+' && !isNaN(parseFloat(input))) {
newbuf = '' + ((parseFloat(newbuf) || 0) + parseFloat(input));
newbuf = this.chrome.format_currency_no_symbol(newbuf_float + parseFloat(input));
} else if (!isNaN(parseInt(input))) {
if (options.firstinput) {
newbuf = '' + input;
+2 -1
View File
@@ -1092,10 +1092,11 @@ exports.Orderline = Backbone.Model.extend({
clone: function(){
var orderline = new exports.Orderline({},{
pos: this.pos,
order: null,
order: this.order,
product: this.product,
price: this.price,
});
orderline.order = null;
orderline.quantity = this.quantity;
orderline.quantityStr = this.quantityStr;
orderline.discount = this.discount;
@@ -205,6 +205,7 @@ var NumberPopupWidget = PopupWidget.extend({
this._super(options);
this.inputbuffer = '' + (options.value || '');
this.decimal_separator = _t.database.parameters.decimal_point;
this.renderElement();
this.firstinput = true;
},
@@ -1300,7 +1300,7 @@ var ClientListScreenWidget = ScreenWidget.extend({
contents.append($(QWeb.render('ClientDetailsEdit',{widget:this,partner:partner})));
this.toggle_save_button();
contents.find('.image-uploader').on('change',function(){
contents.find('.image-uploader').on('change',function(event){
self.load_image_file(event.target.files[0],function(res){
if (res) {
contents.find('.client-picture img, .client-picture .fa').remove();
@@ -1519,7 +1519,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
event.keyCode === 110 || // Decimal point (numpad)
event.keyCode === 188 || // Comma
event.keyCode === 46 ) { // Numpad dot
key = '.';
key = self.decimal_point;
} else if (event.keyCode >= 48 && event.keyCode <= 57) { // Numbers
key = '' + (event.keyCode - 48);
} else if (event.keyCode === 45) { // Minus
@@ -1578,7 +1578,7 @@ var PaymentScreenWidget = ScreenWidget.extend({
order.selected_paymentline.set_amount(amount);
this.order_changes();
this.render_paymentlines();
this.$('.paymentline.selected .edit').text(this.inputbuffer);
this.$('.paymentline.selected .edit').text(this.format_currency_no_symbol(amount));
}
}
},
@@ -1704,9 +1704,9 @@ var PaymentScreenWidget = ScreenWidget.extend({
this.gui.show_popup('number',{
'title': tip ? _t('Change Tip') : _t('Add Tip'),
'value': value,
'value': self.format_currency_no_symbol(value),
'confirm': function(value) {
order.set_tip(Number(value));
order.set_tip(formats.parse_value(value, {type: "float"}, 0));
self.order_changes();
self.render_paymentlines();
}
+3 -6
View File
@@ -559,7 +559,7 @@
<br />
<button class="input-button numpad-char" data-action='CLEAR' >C</button>
<button class="input-button number-char" data-action='0'>0</button>
<button class="input-button number-char" data-action='.'>.</button>
<button class="input-button number-char" t-att-data-action='widget.decimal_point'><t t-esc='widget.decimal_point'/></button>
<button class="input-button numpad-backspace" data-action='BACKSPACE' >
<img src="/point_of_sale/static/src/img/backspace.png" width="24" height="21" />
</button>
@@ -702,9 +702,6 @@
<div>Served by <t t-esc='receipt.cashier' /></div>
</div>
</t>
<t t-if='!receipt.customer_count'>
<div>Guests: <t t-esc='receipt.customer_count' /></div>
</t>
</div>
<br /><br />
@@ -1006,7 +1003,7 @@
<br />
<button class="input-button numpad-char" data-action='CLEAR' >C</button>
<button class="input-button number-char" data-action='0'>0</button>
<button class="input-button number-char dot" data-action='.'>.</button>
<button class="input-button number-char dot" t-att-data-action='widget.decimal_separator'><t t-esc='widget.decimal_separator'/></button>
<button class="input-button numpad-backspace" data-action='BACKSPACE' >
<img style="pointer-events: none;" src="/point_of_sale/static/src/img/backspace.png" width="24" height="21" />
</button>
@@ -1298,7 +1295,7 @@
<tr>
<td>Subtotal:</td>
<td class="pos-right-align">
<t t-esc="widget.format_currency(order.get_subtotal())"/>
<t t-esc="widget.format_currency(order.get_total_without_tax())"/>
</td>
</tr>
<t t-foreach="order.get_tax_details()" t-as="taxdetail">
@@ -3,4 +3,3 @@ data_dir = /var/run/odoo
log_level = error
logfile = /var/log/odoo/odoo-server.log
pidfile = /var/run/odoo/odoo.pid
server_wide_modules = web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos
@@ -26,7 +26,7 @@ test -x $DAEMON || exit 0
set -e
function _start() {
start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos
start-stop-daemon --start --quiet --pidfile $PIDFILE --chuid $USER:$USER --background --make-pidfile --exec $DAEMON -- --config $CONFIG --logfile $LOGFILE --load=web,hw_proxy,hw_posbox_homepage,hw_posbox_upgrade,hw_scale,hw_scanner,hw_escpos,hw_blackbox_be
}
function _stop() {
@@ -6,6 +6,9 @@
<t t-if='receipt.table'>
at table <t t-esc='receipt.table' />
</t>
<t t-if='receipt.customer_count'>
<div>Guests: <t t-esc='receipt.customer_count' /></div>
</t>
</t>
</t>
@@ -14,6 +17,9 @@
<t t-if='receipt.table'>
at table <t t-esc='receipt.table' />
</t>
<t t-if='receipt.customer_count'>
<div>Guests: <t t-esc='receipt.customer_count' /></div>
</t>
</t>
</t>
+1 -1
View File
@@ -571,7 +571,7 @@ class product_template(osv.osv):
if ptype != 'standard_price':
res[product.id] = product[ptype] or 0.0
else:
company_id = product.env.user.company_id.id
company_id = context.get('force_company') or product.env.user.company_id.id
product = product.with_context(force_company=company_id)
res[product.id] = res[product.id] = product.sudo()[ptype]
if ptype == 'list_price':
+12 -4
View File
@@ -3,7 +3,7 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from openerp import api, fields, models, _
from openerp import api, fields, models, _, SUPERUSER_ID
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.translate import _
from openerp.tools.float_utils import float_is_zero, float_compare
@@ -150,7 +150,7 @@ class PurchaseOrder(models.Model):
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\
help="This will determine picking type of incoming shipment")
default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\
help="Technical field used to display the Drop Ship Address")
help="Technical field used to display the Drop Ship Address", readonly=True)
group_id = fields.Many2one('procurement.group', string="Procurement Group")
@api.model
@@ -434,6 +434,7 @@ class PurchaseOrderLine(models.Model):
@api.depends('order_id.state', 'move_ids.state')
def _compute_qty_received(self):
ProductUom = self.env['product.uom']
for line in self:
if line.order_id.state not in ['purchase', 'done']:
line.qty_received = 0.0
@@ -444,7 +445,10 @@ class PurchaseOrderLine(models.Model):
total = 0.0
for move in line.move_ids:
if move.state == 'done':
total += move.product_uom_qty
if move.product_uom != line.product_uom:
total += ProductUom._compute_qty_obj(move.product_uom, move.product_uom_qty, line.product_uom)
else:
total += move.product_uom_qty
line.qty_received = total
name = fields.Text(string='Description', required=True)
@@ -581,7 +585,11 @@ class PurchaseOrderLine(models.Model):
self.name += '\n' + product_lang.description_purchase
fpos = self.order_id.fiscal_position_id
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)
if self.env.uid == SUPERUSER_ID:
company_id = self.env.user.company_id.id
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id.id == company_id))
else:
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)
self._suggest_quantity()
self._onchange_quantity()
+8
View File
@@ -12,6 +12,14 @@ class stock_picking(osv.osv):
readonly=True, relation="many2one"),
}
def _prepare_values_extra_move(self, cr, uid, op, product, remaining_qty, context=None):
res = super(stock_picking, self)._prepare_values_extra_move(cr, uid, op, product, remaining_qty, context=context)
for m in op.linked_move_operation_ids:
if m.move_id.purchase_line_id and m.move_id.product_id == product:
res['purchase_line_id'] = m.move_id.purchase_line_id.id
break
return res
class stock_move(osv.osv):
_inherit = 'stock.move'
+11 -1
View File
@@ -740,7 +740,8 @@ class SaleOrderLine(models.Model):
quantity=self.product_uom_qty,
date_order=self.order_id.date_order,
pricelist=self.order_id.pricelist_id.id,
uom=self.product_uom.id
uom=self.product_uom.id,
fiscal_position=self.env.context.get('fiscal_position')
)
self.price_unit = self.env['account.tax']._fix_tax_included_price(product.price, product.taxes_id, self.tax_id)
@@ -750,6 +751,15 @@ class SaleOrderLine(models.Model):
raise UserError(_('You can not remove a sale order line.\nDiscard changes and try setting the quantity to 0.'))
return super(SaleOrderLine, self).unlink()
def onchange_product_uom(self, cursor, user, ids, pricelist, product, qty=0,
uom=False, qty_uos=0, uos=False, name='', partner_id=False,
lang=False, update_tax=True, date_order=False, fiscal_position=False, context=None):
ctx = dict(context or {}, fiscal_position=fiscal_position)
return self.product_uom_change(cursor, user, ids, pricelist, product,
qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name,
partner_id=partner_id, lang=lang, update_tax=update_tax,
date_order=date_order, context=ctx)
@api.multi
def _get_delivered_qty(self):
'''
+2 -1
View File
@@ -12,7 +12,8 @@ class SaleOrderLine(models.Model):
def _compute_analytic(self, domain=None):
lines = {}
if not domain:
domain = [('so_line', 'in', self.ids), ('unit_amount', '<=', 0.0)]
# To filter on analyic lines linked to an expense
domain = [('so_line', 'in', self.ids), ('amount', '<=', 0.0)]
data = self.env['account.analytic.line'].read_group(
domain,
['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False
+2 -1
View File
@@ -4,6 +4,7 @@
from datetime import datetime, timedelta
from openerp import api, fields, models, _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
from openerp.exceptions import UserError
class SaleOrder(models.Model):
@@ -203,7 +204,7 @@ class SaleOrderLine(models.Model):
mto_route_id = False
try:
mto_route_id = self.env['stock.warehouse']._get_mto_route()
except models.except_orm:
except UserError:
# if route MTO not found in ir_model_data, we treat the product as in MTS
pass
if mto_route_id and mto_route_id in product_routes.ids:
@@ -158,7 +158,8 @@ class SaleOrderLine(models.Model):
@api.multi
def _compute_analytic(self, domain=None):
if not domain:
domain = [('so_line', 'in', self.ids), '|', ('unit_amount', '<=', 0.0), ('is_timesheet', '=', True)]
# To filter on analyic lines linked to an expense
domain = [('so_line', 'in', self.ids), '|', ('amount', '<=', 0.0), ('is_timesheet', '=', True)]
return super(SaleOrderLine, self)._compute_analytic(domain=domain)
@api.model
+1 -2
View File
@@ -1556,7 +1556,6 @@ class stock_picking(models.Model):
for pack in pick.pack_operation_ids:
if pack.product_id and pack.product_id.tracking != 'none':
raise UserError(_('Some products require lots, so you need to specify those first!'))
view = data_obj.xmlid_to_res_id(cr, uid, 'stock.view_immediate_transfer')
wiz_id = self.pool['stock.immediate.transfer'].create(cr, uid, {'pick_id': pick.id}, context=context)
return {
@@ -4206,7 +4205,7 @@ class stock_package(osv.osv):
quant_obj = self.pool.get('stock.quant')
for package in self.browse(cr, uid, ids, context=context):
quant_ids = [quant.id for quant in package.quant_ids]
quant_obj.write(cr, uid, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
quant_obj.write(cr, SUPERUSER_ID, quant_ids, {'package_id': package.parent_id.id or False}, context=context)
children_package_ids = [child_package.id for child_package in package.children_ids]
self.write(cr, uid, children_package_ids, {'parent_id': package.parent_id.id or False}, context=context)
#delete current package since it contains nothing anymore
+1 -1
View File
@@ -380,7 +380,7 @@ class stock_move(osv.osv):
def _store_average_cost_price(self, cr, uid, move, context=None):
''' move is a browe record '''
product_obj = self.pool.get('product.product')
if any([q.qty <= 0 for q in move.quant_ids]):
if any([q.qty <= 0 for q in move.quant_ids]) or move.product_qty == 0:
#if there is a negative quant, the standard price shouldn't be updated
return
#Note: here we can't store a quant.cost directly as we may have moved out 2 units (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the average valuation price we are storing we will valuate it at 6€
+7 -26
View File
@@ -30,8 +30,8 @@ class procurement_order(osv.osv):
date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
purchase_date, delivery_date = self._get_previous_dates(cr, uid, orderpoint, date_planned, context=context)
if purchase_date and delivery_date:
self.write(cr, uid, {'next_delivery_date': self._convert_to_UTC(cr, uid, delivery_date, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'next_purchase_date': self._convert_to_UTC(cr, uid, purchase_date, context=context).strftime(DEFAULT_SERVER_DATETIME_FORMAT),}, context=context)
self.write(cr, uid, {'next_delivery_date': delivery_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'next_purchase_date': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),}, context=context)
@api.v8
def _get_purchase_order_date(self, schedule_date):
@@ -105,8 +105,6 @@ class procurement_order(osv.osv):
att_obj = self.pool.get('resource.calendar.attendance')
context = context or {}
context['no_round_hours'] = True
# Date should be converted to the correct timezone
start_date = self._convert_to_tz(cr, uid, start_date, context=context)
# First check if the orderpoint has a Calendar as it should be delivered at this calendar date
purchase_date = False
delivery_date = start_date
@@ -135,8 +133,8 @@ class procurement_order(osv.osv):
context = context or {}
context['no_round_hours'] = True
if not new_date:
new_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context)
now_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context)
new_date = datetime.utcnow()
now_date = datetime.utcnow()
# Search first calendar day (without group)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
@@ -178,21 +176,6 @@ class procurement_order(osv.osv):
return (date1, res[0][1])
return (False, False)
def _convert_to_tz(self, cr, uid, date, context=None):
if not context or not context.get('tz'):
return date
utc_date = pytz.UTC.localize(date)
timezone = pytz.timezone(context['tz'])
return utc_date.astimezone(timezone)
def _convert_to_UTC(self, cr, uid, date, context=None):
"""
The date should be timezone aware
"""
if not context or not context.get('tz'):
return date
return date.astimezone(pytz.UTC)
def _get_group(self, cr, uid, orderpoint, context=None):
"""
Will return the groups and the end dates of the intervals of the purchase calendar
@@ -207,15 +190,13 @@ class procurement_order(osv.osv):
context = context or {}
context['no_round_hours'] = True
date = False
now_date = self._convert_to_tz(cr, uid, datetime.utcnow(), context=context)
now_date = datetime.utcnow()
res_intervals = []
if orderpoint.purchase_calendar_id and orderpoint.purchase_calendar_id.attendance_ids:
if orderpoint.last_execution_date:
new_date = datetime.strptime(orderpoint.last_execution_date, DEFAULT_SERVER_DATETIME_FORMAT)
else:
new_date = datetime.utcnow()
# Convert to timezone of user
new_date = self._convert_to_tz(cr, uid, new_date, context=context)
intervals = calendar_obj._schedule_days(cr, uid, orderpoint.purchase_calendar_id.id, 1, new_date, compute_leaves=True, context=context)
for interval in intervals:
# If last execution date, interval should start after it in order not to execute the same orderpoint twice
@@ -285,8 +266,8 @@ class procurement_order(osv.osv):
date = res_group[1]
subtract_qty = orderpoint_obj.subtract_procurements_from_orderpoints(cr, uid, [x.id for x in ops_dict[key]], context=context)
first_op = True
ndelivery = date and self._convert_to_UTC(cr, uid, date, context=context) or False
npurchase = res_group[3] and self._convert_to_UTC(cr, uid, res_group[3], context=context) or False
ndelivery = date
npurchase = res_group[3]
for op in ops_dict[key]:
try:
prods = prod_qty[op.product_id.id]['virtual_available']
+2 -7
View File
@@ -48,21 +48,16 @@ class resource_calendar(osv.osv):
leave intervals
"""
resource_calendar = self.browse(cr, uid, id, context=context)
proc_obj = self.pool.get("procurement.order")
leaves = []
for leave in resource_calendar.leave_ids:
if leave.resource_id and not resource_id == leave.resource_id.id:
continue
date_from_db = datetime.datetime.strptime(leave.date_from, DEFAULT_SERVER_DATETIME_FORMAT)
date_from = proc_obj._convert_to_tz(cr, uid, date_from_db, context=context)
date_from = datetime.datetime.strptime(leave.date_from, DEFAULT_SERVER_DATETIME_FORMAT)
if end_datetime and date_from > end_datetime:
continue
date_to_db = datetime.datetime.strptime(leave.date_to, DEFAULT_SERVER_DATETIME_FORMAT)
date_to = proc_obj._convert_to_tz(cr, uid, date_to_db, context=context)
date_to = datetime.datetime.strptime(leave.date_to, DEFAULT_SERVER_DATETIME_FORMAT)
if start_datetime and date_to < start_datetime:
continue
leaves.append((date_from, date_to, leave.group_id.id))
return leaves
+1 -1
View File
@@ -1481,7 +1481,7 @@ class Reports(http.Controller):
if 'name' not in action:
reports = request.session.model('ir.actions.report.xml')
res_id = reports.search([('report_name', '=', action['report_name']),],
0, False, False, context)
context=context)
if len(res_id) > 0:
file_name = reports.read(res_id[0], ['name'], context)['name']
else:
@@ -843,7 +843,7 @@ var CalendarView = View.extend({
var index = this.dataset.get_id_index(id);
if (index !== null) {
event_id = this.dataset.ids[index];
this.dataset.write(event_id, data, {}).done(function() {
this.dataset.write(event_id, data, {}).always(function() {
if (is_virtual_id(event_id)) {
// this is a virtual ID and so this will create a new event
// with an unknown id for us.
+8 -1
View File
@@ -194,7 +194,7 @@ var FieldTextHtml = widget.extend({
return def;
},
get_url: function (_attr) {
var src = this.options.editor_url ? this.options.editor_url+"?" : "/web_editor/field/html?";
var src = this.options.editor_url || "/web_editor/field/html";
var datarecord = this.view.get_fields_values();
var attr = {
@@ -211,6 +211,9 @@ var FieldTextHtml = widget.extend({
if (this.options.snippets) {
attr.snippets = this.options.snippets;
}
if (this.options.template) {
attr.template = this.options.template;
}
if (!this.get("effective_readonly")) {
attr.enable_editor = 1;
}
@@ -227,6 +230,10 @@ var FieldTextHtml = widget.extend({
attr[k] = _attr[k];
}
if (src.indexOf('?') === -1) {
src += "?";
}
for (var k in attr) {
if (attr[k] !== null) {
src += "&"+k+"="+(_.isBoolean(attr[k]) ? +attr[k] : attr[k]);
@@ -191,9 +191,6 @@
}
&.o_kanban_mobile {
.o_kanban_record {
width: 400px;
}
span, div.label {
.o_text_overflow;
}
@@ -675,6 +675,7 @@ table.well tr td {
}
.oe_website_spinner input {
text-align: center;
min-width: 50px;
}
div.carousel .container > .carousel-caption {
@@ -545,6 +545,7 @@ table.well tr
width: 121px
input
text-align: center
min-width: 50px
div.carousel
.container
+2 -2
View File
@@ -19,7 +19,7 @@
<t t-call="website_portal.contact" />
<div class="oe_structure"/>
</div>
<div class="col-md-3 col-md-offset-1 o_my_sidebar">
<div id="o_my_sidebar" class="col-md-3 col-md-offset-1 o_my_sidebar">
<div class="o_my_details">
<h3 class="page-header">Your Details <a href="/my/account" class="btn btn-default btn-xs">Change</a></h3>
<div t-field="user.partner_id" t-field-options='{"widget": "contact", "fields": ["email", "phone", "address", "name"]}'/>
@@ -32,7 +32,7 @@
</template>
<template id="website_portal.custom_panel" inherit_id='website_portal.account' customize_show="True" name="Editable Panel" active="False">
<xpath expr="//div[@class='o_my_sidebar']" position="inside">
<xpath expr="//div[@id='o_my_sidebar']" position="inside">
<div class="oe_structure">
<h3 class="page-header">Custom Panel</h3>
<p>Place your content here.</p>
+26
View File
@@ -133,6 +133,32 @@ class sale_order_line(osv.Model):
'discounted_price': fields.function(_fnct_get_discounted_price, string='Discounted price', type='float', digits_compute=dp.get_precision('Product Price')),
}
class sale_order_line(osv.Model):
_inherit = "sale.order.line"
def _fnct_get_discounted_price(self, cr, uid, ids, field_name, args, context=None):
res = dict.fromkeys(ids, False)
for line in self.browse(cr, uid, ids, context=context):
res[line.id] = (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0))
return res
_columns = {
'discounted_price': fields.function(_fnct_get_discounted_price, string='Discounted price', type='float', digits_compute=dp.get_precision('Product Price')),
}
class sale_order_line(osv.Model):
_inherit = "sale.order.line"
def _fnct_get_discounted_price(self, cr, uid, ids, field_name, args, context=None):
res = dict.fromkeys(ids, False)
for line in self.browse(cr, uid, ids, context=context):
res[line.id] = (line.price_unit * (1.0 - (line.discount or 0.0) / 100.0))
return res
_columns = {
'discounted_price': fields.function(_fnct_get_discounted_price, string='Discounted price', type='float', digits_compute=dp.get_precision('Product Price')),
}
class website(orm.Model):
_inherit = 'website'
@@ -101,7 +101,7 @@ $('.oe_website_sale').each(function () {
$(oe_website_sale).on("change", 'input[name="add_qty"]', function (event) {
var product_ids = [];
var product_dom = $(".js_add_cart_variants[data-attribute_value_ids]").last();
var product_dom = $(".js_product .js_add_cart_variants[data-attribute_value_ids]").last();
if (!product_dom.length) {
return;
}
+6 -3
View File
@@ -1478,10 +1478,13 @@
<!-- Add a link to the product page in the portal orders followup page -->
<template id="orders_followup_products_links" name="Orders Followup Products Links" inherit_id="website_portal_sale.orders_followup">
<xpath expr="//span[@t-esc='ol.product_id.name']" position="replace">
<a t-att-href="ol.product_id.website_url">
<span t-esc="ol.product_id.name"/>
<xpath expr="//div[@id='product_name']/*" position="replace">
<a t-if="ol.product_id.website_published" t-att-href="ol.product_id.website_url">
<span t-esc="ol.name"/>
</a>
<t t-if="not ol.product_id.website_published">
<span t-esc="ol.name"/>
</t>
</xpath>
</template>
@@ -12,7 +12,6 @@ Delivery Costs
'data': [
'views/website_sale_delivery.xml',
'views/website_sale_delivery_view.xml',
'security/ir.model.access.csv',
'demo/website_sale_delivery_data.xml'
],
'demo': [
@@ -109,21 +109,24 @@ class SaleOrder(orm.Model):
def _get_delivery_methods(self, cr, uid, order, context=None):
carrier_obj = self.pool.get('delivery.carrier')
delivery_ids = carrier_obj.search(cr, uid, [('website_published', '=', True)], context=context)
carrier_ids = carrier_obj.search(cr, SUPERUSER_ID, [('website_published', '=', True)], context=context)
available_carrier_ids = []
# Following loop is done to avoid displaying delivery methods who are not available for this order
# This can surely be done in a more efficient way, but at the moment, it mimics the way it's
# done in delivery_set method of sale.py, from delivery module
for carrier in carrier_obj.browse(cr, SUPERUSER_ID, delivery_ids, context=dict(context, order_id=order.id)):
new_context = dict(context, order_id=order.id)
for carrier in carrier_ids:
try:
_logger.debug("Checking availability of %s" % carrier.name)
if carrier.available:
available_carrier_ids.append(carrier.id)
_logger.debug("Checking availability of carrier #%s" % carrier)
available = carrier_obj.read(cr, SUPERUSER_ID, [carrier], fields=['available'], context=new_context)[0]['available']
if available:
available_carrier_ids = available_carrier_ids + [carrier]
except ValidationError as e:
# RIM: hack to remove in master, because available field should not depend on a SOAP call to external shipping provider
# The validation error is used in backend to display errors in fedex config, but should fail silently in frontend
_logger.debug("%s removed from e-commerce carrier list. %s" % (carrier.name, e))
_logger.debug("Carrier #%s removed from e-commerce carrier list. %s" % (carrier, e))
return available_carrier_ids
@@ -1,2 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_delivery_carrier_public,delivery.carrier.public,delivery.model_delivery_carrier,,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_delivery_carrier_public delivery.carrier.public delivery.model_delivery_carrier 1 0 0 0
@@ -12,6 +12,11 @@
@MAX-Z-INDEX : 2147483647;
} .define-color-if-not-defined();
.top_covered_image {
object-fit: cover;
object-position: top;
}
.oe_slides_panel_footer {
background-color: @navbar-default-bg;
}
@@ -45,6 +50,7 @@ section.oe_slides_promote_box {
}
img.oe_slides_channel_thumbnail {
.top_covered_image();
height: 250px;
width: 100%
}
@@ -54,6 +60,7 @@ img.oe_slides_opacity {
}
img.oe_slides_promote_image {
.top_covered_image();
max-height: 300px;
width: 100%;
overflow: hidden;
@@ -65,6 +72,7 @@ img.oe_slides_promote_image {
}
.oe_slides_apart_small {
.top_covered_image();
height: 60px;
width: 90px;
}
@@ -97,6 +105,7 @@ textarea.o_slides_comment {
}
.oe_slides_grid_thumbnail {
.top_covered_image();
height: 200px;
width: 100%;
overflow: hidden;
+18
View File
@@ -0,0 +1,18 @@
Italy, 2015-11-05
Abstract Srl agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Davide Corio davide.corio@abstract.it https://github.com/dcorio
List of contributors:
Davide Corio davide.corio@abstract.it https://github.com/dcorio
Simone Orsi simone.orsi@abstract.it https://github.com/simahawk
Giacomo Spettoli giacomo.spettoli@abstract.it https://github.com/giacomos
+40
View File
@@ -0,0 +1,40 @@
Switzerland, 2015-11-09
brain-tec AG agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Pascal Zenklusen pascal.zenklusen@braintec-group.com https://github.com/BT-pzenklusen
List of contributors:
Álvaro Estébanez López alvaro.estebanez@braintec-group.com https://github.com/BT-aestebanez
Andreas Stauder andreas.stauder@braintec-group.com https://github.com/BT-astauder
Carlos Millán de Silva carlos.millan@braintec-group.com https://github.com/BT-cmillan
Cesar Andres Sanchez cesar-andres.sanchez@braintec-group.com https://github.com/BT-csanchez
Carlos Serra Toro carlos.serra@braintec-group.com https://github.com/BT-cserra
Dominik Schleich dominik.schleich@braintec-group.com https://github.com/BT-dschleich
Frédéric Aebi frederic.aebi@braintec-group.com https://github.com/BT-faebi
Fréderic Garbely frederic.garbely@braintec-group.com https://github.com/BT-fgarbely
Florian Wonneberger florian.wonneberger@braintec-group.com https://github.com/BT-fwonneberger
Jorge Asensio jorge.asensio@braintec-group.com https://github.com/BT-jasensio
Jose J. Duran-Martin jose-javier.duran@braintec-group.com https://github.com/BT-jduran
Federico Javier Mesa Hidalgo javier.mesa@braintec-group.com https://github.com/BT-jmesa
Joël Michaud joel.michaud@braintec-group.com https://github.com/BT-jmichaud
Jose Montero jose.montero@braintec-group.com https://github.com/BT-jmontero
José Luis Vallejo Diaz jose.vallejo@braintec-group.com https://github.com/BT-jvallejo
Kumar Aberer kumar.aberer@braintec-group.com https://github.com/BT-kaberer
Matt Fasola matt.fasola@braintec-group.com https://github.com/BT-mfasola
Miguel Tallon Benitez miguel.tallon@braintec-group.com https://github.com/BT-mtallon
Nicolas Frei nicolas.frei@braintec-group.com https://github.com/BT-nfrei
Nathanael Leutenegger nathanael.leutenegger@braintec-group.com https://github.com/BT-nleutenegger
Olivier Jossen olivier.jossen@braintec-group.com https://github.com/BT-ojossen
Philipp Fux philipp.fux@braintec-group.com https://github.com/BT-pfux
Raúl Martín raul.martin@braintec-group.com https://github.com/BT-rmartin
Silvan Wyden silvan.wyden@braintec-group.com https://github.com/BT-swyden
Timka Piric Muratovic timka.piric@braintec-group.com https://github.com/BT-tpiric
Pascal Zenklusen pascal.zenklusen@braintec-group.com https://github.com/BT-pzenklusen
+30
View File
@@ -0,0 +1,30 @@
China, 2015-12-10
Elico Corporation agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Eric Caudal eric.caudal@elico-corp.com https://github.com/elicoidal
List of contributors:
Eric Caudal eric.caudal@elico-corp.com https://github.com/elicoidal
Sébastien Maillard sebastien.maillard@elico-corp.com https://github.com/seb-elico
Siyuan Gu siyuancn@hotmail.com https://github.com/siyuancn
Wang Liping lynn.config@gmail.com https://github.com/lynndotconfig
Luke Zheng cialuo@126.com https://github.com/cialuo
Gary Wei gary.wei@elico-corp.com https://github.com/Garywei1314
Yaxi Zheng zheng.yaxi@elico-corp.com https://github.com/Elico-Yaxi
Faust Huang faust_huang@hotmail.com https://github.com/fausthuang
Yu Lin yu_lin@me.com https://github.com/uynil
Xie Xiaopeng hellomyjob@163.com https://github.com/xie8899
Connie Xiao connie.xiao@elico-corp.com https://github.com/ConnieXiao
Alex Duan alex.duan@elico-corp.com https://github.com/duanyp1991
Liu Lixia liulixia19901988@163.com https://github.com/liulixia1990
Liliane Li liliane.li@elico-corp.com http://github.com/lilianeandbrody
Rona Lin ssauapw@qq.com https://github.com/Rona111
Bob Luo luo.yumeng0831@gmail.com https://github.com/m6519578
Waller Zhang waller.zhang@elico-corp.com https://github.com/wallerzhang
+21
View File
@@ -0,0 +1,21 @@
KMEE, Brazil, 2015-12-04
KMEE INFORMATICA LTDA agrees to the terms of the Odoo Corporate
Contributor License Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Luis Felipe Miléo mileo@kmee.com.br https://github.com/kmee
List of contributors:
Luis Felipe Miléo mileo@kmee.com.br https://github.com/mileo
Ananias Pereira Batista Filho ananias@kmee.com.br https://github.com/ananiasfilho
Michell Stuttigart michell.stuttigart@kmee.com.br https://github.com/mstuttgart
Fernando Marcato Rodrigues fernando.marcato@kmee.com.br https://github.com/fernandomr
Daniel Sadamo daniel.sadamo@kmee.com.br https://github.com/sadamo
Bianca Tella bianca.tella@kmee.com.br https://github.com/biancatella
Luiz Felipe do Divino luiz.divino@kmee.com.br https://github.com/lfdivino
+1
View File
@@ -17,3 +17,4 @@ Suganthi Karunanithi ksuganthi@sodexis.com https://github.com/suganthikarunanith
Xavier Dass xavier@sodexis.com https://github.com/xavier-dass
Atchuthan atchuthan@sodexis.com https://github.com/atchuthan
Dhinesh dhinesh@sodexis.com https://github.com/dvdhinesh
SodexisTeam dev@sodexis.com https://github.com/SodexisTeam
+16
View File
@@ -0,0 +1,16 @@
FRANCE, 2016-01-06
Sudokeys agrees to the terms of the Odoo Corporate Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
David GUESSAB david@sudokeys.com https://github.com/chouab
List of contributors:
David GUESSAB david@sudokeys.com https://github.com/chouab
Nicolas JEUDY nicolas@sudokeys.com https://github.com/njeudy
+12
View File
@@ -0,0 +1,12 @@
Switzerland, 2015-12-11
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Florian Bruhin git@the-compiler.org https://github.com/The-Compiler
Florian Bruhin me@the-compiler.org https://github.com/The-Compiler
+11
View File
@@ -0,0 +1,11 @@
Spain, 2015-12-15
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Juan Carlos Montoya juancarlos.montoya.chamba@gmail.com https://github.com/jcarlosmontoya
+11
View File
@@ -0,0 +1,11 @@
United States of America, 2015-10-22
I hereby agree to the terms of the Odoo Individual Contributor License
Agreement v1.0.
I declare that I am authorized and able to make this agreement and sign this
declaration.
Signed,
Stephen Medina stephen@lilmail.xyz https://github.com/stephen144
+4 -8
View File
@@ -13,10 +13,6 @@ Ready to create your own theme? Great. Here are some things you should know befo
.. image:: theme_tutorial_assets/img/Intro.jpg
.. warning::
This tutorial requires having installed Odoo v9.0 and the Theme Support Engine (website_less) module. https://www.odoo.com/apps/9.0/website_less/
An introduction for web designers
=================================
@@ -250,7 +246,7 @@ Open the ``__openerp__.py`` you created and copy/paste the following:
'data': [
],
'category': 'Theme/Creative',
'depends': ['website', 'website_less'],
'depends': ['website'],
}
Replace the first four property’s values with anything you like.
@@ -263,7 +259,7 @@ The ``data`` property will contain the xml files list. Right now it’s empty, b
``category`` defines your module category (always “Theme”) and, after a slash, the subcategory. You can use one subcategory from the Odoo Apps categories list. (https://www.odoo.com/apps/themes)
``depends`` specifies the modules needed by our theme to work properly. For our tutorial theme, we only need website and website_less. If you need blogging or eCommerce features as well, you have to add those modules too.
``depends`` specifies the modules needed by our theme to work properly. For our tutorial theme, we only need website. If you need blogging or eCommerce features as well, you have to add those modules too.
.. code-block:: python
@@ -691,7 +687,7 @@ We start by adding a new file in our views folder - name it **options.xml** and
.. code-block:: xml
<template id="snippet_testimonial_opt" name="Snippet Testimonial Options" inherit_id="website_less.snippet_options">
<template id="snippet_testimonial_opt" name="Snippet Testimonial Options" inherit_id="website.snippet_options">
<xpath expr="//div[@data-js='background']" position="after">
<div data-selector=".snippet_testimonial"> <!-- Options group -->
<li class="dropdown-submenu">
@@ -832,7 +828,7 @@ This time we have to inherit ``assets_editor`` instead of ``assets_frontend``.
.. code-block:: xml
<template id="my_js" inherit_id="website_less.assets_editor" name="My Js">
<template id="my_js" inherit_id="website.assets_editor" name="My Js">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/theme_tutorial/static/src/js/tutorial_editor.js" />
</xpath>
+3 -1
View File
@@ -78,7 +78,7 @@ not be fetched by browsers. The ``src/css``, ``src/js`` and ``src/xml``
sub-folders are conventional and not strictly necessary.
``oepetstore/static/css/petstore.css``
currently empty, will hold the CSS_ for pet store content
Currently empty, will hold the CSS_ for pet store content
``oepetstore/static/xml/petstore.xml``
Mostly empty, will hold :ref:`reference/qweb` templates
``oepetstore/static/js/petstore.js``
@@ -103,6 +103,8 @@ sub-folders are conventional and not strictly necessary.
Which only prints a small message in the browser's console.
The files in the ``static`` folder, need to be defined within the module in order for them to be loaded correctly. Everything in ``src/xml`` is defined in ``__openerp__.py`` while the contents of ``src/css`` and ``src/js`` are defined in ``petstore.xml``, or a similar file.
.. warning::
All JavaScript files are concatenated and :term:`minified` to improve
+21
View File
@@ -1099,10 +1099,31 @@ class ir_model_data(osv.osv):
},context=context)
else:
if mode=='init' or (mode=='update' and xml_id):
inherit_xml_ids = []
for table, field_name in model_obj._inherits.items():
xml_ids = self.pool['ir.model.data'].search(cr, uid, [
('module', '=', module),
('name', '=', xml_id + '_' + table.replace('.', '_')),
], context=context)
# XML ID found in the database, try to recover an existing record
if xml_ids:
found_xml_id = self.pool['ir.model.data'].browse(cr, uid, xml_ids[0], context=context)
record = self.pool[found_xml_id.model].browse(cr, uid, [found_xml_id.res_id], context=context)[0]
# The record exists, store the id and don't recreate the XML ID
if record.exists():
inherit_xml_ids.append(found_xml_id.model)
values[field_name] = found_xml_id.res_id
# Orphan XML ID, delete it
else:
found_xml_id.unlink()
res_id = model_obj.create(cr, uid, values, context=context)
if xml_id:
if model_obj._inherits:
for table in model_obj._inherits:
if table in inherit_xml_ids:
continue
inherit_id = model_obj.browse(cr, uid,
res_id,context=context)[model_obj._inherits[table]]
self.create(cr, SUPERUSER_ID, {
+2 -1
View File
@@ -675,7 +675,7 @@ class ir_translation(osv.osv):
return ir_translation_import_cursor(cr, uid, self, context=context)
def load_module_terms(self, cr, modules, langs, context=None):
context = dict(context or {}) # local copy
context_template = dict(context or {}) # local copy
# make sure the given languages are active
lang_obj = self.pool['res.lang']
for lang in langs:
@@ -686,6 +686,7 @@ class ir_translation(osv.osv):
if not modpath:
continue
for lang in langs:
context = dict(context_template)
lang_code = tools.get_iso_codes(lang)
base_lang_code = None
if '_' in lang_code:
+5
View File
@@ -619,10 +619,15 @@ class view(osv.osv):
requested (similar to ``id``)
"""
if context is None: context = {}
context = context.copy()
# if view_id is not a root view, climb back to the top.
base = v = self.browse(cr, uid, view_id, context=context)
check_view_ids = context.setdefault('check_view_ids', [])
while v.mode != 'primary':
# Add inherited views to the list of loading forced views
# Otherwise, inherited views could not find elements created in their direct parents if that parent is defined in the same module
check_view_ids.append(v.id)
v = v.inherit_id
root_id = v.id
+2 -2
View File
@@ -433,8 +433,8 @@ def route(route=None, **kw):
* ``user``: The user must be authenticated and the current request
will perform using the rights of the user.
* ``admin``: The user may not be authenticated and the current request
will perform using the admin user.
* ``public``: The user may or may not be authenticated. If she isn't,
the current request will perform using the shared Public user.
* ``none``: The method is always active, even if there is no
database. Mainly used by the framework and authentication
modules. There request code will not have any facilities to access
+1
View File
@@ -1384,6 +1384,7 @@ class BaseModel(object):
from openerp.http import request
Users = self.pool['res.users']
for group_ext_id in groups.split(','):
group_ext_id = group_ext_id.strip()
if group_ext_id == 'base.group_no_one':
# check: the group_no_one is effective in debug mode only
if Users.has_group(cr, uid, group_ext_id) and request and request.debug:
+15 -3
View File
@@ -11,7 +11,7 @@ import re
import socket
import threading
import time
from email.utils import getaddresses
from email.utils import getaddresses, formataddr
import openerp
from openerp.loglevels import ustr
@@ -632,10 +632,10 @@ def append_content_to_html(html, content, plaintext=True, preserve=False, contai
#----------------------------------------------------------
# matches any email in a body of text
email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6})""", re.VERBOSE)
email_re = re.compile(r"""([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63})""", re.VERBOSE)
# matches a string containing only one email
single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$""", re.VERBOSE)
single_email_re = re.compile(r"""^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$""", re.VERBOSE)
res_re = re.compile(r"\[([0-9]+)\]", re.UNICODE)
command_re = re.compile("^Set-([a-z]+) *: *(.+)$", re.I + re.UNICODE)
@@ -709,3 +709,15 @@ def email_split(text):
# is strictly required in RFC2822's `addr-spec`.
if addr[1]
if '@' in addr[1]]
def email_split_and_format(text):
""" Return a list of email addresses found in ``text``, formatted using
formataddr. """
if not text:
return []
return [formataddr((addr[0], addr[1])) for addr in getaddresses([text])
# getaddresses() returns '' when email parsing fails, and
# sometimes returns emails without at least '@'. The '@'
# is strictly required in RFC2822's `addr-spec`.
if addr[1]
if '@' in addr[1]]