diff --git a/addons/account/views/account_view.xml b/addons/account/views/account_view.xml
index dea42767766..5f83d674e7d 100644
--- a/addons/account/views/account_view.xml
+++ b/addons/account/views/account_view.xml
@@ -282,7 +282,7 @@
-
+
diff --git a/addons/account_asset/account_asset.py b/addons/account_asset/account_asset.py
index 46d579ee7dc..ea436c35505 100644
--- a/addons/account_asset/account_asset.py
+++ b/addons/account_asset/account_asset.py
@@ -441,7 +441,7 @@ class AccountAssetDepreciationLine(models.Model):
current_currency = line.asset_id.currency_id
amount = current_currency.compute(line.amount, company_currency)
sign = (category_id.journal_id.type == 'purchase' or category_id.journal_id.type == 'sale' and 1) or -1
- asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, line.asset_id.method_number)
+ asset_name = line.asset_id.name + ' (%s/%s)' % (line.sequence, len(line.asset_id.depreciation_line_ids))
prec = self.env['decimal.precision'].precision_get('Account')
move_line_1 = {
'name': asset_name,
diff --git a/addons/account_asset/account_asset_invoice.py b/addons/account_asset/account_asset_invoice.py
index a53f2959138..ba2cb3892fc 100644
--- a/addons/account_asset/account_asset_invoice.py
+++ b/addons/account_asset/account_asset_invoice.py
@@ -51,15 +51,15 @@ class AccountInvoiceLine(models.Model):
@api.one
def asset_create(self):
- if self.asset_category_id and self.asset_category_id.method_number > 1:
+ if self.asset_category_id:
vals = {
'name': self.name,
'code': self.invoice_id.number or False,
'category_id': self.asset_category_id.id,
- 'value': self.price_subtotal,
+ 'value': self.price_subtotal_signed,
'partner_id': self.invoice_id.partner_id.id,
'company_id': self.invoice_id.company_id.id,
- 'currency_id': self.invoice_id.currency_id.id,
+ 'currency_id': self.invoice_id.company_currency_id.id,
'date': self.asset_start_date or self.invoice_id.date_invoice,
'invoice_id': self.invoice_id.id,
}
diff --git a/addons/account_tax_cash_basis/tax_cash_basis.py b/addons/account_tax_cash_basis/tax_cash_basis.py
index 8f45ab3e3a9..b68e348c59e 100644
--- a/addons/account_tax_cash_basis/tax_cash_basis.py
+++ b/addons/account_tax_cash_basis/tax_cash_basis.py
@@ -35,30 +35,57 @@ class ResCompany(models.Model):
class AccountPartialReconcileCashBasis(models.Model):
_inherit = 'account.partial.reconcile'
- def create_tax_cash_basis_entry(self, value_before_reconciliation):
- #Search in account_move if we have any taxes account move lines
+ def _check_tax_exigible(self, line):
+ """ Function overwritten in account_tax_exigible to make sure we consider only the lines having tax_exigible set to False """
+ return False
+
+ def _get_tax_cash_basis_lines(self, value_before_reconciliation):
+ # Search in account_move if we have any taxes account move lines
tax_group = {}
total_by_cash_basis_account = {}
- for move in (self.debit_move_id.move_id, self.credit_move_id.move_id):
- for line in move.line_ids:
- if line.tax_line_id and line.tax_line_id.use_cash_basis:
- #amount to write is the current cash_basis amount minus the one before the reconciliation
- matched_percentage = value_before_reconciliation[move.id]
- amount = (line.credit_cash_basis - line.debit_cash_basis) - (line.credit - line.debit) * matched_percentage
- #group by line account
- acc = line.account_id.id
- if tax_group.get(acc, False):
- tax_group[acc] += amount
- else:
- tax_group[acc] = amount
- #Group by cash basis account
- acc = line.tax_line_id.cash_basis_account.id
- if total_by_cash_basis_account.get(acc, False):
- total_by_cash_basis_account[acc] += amount
- else:
- total_by_cash_basis_account[acc] = amount
line_to_create = []
- for k,v in tax_group.items():
+ move_date = self.debit_move_id.date
+ for move in (self.debit_move_id.move_id, self.credit_move_id.move_id):
+ if move_date < move.date:
+ move_date = move.date
+ for line in move.line_ids:
+ #TOCHECK: normal and cash basis taxes shoudn't be mixed together (on the same invoice line for example) as it will
+ #create reporting issues. Not sure of the behavior to implement in that case, though.
+ # amount to write is the current cash_basis amount minus the one before the reconciliation
+ matched_percentage = value_before_reconciliation[move.id]
+ amount = (line.credit_cash_basis - line.debit_cash_basis) - (line.credit - line.debit) * matched_percentage
+ if not self._check_tax_exigible(line):
+ if line.tax_line_id and line.tax_line_id.use_cash_basis:
+ # group by line account
+ acc = line.account_id.id
+ if tax_group.get(acc, False):
+ tax_group[acc] += amount
+ else:
+ tax_group[acc] = amount
+ # Group by cash basis account and tax
+ acc = line.tax_line_id.cash_basis_account.id
+ key = (acc, line.tax_line_id.id)
+ if key in total_by_cash_basis_account:
+ total_by_cash_basis_account[key] += amount
+ else:
+ total_by_cash_basis_account[key] = amount
+ if any([tax.use_cash_basis for tax in line.tax_ids]):
+ for tax in line.tax_ids:
+ line_to_create.append((0, 0, {
+ 'name': '/',
+ 'debit': line.debit_cash_basis - line.debit * matched_percentage,
+ 'credit': line.credit_cash_basis - line.credit * matched_percentage,
+ 'account_id': line.account_id.id,
+ 'tax_ids': [(6, 0, [tax.id])],
+ }))
+ line_to_create.append((0, 0, {
+ 'name': '/',
+ 'credit': line.debit_cash_basis - line.debit * matched_percentage,
+ 'debit': line.credit_cash_basis - line.credit * matched_percentage,
+ 'account_id': line.account_id.id,
+ }))
+
+ for k, v in tax_group.items():
line_to_create.append((0, 0, {
'name': '/',
'debit': v if v > 0 else 0.0,
@@ -66,27 +93,39 @@ class AccountPartialReconcileCashBasis(models.Model):
'account_id': k,
}))
- #Create counterpart vals
- for k,v in total_by_cash_basis_account.items():
+ # Create counterpart vals
+ for key, v in total_by_cash_basis_account.items():
+ k, tax_id = key
line_to_create.append((0, 0, {
'name': '/',
'debit': abs(v) if v < 0 else 0.0,
'credit': v if v > 0 else 0.0,
'account_id': k,
+ 'tax_line_id': tax_id,
}))
+ return line_to_create, move_date
- #Create move
+ def create_tax_cash_basis_entry(self, value_before_reconciliation):
+ line_to_create, move_date = self._get_tax_cash_basis_lines(value_before_reconciliation)
if len(line_to_create) > 0:
- #Check if company_journal for cash basis is set if not, raise exception
+ # Check if company_journal for cash basis is set if not, raise exception
if not self.company_id.tax_cash_basis_journal_id:
raise UserError(_('There is no tax cash basis journal defined ' \
'for this company: "%s" \nConfigure it in Accounting/Configuration/Settings') % \
(self.company_id.name))
- move = self.env['account.move'].create({
+ move_vals = {
'journal_id': self.company_id.tax_cash_basis_journal_id.id,
'line_ids': line_to_create,
- 'tax_cash_basis_rec_id': self.id})
- #post move
+ 'tax_cash_basis_rec_id': self.id
+ }
+ # The move date should be the maximum date between payment and invoice (in case
+ # of payment in advance). However, we should make sure the move date is not
+ # recorded after the period lock date as the tax statement for this period is
+ # probably already sent to the estate.
+ if move_date > self.company_id.period_lock_date:
+ move_vals['date'] = move_date
+ move = self.env['account.move'].with_context(dont_create_taxes=True).create(move_vals)
+ # post move
move.post()
@api.model
diff --git a/addons/account_tax_exigible/__init__.py b/addons/account_tax_exigible/__init__.py
new file mode 100644
index 00000000000..18647072982
--- /dev/null
+++ b/addons/account_tax_exigible/__init__.py
@@ -0,0 +1,46 @@
+import models
+
+from openerp import SUPERUSER_ID
+
+def _migration_script(cr, registry):
+ #1. set the tax_exigible at True on all account.move lines not related to a tax in cash basis
+ cr.execute('''UPDATE account_move_line SET tax_exigible='t' ''')
+
+ cr.execute('''SELECT id FROM account_tax WHERE use_cash_basis = 't' ''')
+ tax_ids = tuple([x[0] for x in cr.fetchall()])
+ if tax_ids:
+ cr.execute('''UPDATE account_move_line SET tax_exigible='f' WHERE id IN (SELECT id FROM account_move_line WHERE tax_line_id IN %s OR id IN (SELECT account_move_line_id FROM account_move_line_account_tax_rel WHERE account_tax_id IN %s))''', (tax_ids, tax_ids))
+
+ #2. Fix the moves already created by the tax case based feature
+ # create account.move.lines for tax_line_id and tax_ids that weren't correctly set/created at the time of reconciliation
+ existing_move = registry['account.move'].search(cr, SUPERUSER_ID, [('tax_cash_basis_rec_id', '!=', False)])
+ for m in registry['account.move'].browse(cr, SUPERUSER_ID, existing_move):
+ partial_rec = m.tax_cash_basis_rec_id
+ for move in (partial_rec.debit_move_id.move_id, partial_rec.credit_move_id.move_id):
+ matched_percentage = partial_rec.amount and (partial_rec.amount / move.amount) or 0.0
+ for l in move.line_ids:
+ if l.tax_exigible:
+ continue
+
+ move_to_fix = registry['account.move'].search(cr, SUPERUSER_ID, [('tax_cash_basis_rec_id', '=', partial_rec.id)])[0]
+ amount = (l.debit - l.credit) * matched_percentage
+
+ registry['account.move.line'].create(cr, SUPERUSER_ID, {
+ 'name': '/',
+ 'debit': amount > 0 and amount or 0.0,
+ 'credit': amount < 0 and -amount or 0.0,
+ 'account_id': l.account_id.id,
+ 'tax_line_id': l.tax_line_id and l.tax_line_id.id or False,
+ 'tax_ids': l.tax_ids and [(6, 0, l.tax_ids.ids)] or [],
+ 'tax_exigible': True,
+ 'move_id': move_to_fix,
+ }, apply_taxes=False, context={'check_move_validity': False})
+ registry['account.move.line'].create(cr, SUPERUSER_ID, {
+ 'name': '/',
+ 'debit': -amount > 0 and -amount or 0.0,
+ 'credit': -amount < 0 and amount or 0.0,
+ 'account_id': l.account_id.id,
+ 'move_id': move_to_fix,
+ 'tax_exigible': True,
+ }, context={'check_move_validity': False})
+ return
diff --git a/addons/account_tax_exigible/__openerp__.py b/addons/account_tax_exigible/__openerp__.py
new file mode 100644
index 00000000000..11244bd9aa7
--- /dev/null
+++ b/addons/account_tax_exigible/__openerp__.py
@@ -0,0 +1,17 @@
+# -*- encoding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+{
+ 'name': 'Tax exigible',
+ 'version': '1.0',
+ 'category': 'Accounting',
+ 'description': """
+ This is a bugfix-module backporting 39d68ac and adding the concept of tax exigibility on journal items.
+ """,
+ 'depends': ['account_tax_cash_basis'],
+ 'data': [],
+ 'test': [],
+ 'demo': [],
+ 'auto_install': True,
+ 'installable': True,
+ 'post_init_hook': '_migration_script',
+}
diff --git a/addons/account_tax_exigible/models/__init__.py b/addons/account_tax_exigible/models/__init__.py
new file mode 100644
index 00000000000..00c67c4c228
--- /dev/null
+++ b/addons/account_tax_exigible/models/__init__.py
@@ -0,0 +1 @@
+import account_move
\ No newline at end of file
diff --git a/addons/account_tax_exigible/models/account_move.py b/addons/account_tax_exigible/models/account_move.py
new file mode 100644
index 00000000000..9f9e8c2bbd8
--- /dev/null
+++ b/addons/account_tax_exigible/models/account_move.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from openerp import fields, models, api
+
+
+class AccountMoveLine(models.Model):
+ _inherit = "account.move.line"
+ tax_exigible = fields.Boolean(string='Appears in VAT report', default=True,
+ help="Technical field used to mark a tax line as exigible in the vat report or not (only exigible journal items are displayed). By default all new journal items are directly exigible, but with the module account_tax_cash_basis, some will become exigible only when the payment is recorded.")
+
+ @api.model
+ def create(self, vals, apply_taxes=True):
+ taxes = False
+ if vals.get('tax_line_id'):
+ taxes = [{'use_cash_basis': self.env['account.tax'].browse(vals['tax_line_id']).use_cash_basis}]
+ if vals.get('tax_ids'):
+ taxes = self.env['account.move.line'].resolve_2many_commands('tax_ids', vals['tax_ids'])
+ if taxes and any([tax['use_cash_basis'] for tax in taxes]) and not vals.get('tax_exigible'):
+ vals['tax_exigible'] = False
+ return super(AccountMoveLine, self).create(vals, apply_taxes=apply_taxes)
+
+class AccountPartialReconcileCashBasis(models.Model):
+ _inherit = 'account.partial.reconcile'
+
+ def _check_tax_exigible(self, line):
+ return line.tax_exigible
+
+ def _get_tax_cash_basis_lines(self, value_before_reconciliation):
+ lines, move_date = super(AccountPartialReconcileCashBasis, self)._get_tax_cash_basis_lines(value_before_reconciliation)
+ for i in range(len(lines)):
+ vals = lines[i][2]
+ vals['tax_exigible'] = True
+ lines[i] = (0, 0, vals)
+ return lines, move_date
diff --git a/addons/link_tracker/controller/main.py b/addons/link_tracker/controller/main.py
index 1d7707fb65a..1f6caf4f7f4 100644
--- a/addons/link_tracker/controller/main.py
+++ b/addons/link_tracker/controller/main.py
@@ -8,6 +8,7 @@ from openerp.http import request
class link_tracker(http.Controller):
@http.route('/r/', type='http', auth='none', website=True)
def full_url_redirect(self, code, **post):
- request.env['link.tracker.click'].add_click(code, request.httprequest.remote_addr, request.session['geoip'].get('country_code'), stat_id=False)
+ country_code = request.session.geoip and request.session.geoip.get('country_code') or False
+ request.env['link.tracker.click'].add_click(code, request.httprequest.remote_addr, country_code, stat_id=False)
redirect_url = request.env['link.tracker'].get_url_from_code(code)
return werkzeug.utils.redirect(redirect_url or '', 301)
diff --git a/addons/mail/static/src/js/chat_manager.js b/addons/mail/static/src/js/chat_manager.js
index 4e0df85c703..b58d35e61ed 100644
--- a/addons/mail/static/src/js/chat_manager.js
+++ b/addons/mail/static/src/js/chat_manager.js
@@ -669,7 +669,7 @@ var chat_manager = {
// post a message in a channel or execute a command
return ChannelModel.call(data.command ? 'execute_command' : 'message_post', [options.channel_id], _.extend(msg, {
message_type: 'comment',
- content_subtype: 'plaintext',
+ content_subtype: 'html',
subtype: 'mail.mt_comment',
command: data.command,
}));
diff --git a/addons/mrp/wizard/mrp_product_produce.py b/addons/mrp/wizard/mrp_product_produce.py
index 365f5f5a466..bc03ff1765c 100644
--- a/addons/mrp/wizard/mrp_product_produce.py
+++ b/addons/mrp/wizard/mrp_product_produce.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from openerp import api, _
from openerp.osv import fields, osv
import openerp.addons.decimal_precision as dp
@@ -104,3 +105,27 @@ class mrp_product_produce(osv.osv_memory):
self.pool.get('mrp.production').action_produce(cr, uid, production_id,
data.product_qty, data.mode, data, context=context)
return {}
+
+ @api.onchange('consume_lines')
+ def _onchange_consume_lines(self):
+ '''
+ The purpose of the method is to warn the user if we plan to consume more than one unit of
+ a product with unique serial number.
+ '''
+ for product in self.consume_lines.mapped('product_id'):
+ if product.tracking != 'serial':
+ continue
+
+ qty_by_lot = {}
+ lines = self.consume_lines.filtered(lambda r: r.product_id == product)
+ for line in lines:
+ qty_by_lot.setdefault(line.lot_id, 0.0)
+ qty_by_lot[line.lot_id] += line.product_qty
+
+ if qty_by_lot[line.lot_id] > 1.0:
+ warning_mess = {
+ 'title': _('Issue with lot quantity!'),
+ 'message' : _('You plan to consume more than 1.00 unit of product %s with unique lot number %s') % \
+ (product.name, line.lot_id.name)
+ }
+ return {'warning': warning_mess}
diff --git a/addons/note/views/note_views.xml b/addons/note/views/note_views.xml
index c04c40ca810..fbc59e0f856 100644
--- a/addons/note/views/note_views.xml
+++ b/addons/note/views/note_views.xml
@@ -73,7 +73,7 @@
-
+
@@ -100,9 +100,9 @@
-
+
-
+
diff --git a/addons/payment_adyen/models/adyen.py b/addons/payment_adyen/models/adyen.py
index d8d06042365..30dd036d65b 100644
--- a/addons/payment_adyen/models/adyen.py
+++ b/addons/payment_adyen/models/adyen.py
@@ -36,9 +36,9 @@ class AcquirerAdyen(osv.Model):
return providers
_columns = {
- 'adyen_merchant_account': fields.char('Merchant Account', required_if_provider='adyen'),
- 'adyen_skin_code': fields.char('Skin Code', required_if_provider='adyen'),
- 'adyen_skin_hmac_key': fields.char('Skin HMAC Key', required_if_provider='adyen'),
+ 'adyen_merchant_account': fields.char('Merchant Account', required_if_provider='adyen', groups='base.group_user'),
+ 'adyen_skin_code': fields.char('Skin Code', required_if_provider='adyen', groups='base.group_user'),
+ 'adyen_skin_hmac_key': fields.char('Skin HMAC Key', required_if_provider='adyen', groups='base.group_user'),
}
def _adyen_generate_merchant_sig_sha256(self, acquirer, inout, values):
diff --git a/addons/payment_authorize/models/authorize.py b/addons/payment_authorize/models/authorize.py
index 39575961969..b74edfcc539 100644
--- a/addons/payment_authorize/models/authorize.py
+++ b/addons/payment_authorize/models/authorize.py
@@ -30,8 +30,8 @@ class PaymentAcquirerAuthorize(models.Model):
providers.append(['authorize', 'Authorize.Net'])
return providers
- authorize_login = fields.Char(string='API Login Id', required_if_provider='authorize')
- authorize_transaction_key = fields.Char(string='API Transaction Key', required_if_provider='authorize')
+ authorize_login = fields.Char(string='API Login Id', required_if_provider='authorize', groups='base.group_user')
+ authorize_transaction_key = fields.Char(string='API Transaction Key', required_if_provider='authorize', groups='base.group_user')
def _authorize_generate_hashing(self, values):
data = '^'.join([
diff --git a/addons/payment_buckaroo/models/buckaroo.py b/addons/payment_buckaroo/models/buckaroo.py
index 0dd11ae0750..bcadb70b84d 100644
--- a/addons/payment_buckaroo/models/buckaroo.py
+++ b/addons/payment_buckaroo/models/buckaroo.py
@@ -44,8 +44,8 @@ class AcquirerBuckaroo(osv.Model):
return providers
_columns = {
- 'brq_websitekey': fields.char('WebsiteKey', required_if_provider='buckaroo'),
- 'brq_secretkey': fields.char('SecretKey', required_if_provider='buckaroo'),
+ 'brq_websitekey': fields.char('WebsiteKey', required_if_provider='buckaroo', groups='base.group_user'),
+ 'brq_secretkey': fields.char('SecretKey', required_if_provider='buckaroo', groups='base.group_user'),
}
def _buckaroo_generate_digital_sign(self, acquirer, inout, values):
diff --git a/addons/payment_ogone/models/ogone.py b/addons/payment_ogone/models/ogone.py
index f09d78139f4..2007099f105 100644
--- a/addons/payment_ogone/models/ogone.py
+++ b/addons/payment_ogone/models/ogone.py
@@ -47,11 +47,11 @@ class PaymentAcquirerOgone(osv.Model):
return providers
_columns = {
- 'ogone_pspid': fields.char('PSPID', required_if_provider='ogone'),
- 'ogone_userid': fields.char('API User ID', required_if_provider='ogone'),
- 'ogone_password': fields.char('API User Password', required_if_provider='ogone'),
- 'ogone_shakey_in': fields.char('SHA Key IN', size=32, required_if_provider='ogone'),
- 'ogone_shakey_out': fields.char('SHA Key OUT', size=32, required_if_provider='ogone'),
+ 'ogone_pspid': fields.char('PSPID', required_if_provider='ogone', groups='base.group_user'),
+ 'ogone_userid': fields.char('API User ID', required_if_provider='ogone', groups='base.group_user'),
+ 'ogone_password': fields.char('API User Password', required_if_provider='ogone', groups='base.group_user'),
+ 'ogone_shakey_in': fields.char('SHA Key IN', size=32, required_if_provider='ogone', groups='base.group_user'),
+ 'ogone_shakey_out': fields.char('SHA Key OUT', size=32, required_if_provider='ogone', groups='base.group_user'),
'ogone_alias_usage': fields.char('Alias Usage', help="""If you want to use Ogone Aliases,
this default Alias Usage will be presented to
the customer as the reason you want to
diff --git a/addons/payment_paypal/models/paypal.py b/addons/payment_paypal/models/paypal.py
index 8be9c63eaa9..4a1ee90714d 100644
--- a/addons/payment_paypal/models/paypal.py
+++ b/addons/payment_paypal/models/paypal.py
@@ -39,17 +39,17 @@ class AcquirerPaypal(osv.Model):
return providers
_columns = {
- 'paypal_email_account': fields.char('Paypal Email ID', required_if_provider='paypal'),
+ 'paypal_email_account': fields.char('Paypal Email ID', required_if_provider='paypal', groups='base.group_user'),
'paypal_seller_account': fields.char(
- 'Paypal Merchant ID',
+ 'Paypal Merchant ID', groups='base.group_user',
help='The Merchant ID is used to ensure communications coming from Paypal are valid and secured.'),
- 'paypal_use_ipn': fields.boolean('Use IPN', help='Paypal Instant Payment Notification'),
+ 'paypal_use_ipn': fields.boolean('Use IPN', help='Paypal Instant Payment Notification', groups='base.group_user'),
# Server 2 server
'paypal_api_enabled': fields.boolean('Use Rest API'),
- 'paypal_api_username': fields.char('Rest API Username'),
- 'paypal_api_password': fields.char('Rest API Password'),
- 'paypal_api_access_token': fields.char('Access Token'),
- 'paypal_api_access_token_validity': fields.datetime('Access Token Validity'),
+ 'paypal_api_username': fields.char('Rest API Username', groups='base.group_user'),
+ 'paypal_api_password': fields.char('Rest API Password', groups='base.group_user'),
+ 'paypal_api_access_token': fields.char('Access Token', groups='base.group_user'),
+ 'paypal_api_access_token_validity': fields.datetime('Access Token Validity', groups='base.group_user'),
}
_defaults = {
diff --git a/addons/payment_sips/controllers/main.py b/addons/payment_sips/controllers/main.py
index 0bf4a5f52b3..02c975d94e2 100644
--- a/addons/payment_sips/controllers/main.py
+++ b/addons/payment_sips/controllers/main.py
@@ -32,7 +32,7 @@ class SipsController(http.Controller):
sips = acquirer_obj.search([('provider', '=', 'sips')], limit=1)
- security = sips._sips_generate_shasign(post)
+ security = sips.sudo()._sips_generate_shasign(post)
if security == post['Seal']:
_logger.debug('Sips: validated data')
res = tx_obj.sudo().form_feedback(post, 'sips')
diff --git a/addons/payment_sips/models/sips.py b/addons/payment_sips/models/sips.py
index 9c74bf5ef06..506a9118ea6 100644
--- a/addons/payment_sips/models/sips.py
+++ b/addons/payment_sips/models/sips.py
@@ -38,8 +38,8 @@ class AcquirerSips(models.Model):
_inherit = 'payment.acquirer'
# Fields
sips_merchant_id = fields.Char('SIPS API User Password',
- required_if_provider='sips')
- sips_secret = fields.Char('SIPS Secret', size=64, required_if_provider='sips')
+ required_if_provider='sips', groups='base.group_user')
+ sips_secret = fields.Char('SIPS Secret', size=64, required_if_provider='sips', groups='base.group_user')
# Methods
def _get_sips_urls(self, environment):
diff --git a/addons/point_of_sale/__openerp__.py b/addons/point_of_sale/__openerp__.py
index a42c2df9106..a4bd8fc247b 100644
--- a/addons/point_of_sale/__openerp__.py
+++ b/addons/point_of_sale/__openerp__.py
@@ -59,6 +59,7 @@ Main Features
'views/report_statement.xml',
'views/report_usersproduct.xml',
'views/report_userlabel.xml',
+ 'views/report_saledetails.xml',
'views/point_of_sale.xml',
'views/point_of_sale_dashboard.xml',
],
diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py
index b945757e65e..0161c2afcdd 100644
--- a/addons/point_of_sale/controllers/main.py
+++ b/addons/point_of_sale/controllers/main.py
@@ -26,3 +26,10 @@ class PosController(http.Controller):
}
return request.render('point_of_sale.index', qcontext={'db_info': json.dumps(db_info)})
+
+ @http.route('/pos/sale_details_report', type='http', auth='user')
+ def print_sale_details(self, date_start=False, date_stop=False, **kw):
+ r = request.env['report.point_of_sale.report_saledetails']
+ pdf = request.env['report'].with_context(date_start=date_start, date_stop=date_stop).get_pdf(r, 'point_of_sale.report_saledetails')
+ pdfhttpheaders = [('Content-Type', 'application/pdf'), ('Content-Length', len(pdf))]
+ return request.make_response(pdf, headers=pdfhttpheaders)
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 2c4a644b421..b3c3fbc3983 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
+from datetime import timedelta
from functools import partial
import psycopg2
@@ -8,7 +9,7 @@ import psycopg2
from odoo import api, fields, models, tools, _
from odoo.tools import float_is_zero
from odoo.exceptions import UserError
-
+from odoo.http import request
import odoo.addons.decimal_precision as dp
_logger = logging.getLogger(__name__)
@@ -734,3 +735,104 @@ class PosOrderLine(models.Model):
def _get_tax_ids_after_fiscal_position(self):
for line in self:
line.tax_ids_after_fiscal_position = line.order_id.fiscal_position_id.map_tax(line.tax_ids)
+
+
+class ReportSaleDetails(models.AbstractModel):
+
+ _name = 'report.point_of_sale.report_saledetails'
+
+
+ @api.model
+ def get_sale_details(self, date_start=False, date_stop=False, company=False):
+ """ Serialise the orders of the day information
+
+ params: date_start, date_stop string representing the day of order
+ """
+ if not company:
+ company = self.env.user.company_id
+
+ today = fields.Datetime.from_string(fields.Date.context_today(self))
+ if date_start:
+ date_start = fields.Datetime.from_string(date_start)
+ else:
+ # start by default today 00:00:00
+ date_start = today
+
+ if date_stop:
+ # set time to 23:59:59
+ date_stop = fields.Datetime.from_string(date_stop) + timedelta(days=1, seconds=-1)
+ else:
+ # stop by default today 23:59:59
+ date_stop = today + timedelta(days=1, seconds=-1)
+
+ # avoid a date_stop smaller than date_start
+ date_stop = max(date_stop, date_start)
+
+ date_start = fields.Datetime.to_string(date_start)
+ date_stop = fields.Datetime.to_string(date_stop)
+ orders = self.env['pos.order'].search([('date_order', '>=', date_start), ('date_order', '<=', date_stop),
+ ('state', 'in', ['paid','invoiced','done']),
+ ('company_id', '=', company.id)])
+ user_currency = self.env.user.company_id.currency_id
+
+ total = 0.0
+ products_sold = {}
+ taxes = {}
+ for order in orders:
+ if user_currency != order.pricelist_id.currency_id:
+ total += order.pricelist_id.currency_id.compute(order.amount_total, user_currency)
+ else:
+ total += order.amount_total
+ currency = order.session_id.currency_id
+
+ for line in order.lines:
+ key = (line.product_id, line.price_unit, line.discount)
+ products_sold.setdefault(key, 0.0)
+ products_sold[key] += line.qty
+
+ if line.tax_ids_after_fiscal_position:
+ line_taxes = line.tax_ids_after_fiscal_position.compute_all(line.price_unit * (1-(line.discount or 0.0)/100.0), currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False)
+ for tax in line_taxes['taxes']:
+ taxes.setdefault(tax['id'], {'name': tax['name'], 'total':0.0})
+ taxes[tax['id']]['total'] += tax['amount']
+
+ st_line_ids = self.env["account.bank.statement.line"].search([('pos_statement_id', 'in', orders.ids)]).ids
+ if st_line_ids:
+ self.env.cr.execute("""
+ SELECT aj.name, sum(amount) total
+ FROM account_bank_statement_line AS absl,
+ account_bank_statement AS abs,
+ account_journal AS aj
+ WHERE absl.statement_id = abs.id
+ AND abs.journal_id = aj.id
+ AND absl.id IN %s
+ GROUP BY aj.name
+ """, (tuple(st_line_ids),))
+ payments = self.env.cr.dictfetchall()
+ else:
+ payments = []
+
+ return {
+ 'total_paid': user_currency.round(total),
+ 'payments': payments,
+ 'company_name': self.env.user.company_id.name,
+ 'taxes': taxes.values(),
+ 'products': [{
+ 'product_id': product.id,
+ 'product_name': product.name[:20],
+ 'code': product.default_code,
+ 'quantity': qty,
+ 'price_unit': price_unit,
+ 'discount': discount,
+ 'uom': product.uom_id.name
+ } for (product, price_unit, discount), qty in products_sold.items()]
+ }
+
+ @api.multi
+ def render_html(self, data=None):
+ company = request.env.user.company_id
+ date_start = self.env.context.get('date_start', False)
+ date_stop = self.env.context.get('date_stop', False)
+ data = dict(data or {}, date_start=date_start, date_stop=date_stop)
+ data.update(self.get_sale_details(date_start, date_stop, company))
+ return self.env['report'].render('point_of_sale.report_saledetails', data)
\ No newline at end of file
diff --git a/addons/point_of_sale/static/src/js/chrome.js b/addons/point_of_sale/static/src/js/chrome.js
index 926fe23eee7..f7bf1f78010 100644
--- a/addons/point_of_sale/static/src/js/chrome.js
+++ b/addons/point_of_sale/static/src/js/chrome.js
@@ -406,6 +406,23 @@ var ProxyStatusWidget = StatusWidget.extend({
},
});
+
+/* --------- The Sale Details --------- */
+
+// Generates a report to print the sales of the
+// day on a ticket
+
+var SaleDetailsButton = PosBaseWidget.extend({
+ template: 'SaleDetailsButton',
+ start: function(){
+ var self = this;
+ this.$el.click(function(){
+ self.pos.proxy.print_sale_details();
+ });
+ },
+});
+
+
/*--------------------------------------*\
| THE CHROME |
\*======================================*/
@@ -662,6 +679,11 @@ var Chrome = PosBaseWidget.extend({
'name': 'order_selector',
'widget': OrderSelectorWidget,
'replace': '.placeholder-OrderSelectorWidget',
+ },{
+ 'name': 'sale_details',
+ 'widget': SaleDetailsButton,
+ 'append': '.pos-rightheader',
+ 'condition': function(){ return this.pos.config.use_proxy; },
},{
'name': 'proxy_status',
'widget': ProxyStatusWidget,
@@ -769,6 +791,7 @@ return {
HeaderButtonWidget: HeaderButtonWidget,
OrderSelectorWidget: OrderSelectorWidget,
ProxyStatusWidget: ProxyStatusWidget,
+ SaleDetailsButton: SaleDetailsButton,
StatusWidget: StatusWidget,
SynchNotificationWidget: SynchNotificationWidget,
UsernameWidget: UsernameWidget,
diff --git a/addons/point_of_sale/static/src/js/devices.js b/addons/point_of_sale/static/src/js/devices.js
index f9dd33e0ead..cfb24bf39ed 100644
--- a/addons/point_of_sale/static/src/js/devices.js
+++ b/addons/point_of_sale/static/src/js/devices.js
@@ -2,8 +2,10 @@ odoo.define('point_of_sale.devices', function (require) {
"use strict";
var core = require('web.core');
+var Model = require('web.DataModel');
var Session = require('web.Session');
+var QWeb = core.qweb;
var _t = core._t;
// the JobQueue schedules a sequence of 'jobs'. each job is
@@ -434,6 +436,23 @@ var ProxyDevice = core.Class.extend(core.mixins.PropertiesMixin,{
send_printing_job();
},
+ print_sale_details: function() {
+ var self = this;
+ new Model('report.point_of_sale.report_saledetails').call('get_sale_details').then(function(result){
+ var env = {
+ company: self.pos.company,
+ pos: self.pos,
+ products: result.products,
+ payments: result.payments,
+ taxes: result.taxes,
+ total_paid: result.total_paid,
+ date: (new Date()).toLocaleString(),
+ };
+ var report = QWeb.render('SaleDetailsReport', env);
+ self.print_receipt(report);
+ })
+ },
+
// asks the proxy to log some information, as with the debug.log you can provide several arguments.
log: function(){
return this.message('log',{'arguments': _.toArray(arguments)});
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index fb3d685e19e..50b6dda2b5a 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -1361,13 +1361,14 @@ exports.Orderline = Backbone.Model.extend({
},
compute_all: function(taxes, price_unit, quantity, currency_rounding) {
var self = this;
- var total_excluded = round_pr(price_unit * quantity, currency_rounding);
- var total_included = total_excluded;
- var base = total_excluded;
var list_taxes = [];
+ var currency_rounding_bak = currency_rounding;
if (this.pos.company.tax_calculation_rounding_method == "round_globally"){
currency_rounding = currency_rounding * 0.00001;
}
+ var total_excluded = round_pr(price_unit * quantity, currency_rounding);
+ var total_included = total_excluded;
+ var base = total_excluded;
_(taxes).each(function(tax) {
tax = self._map_tax_fiscal_position(tax);
if (tax.amount_type === 'group'){
@@ -1401,7 +1402,11 @@ exports.Orderline = Backbone.Model.extend({
}
}
});
- return {taxes: list_taxes, total_excluded: total_excluded, total_included: total_included};
+ return {
+ taxes: list_taxes,
+ total_excluded: round_pr(total_excluded, currency_rounding_bak),
+ total_included: round_pr(total_included, currency_rounding_bak)
+ };
},
get_all_prices: function(){
var price_unit = this.get_unit_price() * (1.0 - (this.get_discount() / 100.0));
diff --git a/addons/point_of_sale/static/src/js/report.js b/addons/point_of_sale/static/src/js/report.js
new file mode 100644
index 00000000000..783ace7f655
--- /dev/null
+++ b/addons/point_of_sale/static/src/js/report.js
@@ -0,0 +1,63 @@
+odoo.define('point_of_sale.report', function (require) {
+"use strict";
+
+var chat_manager = require('mail.chat_manager');
+var composer = require('mail.composer');
+var ChatThread = require('mail.ChatThread');
+var utils = require('mail.utils');
+
+var config = require('web.config');
+var core = require('web.core');
+var data = require('web.data');
+var Dialog = require('web.Dialog');
+var framework = require('web.framework');
+var Model = require('web.Model');
+
+var pyeval = require('web.pyeval');
+var SearchView = require('web.SearchView');
+
+var ActionManager = require('web.ActionManager');
+var ControlPanelMixin = require('web.ControlPanelMixin');
+var datepicker = require('web.datepicker');
+var Widget = require('web.Widget');
+
+var QWeb = core.qweb;
+var _t = core._t;
+
+// TODO replace in v10 with proper wizard
+var PosDetailsWidget = Widget.extend(ControlPanelMixin, {
+ template: 'WizardSaleDetailsReport',
+ events: {
+ "click .js_generate": "renderReport",
+ },
+
+ renderElement: function() {
+ this._super();
+
+ var current_date = new moment();
+ this.dp_start = new datepicker.DateWidget(this);
+ this.dp_start.insertBefore(this.$el.find('.js_date_start'));
+ this.dp_start.set_value(current_date.format('YYYY-MM-DD'));
+
+ this.dp_stop = new datepicker.DateWidget(this);
+ this.dp_stop.insertAfter(this.$el.find('.js_date_stop'));
+ this.dp_stop.set_value(current_date.format('YYYY-MM-DD'));
+
+ },
+
+ renderReport: function() {
+ var start = this.dp_start.get_value();
+ var stop = this.dp_stop.get_value();
+ var url = '/pos/sale_details_report?date_start='+start+'&date_stop='+stop;
+ window.open(url, '_blank');
+
+ },
+
+});
+
+
+core.action_registry.add('report_pos_details', PosDetailsWidget);
+
+return PosDetailsWidget;
+
+});
diff --git a/addons/point_of_sale/static/src/js/widget_base.js b/addons/point_of_sale/static/src/js/widget_base.js
index 4a529dd1bf5..5f7f4285d95 100644
--- a/addons/point_of_sale/static/src/js/widget_base.js
+++ b/addons/point_of_sale/static/src/js/widget_base.js
@@ -38,7 +38,7 @@ var PosBaseWidget = Widget.extend({
var currency = (this.pos && this.pos.currency) ? this.pos.currency : {symbol:'$', position: 'after', rounding: 0.01, decimals: 2};
var decimals = currency.decimals;
- if (precision && (typeof this.pos.dp[precision]) !== undefined) {
+ if (precision && this.pos.dp[precision] !== undefined) {
decimals = this.pos.dp[precision];
}
diff --git a/addons/point_of_sale/static/src/xml/pos.xml b/addons/point_of_sale/static/src/xml/pos.xml
index fad17173dd2..b593e8b72c1 100644
--- a/addons/point_of_sale/static/src/xml/pos.xml
+++ b/addons/point_of_sale/static/src/xml/pos.xml
@@ -84,6 +84,14 @@
+
+
+
+