diff --git a/addons/account/models/account.py b/addons/account/models/account.py
index 50388f30f49..19842d28fe5 100644
--- a/addons/account/models/account.py
+++ b/addons/account/models/account.py
@@ -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"
diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py
index 763290d4619..a47ebeb8a2d 100644
--- a/addons/account/models/account_invoice.py
+++ b/addons/account/models/account_invoice.py
@@ -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')
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 3564ba348b7..b59e43a152e 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -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"
diff --git a/addons/account/models/chart_template.py b/addons/account/models/chart_template.py
index 1d0ec80306c..c094bef3db4 100644
--- a/addons/account/models/chart_template.py
+++ b/addons/account/models/chart_template.py
@@ -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,})
diff --git a/addons/account/models/partner.py b/addons/account/models/partner.py
index 2bde6466a4c..88850c2153c 100644
--- a/addons/account/models/partner.py
+++ b/addons/account/models/partner.py
@@ -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()
diff --git a/addons/account/tests/__init__.py b/addons/account/tests/__init__.py
index e4a6224852d..01e519c02cf 100644
--- a/addons/account/tests/__init__.py
+++ b/addons/account/tests/__init__.py
@@ -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
diff --git a/addons/account/tests/test_product_id_change.py b/addons/account/tests/test_product_id_change.py
new file mode 100644
index 00000000000..a2008b64bc7
--- /dev/null
+++ b/addons/account/tests/test_product_id_change.py
@@ -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")
diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index 1b6249c4cdb..be22f4e63d7 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -21,7 +21,11 @@
| Date | Name | Unit Price | Qty | @@ -38,35 +39,46 @@|||
|---|---|---|---|---|---|---|
| - - - | -- - | -- - | -- - | -
- |
- - - | -|
|
+
+ |
+
+
+ |
+ + + | ++ + | ++ + | +
+ |
+ + + | +
Certified honest and conform,
(Date and signature).
This document must be dated and signed for reimbursement.
diff --git a/addons/hr_payroll/report/report_payslip_details.py b/addons/hr_payroll/report/report_payslip_details.py index ba31bd8b100..f5858f8e7ef 100644 --- a/addons/hr_payroll/report/report_payslip_details.py +++ b/addons/hr_payroll/report/report_payslip_details.py @@ -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 diff --git a/addons/hw_blackbox_be/__init__.py b/addons/hw_blackbox_be/__init__.py new file mode 100644 index 00000000000..bf7fb7b45c1 --- /dev/null +++ b/addons/hw_blackbox_be/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import controllers diff --git a/addons/hw_blackbox_be/__openerp__.py b/addons/hw_blackbox_be/__openerp__.py new file mode 100644 index 00000000000..d33930e2143 --- /dev/null +++ b/addons/hw_blackbox_be/__openerp__.py @@ -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, +} diff --git a/addons/hw_blackbox_be/controllers/__init__.py b/addons/hw_blackbox_be/controllers/__init__.py new file mode 100644 index 00000000000..f48716d3a69 --- /dev/null +++ b/addons/hw_blackbox_be/controllers/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import main diff --git a/addons/hw_blackbox_be/controllers/main.py b/addons/hw_blackbox_be/controllers/main.py new file mode 100644 index 00000000000..fbc7664cbc0 --- /dev/null +++ b/addons/hw_blackbox_be/controllers/main.py @@ -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 diff --git a/addons/im_livechat/controllers/main.py b/addons/im_livechat/controllers/main.py index 43e547e2fb1..30ae212c8b2 100644 --- a/addons/im_livechat/controllers/main.py +++ b/addons/im_livechat/controllers/main.py @@ -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): diff --git a/addons/im_livechat/models/im_livechat_channel.py b/addons/im_livechat/models/im_livechat_channel.py index e15d4212a43..ac894a2c7ab 100644 --- a/addons/im_livechat/models/im_livechat_channel.py +++ b/addons/im_livechat/models/im_livechat_channel.py @@ -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) diff --git a/addons/im_livechat/views/im_livechat_channel_templates.xml b/addons/im_livechat/views/im_livechat_channel_templates.xml index d97df04989e..0efd2310a14 100644 --- a/addons/im_livechat/views/im_livechat_channel_templates.xml +++ b/addons/im_livechat/views/im_livechat_channel_templates.xml @@ -143,7 +143,6 @@