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 @@
+ +
+
+ +
+
+
+
@@ -839,6 +847,114 @@ + + + + +
+
+ +

+
+
+

+ + + +
+ + + + + + + + + + + + + + + + + Discount: % + + +
+ +
+ ------------------------ +
+ + + Payments: + + + + + + + + +
+ ------------------------ +
+ + + Taxes: + + + + + + + + +
+ ------------------------ +
+ + + Total: + + + + + +
+
+
+
+ +
+
+ + + +
+
+

Select Dates for the report

+ + + + + + + + + +
Start date
Stop date
+

+ +

+
+ + +
+
+ +