[MERGE] forward port branch saas-10 up to e1bd405

This commit is contained in:
Christophe Simonis
2016-09-09 15:14:39 +02:00
40 changed files with 709 additions and 101 deletions
+1 -1
View File
@@ -282,7 +282,7 @@
<field name="code"/>
<field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}" groups="base.group_no_one"/>
<field name="sequence_id" required="0" attrs="{'readonly': 1}" groups="base.group_no_one"/>
<field name="refund_sequence_id" attrs="{'required': [('refund_sequence', '=', True)], 'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)]}" required="0" groups="base.group_no_one"/>
<field name="refund_sequence_id" attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)], 'required': [('type', 'in', ['sale', 'purchase']), ('refund_sequence', '=', True)]}" groups="base.group_no_one"/>
</group>
<group>
<field name="default_debit_account_id" domain="[('deprecated', '=', False)]" />
+1 -1
View File
@@ -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,
@@ -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,
}
+67 -28
View File
@@ -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
+46
View File
@@ -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
@@ -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',
}
@@ -0,0 +1 @@
import account_move
@@ -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
+2 -1
View File
@@ -8,6 +8,7 @@ from openerp.http import request
class link_tracker(http.Controller):
@http.route('/r/<string:code>', 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)
+1 -1
View File
@@ -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,
}));
+25
View File
@@ -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}
+3 -3
View File
@@ -73,7 +73,7 @@
<field name="open"/>
<field name="memo"/>
<field name="date_done"/>
<field name="message_follower_ids"/>
<field name="message_partner_ids"/>
<field name="tag_ids"/>
<templates>
<t t-name="kanban-box">
@@ -100,9 +100,9 @@
<!-- title -->
<field name="name"/>
</span>
<t t-if="record.message_follower_ids.raw_value.length &gt; 1">
<t t-if="record.message_partner_ids.raw_value.length &gt; 1">
<div class="clearfix"></div>
<t t-foreach="record.message_follower_ids.raw_value" t-as="follower">
<t t-foreach="record.message_partner_ids.raw_value" t-as="follower">
<img t-att-src="kanban_image('res.partner', 'image_small', follower)" width="24" height="24" class="oe_kanban_avatar pull-right" t-att-data-member_id="follower"/>
</t>
<div class="clearfix"></div>
+3 -3
View File
@@ -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):
+2 -2
View File
@@ -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([
+2 -2
View File
@@ -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):
+5 -5
View File
@@ -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
+7 -7
View File
@@ -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 = {
+1 -1
View File
@@ -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')
+2 -2
View File
@@ -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):
+1
View File
@@ -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',
],
+7
View File
@@ -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)
+103 -1
View File
@@ -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)
@@ -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,
@@ -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)});
+9 -4
View File
@@ -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));
@@ -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;
});
@@ -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];
}
+116
View File
@@ -84,6 +84,14 @@
</div>
</t>
<t t-name="SaleDetailsButton">
<div class="oe_status">
<div class="js_connected oe_icon">
<i class='fa fa-fw fa-print'></i>
</div>
</div>
</t>
<t t-name="HeaderButtonWidget">
<div class="header-button">
<t t-esc="widget.label" />
@@ -839,6 +847,114 @@
</receipt>
</t>
<t t-name="SaleDetailsReport">
<receipt align='center' width='40' value-thousands-separator='' >
<t t-if='pos.company_logo_base64'>
<img t-att-src='pos.company_logo_base64' />
<br/>
</t>
<t t-if='!pos.company_logo_base64'>
<h1><t t-esc='company.name' /></h1>
<br/>
</t>
<br /><br />
<!-- Orderlines -->
<div class='orderlines' line-ratio='0.6'>
<t t-foreach='products' t-as='line'>
<line>
<left><t t-esc='line.product_name' /></left>
<right>
<value value-decimals='2' value-autoint='on'>
<t t-esc='line.quantity' />
</value>
<t t-if='line.uom !== "Unit(s)"'>
<t t-esc='line.uom' />
</t>
</right>
<right>
<value><t t-esc='line.price_unit' /></value>
</right>
</line>
<t t-if='line.discount !== 0'>
<line indent='1'><left>Discount: <t t-esc='line.discount' />%</left></line>
</t>
</t>
</div>
<br/>
<line><left>------------------------</left></line>
<br/>
<line>
<left>Payments:</left>
</line>
<line t-foreach='payments' t-as='payment'>
<left><t t-esc='payment.name' /></left>
<right>
<t t-esc='payment.total'/>
</right>
</line>
<br/>
<line><left>------------------------</left></line>
<br/>
<line>
<left>Taxes:</left>
</line>
<line t-foreach='taxes' t-as='taxe'>
<left><t t-esc='taxe.name' /></left>
<right>
<t t-esc='taxe.total'/>
</right>
</line>
<br/>
<line><left>------------------------</left></line>
<br/>
<line>
<left>Total:</left>
<right>
<t t-esc='total_paid'/>
</right>
</line>
<br/>
<div font='b'>
<div><t t-esc='date' /></div>
</div>
</receipt>
</t>
<t t-name="WizardSaleDetailsReport">
<div class="o_form_view"><div class="oe_form_sheetbg o_form_sheet_bg"><div class="oe_form_sheet oe_form_sheet_width o_form_sheet">
<div class="pos_details_dates">
<h3>Select Dates for the report</h3>
<table class="table-condensed">
<tr>
<td><strong>Start date</strong></td>
<td><div class="js_date_start"></div></td>
</tr>
<tr>
<td><strong>Stop date</strong></td>
<td><div class="js_date_stop"></div></td>
</tr>
</table>
<p>
<button class="js_generate">Generate report</button>
</p>
</div>
</div></div></div>
</t>
<t t-name="PopupWidget">
<div class="modal-dialog">
<div class="popup popup-alert">
@@ -17,5 +17,11 @@
<field name="report_name">point_of_sale.report_invoice</field>
</record>
<record id="sale_details_report" model="ir.actions.report.xml">
<field name="name">Sale Details</field>
<field name="model">report.point_of_sale.details</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">point_of_sale.report_saledetails</field>
</record>
</odoo>
@@ -3,6 +3,8 @@
<template id="assets_backend" name="hr assets" inherit_id="web.assets_backend">
<xpath expr="." position="inside">
<link rel="stylesheet" href="/point_of_sale/static/src/less/pos_dashboard.less"/>
<script type="text/javascript" src="/point_of_sale/static/src/js/report.js"></script>
</xpath>
</template>
</odoo>
@@ -68,5 +68,11 @@
<field name="context">{'group_by_no_leaf':1,'group_by':[]}</field>
</record>
<record id="action_report_pos_details" model="ir.actions.client">
<field name="name">Sale Details</field>
<field name="tag">report_pos_details</field>
</record>
<menuitem id="menu_report_pos_order_all" name="Orders" action="action_report_pos_order_all" parent="menu_point_rep" sequence="3"/>
<menuitem id="menu_report_order_details" name="Sale Details" action="action_report_pos_details" parent="menu_point_rep" sequence="4"/>
</odoo>
@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<template id="report_saledetails">
<t t-call="report.html_container">
<t t-call="report.internal_layout">
<div class="page">
<div class="text-center">
<h2>Sale Details</h2>
<strong><t t-esc="date_start"/> - <t t-esc="date_stop"/></strong>
</div>
<!-- Orderlines -->
<h3>Products</h3>
<table class="table table-condensed">
<thead><tr>
<th>Product</th>
<th>Quantity</th>
<th>Price Unit</th>
</tr></thead>
<tbody>
<tr t-foreach='products' t-as='line'>
<td><t t-esc="line['product_name']" /></td>
<td>
<t t-esc="line['quantity']" />
<t t-if='line["uom"] != "Unit(s)"'>
<t t-esc='line["uom"]' />
</t>
</td>
<td>
<t t-esc='line["price_unit"]' />
<t t-if='line["discount"] != 0'>
Disc: <t t-esc='line["discount"]' />%
</t>
</td>
</tr>
</tbody>
</table>
<br/>
<h3>Payments</h3>
<table class="table table-condensed">
<thead><tr>
<th>Name</th>
<th>Total</th>
</tr></thead>
<tbody>
<tr t-foreach='payments' t-as='payment'>
<td><t t-esc="payment['name']" /></td>
<td><t t-esc="payment['total']" /></td>
</tr>
</tbody>
</table>
<br/>
<h3>Taxes</h3>
<table class="table table-condensed">
<thead><tr>
<th>Name</th>
<th>Total</th>
</tr></thead>
<tbody>
<tr t-foreach='taxes' t-as='tax'>
<td><t t-esc="tax['name']" /></td>
<td><t t-esc="tax['total']" /></td>
</tr>
</tbody>
</table>
<br/>
<br/>
<strong>Total: <t t-esc='total_paid'/></strong>
</div>
</t>
</t>
</template>
</data>
</openerp>
+1 -1
View File
@@ -98,7 +98,7 @@ def ensure_db(redirect='/web/database/selector'):
# If the db is taken out of a query parameter, it will be checked against
# `http.db_filter()` in order to ensure it's legit and thus avoid db
# forgering that could lead to xss attacks.
db = request.params.get('db')
db = request.params.get('db') and request.params.get('db').strip()
# Ensure db is legit
if db and db not in http.db_filter([db]):
@@ -710,6 +710,13 @@ var DataSetSearch = DataSet.extend({
this._length -= (before - this.ids.length);
}
},
add_ids: function(ids, at) {
var before = this.ids.length;
this._super(ids, at);
if(this._length){
this._length += (this.ids.length - before);
}
},
unlink: function(ids, callback, error_callback) {
var self = this;
return this._super(ids).done(function() {
@@ -116,6 +116,7 @@ var KanbanColumn = Widget.extend({
} else {
// adding record to this column
self.records.push(record);
self.dataset.add_ids([record.id]);
record.setParent(self);
ui.item.addClass('o_updating');
self.trigger_up('kanban_column_add_record', {record: record});
@@ -529,9 +529,7 @@ var KanbanView = View.extend({
active_model: this.model,
});
}
this.do_execute_action(event.data, this.dataset, event.target.id).then(function () {
self.reload_record(event.target);
});
this.do_execute_action(event.data, this.dataset, event.target.id, _.bind(self.reload_record, this, event.target));
},
/*
@@ -106,35 +106,30 @@ options.registry.slider = options.Class.extend({
},0);
return $clone;
},
remove_slide: function (type, value) {
if(type !== "click") return;
if (this.remove_process) {
return;
}
remove_slide: function (type) {
if (type !== "click" || this.remove_process) return;
var self = this;
var new_index = 0;
var cycle = this.$inner.find('.item').length - 1;
var index = this.$inner.find('.item.active').index();
var $items = this.$inner.find('.item');
var cycle = $items.length - 1;
var $active = $items.filter('.active');
var index = $active.index();
if (cycle > 0) {
this.remove_process = true;
var $el = this.$inner.find('.item.active');
self.$target.on('slid.bs.carousel', function (event) {
$el.remove();
this.$target.on('slid.bs.carousel.slide_removal', function (event) {
$active.remove();
self.$indicators.find("li:last").remove();
self.$target.off('slid.bs.carousel');
self.$target.off('slid.bs.carousel.slide_removal');
self.rebind_event();
self.remove_process = false;
if (cycle == 1) {
self.on_remove_slide(event);
if (cycle === 1) {
self.$target.find('.carousel-control, .carousel-indicators').addClass("hidden");
}
});
setTimeout(function () {
self.$target.carousel( index > 0 ? --index : cycle );
}, 500);
} else {
this.$target.find('.carousel-control, .carousel-indicators').addClass("hidden");
_.defer(function () {
self.$target.carousel(index > 0 ? --index : cycle);
});
}
},
interval : function (type, value) {
@@ -162,7 +157,7 @@ options.registry.carousel = options.registry.slider.extend({
},
start : function () {
var self = this;
this._super();
this._super.apply(this, arguments);
// set background and prepare to clean for save
var add_class = function (c) {
@@ -170,11 +165,17 @@ options.registry.carousel = options.registry.slider.extend({
return self._class || "";
};
this.$target.on('slid.bs.carousel', function () {
if(self.editor && self.editor.styles.background) {
self.editor.styles.background.$target = self.$target.find(".item.active");
self.editor.styles.background.set_active();
}
self.$target.carousel("pause");
if (!self.editor) return;
_.each(["background", "background_position", "colorpicker"], function (opt_name) {
var s_option = self.editor.styles[opt_name];
if (!s_option) return;
s_option.$target = self.$target.find(".item.active");
s_option.set_active();
s_option.$target.trigger("snippet-option-change", [s_option]);
});
});
this.$target.trigger('slid.bs.carousel');
},
@@ -376,7 +377,7 @@ options.registry.marginAndResize = options.Class.extend({
this.$overlay.attr("class", overlay_class);
},
/* on_resize
* called when the box is resizing and the class change, before the cover_target
* @compass: resize direction : 'n', 's', 'e', 'w'
+1 -1
View File
@@ -46,7 +46,7 @@ class website_payment(http.Controller):
partner_id = user.partner_id.id if user.partner_id.id != request.website.partner_id.id else False
payment_form = acquirer.render(reference, float(amount), currency.id, values={'return_url': '/website_payment/confirm', 'partner_id': partner_id})[0]
payment_form = acquirer.sudo().render(reference, float(amount), currency.id, values={'return_url': '/website_payment/confirm', 'partner_id': partner_id})[0]
values = {
'reference': reference,
'acquirer': acquirer,
@@ -97,6 +97,9 @@ $('.oe_website_sale').each(function () {
$inputs.each(function () {
$(this).val( data[$(this).attr("name")] || "" );
});
$selects.filter('[name="shipping_country_id"]').val(data['shipping_country_id']).change();
$selects.filter('[name="shipping_state_id"]').val(data['shipping_state_id']);
});
$(oe_website_sale).on("change", 'input[name="add_qty"]', function (event) {
@@ -332,7 +335,7 @@ $('.oe_website_sale').each(function () {
$('input.js_variant_change, select.js_variant_change', this).first().trigger('change');
});
var state_options = $("select[name='state_id']:enabled option:not(:first)");
var state_options = $("select[name='state_id'] option:not(:first)");
$(oe_website_sale).on('change', "select[name='country_id']", function () {
var select = $("select[name='state_id']");
state_options.detach();
@@ -342,7 +345,7 @@ $('.oe_website_sale').each(function () {
});
$(oe_website_sale).find("select[name='country_id']").change();
var shipping_state_options = $("select[name='shipping_state_id']:enabled option:not(:first)");
var shipping_state_options = $("select[name='shipping_state_id'] option:not(:first)");
$(oe_website_sale).on('change', "select[name='shipping_country_id']", function () {
var select = $("select[name='shipping_state_id']");
shipping_state_options.detach();
+2 -2
View File
@@ -1462,7 +1462,7 @@ class BaseModel(object):
def _get_default_form_view(self, cr, user, context=None):
""" Generates a default single-line form view using all fields
of the current model except the m2m and o2m ones.
of the current model.
:param cr: database cursor
:param int user: user id
@@ -1473,7 +1473,7 @@ class BaseModel(object):
view = etree.Element('form', string=self._description)
group = etree.SubElement(view, 'group', col="4")
for fname, field in self._fields.iteritems():
if field.automatic or field.type in ('one2many', 'many2many'):
if field.automatic:
continue
etree.SubElement(group, 'field', name=fname)