[MERGE] forward port branch saas-12.2 up to 995aa1685e

This commit is contained in:
Christophe Simonis
2019-06-03 15:49:31 +02:00
209 changed files with 2398 additions and 1059 deletions
+40
View File
@@ -0,0 +1,40 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 12.0 | :white_check_mark: |
| 11.0 | :white_check_mark: |
| 10.0 | :white_check_mark: |
| <=9.0 | :x: |
## Reporting a Vulnerability
Please share privately the details of your security vulnerability by contacting our Security Team:
[Contact Info](https://www.odoo.com/security-report)
Make sure to include as much information as possible, with the detailed steps to reproduce the problem,
the versions that are affected, the expected results and actual results, and any other information that
might help us react faster and more efficiently.
We tend to prefer _text-based descriptions_ accompanied with a proof-of-concept script/exploit, rather
than screenshots and videos.
Our [Responsible Disclosure](https://www.odoo.com/security-report) page gives an overview of the
process, including:
- Our Incident Response Procedure (what will happen after you report an issue)
- Our Rules (what you can and cannot do while researching security issues)
- Guidelines with **DO REPORT** and **DO NOT REPORT** issues
(what kind of issues will be accepted/rejected)
## Important note
We receive a majority of security reports that have little to no impact on the security of Odoo or
the Odoo Cloud, and we ultimately have to reject them. To avoid a disappointing experience when
contacting us, please try to put together a proof-of-concept attack and take a critical look at
what's really at risk.
If the proposed attack scenario turns out unrealistic, your report will probably be rejected.
Also be sure to review our list of [non-qualifying issues](https://www.odoo.com/security-report#what).
+31 -92
View File
@@ -446,7 +446,6 @@ class AccountBankStatementLine(models.Model):
""" Prepare the dict of values to balance the move.
:param recordset move: the account.move to link the move line
:param dict move: a dict of vals of a account.move which will be created later
:param float amount: the amount of transaction that wasn't already reconciled
"""
company_currency = self.journal_id.company_id.currency_id
@@ -454,23 +453,19 @@ class AccountBankStatementLine(models.Model):
st_line_currency = self.currency_id or statement_currency
amount_currency = False
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
if isinstance(move, dict):
amount_sum = sum(x[2].get('amount_currency', 0) for x in move['line_ids'])
else:
amount_sum = sum(x.amount_currency for x in move.line_ids)
# We have several use case here to compare the currency and amount currency of counterpart line to balance the move:
if st_line_currency != company_currency and st_line_currency == statement_currency:
# company in currency A, statement in currency B and transaction in currency B
# counterpart line must have currency B and correct amount is inverse of already existing lines
amount_currency = -amount_sum
amount_currency = -sum([x.amount_currency for x in move.line_ids])
elif st_line_currency != company_currency and statement_currency == company_currency:
# company in currency A, statement in currency A and transaction in currency B
# counterpart line must have currency B and correct amount is inverse of already existing lines
amount_currency = -amount_sum
amount_currency = -sum([x.amount_currency for x in move.line_ids])
elif st_line_currency != company_currency and st_line_currency != statement_currency:
# company in currency A, statement in currency B and transaction in currency C
# counterpart line must have currency B and use rate between B and C to compute correct amount
amount_currency = -amount_sum/st_line_currency_rate
amount_currency = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate
elif st_line_currency == company_currency and statement_currency != company_currency:
# company in currency A, statement in currency B and transaction in currency A
# counterpart line must have currency B and amount is computed using the rate between A and B
@@ -479,8 +474,9 @@ class AccountBankStatementLine(models.Model):
# last case is company in currency A, statement in currency A and transaction in currency A
# and in this case counterpart line does not need any second currency nor amount_currency
aml_dict = {
return {
'name': self.name,
'move_id': move.id,
'partner_id': self.partner_id and self.partner_id.id or False,
'account_id': amount >= 0 \
and self.statement_id.journal_id.default_credit_account_id.id \
@@ -491,71 +487,22 @@ class AccountBankStatementLine(models.Model):
'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False),
'amount_currency': amount_currency,
}
if isinstance(move, self.env['account.move'].__class__):
aml_dict['move_id'] = move.id
return aml_dict
@api.multi
def fast_counterpart_creation(self):
"""This function is called when confirming a bank statement and will allow to automatically process lines without
going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal
entry using that account to counterpart the bank account
"""
payment_list = []
move_list = []
account_type_receivable = self.env.ref('account.data_account_type_receivable')
already_done_stmt_line_ids = [a['statement_line_id'][0] for a in self.env['account.move.line'].read_group([('statement_line_id', 'in', self.ids)], ['statement_line_id'], ['statement_line_id'])]
managed_st_line = []
for st_line in self:
# Technical functionality to automatically reconcile by creating a new move line
if st_line.account_id and not st_line.id in already_done_stmt_line_ids:
managed_st_line.append(st_line.id)
# Create payment vals
total = st_line.amount
payment_methods = (total > 0) and st_line.journal_id.inbound_payment_method_ids or st_line.journal_id.outbound_payment_method_ids
currency = st_line.journal_id.currency_id or st_line.company_id.currency_id
partner_type = 'customer' if st_line.account_id.user_type_id == account_type_receivable else 'supplier'
payment_list.append({
'payment_method_id': payment_methods and payment_methods[0].id or False,
'payment_type': total > 0 and 'inbound' or 'outbound',
'partner_id': st_line.partner_id.id,
'partner_type': partner_type,
'journal_id': st_line.statement_id.journal_id.id,
'payment_date': st_line.date,
'state': 'reconciled',
'currency_id': currency.id,
'amount': abs(total),
'communication': st_line._get_communication(payment_methods[0] if payment_methods else False),
'name': st_line.statement_id.name or _("Bank Statement %s") % st_line.date,
})
# Create move and move line vals
move_vals = st_line._prepare_reconciliation_move(st_line.statement_id.name)
aml_dict = {
if st_line.account_id and not st_line.journal_entry_ids.ids:
vals = {
'name': st_line.name,
'debit': st_line.amount < 0 and -st_line.amount or 0.0,
'credit': st_line.amount > 0 and st_line.amount or 0.0,
'account_id': st_line.account_id.id,
'partner_id': st_line.partner_id.id,
'statement_line_id': st_line.id,
}
st_line._prepare_move_line_for_currency(aml_dict, st_line.date or fields.Date.context_today())
move_vals['line_ids'] = [(0, 0, aml_dict)]
balance_line = self._prepare_reconciliation_move_line(move_vals, st_line.amount)
move_vals['line_ids'].append((0, 0, balance_line))
move_list.append(move_vals)
# Creates
payment_ids = self.env['account.payment'].create(payment_list)
for payment_id, move_vals in zip(payment_ids, move_list):
for line in move_vals['line_ids']:
line[2]['payment_id'] = payment_id.id
move_ids = self.env['account.move'].create(move_list)
move_ids.post()
for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids):
st_line.write({'move_name': move.name})
payment.write({'payment_reference': move.name})
st_line.process_reconciliation(new_aml_dicts=[vals])
def _get_communication(self, payment_method_id):
return self.name or ''
@@ -683,6 +630,8 @@ class AccountBankStatementLine(models.Model):
# Create move line(s). Either matching an existing journal entry (eg. invoice), in which
# case we reconcile the existing and the new move lines together, or being a write-off.
if counterpart_aml_dicts or new_aml_dicts:
st_line_currency = self.currency_id or statement_currency
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
# Create the move
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
@@ -705,12 +654,32 @@ class AccountBankStatementLine(models.Model):
# Complete dicts to create both counterpart move lines and write-offs
to_create = (counterpart_aml_dicts + new_aml_dicts)
company = self.company_id
date = self.date or fields.Date.today()
for aml_dict in to_create:
aml_dict['move_id'] = move.id
aml_dict['partner_id'] = self.partner_id.id
aml_dict['statement_line_id'] = self.id
self._prepare_move_line_for_currency(aml_dict, date)
if st_line_currency.id != company_currency.id:
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
aml_dict['currency_id'] = st_line_currency.id
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
# Statement is in company currency but the transaction is in foreign currency
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
elif self.currency_id and st_line_currency_rate:
# Statement is in foreign currency and the transaction is in another one
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
else:
# Statement is in foreign currency and no extra currency is given for the transaction
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
elif statement_currency.id != company_currency.id:
# Statement is in foreign currency but the transaction is in company currency
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
aml_dict['amount_currency'] = prorata_factor * self.amount
aml_dict['currency_id'] = statement_currency.id
# Create write-offs
for aml_dict in new_aml_dicts:
@@ -760,36 +729,6 @@ class AccountBankStatementLine(models.Model):
counterpart_moves.assert_balanced()
return counterpart_moves
@api.multi
def _prepare_move_line_for_currency(self, aml_dict, date):
self.ensure_one()
company_currency = self.journal_id.company_id.currency_id
statement_currency = self.journal_id.currency_id or company_currency
st_line_currency = self.currency_id or statement_currency
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
company = self.company_id
if st_line_currency.id != company_currency.id:
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
aml_dict['currency_id'] = st_line_currency.id
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
# Statement is in company currency but the transaction is in foreign currency
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
elif self.currency_id and st_line_currency_rate:
# Statement is in foreign currency and the transaction is in another one
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
else:
# Statement is in foreign currency and no extra currency is given for the transaction
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
elif statement_currency.id != company_currency.id:
# Statement is in foreign currency but the transaction is in company currency
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
aml_dict['amount_currency'] = prorata_factor * self.amount
aml_dict['currency_id'] = statement_currency.id
def _check_invoice_state(self, invoice):
if invoice.state == 'in_payment' and all([payment.state == 'reconciled' for payment in invoice.mapped('payment_move_line_ids.payment_id')]):
invoice.write({'state': 'paid'})
+1 -1
View File
@@ -875,7 +875,7 @@ class AccountInvoice(models.Model):
domain = {}
company_id = self.company_id.id
p = self.partner_id if not company_id else self.partner_id.with_context(force_company=company_id)
type = self.type
type = self.type or self.env.context.get('type', 'out_invoice')
if p:
rec_account = p.property_account_receivable_id
pay_account = p.property_account_payable_id
@@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from babel.dates import format_datetime, format_date
from odoo import models, api, _, fields
from odoo.osv import expression
from odoo.release import version
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DF, safe_eval
from odoo.tools.misc import formatLang, format_date as odoo_format_date
@@ -427,10 +428,16 @@ class account_journal(models.Model):
action_name = 'action_view_bank_statement_tree'
elif self.type == 'sale':
action_name = 'action_invoice_tree1'
self = self.with_context(use_domain=[('journal_id', '=', self.id)])
use_domain = expression.AND(
[self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]]
)
self = self.with_context(use_domain=use_domain)
elif self.type == 'purchase':
action_name = 'action_vendor_bill_template'
self = self.with_context(use_domain=[('journal_id', '=', self.id)])
use_domain = expression.AND(
[self.env.context.get('use_domain', []), [('journal_id', '=', self.id)]]
)
self = self.with_context(use_domain=use_domain)
else:
action_name = 'action_move_journal_line'
+19 -17
View File
@@ -87,14 +87,12 @@ class AccountMove(models.Model):
if line.account_id.user_type_id.type in ('receivable', 'payable'):
amount = abs(line.debit - line.credit)
total_amount += amount
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
total_reconciled += partial_line.amount
precision_currency = move.currency_id or move.company_id.currency_id
if float_is_zero(total_amount, precision_rounding=precision_currency.rounding):
move.matched_percentage = 1.0
else:
for line in move.line_ids:
if line.account_id.user_type_id.type in ('receivable', 'payable'):
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
total_reconciled += partial_line.amount
move.matched_percentage = total_reconciled / total_amount
@api.multi
@@ -580,7 +578,7 @@ class AccountMoveLine(models.Model):
else:
date = partial_line.credit_move_id.date if partial_line.debit_move_id == line else partial_line.debit_move_id.date
rate = line.currency_id.with_context(date=date).rate
amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
amount_residual_currency += sign_partial_line * partial_line.amount * rate
#computing the `reconciled` field.
reconciled = False
@@ -829,21 +827,22 @@ class AccountMoveLine(models.Model):
In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object.
"""
# Get first all aml involved
todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id'])
amls = set(self.ids)
part_recs = self.env['account.partial.reconcile'].search(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)])
amls = self
todo = set(part_recs)
seen = set()
while todo:
aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']]
amls |= set(aml_ids)
seen |= set([rec['id'] for rec in todo])
todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id'])
partial_rec_ids = list(seen)
partial_rec = todo.pop()
seen.add(partial_rec)
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
if aml not in amls:
amls += aml
for x in aml.matched_debit_ids | aml.matched_credit_ids:
if x not in seen:
todo.add(x)
partial_rec_ids = [x.id for x in seen]
if not amls:
return
else:
amls = self.browse(list(amls))
# If we have multiple currency, we can only base ourselve on debit-credit to see if it is fully reconciled
currency = set([a.currency_id for a in amls if a.currency_id.id != False])
multiple_currency = False
@@ -1006,7 +1005,10 @@ class AccountMoveLine(models.Model):
debit_moves = debit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id))
credit_moves = credit_moves.sorted(key=lambda a: (a.date_maturity or a.date, a.currency_id))
# Compute on which field reconciliation should be based upon:
field = self[0].account_id.currency_id and 'amount_residual_currency' or 'amount_residual'
if self[0].account_id.currency_id and self[0].account_id.currency_id != self[0].account_id.company_id.currency_id:
field = 'amount_residual_currency'
else:
field = 'amount_residual'
#if all lines share the same currency, use amount_residual_currency to avoid currency rounding error
if self[0].currency_id and all([x.amount_currency and x.currency_id == self[0].currency_id for x in self]):
field = 'amount_residual_currency'
+30 -15
View File
@@ -83,28 +83,43 @@ class AccountReconciliation(models.AbstractModel):
@api.model
def _get_bank_statement_line_partners(self, st_lines):
params = []
# Add the res.partner.ban's IR rules. In case partners are not shared between companies,
# identical bank accounts may exist in a company we don't have access to.
ir_rules_query = self.env['res.partner.bank']._where_calc([])
self.env['res.partner.bank']._apply_ir_rules(ir_rules_query, 'read')
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
if where_clause:
where_bank = ('AND %s' % where_clause).replace('res_partner_bank', 'bank')
params += where_clause_params
else:
where_bank = ''
# Add the res.partner's IR rules. In case partners are not shared between companies,
# identical partners may exist in a company we don't have access to.
ir_rules_query = self.env['res.partner']._where_calc([])
self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read')
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
if where_clause:
where_partner = ('AND %s' % where_clause).replace('res_partner', 'p3')
params += where_clause_params
else:
where_partner = ''
query = '''
SELECT
st_line.id AS id,
COALESCE(p1.id,p2.id,p3.id) AS partner_id
FROM account_bank_statement_line st_line
LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number
LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id
LEFT JOIN res_partner p2 ON bank.partner_id=p2.id
LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name
WHERE st_line.id IN %s
'''
params = [tuple(st_lines.ids)]
query += 'LEFT JOIN res_partner_bank bank ON bank.id = st_line.bank_account_id OR bank.acc_number = st_line.account_number %s\n' % (where_bank)
query += 'LEFT JOIN res_partner p1 ON st_line.partner_id=p1.id \n'
query += 'LEFT JOIN res_partner p2 ON bank.partner_id=p2.id \n'
query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s\n' % (where_partner)
query += 'WHERE st_line.id IN %s'
# Add the res.partner's IR rules to the WHERE clause. In case partners are not shared
# between companies, identical partners may exist in a company we don't have access to.
ir_rules_query = self.env['res.partner']._where_calc([])
self.env['res.partner']._apply_ir_rules(ir_rules_query, 'read')
from_clause, where_clause, where_clause_params = ir_rules_query.get_sql()
where_p3 = (" AND %s" % where_clause).replace('res_partner', 'p3') if where_clause else ''
if where_p3:
query += where_p3
params += where_clause_params
params += [tuple(st_lines.ids)]
self._cr.execute(query, params)
@@ -112,13 +112,13 @@ class AccountInvoiceReport(models.Model):
1 AS nbr,
ai.id AS invoice_id, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id,
ai.partner_bank_id,
SUM ((invoice_type.sign_qty * ail.quantity) / u.factor * u2.factor) AS product_qty,
SUM ((invoice_type.sign_qty * ail.quantity) / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) AS product_qty,
SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
(ai.amount_total * invoice_type.sign) / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
ai.amount_total_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
count(*) * invoice_type.sign AS amount_total,
SUM(ABS(ail.price_subtotal_signed)) / CASE
WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric
THEN SUM(ail.quantity / u.factor * u2.factor)
WHEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) <> 0::numeric
THEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1))
ELSE 1::numeric
END AS price_average,
ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
@@ -346,9 +346,12 @@ var StatementAction = AbstractAction.extend({
'context': self.model.getContext(),
});
_.each(result.handles, function (handle) {
self._getWidget(handle).destroy();
var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
self.widgets.splice(index, 1);
var widget = self._getWidget(handle);
if (widget) {
widget.destroy();
var index = _.findIndex(self.widgets, function (widget) {return widget.handle===handle;});
self.widgets.splice(index, 1);
}
});
// Get number of widget and if less than constant and if there are more to laod, load until constant
if (self.widgets.length < self.model.defaultDisplayQty
@@ -623,13 +623,13 @@ var StatementModel = BasicModel.extend({
prop = this._formatQuickCreate(line);
line.reconciliation_proposition.push(prop);
}
if (!line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) {
if ('to_check' in values && !line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) {
new CrashManager().show_warning({data: {
exception_type: _t("Incorrect Operation"),
message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.")
}});
// FIXME: model should not be tied to the DOM !
$('.create_to_check input').click();
$('.create_to_check input:visible').prop('checked', !values.to_check).change();
return Promise.resolve();
}
_.each(values, function (value, fieldName) {
@@ -306,30 +306,30 @@ class TestAccountCustomerInvoice(AccountTestUsers):
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 2)
self.assertEquals(dashboard_data['sum_draft'], '$ 68.42')
self.assertIn('68.42', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 0)
self.assertEquals(dashboard_data['sum_waiting'], '$ 0.00')
self.assertIn('0.00', dashboard_data['sum_waiting'])
# Check Both
invoice.action_invoice_open()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 1)
self.assertEquals(dashboard_data['sum_draft'], '$ -13.30')
self.assertIn('-13.30', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 1)
self.assertEquals(dashboard_data['sum_waiting'], '$ 81.72')
self.assertIn('81.72', dashboard_data['sum_waiting'])
# Check waiting payment
refund.action_invoice_open()
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 0)
self.assertEquals(dashboard_data['sum_draft'], '$ 0.00')
self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 2)
self.assertEquals(dashboard_data['sum_waiting'], '$ 68.42')
self.assertIn('68.42', dashboard_data['sum_waiting'])
# Check partial
receivable_account = refund.move_id.line_ids.mapped('account_id').filtered(lambda a: a.internal_type == 'receivable')
@@ -353,12 +353,12 @@ class TestAccountCustomerInvoice(AccountTestUsers):
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_draft'], 0)
self.assertEquals(dashboard_data['sum_draft'], '$ 0.00')
self.assertIn('0.00', dashboard_data['sum_draft'])
self.assertEquals(dashboard_data['number_waiting'], 2)
self.assertEquals(dashboard_data['sum_waiting'], '$ 78.42')
self.assertIn('78.42', dashboard_data['sum_waiting'])
with patch('odoo.fields.Date.today', patched_today):
dashboard_data = journal.get_journal_dashboard_datas()
self.assertEquals(dashboard_data['number_late'], 2)
self.assertEquals(dashboard_data['sum_late'], '$ 78.42')
self.assertIn('78.42', dashboard_data['sum_late'])
@@ -91,6 +91,11 @@ class TestReconciliation(AccountingTestCase):
'code': 'CABA',
'type': 'general',
})
self.general_journal = self.env['account.journal'].create({
'name': 'general',
'code': 'GENE',
'type': 'general',
})
# Tax Cash Basis
self.tax_cash_basis = self.env['account.tax'].create({
@@ -2062,3 +2067,68 @@ class TestReconciliationExec(TestReconciliation):
pay_receivable_line1 = payment1.move_line_ids.filtered(lambda l: l.account_id == self.account_rcv)
self.assertTrue(pay_receivable_line1.reconciled)
self.assertEqual(pay_receivable_line1.matched_debit_ids, move_caba1.tax_cash_basis_rec_id)
def test_reconciliation_with_currency(self):
#reconciliation on an account having a foreign currency being
#the same as the company one
account_rcv = self.account_rcv
account_rcv.currency_id = self.currency_euro_id
aml_obj = self.env['account.move.line'].with_context(
check_move_validity=False)
general_move1 = self.env['account.move'].create({
'name': 'general1',
'journal_id': self.general_journal.id,
})
aml_obj.create({
'name': 'debit1',
'account_id': account_rcv.id,
'debit': 11,
'move_id': general_move1.id,
})
aml_obj.create({
'name': 'credit1',
'account_id': self.account_rsa.id,
'credit': 11,
'move_id': general_move1.id,
})
general_move1.post()
general_move2 = self.env['account.move'].create({
'name': 'general2',
'journal_id': self.general_journal.id,
})
aml_obj.create({
'name': 'credit2',
'account_id': account_rcv.id,
'credit': 10,
'move_id': general_move2.id,
})
aml_obj.create({
'name': 'debit2',
'account_id': self.account_rsa.id,
'debit': 10,
'move_id': general_move2.id,
})
general_move2.post()
general_move3 = self.env['account.move'].create({
'name': 'general3',
'journal_id': self.general_journal.id,
})
aml_obj.create({
'name': 'credit3',
'account_id': account_rcv.id,
'credit': 1,
'move_id': general_move3.id,
})
aml_obj.create({
'name': 'debit3',
'account_id': self.account_rsa.id,
'debit': 1,
'move_id': general_move3.id,
})
general_move3.post()
to_reconcile = ((general_move1 + general_move2 + general_move3)
.mapped('line_ids')
.filtered(lambda l: l.account_id.id == account_rcv.id))
to_reconcile.reconcile()
for aml in to_reconcile:
self.assertEqual(aml.amount_residual, 0.0)
@@ -99,7 +99,8 @@
<filter string="Payment Method" name="paymentmethod" domain="[]" context="{'group_by': 'payment_method_id'}"/>
<filter string="State" name="state" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Currency" name="currency" domain="[]" context="{'group_by': 'currency_id'}" groups="base.group_multi_currency"/>
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
<!-- company_id is not stored (cdca5d4751a979f6b694f02d88160843f81a77f5). But we can't break structure-->
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company" invisible="1" />
<separator/>
<filter string="Late Activities" name="activities_overdue"
domain="[('activity_ids.date_deadline', '&lt;', context_today().strftime('%Y-%m-%d'))]"
@@ -66,8 +66,13 @@ class AccountInvoiceLine(models.Model):
@api.onchange('product_id')
def _onchange_product_id(self):
res = super(AccountInvoiceLine, self)._onchange_product_id()
rec = self.env['account.analytic.default'].account_get(self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=self.company_id.id)
rec = self.env['account.analytic.default'].account_get(
self.product_id.id,
self.invoice_id.commercial_partner_id.id,
self.invoice_id.user_id.id or self.env.uid,
fields.Date.today(),
company_id=self.company_id.id
)
self.account_analytic_id = rec.analytic_id.id
self.analytic_tag_ids = rec.analytic_tag_ids.ids
return res
@@ -75,8 +80,12 @@ class AccountInvoiceLine(models.Model):
def _set_additional_fields(self, invoice):
if not self.account_analytic_id or not self.analytic_tag_ids:
rec = self.env['account.analytic.default'].account_get(
self.product_id.id, self.invoice_id.commercial_partner_id.id, self.env.uid,
fields.Date.today(), company_id=invoice.company_id.id)
self.product_id.id,
self.invoice_id.commercial_partner_id.id,
self.invoice_id.user_id.id or self.env.uid,
fields.Date.today(),
company_id=invoice.company_id.id
)
if rec:
if self.account_analytic_id:
self.account_analytic_id = rec.analytic_id.id
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from werkzeug.urls import url_encode
from odoo import http, _
from odoo.addons.portal.controllers.portal import _build_url_w_params
from odoo.addons.payment.controllers.portal import PaymentProcessing
@@ -17,8 +19,6 @@ class PaymentPortal(http.Controller):
:return html: form containing all values related to the acquirer to
redirect customers to the acquirer website """
success_url = kwargs.get('success_url', '/my')
invoice_sudo = request.env['account.invoice'].sudo().browse(invoice_id)
if not invoice_sudo:
return False
@@ -30,6 +30,10 @@ class PaymentPortal(http.Controller):
if request.env.user._is_public():
save_token = False # we avoid to create a token for the public user
success_url = kwargs.get(
'success_url', "%s?%s" % (invoice_sudo.access_url, url_encode({'access_token': access_token}) if access_token else '')
)
vals = {
'acquirer_id': acquirer_id,
'return_url': success_url,
@@ -54,7 +58,6 @@ class PaymentPortal(http.Controller):
def invoice_pay_token(self, invoice_id, pm_id=None, **kwargs):
""" Use a token to perform a s2s transaction """
error_url = kwargs.get('error_url', '/my')
success_url = kwargs.get('success_url', '/my')
access_token = kwargs.get('access_token')
params = {}
if access_token:
@@ -65,6 +68,9 @@ class PaymentPortal(http.Controller):
params['error'] = 'pay_invoice_invalid_doc'
return request.redirect(_build_url_w_params(error_url, params))
success_url = kwargs.get(
'success_url', "%s?%s" % (invoice_sudo.access_url, url_encode({'access_token': access_token}) if access_token else '')
)
try:
token = request.env['payment.token'].sudo().browse(int(pm_id))
except (ValueError, TypeError):
@@ -37,7 +37,7 @@ class AccountVoucher(models.Model):
('purchase', 'Purchase')
], string='Type', readonly=True, states={'draft': [('readonly', False)]}, oldname="type")
name = fields.Char('Payment Memo',
readonly=True, states={'draft': [('readonly', False)]}, default='')
readonly=True, states={'draft': [('readonly', False)]}, default='',copy=False)
date = fields.Date("Bill Date", readonly=True,
index=True, states={'draft': [('readonly', False)]},
copy=False, default=fields.Date.context_today)
+4
View File
@@ -38,3 +38,7 @@ class BusController(Controller):
if request.registry.in_test_mode():
raise exceptions.UserError(_("bus.Bus not available in test mode"))
return self._poll(request.db, channels, last, options)
@route('/longpolling/im_status', type="json", auth="user")
def im_status(self, partner_ids):
return request.env['res.partner'].search_read([['id', 'in', partner_ids]], ['id', 'im_status'])
+9 -3
View File
@@ -243,7 +243,7 @@ class AlarmManager(models.AbstractModel):
_name = 'calendar.alarm_manager'
_description = 'Event Alarm Manager'
def get_next_potential_limit_alarm(self, alarm_type, seconds=None, partner_id=None):
def _get_next_potential_limit_alarm(self, alarm_type, seconds=None, partner_id=None):
result = {}
delta_request = """
SELECT
@@ -323,6 +323,12 @@ class AlarmManager(models.AbstractModel):
'rrule': rule
}
# determine accessible events
events = self.env['calendar.event'].browse(result)
result = {
key: result[key]
for key in set(events._filter_access_rules('read').ids)
}
return result
def do_check_alarm_for_one_date(self, one_date, event, event_maxdelta, in_the_next_X_seconds, alarm_type, after=False, missing=False):
@@ -374,7 +380,7 @@ class AlarmManager(models.AbstractModel):
cron_interval = cron.interval_number * interval_to_second[cron.interval_type]
all_meetings = self.get_next_potential_limit_alarm('email', seconds=cron_interval)
all_meetings = self._get_next_potential_limit_alarm('email', seconds=cron_interval)
for meeting in self.env['calendar.event'].browse(all_meetings):
max_delta = all_meetings[meeting.id]['max_duration']
@@ -404,7 +410,7 @@ class AlarmManager(models.AbstractModel):
if not partner:
return []
all_meetings = self.get_next_potential_limit_alarm('notification', partner_id=partner.id)
all_meetings = self._get_next_potential_limit_alarm('notification', partner_id=partner.id)
time_limit = 3600 * 24 # return alarms of the next 24 hours
for event_id in all_meetings:
max_delta = all_meetings[event_id]['max_duration']
+1 -1
View File
@@ -52,7 +52,7 @@ class AlarmManager(models.AbstractModel):
}
cron_interval = cron.interval_number * interval_to_second[cron.interval_type]
events_data = self.get_next_potential_limit_alarm('sms', seconds=cron_interval)
events_data = self._get_next_potential_limit_alarm('sms', seconds=cron_interval)
for event in self.env['calendar.event'].browse(events_data):
max_delta = events_data[event.id]['max_duration']
+10 -5
View File
@@ -54,7 +54,10 @@ class Lead(models.Model):
_primary_email = 'email_from'
def _default_probability(self):
stage_id = self._default_stage_id()
if 'default_stage_id' in self._context:
stage_id = self._context.get('default_stage_id')
else:
stage_id = self._default_stage_id()
if stage_id:
return self.env['crm.stage'].browse(stage_id).probability
return 10
@@ -308,9 +311,10 @@ class Lead(models.Model):
if vals.get('user_id') and 'date_open' not in vals:
vals['date_open'] = fields.Datetime.now()
if context.get('default_partner_id') and not vals.get('email_from'):
partner = self.env['res.partner'].browse(context['default_partner_id'])
vals['email_from'] = partner.email
partner_id = vals.get('partner_id') or context.get('default_partner_id')
onchange_values = self._onchange_partner_id_values(partner_id)
onchange_values.update(vals) # we don't want to overwrite any existing key
vals = onchange_values
return super(Lead, self.with_context(context)).create(vals)
@@ -319,7 +323,8 @@ class Lead(models.Model):
# stage change: update date_last_stage_update
if 'stage_id' in vals:
vals['date_last_stage_update'] = fields.Datetime.now()
if vals.get('user_id') and 'date_open' not in vals:
# Only write the 'date_open' if no salesperson was assigned.
if vals.get('user_id') and 'date_open' not in vals and not self.mapped('user_id'):
vals['date_open'] = fields.Datetime.now()
# stage change with new stage: update probability and date_closed
if vals.get('stage_id') and 'probability' not in vals:
+1 -1
View File
@@ -81,7 +81,7 @@ class SaleOrderLine(models.Model):
@api.onchange('event_ticket_id')
def _onchange_event_ticket_id(self):
company = self.event_id.company_id or self.env.user.company_id
currency = company.currency_id
currency = company.currency_id or self.env.user.company_id.currency_id
self.price_unit = currency._convert(
self.event_ticket_id.price, self.order_id.currency_id, self.order_id.company_id, self.order_id.date_order or fields.Date.today())
+5 -19
View File
@@ -3,6 +3,9 @@
from odoo import api, fields, models
import logging
_logger = logging.getLogger(__name__)
class Users(models.Model):
_inherit = 'res.users'
@@ -18,26 +21,9 @@ class Users(models.Model):
@api.depends('karma')
def _compute_karma_position(self):
where_query = self._where_calc([])
self._apply_ir_rules(where_query, 'read')
from_clause, where_clause, where_clause_params = where_query.get_sql()
query = """
SELECT sub.id, sub.karma_position
FROM (
SELECT id, row_number() OVER (ORDER BY res_users.karma DESC) AS karma_position
FROM {from_clause}
WHERE {where_clause}
) sub
WHERE sub.id IN %s
""".format(from_clause=from_clause, where_clause=where_clause)
self.env.cr.execute(query, where_clause_params + [tuple(self.ids)])
position_map = {item['id']: item['karma_position'] for item in self.env.cr.dictfetchall()}
_logger.warning("The field karma_position from res.users is deprecated. Don't use it anymore.")
for user in self:
user.karma_position = position_map.get(user.id, 0)
user.karma_position = 0
@api.multi
@api.depends('badge_ids')
@@ -189,12 +189,12 @@ class GoogleService(models.TransientModel):
status = error.response.status_code
response = ""
else:
req = json.loads(error.request.body)
req = json.loads(error.request.body or 'null')
res = error.response.json()
_logger.exception("Error while requesting Google Services\nRequest:\n%s\nResponse:\n%s", pformat(req), pformat(res))
if error.response.status_code in (400, 401, 410):
raise UserError(_("Error while requesting Google Services: %s" % res['error']['message']))
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google"))
raise UserError(_("Error while requesting Google Services: %s") % res['error']['message'])
raise self.env['res.config.settings'].get_config_warning(_("Something went wrong with your request to google: %s") % res['error']['message'])
return (status, response, ask_time)
# TODO : remove me, it is only used in google calendar. Make google_calendar use the constants
+2 -2
View File
@@ -15,7 +15,7 @@ class User(models.Model):
job_title = fields.Char(related='employee_id.job_title', readonly=False)
work_phone = fields.Char(related='employee_id.work_phone', readonly=False)
mobile_phone = fields.Char(related='employee_id.mobile_phone', readonly=False)
phone = fields.Char(related='employee_id.phone', readonly=False, related_sudo=False)
employee_phone = fields.Char(related='employee_id.phone', readonly=False, related_sudo=False)
work_email = fields.Char(related='employee_id.work_email', readonly=False, related_sudo=False)
category_ids = fields.Many2many(related='employee_id.category_ids', string="Employee Tags", readonly=False, related_sudo=False)
department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False)
@@ -90,7 +90,7 @@ class User(models.Model):
'parent_id',
'passport_id',
'permit_no',
'phone',
'employee_phone',
'pin',
'place_of_birth',
'spouse_birthdate',
+1 -1
View File
@@ -87,7 +87,7 @@
It should be different from the work address.
</div>
<field name="km_home_work"/>
<field name="phone"/>
<field name="employee_phone"/>
<field name="employee_bank_account_id"/>
</group>
<group string="Citizenship">
@@ -4,7 +4,7 @@
<record id="hr_employee_view_kanban" model="ir.ui.view">
<field name="name">hr.employee</field>
<field name="model">hr.employee</field>
<field name="inherit_id" ref="hr_presence.hr_employee_view_kanban"/>
<field name="inherit_id" ref="hr_presence.hr_employee_view_kanban_inherit"/>
<field name="priority">20</field>
<field name="arch" type="xml">
<xpath expr="//span[@name='presence_absent']" position="attributes">
+3 -3
View File
@@ -96,12 +96,12 @@ class Contract(models.Model):
@api.constrains('employee_id', 'state', 'date_start', 'date_end')
def _check_current_contract(self):
""" Two contracts in state [incoming | pending | open | close] cannot overlap """
for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel']):
""" Two contracts in state [incoming | pending | open] cannot overlap """
for contract in self.filtered(lambda c: c.state not in ['draft', 'cancel', 'close']):
domain = [
('id', '!=', contract.id),
('employee_id', '=', contract.employee_id.id),
('state', 'in', ['incoming', 'pending', 'open', 'close']),
('state', 'in', ['incoming', 'pending', 'open']),
]
if not contract.date_end:
+22 -13
View File
@@ -151,14 +151,14 @@ class HrExpense(models.Model):
@api.model
def get_empty_list_help(self, help_message):
if help_message and "oe_view_nocontent_smiling_face" not in help_message:
if help_message and "o_view_nocontent_smiling_face" not in help_message:
use_mailgateway = self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway')
alias_record = use_mailgateway and self.env.ref('hr_expense.mail_alias_expense') or False
if alias_record and alias_record.alias_domain and alias_record.alias_name:
link = "<a id='o_mail_test' href='mailto:%(email)s?subject=Lunch%%20with%%20customer%%3A%%20%%2412.32'>%(email)s</a>" % {
'email': '%s@%s' % (alias_record.alias_name, alias_record.alias_domain)
}
return '<p class="oe_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (
return '<p class="o_view_nocontent_smiling_face">%s</p><p class="oe_view_nocontent_alias">%s</p>' % (
_('Add a new expense,'),
_('or send receipts by email to %s.') % (link),)
return super(HrExpense, self).get_empty_list_help(help_message)
@@ -211,6 +211,25 @@ class HrExpense(models.Model):
# Business
# ----------------------------------------
@api.multi
def _prepare_move_values(self):
"""
This function prepares move values related to an expense
"""
self.ensure_one()
journal = self.sheet_id.bank_journal_id if self.payment_mode == 'company_account' else self.sheet_id.journal_id
account_date = self.sheet_id.accounting_date or self.date
move_values = {
'journal_id': journal.id,
'company_id': self.env.user.company_id.id,
'date': account_date,
'ref': self.sheet_id.name,
# force the name to the default value, to avoid an eventual 'default_name' in the context
# to set it to '' which cause no number to be given to the account.move when posted.
'name': '/',
}
return move_values
@api.multi
def _get_account_move_by_sheet(self):
""" Return a mapping between the expense sheet of current expense and its account move
@@ -219,18 +238,8 @@ class HrExpense(models.Model):
move_grouped_by_sheet = {}
for expense in self:
# create the move that will contain the accounting entries
account_date = expense.sheet_id.accounting_date or expense.date
if expense.sheet_id.id not in move_grouped_by_sheet:
journal = expense.sheet_id.bank_journal_id if expense.payment_mode == 'company_account' else expense.sheet_id.journal_id
move = self.env['account.move'].create({
'journal_id': journal.id,
'company_id': self.env.user.company_id.id,
'date': account_date,
'ref': expense.sheet_id.name,
# force the name to the default value, to avoid an eventual 'default_name' in the context
# to set it to '' which cause no number to be given to the account.move when posted.
'name': '/',
})
move = self.env['account.move'].create(expense._prepare_move_values())
move_grouped_by_sheet[expense.sheet_id.id] = move
else:
move = move_grouped_by_sheet[expense.sheet_id.id]
+2 -2
View File
@@ -206,8 +206,8 @@ class Employee(models.Model):
hr_vals['manager_id'] = values['parent_id']
if values.get('department_id') is not None:
hr_vals['department_id'] = values['department_id']
holidays = self.env['hr.leave'].search(['|',('state', 'in', ['draft', 'confirm']),('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
holidays = self.env['hr.leave'].sudo().search(['|',('state', 'in', ['draft', 'confirm']),('date_from', '>', today_date), ('employee_id', 'in', self.ids)])
holidays.write(hr_vals)
allocations = self.env['hr.leave.allocation'].search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
allocations = self.env['hr.leave.allocation'].sudo().search([('state', 'in', ['draft', 'confirm']), ('employee_id', 'in', self.ids)])
allocations.write(hr_vals)
return res
+41 -12
View File
@@ -13,6 +13,7 @@ from odoo import api, fields, models, tools, SUPERUSER_ID
from odoo.addons.resource.models.resource import float_to_time, HOURS_PER_DAY
from odoo.exceptions import AccessError, UserError, ValidationError
from odoo.tools import float_compare
from odoo.tools.float_utils import float_round
from odoo.tools.translate import _
_logger = logging.getLogger(__name__)
@@ -147,6 +148,8 @@ class HolidaysRequest(models.Model):
number_of_hours_display = fields.Float(
'Duration in hours', compute='_compute_number_of_hours_display', copy=False, readonly=True,
help='Number of hours of the time off request according to your working schedule. Used for interface.')
duration_display = fields.Char('Requested (Days/Hours)', compute='_compute_duration_display',
help="Field allowing to see the leave request duration in days or hours depending on the leave_type_request_unit") # details
# details
meeting_id = fields.Many2one('calendar.event', string='Meeting')
parent_id = fields.Many2one('hr.leave', string='Parent', copy=False)
@@ -356,11 +359,17 @@ class HolidaysRequest(models.Model):
self.mode_company_id = False
self.department_id = False
@api.multi
def _sync_employee_details(self):
for holiday in self:
holiday.manager_id = holiday.employee_id.parent_id.id
if holiday.employee_id:
holiday.department_id = holiday.employee_id.department_id
@api.onchange('employee_id')
def _onchange_employee_id(self):
self.manager_id = self.employee_id.parent_id.id
if self.employee_id:
self.department_id = self.employee_id.department_id
self._sync_employee_details()
self.holiday_status_id = False
@api.onchange('date_from', 'date_to', 'employee_id')
def _onchange_leave_dates(self):
@@ -386,6 +395,16 @@ class HolidaysRequest(models.Model):
else:
holiday.number_of_hours_display = 0
@api.multi
@api.depends('number_of_hours_display', 'number_of_days_display')
def _compute_duration_display(self):
for leave in self:
leave.duration_display = '%g %s' % (
(float_round(leave.number_of_hours_display, precision_digits=2)
if leave.leave_type_request_unit == 'hour'
else float_round(leave.number_of_days_display, precision_digits=2)),
_('hour(s)') if leave.leave_type_request_unit == 'hour' else _('day(s)'))
@api.multi
@api.depends('state', 'employee_id', 'department_id')
def _compute_can_reset(self):
@@ -414,7 +433,7 @@ class HolidaysRequest(models.Model):
def _check_date(self):
for holiday in self:
domain = [
('date_from', '<=', holiday.date_to),
('date_from', '<', holiday.date_to),
('date_to', '>', holiday.date_from),
('employee_id', '=', holiday.employee_id.id),
('id', '!=', holiday.id),
@@ -457,7 +476,10 @@ class HolidaysRequest(models.Model):
res = []
for leave in self:
if self.env.context.get('short_name'):
res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
if leave.leave_type_request_unit == 'hour':
res.append((leave.id, _("%s : %.2f hour(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_hours_display)))
else:
res.append((leave.id, _("%s : %.2f day(s)") % (leave.name or leave.holiday_status_id.name, leave.number_of_days)))
else:
if leave.holiday_type == 'company':
target = leave.mode_company_id.name
@@ -467,11 +489,18 @@ class HolidaysRequest(models.Model):
target = leave.category_id.name
else:
target = leave.employee_id.name
res.append(
(leave.id,
_("%s on %s :%.2f day(s)") %
(target, leave.holiday_status_id.name, leave.number_of_days))
)
if leave.leave_type_request_unit == 'hour':
res.append(
(leave.id,
_("%s on %s : %.2f hour(s)") %
(target, leave.holiday_status_id.name, leave.number_of_hours_display))
)
else:
res.append(
(leave.id,
_("%s on %s : %.2f day(s)") %
(target, leave.holiday_status_id.name, leave.number_of_days))
)
return res
@api.multi
@@ -512,7 +541,7 @@ class HolidaysRequest(models.Model):
if holiday.validation_type == 'hr':
holiday.message_subscribe(partner_ids=(holiday.employee_id.parent_id.user_id.partner_id | holiday.employee_id.leave_manager_id.partner_id).ids)
if employee_id:
holiday._onchange_employee_id()
holiday._sync_employee_details()
if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values):
holiday._onchange_leave_dates()
if leave_type.validation_type == 'no_validation':
@@ -551,7 +580,7 @@ class HolidaysRequest(models.Model):
for holiday in self:
if employee_id:
holiday.add_follower(employee_id)
holiday._onchange_employee_id()
self._sync_employee_details()
if 'number_of_days' not in values and ('date_from' in values or 'date_to' in values):
holiday._onchange_leave_dates()
return result
@@ -207,7 +207,10 @@ class HolidaysAllocation(models.Model):
@api.depends('number_of_days', 'employee_id')
def _compute_number_of_hours_display(self):
for allocation in self:
allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
if allocation.parent_id and allocation.parent_id.type_request_unit == "hour":
allocation.number_of_hours_display = allocation.number_of_days * HOURS_PER_DAY
else:
allocation.number_of_hours_display = allocation.number_of_days * (allocation.employee_id.resource_calendar_id.hours_per_day or HOURS_PER_DAY)
@api.multi
@api.depends('number_of_hours_display', 'number_of_days_display')
@@ -158,9 +158,9 @@ class TestLeaveRequests(TestHrHolidaysBase):
self.employee_hrmanager.write({'department_id': self.hr_dept.id})
self.assertFalse(self.env['hr.leave'].search([('employee_id', 'in', self.hr_dept.member_ids.ids)]))
leave_form = Form(self.env['hr.leave'].sudo(self.user_hrmanager))
leave_form.holiday_status_id = self.holidays_type_1
leave_form.holiday_type = 'department'
leave_form.department_id = self.hr_dept
leave_form.holiday_status_id = self.holidays_type_1
leave = leave_form.save()
leave.action_approve()
member_ids = self.hr_dept.member_ids.ids
+1 -1
View File
@@ -313,7 +313,7 @@
<field name="name"/>
<field name="date_from"/>
<field name="date_to"/>
<field name="number_of_days" string="Number of Days" sum="Total time off"/>
<field name="duration_display"/>
<field name="state"/>
<field name="payslip_status" widget="toggle_button" options='{"active": "Reported in last payslips", "inactive": "To Report in Payslip"}' groups="hr_holidays.group_hr_holidays_manager"/>
<field name="category_id" invisible="1"/>
+1 -1
View File
@@ -7,7 +7,7 @@
<field name="arch" type="xml">
<xpath expr="//div[@name='button_box']" position="inside">
<button name="%(maintenance.hr_equipment_action)d"
context="{'search_default_my':1}"
context="{'search_default_employee_id': id}"
groups="maintenance.group_equipment_manager"
class="o_stat_button"
icon="fa-cubes"
@@ -45,7 +45,7 @@ class HrOrgChartController(http.Controller):
return {}
# compute employee data for org chart
ancestors, current = request.env['hr.employee'], employee
ancestors, current = request.env['hr.employee'].sudo(), employee.sudo()
while current.parent_id and len(ancestors) < self._managers_level+1:
ancestors += current.parent_id
current = current.parent_id
+17 -8
View File
@@ -893,14 +893,23 @@ class Escpos:
self._raw(PAPER_FULL_CUT)
def cashdraw(self, pin):
""" Send pulse to kick the cash drawer """
if pin == 2:
self._raw(CD_KICK_2)
elif pin == 5:
self._raw(CD_KICK_5)
else:
raise CashDrawerError()
def cashdraw(self, pin, tries=5):
""" Send pulse to kick the cash drawer
With some printers the drawer will not open after one pulse, for this reason we will check
the drawer status up to 'tries' times and send a new pulse if the drawer is still closed. If the
drawer status is open, we will stop sending pulses.
"""
for i in range(tries):
if pin == 2:
self._raw(CD_KICK_2)
elif pin == 5:
self._raw(CD_KICK_5)
else:
raise CashDrawerError()
if i != tries - 1 and not self.get_printer_status()['printer']['drawer_pin_high']:
break
def hw(self, hw):
+7 -4
View File
@@ -8,7 +8,7 @@ import time
from collections import namedtuple
from os import listdir
from threading import Thread, Lock
from threading import Event, Lock, Thread
from odoo import http
@@ -134,6 +134,7 @@ class Scale(Thread):
self.path_to_scale = ''
self.protocol = None
self.disabled = False
self.weight_request_event = Event()
def lockedstart(self):
with self.lock:
@@ -261,8 +262,8 @@ class Scale(Thread):
return None
def get_weight(self):
self.repeats = 5
self.disabled = False
self.weight_request_event.set()
self.lockedstart()
return self.weight
@@ -361,11 +362,13 @@ class Scale(Thread):
self.device = self.get_device()
if not self.device:
# retry later to support "plug and play"
time.sleep(10)
self.weight_request_event.clear()
self.weight_request_event.wait(10)
else:
self.disabled = self.protocol.disable
else:
time.sleep(10)
self.weight_request_event.clear()
self.weight_request_event.wait(10)
scale_thread = None
+1 -1
View File
@@ -162,7 +162,7 @@ class IapAccount(models.Model):
@api.model
def get(self, service_name, force_create=True):
account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.user.company_id.id, False])])
account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.user.company_id.id, False])], limit=1, order='company_id desc, id desc')
if not account and force_create:
account = self.create({'service_name': service_name})
# Since the account did not exist yet, we will encounter a NoCreditError,
+1 -1
View File
@@ -1,3 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_client_iap_account_manager,iap.account.manager,model_iap_account,base.group_system,1,1,1,0
access_client_iap_account_manager,iap.account.manager,model_iap_account,base.group_system,1,1,1,1
access_client_iap_account_user,iap.account.user,model_iap_account,base.group_user,1,0,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_client_iap_account_manager iap.account.manager model_iap_account base.group_system 1 1 1 0 1
3 access_client_iap_account_user iap.account.user model_iap_account base.group_user 1 0 1 0
@@ -14,7 +14,10 @@ var SupportMessage = Message.extend({
*/
init: function () {
this._super.apply(this, arguments);
if (this._serverAuthorID !== this.call('mail_service', 'getOdoobotID')) {
if (
!this._serverAuthorID ||
this._serverAuthorID[0] !== this.call('mail_service', 'getOdoobotID')[0]
) {
if (!this._serverAuthorID[0]) {
// the author is the client
this._serverAuthorID = [session.partner_id, session.name];
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Note for later versions, the group Employee Manager should become contract manager -->
<record id="hr.group_hr_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('fleet.fleet_group_manager'))]"/>
</record>
</odoo>
+2 -2
View File
@@ -305,7 +305,7 @@ class AccountFrFec(models.TransientModel):
END
AS PieceRef,
TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate,
CASE WHEN aml.name IS NULL THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib,
CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' ELSE replace(replace(aml.name, '|', '/'), '\t', '') END AS EcritureLib,
replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit,
replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit,
CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet,
@@ -388,7 +388,7 @@ class AccountFrFec(models.TransientModel):
rows_length = len(rows)
for i, row in enumerate(rows):
if not i == rows_length - 1:
row.append(lineterminator)
row[-1] += lineterminator
writer.writerow(row)
fecvalue = fecfile.getvalue()
+1 -1
View File
@@ -12,7 +12,7 @@ This is the module to manage the accounting chart for Mongolia.
* the Mongolia Official Chart of Accounts,
* the Tax Code Chart for Mongolia
* the main taxes used in Mongolia
Financial requirement contributor: Baskhuu Lodoikhuu. BumanIT LLC
""",
"depends": ['account'],
@@ -19,7 +19,7 @@
<field name="name">НӨАТ-гүй (2000000)</field>
<field name="chart_template_id" ref="mn_chart_1"/>
</record>
<record id="fiscal_position_vat4" model="account.fiscal.position.template">
<field name="name">Гадаад борлуулалт - Худалдан авалт</field>
<field name="chart_template_id" ref="mn_chart_1"/>
+11 -11
View File
@@ -479,25 +479,25 @@
<field name='name'>119.01 IVA pendiente de pago</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_119_02' model='account.account.tag'>
<field name='name'>119.02 IVA de importación pendiente de pago</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_119_03' model='account.account.tag'>
<field name='name'>119.03 IEPS pendiente de pago</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_119_04' model='account.account.tag'>
<field name='name'>119.04 IEPS pendiente de pago en importación</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_120_01' model='account.account.tag'>
<field name='name'>120.01 Anticipo a proveedores nacional</field>
@@ -917,7 +917,7 @@
<field name='name'>180.01 Crédito mercantil</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_181_01' model='account.account.tag'>
<field name='name'>181.01 Gastos de instalación</field>
@@ -2423,7 +2423,7 @@
<field name='name'>503 Devoluciones, descuentos o bonificaciones sobre compras</field>
<field name='color'>1</field>
<field name='applicability'>accounts</field>
<field name='nature'>D</field>
<field name='nature'>A</field>
</record>
<record id='account_tag_503_01' model='account.account.tag'>
<field name='name'>503.01 Devoluciones, descuentos o bonificaciones sobre compras</field>
@@ -2585,13 +2585,13 @@
<field name='name'>505.01 Costo por venta de activo fijo</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_505_02' model='account.account.tag'>
<field name='name'>505.02 Costo por baja de activo fijo</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_601_01' model='account.account.tag'>
<field name='name'>601.01 Sueldos y salarios</field>
@@ -4781,7 +4781,7 @@
<field name='name'>607.01 Participación de los trabajadores en las utilidades</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_608_01' model='account.account.tag'>
<field name='name'>608.01 Participación en resultados de subsidiarias</field>
@@ -4799,7 +4799,7 @@
<field name='name'>610.01 Participación de los trabajadores en las utilidades diferida</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_611_01' model='account.account.tag'>
<field name='name'>611.01 Impuesto Sobre la renta</field>
@@ -4817,7 +4817,7 @@
<field name='name'>612.01 Gastos no deducibles para CUFIN</field>
<field name='color'>4</field>
<field name='applicability'>accounts</field>
<field name='nature'>A</field>
<field name='nature'>D</field>
</record>
<record id='account_tag_613_01' model='account.account.tag'>
<field name='name'>613.01 Depreciación de edificios</field>
+17 -17
View File
@@ -1,17 +1,17 @@
id,name,applicability
tag_ST1,ST1,taxes
tag_ST1A,ST1A,taxes
tag_ST2,ST2,taxes
tag_ST2A,ST2A,taxes
tag_ST3,ST3,taxes
tag_ST5,ST5,taxes
tag_ST7,ST7,taxes
tag_ST10,ST10,taxes
tag_ST12,ST12,taxes
tag_PT14,PT14,taxes
tag_PT14A,PT14A,taxes
tag_PT15,PT15,taxes
tag_PT15A,PT15A,taxes
tag_PT16,PT16,taxes
tag_PT17,PT17,taxes
tag_PT18,PT18,taxes
id,name,applicability
tag_ST1,ST1,taxes
tag_ST1A,ST1A,taxes
tag_ST2,ST2,taxes
tag_ST2A,ST2A,taxes
tag_ST3,ST3,taxes
tag_ST5,ST5,taxes
tag_ST7,ST7,taxes
tag_ST10,ST10,taxes
tag_ST12,ST12,taxes
tag_PT14,PT14,taxes
tag_PT14A,PT14A,taxes
tag_PT15,PT15,taxes
tag_PT15A,PT15A,taxes
tag_PT16,PT16,taxes
tag_PT17,PT17,taxes
tag_PT18,PT18,taxes
1 id name applicability
2 tag_ST1 ST1 taxes
3 tag_ST1A ST1A taxes
4 tag_ST2 ST2 taxes
5 tag_ST2A ST2A taxes
6 tag_ST3 ST3 taxes
7 tag_ST5 ST5 taxes
8 tag_ST7 ST7 taxes
9 tag_ST10 ST10 taxes
10 tag_ST12 ST12 taxes
11 tag_PT14 PT14 taxes
12 tag_PT14A PT14A taxes
13 tag_PT15 PT15 taxes
14 tag_PT15A PT15A taxes
15 tag_PT16 PT16 taxes
16 tag_PT17 PT17 taxes
17 tag_PT18 PT18 taxes
+111 -111
View File
@@ -1,111 +1,111 @@
id,code,name,user_type_id/id,reconcile,chart_template_id/id
100020,100020,Stock Valuation Account,account.data_account_type_current_assets,True,default_chart_template
100030,100030,Stock Work In Progress,account.data_account_type_current_assets,False,default_chart_template
100040,100040,Stock Finished Goods,account.data_account_type_current_assets,False,default_chart_template
100050,100050,Stock Delivered Control Account,account.data_account_type_current_assets,True,default_chart_template
100060,100060,Purchase Tax Control Account,account.data_account_type_current_assets,False,default_chart_template
100070,100070,Other Current Assets,account.data_account_type_current_assets,False,default_chart_template
110010,110010,Debtors Control Account,account.data_account_type_receivable,True,default_chart_template
110020,110020,Sundry Debtors,account.data_account_type_receivable,True,default_chart_template
124010,124010,Credit Card Merchant Account,account.data_account_type_liquidity,False,default_chart_template
126010,126010,Cash In Hand,account.data_account_type_liquidity,False,default_chart_template
130010,130010,Prepayments,account.data_account_type_prepayments,False,default_chart_template
140010,140010,Software,account.data_account_type_fixed_assets,False,default_chart_template
140020,140020,Patents & Trademarks,account.data_account_type_fixed_assets,False,default_chart_template
140030,140030,Fixtures & Fittings,account.data_account_type_fixed_assets,False,default_chart_template
140040,140040,Land & Buildings,account.data_account_type_fixed_assets,False,default_chart_template
140050,140050,Motor Vehicles,account.data_account_type_fixed_assets,False,default_chart_template
140060,140060,Office Equipment (incl computer equipment),account.data_account_type_fixed_assets,False,default_chart_template
140070,140070,Plant & Machinery,account.data_account_type_fixed_assets,False,default_chart_template
150010,150010,Non-current assets,account.data_account_type_non_current_assets,False,default_chart_template
200010,200010,Stock Received Control Account,account.data_account_type_current_liabilities,True,default_chart_template
200020,200020,Sundry Creditors,account.data_account_type_current_liabilities,False,default_chart_template
200030,200030,Other Creditors,account.data_account_type_current_liabilities,False,default_chart_template
200040,200040,Accruals,account.data_account_type_current_liabilities,False,default_chart_template
200050,200050,Bad debt provision,account.data_account_type_current_liabilities,False,default_chart_template
200060,200060,Sales Tax Control Account,account.data_account_type_current_liabilities,False,default_chart_template
200070,200070,Manual Adjustments & VAT,account.data_account_type_current_liabilities,False,default_chart_template
200080,200080,Loans,account.data_account_type_current_liabilities,False,default_chart_template
200090,200090,Hire Purchase,account.data_account_type_current_liabilities,False,default_chart_template
200100,200100,Mortgages,account.data_account_type_current_liabilities,False,default_chart_template
210010,210010,Company Credit Card,account.data_account_type_credit_card,False,default_chart_template
220010,220010,Creditors Control Account,account.data_account_type_payable,True,default_chart_template
220020,220020,SARS - VAT Account,account.data_account_type_payable,True,default_chart_template
220030,220030,P.A.Y.E. & UIF,account.data_account_type_payable,True,default_chart_template
220040,220040,Net Wages,account.data_account_type_payable,True,default_chart_template
220050,220050,Pension Fund,account.data_account_type_payable,True,default_chart_template
220060,220060,Corporation Tax,account.data_account_type_payable,True,default_chart_template
300010,300010,Called up share capital,account.data_account_type_equity,False,default_chart_template
300020,300020,Share premium account,account.data_account_type_equity,False,default_chart_template
300030,300030,Revaluation reserve,account.data_account_type_equity,False,default_chart_template
300040,300040,Other reserves,account.data_account_type_equity,False,default_chart_template
300050,300050,Capital,account.data_account_type_equity,False,default_chart_template
300060,300060,Dividends,account.data_account_type_equity,False,default_chart_template
300070,300070,Drawings,account.data_account_type_equity,False,default_chart_template
400010,400010,Undistributed Profits/Losses,account.data_unaffected_earnings,False,default_chart_template
500010,500010,Sales category 1,account.data_account_type_revenue,False,default_chart_template
500020,500020,Sales category 2,account.data_account_type_revenue,False,default_chart_template
500030,500030,Sales category 3,account.data_account_type_revenue,False,default_chart_template
500040,500040,Sales category 4,account.data_account_type_revenue,False,default_chart_template
500050,500050,Bank Interest received,account.data_account_type_revenue,False,default_chart_template
500060,500060,Investment Interest received,account.data_account_type_revenue,False,default_chart_template
500070,500070,Profits/Losses on disposals of assets,account.data_account_type_revenue,False,default_chart_template
500080,500080,Rental Income,account.data_account_type_revenue,False,default_chart_template
510010,510010,Other Income,account.data_account_type_other_income,False,default_chart_template
600010,600010,Cost of sales 1,account.data_account_type_direct_costs,False,default_chart_template
600020,600020,Cost of sales 2,account.data_account_type_direct_costs,False,default_chart_template
600030,600030,Cost of sales 3,account.data_account_type_direct_costs,False,default_chart_template
600040,600040,Cost of sales 4,account.data_account_type_direct_costs,False,default_chart_template
610010,610010,Marketing,account.data_account_type_expenses,False,default_chart_template
610020,610020,Exhibitions and events,account.data_account_type_expenses,False,default_chart_template
610030,610030,PR,account.data_account_type_expenses,False,default_chart_template
610040,610040,Distribution vehicles,account.data_account_type_expenses,False,default_chart_template
610050,610050,Distribution salaries and wages,account.data_account_type_expenses,False,default_chart_template
610060,610060,Shipping,account.data_account_type_expenses,False,default_chart_template
610070,610070,Directors pension,account.data_account_type_expenses,False,default_chart_template
610080,610080,Directors remuneration,account.data_account_type_expenses,False,default_chart_template
610090,610090,Gross Salaries,account.data_account_type_expenses,False,default_chart_template
610100,610100,Employers SDL & UIF,account.data_account_type_expenses,False,default_chart_template
610110,610110,Subcontractors payments,account.data_account_type_expenses,False,default_chart_template
610120,610120,Rent and rates,account.data_account_type_expenses,False,default_chart_template
610130,610130,Light / heat and power,account.data_account_type_expenses,False,default_chart_template
610140,610140,Repairs and maintenance,account.data_account_type_expenses,False,default_chart_template
610150,610150,Car hire,account.data_account_type_expenses,False,default_chart_template
610160,610160,Car fuel,account.data_account_type_expenses,False,default_chart_template
610170,610170,Car maintenance,account.data_account_type_expenses,False,default_chart_template
610180,610180,Telephone,account.data_account_type_expenses,False,default_chart_template
610190,610190,Internet & hosting,account.data_account_type_expenses,False,default_chart_template
610200,610200,Mobiles,account.data_account_type_expenses,False,default_chart_template
610210,610210,Stationery,account.data_account_type_expenses,False,default_chart_template
610220,610220,Office consumables,account.data_account_type_expenses,False,default_chart_template
610230,610230,Postage and Carriage,account.data_account_type_expenses,False,default_chart_template
610240,610240,Books,account.data_account_type_expenses,False,default_chart_template
610250,610250,Network costs,account.data_account_type_expenses,False,default_chart_template
610260,610260,Software expenses,account.data_account_type_expenses,False,default_chart_template
610270,610270,Other computer costs,account.data_account_type_expenses,False,default_chart_template
610280,610280,Recruitment fees,account.data_account_type_expenses,False,default_chart_template
610290,610290,Other admin expenses,account.data_account_type_expenses,False,default_chart_template
610300,610300,Accounting,account.data_account_type_expenses,False,default_chart_template
610310,610310,Auditing,account.data_account_type_expenses,False,default_chart_template
610320,610320,Consultancy,account.data_account_type_expenses,False,default_chart_template
610330,610330,Legal and professional charges,account.data_account_type_expenses,False,default_chart_template
610340,610340,Exchange gains/losses,account.data_account_type_expenses,False,default_chart_template
610350,610350,Other sundry expenses,account.data_account_type_expenses,False,default_chart_template
610360,610360,Bad debts,account.data_account_type_expenses,False,default_chart_template
610370,610370,Interest paid,account.data_account_type_expenses,False,default_chart_template
610380,610380,Bank Charges,account.data_account_type_expenses,False,default_chart_template
610390,610390,Donations,account.data_account_type_expenses,False,default_chart_template
610400,610400,Entertaining,account.data_account_type_expenses,False,default_chart_template
610410,610410,Insurance,account.data_account_type_expenses,False,default_chart_template
610420,610420,Travel and subsistence,account.data_account_type_expenses,False,default_chart_template
610430,610430,Corporation tax expense,account.data_account_type_expenses,False,default_chart_template
610440,610440,Foreign Exchange Gains/Losses,account.data_account_type_expenses,False,default_chart_template
610450,610450,Price Differences Control Account,account.data_account_type_expenses,False,default_chart_template
610460,610460,Cash Register Gains/Losses,account.data_account_type_expenses,False,default_chart_template
620010,620010,Software Depreciation,account.data_account_type_depreciation,False,default_chart_template
620020,620020,Patents & Trademarks Depreciation,account.data_account_type_depreciation,False,default_chart_template
620030,620030,Fixtures and fittings Depreciation,account.data_account_type_depreciation,False,default_chart_template
620040,620040,Land and buildings Depreciation,account.data_account_type_depreciation,False,default_chart_template
620050,620050,Motor vehicles Depreciation,account.data_account_type_depreciation,False,default_chart_template
620060,620060,Office equipment (inc computer equipment) Depreciation,account.data_account_type_depreciation,False,default_chart_template
620070,620070,Plant and machinery Depreciation,account.data_account_type_depreciation,False,default_chart_template
id,code,name,user_type_id/id,reconcile,chart_template_id/id
100020,100020,Stock Valuation Account,account.data_account_type_current_assets,True,default_chart_template
100030,100030,Stock Work In Progress,account.data_account_type_current_assets,False,default_chart_template
100040,100040,Stock Finished Goods,account.data_account_type_current_assets,False,default_chart_template
100050,100050,Stock Delivered Control Account,account.data_account_type_current_assets,True,default_chart_template
100060,100060,Purchase Tax Control Account,account.data_account_type_current_assets,False,default_chart_template
100070,100070,Other Current Assets,account.data_account_type_current_assets,False,default_chart_template
110010,110010,Debtors Control Account,account.data_account_type_receivable,True,default_chart_template
110020,110020,Sundry Debtors,account.data_account_type_receivable,True,default_chart_template
124010,124010,Credit Card Merchant Account,account.data_account_type_liquidity,False,default_chart_template
126010,126010,Cash In Hand,account.data_account_type_liquidity,False,default_chart_template
130010,130010,Prepayments,account.data_account_type_prepayments,False,default_chart_template
140010,140010,Software,account.data_account_type_fixed_assets,False,default_chart_template
140020,140020,Patents & Trademarks,account.data_account_type_fixed_assets,False,default_chart_template
140030,140030,Fixtures & Fittings,account.data_account_type_fixed_assets,False,default_chart_template
140040,140040,Land & Buildings,account.data_account_type_fixed_assets,False,default_chart_template
140050,140050,Motor Vehicles,account.data_account_type_fixed_assets,False,default_chart_template
140060,140060,Office Equipment (incl computer equipment),account.data_account_type_fixed_assets,False,default_chart_template
140070,140070,Plant & Machinery,account.data_account_type_fixed_assets,False,default_chart_template
150010,150010,Non-current assets,account.data_account_type_non_current_assets,False,default_chart_template
200010,200010,Stock Received Control Account,account.data_account_type_current_liabilities,True,default_chart_template
200020,200020,Sundry Creditors,account.data_account_type_current_liabilities,False,default_chart_template
200030,200030,Other Creditors,account.data_account_type_current_liabilities,False,default_chart_template
200040,200040,Accruals,account.data_account_type_current_liabilities,False,default_chart_template
200050,200050,Bad debt provision,account.data_account_type_current_liabilities,False,default_chart_template
200060,200060,Sales Tax Control Account,account.data_account_type_current_liabilities,False,default_chart_template
200070,200070,Manual Adjustments & VAT,account.data_account_type_current_liabilities,False,default_chart_template
200080,200080,Loans,account.data_account_type_current_liabilities,False,default_chart_template
200090,200090,Hire Purchase,account.data_account_type_current_liabilities,False,default_chart_template
200100,200100,Mortgages,account.data_account_type_current_liabilities,False,default_chart_template
210010,210010,Company Credit Card,account.data_account_type_credit_card,False,default_chart_template
220010,220010,Creditors Control Account,account.data_account_type_payable,True,default_chart_template
220020,220020,SARS - VAT Account,account.data_account_type_payable,True,default_chart_template
220030,220030,P.A.Y.E. & UIF,account.data_account_type_payable,True,default_chart_template
220040,220040,Net Wages,account.data_account_type_payable,True,default_chart_template
220050,220050,Pension Fund,account.data_account_type_payable,True,default_chart_template
220060,220060,Corporation Tax,account.data_account_type_payable,True,default_chart_template
300010,300010,Called up share capital,account.data_account_type_equity,False,default_chart_template
300020,300020,Share premium account,account.data_account_type_equity,False,default_chart_template
300030,300030,Revaluation reserve,account.data_account_type_equity,False,default_chart_template
300040,300040,Other reserves,account.data_account_type_equity,False,default_chart_template
300050,300050,Capital,account.data_account_type_equity,False,default_chart_template
300060,300060,Dividends,account.data_account_type_equity,False,default_chart_template
300070,300070,Drawings,account.data_account_type_equity,False,default_chart_template
400010,400010,Undistributed Profits/Losses,account.data_unaffected_earnings,False,default_chart_template
500010,500010,Sales category 1,account.data_account_type_revenue,False,default_chart_template
500020,500020,Sales category 2,account.data_account_type_revenue,False,default_chart_template
500030,500030,Sales category 3,account.data_account_type_revenue,False,default_chart_template
500040,500040,Sales category 4,account.data_account_type_revenue,False,default_chart_template
500050,500050,Bank Interest received,account.data_account_type_revenue,False,default_chart_template
500060,500060,Investment Interest received,account.data_account_type_revenue,False,default_chart_template
500070,500070,Profits/Losses on disposals of assets,account.data_account_type_revenue,False,default_chart_template
500080,500080,Rental Income,account.data_account_type_revenue,False,default_chart_template
510010,510010,Other Income,account.data_account_type_other_income,False,default_chart_template
600010,600010,Cost of sales 1,account.data_account_type_direct_costs,False,default_chart_template
600020,600020,Cost of sales 2,account.data_account_type_direct_costs,False,default_chart_template
600030,600030,Cost of sales 3,account.data_account_type_direct_costs,False,default_chart_template
600040,600040,Cost of sales 4,account.data_account_type_direct_costs,False,default_chart_template
610010,610010,Marketing,account.data_account_type_expenses,False,default_chart_template
610020,610020,Exhibitions and events,account.data_account_type_expenses,False,default_chart_template
610030,610030,PR,account.data_account_type_expenses,False,default_chart_template
610040,610040,Distribution vehicles,account.data_account_type_expenses,False,default_chart_template
610050,610050,Distribution salaries and wages,account.data_account_type_expenses,False,default_chart_template
610060,610060,Shipping,account.data_account_type_expenses,False,default_chart_template
610070,610070,Directors pension,account.data_account_type_expenses,False,default_chart_template
610080,610080,Directors remuneration,account.data_account_type_expenses,False,default_chart_template
610090,610090,Gross Salaries,account.data_account_type_expenses,False,default_chart_template
610100,610100,Employers SDL & UIF,account.data_account_type_expenses,False,default_chart_template
610110,610110,Subcontractors payments,account.data_account_type_expenses,False,default_chart_template
610120,610120,Rent and rates,account.data_account_type_expenses,False,default_chart_template
610130,610130,Light / heat and power,account.data_account_type_expenses,False,default_chart_template
610140,610140,Repairs and maintenance,account.data_account_type_expenses,False,default_chart_template
610150,610150,Car hire,account.data_account_type_expenses,False,default_chart_template
610160,610160,Car fuel,account.data_account_type_expenses,False,default_chart_template
610170,610170,Car maintenance,account.data_account_type_expenses,False,default_chart_template
610180,610180,Telephone,account.data_account_type_expenses,False,default_chart_template
610190,610190,Internet & hosting,account.data_account_type_expenses,False,default_chart_template
610200,610200,Mobiles,account.data_account_type_expenses,False,default_chart_template
610210,610210,Stationery,account.data_account_type_expenses,False,default_chart_template
610220,610220,Office consumables,account.data_account_type_expenses,False,default_chart_template
610230,610230,Postage and Carriage,account.data_account_type_expenses,False,default_chart_template
610240,610240,Books,account.data_account_type_expenses,False,default_chart_template
610250,610250,Network costs,account.data_account_type_expenses,False,default_chart_template
610260,610260,Software expenses,account.data_account_type_expenses,False,default_chart_template
610270,610270,Other computer costs,account.data_account_type_expenses,False,default_chart_template
610280,610280,Recruitment fees,account.data_account_type_expenses,False,default_chart_template
610290,610290,Other admin expenses,account.data_account_type_expenses,False,default_chart_template
610300,610300,Accounting,account.data_account_type_expenses,False,default_chart_template
610310,610310,Auditing,account.data_account_type_expenses,False,default_chart_template
610320,610320,Consultancy,account.data_account_type_expenses,False,default_chart_template
610330,610330,Legal and professional charges,account.data_account_type_expenses,False,default_chart_template
610340,610340,Exchange gains/losses,account.data_account_type_expenses,False,default_chart_template
610350,610350,Other sundry expenses,account.data_account_type_expenses,False,default_chart_template
610360,610360,Bad debts,account.data_account_type_expenses,False,default_chart_template
610370,610370,Interest paid,account.data_account_type_expenses,False,default_chart_template
610380,610380,Bank Charges,account.data_account_type_expenses,False,default_chart_template
610390,610390,Donations,account.data_account_type_expenses,False,default_chart_template
610400,610400,Entertaining,account.data_account_type_expenses,False,default_chart_template
610410,610410,Insurance,account.data_account_type_expenses,False,default_chart_template
610420,610420,Travel and subsistence,account.data_account_type_expenses,False,default_chart_template
610430,610430,Corporation tax expense,account.data_account_type_expenses,False,default_chart_template
610440,610440,Foreign Exchange Gains/Losses,account.data_account_type_expenses,False,default_chart_template
610450,610450,Price Differences Control Account,account.data_account_type_expenses,False,default_chart_template
610460,610460,Cash Register Gains/Losses,account.data_account_type_expenses,False,default_chart_template
620010,620010,Software Depreciation,account.data_account_type_depreciation,False,default_chart_template
620020,620020,Patents & Trademarks Depreciation,account.data_account_type_depreciation,False,default_chart_template
620030,620030,Fixtures and fittings Depreciation,account.data_account_type_depreciation,False,default_chart_template
620040,620040,Land and buildings Depreciation,account.data_account_type_depreciation,False,default_chart_template
620050,620050,Motor vehicles Depreciation,account.data_account_type_depreciation,False,default_chart_template
620060,620060,Office equipment (inc computer equipment) Depreciation,account.data_account_type_depreciation,False,default_chart_template
620070,620070,Plant and machinery Depreciation,account.data_account_type_depreciation,False,default_chart_template
1 id code name user_type_id/id reconcile chart_template_id/id
2 100020 100020 Stock Valuation Account account.data_account_type_current_assets True default_chart_template
3 100030 100030 Stock Work In Progress account.data_account_type_current_assets False default_chart_template
4 100040 100040 Stock Finished Goods account.data_account_type_current_assets False default_chart_template
5 100050 100050 Stock Delivered Control Account account.data_account_type_current_assets True default_chart_template
6 100060 100060 Purchase Tax Control Account account.data_account_type_current_assets False default_chart_template
7 100070 100070 Other Current Assets account.data_account_type_current_assets False default_chart_template
8 110010 110010 Debtors Control Account account.data_account_type_receivable True default_chart_template
9 110020 110020 Sundry Debtors account.data_account_type_receivable True default_chart_template
10 124010 124010 Credit Card Merchant Account account.data_account_type_liquidity False default_chart_template
11 126010 126010 Cash In Hand account.data_account_type_liquidity False default_chart_template
12 130010 130010 Prepayments account.data_account_type_prepayments False default_chart_template
13 140010 140010 Software account.data_account_type_fixed_assets False default_chart_template
14 140020 140020 Patents & Trademarks account.data_account_type_fixed_assets False default_chart_template
15 140030 140030 Fixtures & Fittings account.data_account_type_fixed_assets False default_chart_template
16 140040 140040 Land & Buildings account.data_account_type_fixed_assets False default_chart_template
17 140050 140050 Motor Vehicles account.data_account_type_fixed_assets False default_chart_template
18 140060 140060 Office Equipment (incl computer equipment) account.data_account_type_fixed_assets False default_chart_template
19 140070 140070 Plant & Machinery account.data_account_type_fixed_assets False default_chart_template
20 150010 150010 Non-current assets account.data_account_type_non_current_assets False default_chart_template
21 200010 200010 Stock Received Control Account account.data_account_type_current_liabilities True default_chart_template
22 200020 200020 Sundry Creditors account.data_account_type_current_liabilities False default_chart_template
23 200030 200030 Other Creditors account.data_account_type_current_liabilities False default_chart_template
24 200040 200040 Accruals account.data_account_type_current_liabilities False default_chart_template
25 200050 200050 Bad debt provision account.data_account_type_current_liabilities False default_chart_template
26 200060 200060 Sales Tax Control Account account.data_account_type_current_liabilities False default_chart_template
27 200070 200070 Manual Adjustments & VAT account.data_account_type_current_liabilities False default_chart_template
28 200080 200080 Loans account.data_account_type_current_liabilities False default_chart_template
29 200090 200090 Hire Purchase account.data_account_type_current_liabilities False default_chart_template
30 200100 200100 Mortgages account.data_account_type_current_liabilities False default_chart_template
31 210010 210010 Company Credit Card account.data_account_type_credit_card False default_chart_template
32 220010 220010 Creditors Control Account account.data_account_type_payable True default_chart_template
33 220020 220020 SARS - VAT Account account.data_account_type_payable True default_chart_template
34 220030 220030 P.A.Y.E. & UIF account.data_account_type_payable True default_chart_template
35 220040 220040 Net Wages account.data_account_type_payable True default_chart_template
36 220050 220050 Pension Fund account.data_account_type_payable True default_chart_template
37 220060 220060 Corporation Tax account.data_account_type_payable True default_chart_template
38 300010 300010 Called up share capital account.data_account_type_equity False default_chart_template
39 300020 300020 Share premium account account.data_account_type_equity False default_chart_template
40 300030 300030 Revaluation reserve account.data_account_type_equity False default_chart_template
41 300040 300040 Other reserves account.data_account_type_equity False default_chart_template
42 300050 300050 Capital account.data_account_type_equity False default_chart_template
43 300060 300060 Dividends account.data_account_type_equity False default_chart_template
44 300070 300070 Drawings account.data_account_type_equity False default_chart_template
45 400010 400010 Undistributed Profits/Losses account.data_unaffected_earnings False default_chart_template
46 500010 500010 Sales category 1 account.data_account_type_revenue False default_chart_template
47 500020 500020 Sales category 2 account.data_account_type_revenue False default_chart_template
48 500030 500030 Sales category 3 account.data_account_type_revenue False default_chart_template
49 500040 500040 Sales category 4 account.data_account_type_revenue False default_chart_template
50 500050 500050 Bank Interest received account.data_account_type_revenue False default_chart_template
51 500060 500060 Investment Interest received account.data_account_type_revenue False default_chart_template
52 500070 500070 Profits/Losses on disposals of assets account.data_account_type_revenue False default_chart_template
53 500080 500080 Rental Income account.data_account_type_revenue False default_chart_template
54 510010 510010 Other Income account.data_account_type_other_income False default_chart_template
55 600010 600010 Cost of sales 1 account.data_account_type_direct_costs False default_chart_template
56 600020 600020 Cost of sales 2 account.data_account_type_direct_costs False default_chart_template
57 600030 600030 Cost of sales 3 account.data_account_type_direct_costs False default_chart_template
58 600040 600040 Cost of sales 4 account.data_account_type_direct_costs False default_chart_template
59 610010 610010 Marketing account.data_account_type_expenses False default_chart_template
60 610020 610020 Exhibitions and events account.data_account_type_expenses False default_chart_template
61 610030 610030 PR account.data_account_type_expenses False default_chart_template
62 610040 610040 Distribution vehicles account.data_account_type_expenses False default_chart_template
63 610050 610050 Distribution salaries and wages account.data_account_type_expenses False default_chart_template
64 610060 610060 Shipping account.data_account_type_expenses False default_chart_template
65 610070 610070 Directors pension account.data_account_type_expenses False default_chart_template
66 610080 610080 Directors remuneration account.data_account_type_expenses False default_chart_template
67 610090 610090 Gross Salaries account.data_account_type_expenses False default_chart_template
68 610100 610100 Employers SDL & UIF account.data_account_type_expenses False default_chart_template
69 610110 610110 Subcontractors payments account.data_account_type_expenses False default_chart_template
70 610120 610120 Rent and rates account.data_account_type_expenses False default_chart_template
71 610130 610130 Light / heat and power account.data_account_type_expenses False default_chart_template
72 610140 610140 Repairs and maintenance account.data_account_type_expenses False default_chart_template
73 610150 610150 Car hire account.data_account_type_expenses False default_chart_template
74 610160 610160 Car fuel account.data_account_type_expenses False default_chart_template
75 610170 610170 Car maintenance account.data_account_type_expenses False default_chart_template
76 610180 610180 Telephone account.data_account_type_expenses False default_chart_template
77 610190 610190 Internet & hosting account.data_account_type_expenses False default_chart_template
78 610200 610200 Mobiles account.data_account_type_expenses False default_chart_template
79 610210 610210 Stationery account.data_account_type_expenses False default_chart_template
80 610220 610220 Office consumables account.data_account_type_expenses False default_chart_template
81 610230 610230 Postage and Carriage account.data_account_type_expenses False default_chart_template
82 610240 610240 Books account.data_account_type_expenses False default_chart_template
83 610250 610250 Network costs account.data_account_type_expenses False default_chart_template
84 610260 610260 Software expenses account.data_account_type_expenses False default_chart_template
85 610270 610270 Other computer costs account.data_account_type_expenses False default_chart_template
86 610280 610280 Recruitment fees account.data_account_type_expenses False default_chart_template
87 610290 610290 Other admin expenses account.data_account_type_expenses False default_chart_template
88 610300 610300 Accounting account.data_account_type_expenses False default_chart_template
89 610310 610310 Auditing account.data_account_type_expenses False default_chart_template
90 610320 610320 Consultancy account.data_account_type_expenses False default_chart_template
91 610330 610330 Legal and professional charges account.data_account_type_expenses False default_chart_template
92 610340 610340 Exchange gains/losses account.data_account_type_expenses False default_chart_template
93 610350 610350 Other sundry expenses account.data_account_type_expenses False default_chart_template
94 610360 610360 Bad debts account.data_account_type_expenses False default_chart_template
95 610370 610370 Interest paid account.data_account_type_expenses False default_chart_template
96 610380 610380 Bank Charges account.data_account_type_expenses False default_chart_template
97 610390 610390 Donations account.data_account_type_expenses False default_chart_template
98 610400 610400 Entertaining account.data_account_type_expenses False default_chart_template
99 610410 610410 Insurance account.data_account_type_expenses False default_chart_template
100 610420 610420 Travel and subsistence account.data_account_type_expenses False default_chart_template
101 610430 610430 Corporation tax expense account.data_account_type_expenses False default_chart_template
102 610440 610440 Foreign Exchange Gains/Losses account.data_account_type_expenses False default_chart_template
103 610450 610450 Price Differences Control Account account.data_account_type_expenses False default_chart_template
104 610460 610460 Cash Register Gains/Losses account.data_account_type_expenses False default_chart_template
105 620010 620010 Software Depreciation account.data_account_type_depreciation False default_chart_template
106 620020 620020 Patents & Trademarks Depreciation account.data_account_type_depreciation False default_chart_template
107 620030 620030 Fixtures and fittings Depreciation account.data_account_type_depreciation False default_chart_template
108 620040 620040 Land and buildings Depreciation account.data_account_type_depreciation False default_chart_template
109 620050 620050 Motor vehicles Depreciation account.data_account_type_depreciation False default_chart_template
110 620060 620060 Office equipment (inc computer equipment) Depreciation account.data_account_type_depreciation False default_chart_template
111 620070 620070 Plant and machinery Depreciation account.data_account_type_depreciation False default_chart_template
+15 -3
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, fields
from odoo import _, http, fields
from odoo.exceptions import AccessError
from odoo.http import request
from odoo.osv import expression
from odoo.tools import float_round, float_repr
@@ -10,6 +11,7 @@ from odoo.tools import float_round, float_repr
class LunchController(http.Controller):
@http.route('/lunch/infos', type='json', auth='user')
def infos(self, user_id=None):
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
infos = self._make_infos(user, order=False)
@@ -35,6 +37,7 @@ class LunchController(http.Controller):
@http.route('/lunch/trash', type='json', auth='user')
def trash(self, user_id=None):
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
lines = self._get_current_lines(user.id)
@@ -43,6 +46,7 @@ class LunchController(http.Controller):
@http.route('/lunch/pay', type='json', auth='user')
def pay(self, user_id=None):
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
lines = self._get_current_lines(user.id)
@@ -60,13 +64,15 @@ class LunchController(http.Controller):
@http.route('/lunch/user_location_set', type='json', auth='user')
def set_user_location(self, location_id, user_id=None):
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
user.last_lunch_location_id = request.env['lunch.location'].browse(location_id)
user.sudo().last_lunch_location_id = request.env['lunch.location'].browse(location_id)
return True
@http.route('/lunch/user_location_get', type='json', auth='user')
def get_user_location(self, user_id=None):
self._check_user_impersonification(user_id)
user = request.env['res.users'].browse(user_id) if user_id else request.env.user
return user.last_lunch_location_id.id
@@ -87,7 +93,9 @@ class LunchController(http.Controller):
'currency': {'symbol': currency.symbol, 'position': currency.position},
})
user_location = user.last_lunch_location_id if user.last_lunch_location_id else request.env['lunch.location'].search([], limit=1)
if not user.last_lunch_location_id:
user.last_lunch_location_id = request.env['lunch.location'].search([], limit=1)
user_location = user.last_lunch_location_id
alert_domain = expression.AND([
[('available_today', '=', True)],
@@ -101,6 +109,10 @@ class LunchController(http.Controller):
return res
def _check_user_impersonification(self, user_id=None):
if (user_id and request.env.uid != user_id and not request.env.user.has_group('lunch.group_lunch_manager')):
raise AccessError(_('You are trying to impersonate another user, but this can only be done by a lunch manager'))
def _get_current_lines(self, user_id):
return request.env['lunch.order'].search([('user_id', '=', user_id), ('date', '=', fields.Date.today()), ('state', '!=', 'cancelled')])
+5 -1
View File
@@ -42,6 +42,10 @@ class LunchOrder(models.Model):
display_toppings = fields.Text('Toppings', compute='_compute_display_toppings', store=True)
def init(self):
self._cr.execute("""CREATE INDEX IF NOT EXISTS lunch_order_user_product_date ON %s (user_id, product_id, date)"""
% self._table)
@api.depends('topping_ids_1', 'topping_ids_2', 'topping_ids_3', 'product_id', 'quantity')
def _compute_total_price(self):
for line in self:
@@ -69,8 +73,8 @@ class LunchOrder(models.Model):
'To add some money to your wallet, please contact your lunch manager.'))
def action_order(self):
self._check_wallet()
self.write({'state': 'ordered'})
self._check_wallet()
def action_confirm(self):
self.write({'state': 'confirmed'})
+5 -6
View File
@@ -81,13 +81,12 @@ class LunchProductReport(models.Model):
users.id AS user_id,
fav.user_id IS NOT NULL AS is_favorite,
product.new_until >= current_date AS is_new,
max(l_order.date) AS last_order_date
orders.last_order_date
FROM lunch_product product
RIGHT JOIN res_users users ON users.company_id = product.company_id -- multi company
LEFT JOIN lunch_product_favorite_user_rel fav ON fav.product_id = product.id AND fav.user_id = users.id
JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users
JOIN lunch_order l_order ON l_order.product_id = product.id AND l_order.user_id = users.id AND l_order.date <= current_date
INNER JOIN res_users users ON users.company_id = product.company_id -- multi company
INNER JOIN res_groups_users_rel groups ON groups.uid = users.id -- only generate for internal users
LEFT JOIN LATERAL (select max(date) AS last_order_date FROM lunch_order where user_id=users.id and product_id=product.id) AS orders ON TRUE
LEFT JOIN LATERAL (select user_id FROM lunch_product_favorite_user_rel where user_id=users.id and product_id=product.id) AS fav ON TRUE
WHERE users.active AND product.active AND groups.gid = %%s --only take into account active products and users
GROUP BY product.id, product.name, product.category_id, product.description, product.price, product.supplier_id, product.company_id, product.active, users.id, is_favorite, is_new
);
""" % self._table, (self.env.ref('base.group_user').id,))
@@ -77,13 +77,14 @@ var LunchKanbanController = KanbanController.extend({
*/
_renderLunchKanbanWidget: function () {
var self = this;
if (this.widget) {
this.widget.destroy();
}
var oldWidget = this.widget;
this.widgetData.wallet = parseFloat(this.widgetData.wallet).toFixed(2);
this.widget = new LunchKanbanWidget(this, _.extend(this.widgetData, {edit: this.editMode}));
return this.widget.appendTo(document.createDocumentFragment()).then(function () {
self.$('.o_lunch_kanban').prepend(self.widget.$el);
if (oldWidget) {
oldWidget.destroy();
}
});
},
_showPaymentDialog: function (title) {
@@ -128,6 +128,52 @@ QUnit.module('LunchKanbanView', {
kanban.destroy();
});
QUnit.test('no flickering at reload', async function (assert) {
assert.expect(2);
const self = this;
let infosProm = testUtils.makeTestPromise();
const kanban = await createLunchKanbanView({
View: LunchKanbanView,
model: 'product',
data: this.data,
arch: `
<kanban>
<templates>
<t t-name="kanban-box">
<div><field name="name"/></div>
</t>
</templates>
</kanban>
`,
mockRPC: function (route, args) {
if (route === '/lunch/user_location_get') {
return Promise.resolve(self.data['lunch.location'].records[0].id);
}
if (route === '/lunch/infos') {
return Promise.resolve(self.regularInfos);
}
var result = this._super.apply(this, arguments);
if (args.method === 'xmlid_to_res_id') {
// delay the rendering of the lunch widget
return infosProm.then(_.constant(result));
}
return result;
},
});
kanban.reload();
assert.strictEqual(kanban.$('.o_lunch_widget').length, 1,
"old widget should still be present");
await infosProm.resolve();
assert.strictEqual(kanban.$('.o_lunch_widget').length, 1);
kanban.destroy();
});
QUnit.module('LunchKanbanWidget', function () {
QUnit.test('empty cart', async function (assert) {
@@ -814,7 +860,6 @@ QUnit.module('LunchKanbanView', {
kanban.destroy();
});
});
});
@@ -14,6 +14,7 @@
title="None">
<div class="o_setting_right_pane">
<span class="o_form_label">Lunch Overdraft</span>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." aria-label="Values set here are company-specific." groups="base.group_multi_company" role="img"/>
<div class="text-muted">
Maximum overdraft that your employees can reach
</div>
+1 -1
View File
@@ -86,7 +86,7 @@ class LunchOrderWizard(models.TransientModel):
sum((wizard.topping_ids_1 | wizard.topping_ids_2 | wizard.topping_ids_3).mapped('price')))
def _get_matching_lines(self):
domain = [('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())]
domain = [('user_id', '=', self.user_id.id), ('product_id', '=', self.product_id.id), ('date', '=', fields.Date.today()), ('note', '=', self._get_note())]
lines = self.env['lunch.order'].search(domain)
return lines.filtered(lambda line: (line.topping_ids_1 | line.topping_ids_2 | line.topping_ids_3) == self.topping_ids_1)
+2 -2
View File
@@ -147,7 +147,7 @@ class MailActivity(models.Model):
('today', 'Today'),
('planned', 'Planned')], 'State',
compute='_compute_state')
recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type", readonly=True)
recommended_activity_type_id = fields.Many2one('mail.activity.type', string="Recommended Activity Type")
previous_activity_type_id = fields.Many2one('mail.activity.type', string='Previous Activity Type', readonly=True)
has_recommended_activities = fields.Boolean(
'Next activities available',
@@ -298,7 +298,7 @@ class MailActivity(models.Model):
target_record = self.env[activity.res_model].browse(activity.res_id)
if hasattr(target_record, 'company_id') and (
target_record.company_id != target_user.company_id and (
len(target_user.company_ids) > 1)):
len(target_user.sudo().company_ids) > 1)):
return # in that case we skip the check, assuming it would fail because of the company
model.browse(activity.res_id).check_access_rule('read')
except exceptions.AccessError:
+17 -11
View File
@@ -4,6 +4,7 @@
import logging
import re
from binascii import Error as binascii_error
from operator import itemgetter
from email.utils import formataddr
from openerp.http import request
@@ -933,7 +934,7 @@ class Message(models.Model):
def _invalidate_documents(self):
""" Invalidate the cache of the documents followed by ``self``. """
for record in self:
if record.model and record.res_id and 'message_ids' in self.env[record.model]:
if record.model and record.res_id and issubclass(self.pool[record.model], self.pool['mail.thread']):
self.env[record.model].invalidate_cache(fnames=[
'message_ids',
'message_unread',
@@ -968,16 +969,21 @@ class Message(models.Model):
key = match.group(2)
if not data_to_url.get(key):
name = match.group(4) if match.group(4) else 'image%s' % len(data_to_url)
attachment = Attachments.create({
'name': name,
'datas': match.group(2),
'datas_fname': name,
'res_model': values.get('model'),
'res_id': values.get('res_id'),
})
attachment.generate_access_token()
values['attachment_ids'].append((4, attachment.id))
data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
try:
attachment = Attachments.create({
'name': name,
'datas': match.group(2),
'datas_fname': name,
'res_model': values.get('model'),
'res_id': values.get('res_id'),
})
except binascii_error:
_logger.warning("Impossible to create an attachment out of badly formated base64 embedded image. Image has been removed.")
return match.group(3) # group(3) is the url ending single/double quote matched by the regexp
else:
attachment.generate_access_token()
values['attachment_ids'].append((4, attachment.id))
data_to_url[key] = ['/web/image/%s?access_token=%s' % (attachment.id, attachment.access_token), name]
return '%s%s alt="%s"' % (data_to_url[key][0], match.group(3), data_to_url[key][1])
values['body'] = _image_dataurl.sub(base64_to_boundary, tools.ustr(values['body']))
+2 -2
View File
@@ -139,7 +139,7 @@ var Chatter = Widget.extend({
if (this.fields.activity) {
this.fields.activity.$el.detach();
}
if (this.fields.thread) {
if (this.fields.thread && this.fields.thread.$el ) {
this.fields.thread.$el.detach();
}
@@ -404,7 +404,7 @@ var Chatter = Widget.extend({
self.fields.followers.$el.insertBefore(self.$('.o_chatter_button_attachment'));
}
}
if (self.fields.thread) {
if (self.fields.thread && self.fields.thread.$el) {
self.fields.thread.$el.appendTo(self.$el);
}
}).then(always).guardedCatch(always);
@@ -578,7 +578,8 @@ var Message = AbstractMessage.extend(Mixins.EventDispatcherMixin, ServicesMixin
* @return {boolean}
*/
_isOdoobotAuthor: function () {
return this._serverAuthorID === this.call('mail_service', 'getOdoobotID');
return this._serverAuthorID &&
this._serverAuthorID[0] === this.call('mail_service', 'getOdoobotID')[0];
},
/**
* State whether the message is transient or not
@@ -46,7 +46,7 @@ var PREVIEW_MSG_MAX_SIZE = 350; // optimal for native english speakers
var MailManager = AbstractService.extend({
dependencies: ['ajax', 'bus_service', 'local_storage'],
_ODOOBOT_ID: "ODOOBOT", // default authorID for transient messages
_ODOOBOT_ID: ["ODOOBOT", "ODOOBOT"], // authorID for transient messages
/**
* @override
@@ -258,7 +258,7 @@ var MailManager = AbstractService.extend({
/**
* Get the OdooBot ID, which is the default authorID for transient messages
*
* @returns {string}
* @returns {Array<string>}
*/
getOdoobotID: function () {
return this._ODOOBOT_ID;
@@ -471,12 +471,11 @@ MailManager.include({
*
* @private
* @param {Object} data
* @param {string} data.author_id
*/
_handlePartnerTransientMessageNotification: function (data) {
var lastMessage = _.last(this._messages);
data.id = (lastMessage ? lastMessage.getID() : 0) + 0.01;
data.author_id = data.author_id || this.getOdoobotID();
data.author_id = this.getOdoobotID();
this.addMessage(data);
},
/**
@@ -30,6 +30,9 @@ MailManager.include({
getImStatus: function (data) {
var self = this;
var partnerID = data.partnerID;
if (partnerID === this.getOdoobotID()[0]) {
return 'bot';
}
if (!this._imStatus[partnerID]) {
// Add to list to call it in next bus update or _fetchMissingImStatus
this._imStatus[partnerID] = undefined;
@@ -84,9 +87,10 @@ MailManager.include({
return Promise.resolve();
}
return this._rpc({
model: 'res.partner',
method: 'read',
args: [partnerIDs, ['id', 'im_status']],
route: '/longpolling/im_status',
params: { partner_ids: partnerIDs },
}, {
shadow: true,
}).then( function (results) {
self.updateImStatus(results);
});
@@ -39,7 +39,7 @@ QUnit.test('simple set im_status', function (assert) {
});
QUnit.test('multi get_im_status', async function (assert) {
assert.expect(9);
assert.expect(8);
var readCount = 0;
var parent = testUtils.createParent({
//data: this.data,
@@ -48,9 +48,8 @@ QUnit.test('multi get_im_status', async function (assert) {
if (route === '/mail/init_messaging') {
return this._super.apply(this, arguments);
}
if (args.method === 'read' && args.model === 'res.partner') {
assert.deepEqual(args.args[0], [2,3]);
assert.deepEqual(args.args[1], ['id', 'im_status']);
if (route === '/longpolling/im_status') {
assert.deepEqual(args.partner_ids, [2,3]);
readCount++;
return Promise.resolve([
{id: 2, im_status: 'away'},
@@ -85,7 +84,7 @@ QUnit.test('multi get_im_status', async function (assert) {
});
QUnit.test('update loop', async function (assert) {
assert.expect(12);
assert.expect(10);
var readCount = 0;
var parent = testUtils.createParent({
services: this.services,
@@ -93,9 +92,8 @@ QUnit.test('update loop', async function (assert) {
if (route === '/mail/init_messaging') {
return this._super.apply(this, arguments);
}
if (args.method === 'read' && args.model === 'res.partner') {
assert.deepEqual(args.args[0], [1, 2]);
assert.deepEqual(args.args[1], ['id', 'im_status']);
if (route === '/longpolling/im_status') {
assert.deepEqual(args.partner_ids, [1, 2]);
readCount++;
return Promise.resolve([
{"id": 1, "im_status": "online"},
+4 -1
View File
@@ -387,6 +387,7 @@ class MailComposer(models.TransientModel):
values = self.generate_email_for_composer(template_id, [res_id])[res_id]
# transform attachments into attachment_ids; not attached to the document because this will
# be done further in the posting process, allowing to clean database if email not send
attachment_ids = []
Attachment = self.env['ir.attachment']
for attach_fname, attach_datas in values.pop('attachments', []):
data_attach = {
@@ -397,7 +398,9 @@ class MailComposer(models.TransientModel):
'res_id': 0,
'type': 'binary', # override default_type from context, possibly meant for another model!
}
values.setdefault('attachment_ids', list()).append(Attachment.create(data_attach).id)
attachment_ids.append(Attachment.create(data_attach).id)
if values.get('attachment_ids', []) or attachment_ids:
values['attachment_ids'] = [(5,)] + values.get('attachment_ids', []) + attachment_ids
else:
default_values = self.with_context(default_composition_mode=composition_mode, default_model=model, default_res_id=res_id).default_get(['composition_mode', 'model', 'res_id', 'parent_id', 'partner_ids', 'subject', 'body', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'])
values = dict((key, default_values[key]) for key in ['subject', 'body', 'partner_ids', 'email_from', 'reply_to', 'attachment_ids', 'mail_server_id'] if key in default_values)
+1 -1
View File
@@ -457,7 +457,7 @@ class MassMailing(models.Model):
return res
active = fields.Boolean(default=True)
subject = fields.Char('Subject', help='Subject of emails to send', required=True)
subject = fields.Char('Subject', help='Subject of emails to send', required=True, translate=True)
email_from = fields.Char(string='From', required=True,
default=lambda self: self.env['mail.message']._get_default_from())
sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False)
+1 -1
View File
@@ -27,7 +27,7 @@ class TestMassMailing(models.TransientModel):
'email_from': mailing.email_from,
'reply_to': mailing.reply_to,
'email_to': test_mail,
'subject': mailing.name,
'subject': mailing.subject,
'body_html': mass_mail_layout.render({'body': body}, engine='ir.qweb', minimal_qcontext=True),
'notification': True,
'mailing_id': mailing.id,
+2
View File
@@ -487,6 +487,8 @@ class MrpProduction(models.Model):
}
def _generate_finished_moves(self):
if self.product_id in self.bom_id.byproduct_ids.mapped('product_id'):
raise UserError(_("You cannot have %s as the finished product and in the Byproducts") % self.product_id.name)
moves_values = [self._get_finished_move_value(self.product_id.id, self.product_qty, self.product_uom_id.id)]
for byproduct in self.bom_id.byproduct_ids:
product_uom_factor = self.product_uom_id._compute_quantity(self.product_qty, self.bom_id.product_uom_id)
+1 -1
View File
@@ -249,7 +249,7 @@ class StockWarehouse(models.Model):
@api.multi
def _get_all_routes(self):
routes = super(StockWarehouse, self).get_all_routes_for_wh()
routes = super(StockWarehouse, self)._get_all_routes()
routes |= self.filtered(lambda self: self.manufacture_to_resupply and self.manufacture_pull_id and self.manufacture_pull_id.route_id).mapped('manufacture_pull_id').mapped('route_id')
return routes
+9 -4
View File
@@ -2,6 +2,7 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import psycopg2
from odoo import http, _
from odoo.http import request
@@ -40,7 +41,7 @@ class PaymentProcessing(http.Controller):
# I prefer to let the controller chose when to access to payment.transaction using sudo
return request.session.get("__payment_tx_ids__", [])
@http.route(['/payment/process'], type="http", auth="public", website=True)
@http.route(['/payment/process'], type="http", auth="public", website=True, sitemap=False)
def payment_status_page(self, **kwargs):
# When the customer is redirect to this website page,
# we retrieve the payment transaction list from his session
@@ -93,11 +94,15 @@ class PaymentProcessing(http.Controller):
tx_to_process = payment_transaction_ids.filtered(lambda x: x.state == 'done' and x.is_processed is False)
try:
tx_to_process._post_process_after_done()
except psycopg2.OperationalError as e:
request.env.cr.rollback()
result['success'] = False
result['error'] = "tx_process_retry"
except Exception as e:
request.env.cr.rollback()
result['success'] = False
result['error'] = str(e)
_logger.error("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e))
_logger.exception("Error while processing transaction(s) %s, exception \"%s\"", tx_to_process.ids, str(e))
return result
@@ -122,7 +127,7 @@ class WebsitePayment(http.Controller):
}
return request.render("payment.pay_methods", values)
@http.route(['/website_payment/pay'], type='http', auth='public', website=True)
@http.route(['/website_payment/pay'], type='http', auth='public', website=True, sitemap=False)
def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, **kw):
env = request.env
user = env.user.sudo()
@@ -262,7 +267,7 @@ class WebsitePayment(http.Controller):
except Exception as e:
return request.redirect('/payment/process')
@http.route(['/website_payment/confirm'], type='http', auth='public', website=True)
@http.route(['/website_payment/confirm'], type='http', auth='public', website=True, sitemap=False)
def confirm(self, **kw):
tx_id = int(kw.get('tx_id', 0))
if tx_id:
+1 -1
View File
@@ -899,7 +899,7 @@ class PaymentTransaction(models.Model):
# custom create
custom_method_name = '%s_create' % acquirer.provider
if hasattr(acquirer, custom_method_name):
if hasattr(self, custom_method_name):
values.update(getattr(self, custom_method_name)(values))
if not values.get('reference'):
@@ -54,6 +54,8 @@ odoo.define('payment.processing', function (require) {
}
else {
switch(data.error) {
case "tx_process_retry":
break;
case "no_tx_found":
self.displayContent("payment.no_tx_found", {});
break;
@@ -63,18 +65,12 @@ odoo.define('payment.processing', function (require) {
}
}
self.startPolling();
}).then(function() {
if ($.blockUI) {
$.unblockUI();
}
}).guardedCatch(function() {
if ($.blockUI) {
$.unblockUI();
}
self.displayContent("payment.rpc_error", {});
self.startPolling();
});
},
},g
processPolledData: function (transactions) {
var render_values = {
'tx_draft': [],
@@ -120,6 +116,7 @@ odoo.define('payment.processing', function (require) {
},
displayContent: function (xmlid, render_values) {
var html = Qweb.render(xmlid, render_values);
$.unblockUI();
this.$el.find('.o_payment_processing_content').html(html);
},
displayLoading: function () {
@@ -226,6 +226,12 @@ class AuthorizeAPI():
etree.SubElement(customer, "email").text = partner.email or ''
response = self._authorize_request(root)
res = dict()
if not response.find('customerProfileId'):
_logger.warning(
'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s'
% (transaction_id, partner)
)
return res
res['profile_id'] = response.find('customerProfileId').text
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
root_profile = self._base_tree('getCustomerPaymentProfileRequest')
+11 -10
View File
@@ -228,17 +228,18 @@ class TxAuthorize(models.Model):
(self.type == 'form_save' or self.acquirer_id.save_token == 'always'):
transaction = AuthorizeAPI(self.acquirer_id)
res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference)
token_id = self.env['payment.token'].create({
'authorize_profile': res.get('profile_id'),
'name': res.get('name'),
'acquirer_ref': res.get('payment_profile_id'),
'acquirer_id': self.acquirer_id.id,
'partner_id': self.partner_id.id,
})
self.payment_token_id = token_id
if res:
token_id = self.env['payment.token'].create({
'authorize_profile': res.get('profile_id'),
'name': res.get('name'),
'acquirer_ref': res.get('payment_profile_id'),
'acquirer_id': self.acquirer_id.id,
'partner_id': self.partner_id.id,
})
self.payment_token_id = token_id
if self.payment_token_id:
self.payment_token_id.verified = True
if self.payment_token_id:
self.payment_token_id.verified = True
return True
elif status_code == self._authorize_pending_tx_status:
self.write({'acquirer_reference': data.get('x_trans_id')})
+1 -1
View File
@@ -891,7 +891,7 @@ class PosOrder(models.Model):
# a serialnumber always has a quantity of 1 product, a lot number takes the full quantity of the order line
qty = 1.0
if stock_production_lot.product_id.tracking == 'lot':
qty = pos_pack_lot.pos_order_line_id.qty
qty = abs(pos_pack_lot.pos_order_line_id.qty)
qty_done += qty
pack_lots.append({'lot_id': stock_production_lot.id, 'qty': qty})
else:
+4 -1
View File
@@ -405,7 +405,7 @@ var Gui = core.Class.extend({
}
} else if (input === '-') {
if (options.firstinput) {
newbuf = '-';
newbuf = '-0';
} else if ( newbuf[0] === '-' ) {
newbuf = newbuf.substring(1,newbuf.length);
} else {
@@ -420,6 +420,9 @@ var Gui = core.Class.extend({
newbuf += input;
}
}
if (newbuf === "-") {
newbuf = "";
}
// End of input buffer at 12 characters.
if (newbuf.length > buffer.length && newbuf.length > 12) {
+6 -3
View File
@@ -1450,7 +1450,7 @@ exports.Orderline = Backbone.Model.extend({
var lots_required = 1;
if (this.product.tracking == 'serial') {
lots_required = this.quantity;
lots_required = Math.abs(this.quantity);
}
return lots_required;
@@ -1998,8 +1998,11 @@ var PacklotlineCollection = Backbone.Collection.extend({
set_quantity_by_lot: function() {
if (this.order_line.product.tracking == 'serial') {
var valid_lots = this.get_valid_lots();
this.order_line.set_quantity(valid_lots.length);
var valid_lots_quantity = this.get_valid_lots().length;
if (this.order_line.quantity < 0){
valid_lots_quantity = -valid_lots_quantity;
}
this.order_line.set_quantity(valid_lots_quantity);
}
}
});
+1 -1
View File
@@ -990,7 +990,7 @@
<line t-foreach='taxes' t-as='taxe'>
<left><t t-esc='taxe.name' /></left>
<right>
<t t-esc='widget.format_currency_no_symbol(taxe.total)'/>
<t t-esc='widget.format_currency_no_symbol(taxe.tax_amount)'/>
</right>
</line>
@@ -47,7 +47,6 @@
<field name="arch" type="xml">
<kanban create="false" class="o_kanban_dashboard o_pos_kanban">
<field name="name"/>
<field name="session_ids"/>
<field name="current_session_id"/>
<field name="current_session_state"/>
<field name="last_session_closing_date"/>
+1 -1
View File
@@ -137,7 +137,7 @@ class PortalChatter(http.Controller):
raise Forbidden()
# Non-employee see only messages with not internal subtype (aka, no internal logs)
if not request.env['res.users'].has_group('base.group_user'):
domain = expression.AND([['&', '&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False), ('website_published', '=', True)], domain])
domain = expression.AND([Message._non_employee_message_domain(), domain])
Message = request.env['mail.message'].sudo()
return {
'messages': Message.search(domain, limit=limit, offset=offset).portal_message_format(),
+4
View File
@@ -21,3 +21,7 @@ class MailMessage(models.Model):
message_tree = dict((m.id, m) for m in self.sudo())
self._message_read_dict_postprocess(message_values, message_tree)
return message_values
@api.model
def _non_employee_message_domain(self):
return ['&', ('subtype_id', '!=', False), ('subtype_id.internal', '=', False)]
+4 -11
View File
@@ -14,11 +14,6 @@ class SaleReport(models.Model):
('invoiced', 'Invoiced')], string='Status', readonly=True)
def _query(self, with_clause='', fields={}, groupby='', from_clause=''):
with_clause += '''pol_tax AS (SELECT line_tax.pos_order_line_id AS pol_id,sum(tax.amount/100) AS amount
FROM account_tax_pos_order_line_rel line_tax
JOIN account_tax tax ON line_tax.account_tax_id = tax.id
GROUP BY line_tax.pos_order_line_id)'''
res = super(SaleReport, self)._query(with_clause, fields, groupby, from_clause)
select_ = '''
@@ -29,10 +24,10 @@ class SaleReport(models.Model):
sum(l.qty * u.factor) AS qty_delivered,
CASE WHEN pos.state = 'invoiced' THEN sum(qty) ELSE 0 END AS qty_invoiced,
CASE WHEN pos.state != 'invoiced' THEN sum(qty) ELSE 0 END AS qty_to_invoice,
(COALESCE(COALESCE((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) )) + (l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) ) * pol_tax.amount), (l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) ))), 1.0)) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_total,
(COALESCE((l.qty * u.factor * l.price_unit * ((1 - COALESCE(l.discount / 100, 0)) )), 1.0)) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_subtotal,
(CASE WHEN pos.state != 'invoiced' THEN ((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)))) + (l.price_unit * l.qty * u.factor * (1 - COALESCE(l.discount / 100, 0)) * pol_tax.amount)) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_to_invoice,
(CASE WHEN pos.state = 'invoiced' THEN ((l.price_unit * l.qty * u.factor * ((1 - COALESCE(l.discount / 100, 0)) )) + (l.price_unit * l.qty * u.factor * (1 - COALESCE(l.discount / 100, 0)) * pol_tax.amount)) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_invoiced,
SUM(l.price_subtotal_incl) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_total,
SUM(l.price_subtotal) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS price_subtotal,
(CASE WHEN pos.state != 'invoiced' THEN SUM(l.price_subtotal_incl) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_to_invoice,
(CASE WHEN pos.state = 'invoiced' THEN SUM(l.price_subtotal_incl) ELSE 0 END) / MIN(CASE COALESCE(pos.currency_rate, 0) WHEN 0 THEN 1.0 ELSE pos.currency_rate END) AS amount_invoiced,
count(*) AS nbr,
pos.name AS name,
pos.date_order AS date,
@@ -70,7 +65,6 @@ class SaleReport(models.Model):
pos_order_line l
join pos_order pos on (l.order_id=pos.id)
left join res_partner partner ON (pos.partner_id = partner.id OR pos.partner_id = NULL)
left join pol_tax pol_tax on pol_tax.pol_id=l.id
left join product_product p on (l.product_id=p.id)
left join product_template t on (p.product_tmpl_id=t.id)
LEFT JOIN uom_uom u ON (u.id=t.uom_id)
@@ -98,7 +92,6 @@ class SaleReport(models.Model):
partner.country_id,
partner.industry_id,
partner.commercial_partner_id,
pol_tax.amount,
u.factor,
config.crm_team_id
'''
+11
View File
@@ -196,6 +196,7 @@ class ProductProduct(models.Model):
('active', '=', True),
]) <= 1
):
record.image_raw_original = False
record.product_tmpl_id.image_original = record.image_original
else:
record.image_raw_original = record.image_original
@@ -391,6 +392,16 @@ class ProductProduct(models.Model):
product._set_standard_price(vals.get('standard_price') or 0.0)
# `_get_variant_id_for_combination` depends on existing variants
self.clear_caches()
self.env['product.template'].invalidate_cache(
fnames=[
'valid_archived_variant_ids',
'valid_existing_variant_ids',
'product_variant_ids',
'product_variant_id',
'product_variant_count'
],
ids=products.mapped('product_tmpl_id').ids
)
return products
@api.multi
+33 -33
View File
@@ -135,8 +135,10 @@ class ProductTemplate(models.Model):
compute="_compute_valid_attributes", string='Valid Product Attribute Values Without No Variant Attributes', help="Technical compute")
valid_product_attribute_wnva_ids = fields.Many2many('product.attribute',
compute="_compute_valid_attributes", string='Valid Product Attributes Without No Variant Attributes', help="Technical compute")
# valid_archived_variant_ids deprecated
valid_archived_variant_ids = fields.Many2many('product.product',
compute="_compute_valid_attributes", string='Valid Archived Variants', help="Technical compute")
compute="_compute_valid_archived_variant_ids", string='Valid Archived Variants', help="Technical compute")
# valid_existing_variant_ids deprecated
valid_existing_variant_ids = fields.Many2many('product.product',
compute="_compute_valid_existing_variant_ids", string='Valid Existing Variants', help="Technical compute")
@@ -606,8 +608,6 @@ class ProductTemplate(models.Model):
For what is considered an archived variant, see `_has_valid_attributes`.
"""
archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)])
# prefetch
self.mapped('attribute_line_ids.value_ids.id')
self.mapped('attribute_line_ids.attribute_id.create_variant')
@@ -622,6 +622,12 @@ class ProductTemplate(models.Model):
record.valid_product_attribute_ids = record.valid_product_template_attribute_line_ids.mapped('attribute_id')
record.valid_product_attribute_wnva_ids = record.valid_product_template_attribute_line_wnva_ids.mapped('attribute_id')
@api.multi
def _compute_valid_archived_variant_ids(self):
"""This compute is done outside of `_compute_valid_attributes` because
it is often not needed, and it can be very bad on performance."""
archived_variants = self.env['product.product'].search([('product_tmpl_id', 'in', self.ids), ('active', '=', False)])
for record in self:
valid_value_ids = record.valid_product_attribute_value_wnva_ids
valid_attribute_ids = record.valid_product_attribute_wnva_ids
@@ -699,14 +705,12 @@ class ProductTemplate(models.Model):
:return: dict of exclusions
- exclusions: from this product itself
- parent_combination: ids of the given parent_combination
- parent_exclusions: from the parent_combination
- archived_combinations: variants that are archived
- existing_combinations: variants that are existing (as opposed to
deleted, because deleted need to be considered impossible if
there are no dynamic attributes).
- has_dynamic_attributes: whether there is a dynamic attribute
- no_variant_product_template_attribute_value_ids: values that are
no_variant
- archived_combinations: deprecated
- existing_combinations: deprecated
- has_dynamic_attributes: deprecated
- no_variant_product_template_attribute_value_ids: deprecated
- parent_product_name: the name of the parent product if any, used in the interface
to explain why some combinations are not available.
(e.g: Not available with Customizable Desk (Legs: Steel))
@@ -715,13 +719,15 @@ class ProductTemplate(models.Model):
(e.g: Not available with Color: Black)
"""
self.ensure_one()
parent_combination = parent_combination or self.env['product.template.attribute.value']
return {
'exclusions': self._complete_inverse_exclusions(self._get_own_attribute_exclusions()),
'parent_exclusions': self._get_parent_attribute_exclusions(parent_combination),
'archived_combinations': self._get_archived_combinations(),
'parent_combination': parent_combination.ids,
'archived_combinations': [],
'has_dynamic_attributes': self.has_dynamic_attributes(),
'existing_combinations': self._get_existing_combinations(),
'no_variant_product_template_attribute_value_ids': self._get_no_variant_product_template_attribute_values(),
'existing_combinations': [],
'no_variant_product_template_attribute_value_ids': [],
'parent_product_name': parent_name,
'mapped_attribute_names': self._get_mapped_attribute_names(parent_combination),
}
@@ -789,29 +795,21 @@ class ProductTemplate(models.Model):
@api.multi
def _get_archived_combinations(self):
"""Get archived combinations.
Array, each element is an array with ids of an archived combination.
"""
"""Deprecated"""
self.ensure_one()
return [archived_variant.product_template_attribute_value_ids.ids
for archived_variant in self.valid_archived_variant_ids]
@api.multi
def _get_existing_combinations(self):
"""Get existing combinations.
Needed because when not using dynamic attributes, the combination is
not ok if it doesn't exist (= if the variant has been deleted).
Array, each element is an array with ids of an existing combination.
"""
"""Deprecated"""
self.ensure_one()
return [variant.product_template_attribute_value_ids.ids
for variant in self.valid_existing_variant_ids]
@api.multi
def _get_no_variant_product_template_attribute_values(self):
"""Deprecated"""
self.ensure_one()
product_template_attribute_values = self._get_valid_product_template_attribute_lines().mapped('product_template_value_ids')
return product_template_attribute_values.filtered(
@@ -876,9 +874,16 @@ class ProductTemplate(models.Model):
# combination has different values than the ones configured on the template
return False
if not self.has_dynamic_attributes() and not self._get_variant_for_combination(combination):
# the variant has been deleted
return False
variant = self._get_variant_for_combination(combination)
if self.has_dynamic_attributes():
if variant and not variant.active:
# dynamic and the variant has been archived
return False
else:
if not variant or not variant.active:
# not dynamic, the variant has been archived or deleted
return False
exclusions = self._get_own_attribute_exclusions()
if exclusions:
@@ -898,11 +903,6 @@ class ProductTemplate(models.Model):
if exclusion in combination.ids:
return False
filtered_combination = combination._without_no_variant_attributes()
archived_combinations = self._get_archived_combinations()
if archived_combinations and filtered_combination.ids in archived_combinations:
return False
return True
@api.multi
@@ -936,7 +936,7 @@ class ProductTemplate(models.Model):
for pav in attribute_values:
domain = expression.AND([[('attribute_value_ids', 'in', pav.id)], domain])
res = self.env['product.product'].with_context(active_test=False).search(domain)
res = self.env['product.product'].with_context(active_test=False).search(domain, order='active DESC')
# The domain above is checking for the `product.attribute.value`, but we
# need to make sure it's the same `product.template.attribute.value`.
@@ -312,6 +312,21 @@ class TestProductAttributeValueConfig(TestProductAttributeValueSetup):
variant.unlink()
self.assertFalse(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
# CASE: if multiple variants exist for the same combination and at least
# one of them is not archived, the combination is possible
values = self.ssd_256 + self.ram_8 + self.hdd_1
self.env['product.product'].create({
'product_tmpl_id': self.computer.id,
'attribute_value_ids': [(6, 0, values.ids)],
'active': False,
})
self.env['product.product'].create({
'product_tmpl_id': self.computer.id,
'attribute_value_ids': [(6, 0, values.ids)],
'active': True,
})
self.assertTrue(self.computer._is_combination_possible(computer_ssd_256 + computer_ram_8 + computer_hdd_1))
def test_get_first_possible_combination(self):
computer_ssd_256 = self._get_product_template_attribute_value(self.ssd_256)
computer_ssd_512 = self._get_product_template_attribute_value(self.ssd_512)
+73
View File
@@ -1,6 +1,10 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import io
from PIL import Image
from . import common
from odoo.exceptions import UserError
from odoo.tests.common import TransactionCase
@@ -461,3 +465,72 @@ class TestVariantsManyAttributes(common.TestAttributesCommon):
self.assertEqual(len(toto.attribute_line_ids.mapped('attribute_id')), 10)
self.assertEqual(len(toto.attribute_line_ids.mapped('value_ids')), 100)
self.assertEqual(len(toto.product_variant_ids), 0)
class TestVariantsImages(common.TestProductCommon):
def setUp(self):
res = super(TestVariantsImages, self).setUp()
self.colors = {'red': '#FF0000', 'green': '#00FF00', 'blue': '#0000FF'}
self.images = {}
product_attribute = self.env['product.attribute'].create({'name': 'Color'})
self.template = self.env['product.template'].create({
'name': 'template',
})
for color in self.colors:
attr = self.env['product.attribute.value'].create({
'name': color,
'attribute_id': product_attribute.id,
})
f = io.BytesIO()
Image.new('RGB', (800, 500), self.colors[color]).save(f, 'PNG')
f.seek(0)
self.images.update({color: base64.b64encode(f.read())})
self.env['product.product'].create({
'image_raw_original': self.images[color],
'attribute_value_ids': [(6, 0, [attr.id])],
'product_tmpl_id': self.template.id,
})
# the first one has no image, no color
self.variants = self.template.product_variant_ids.sorted('id')
return res
def test_variant_images(self):
"""Check that on variant, the image used is the image_raw_original if set,
and defaults to the template image otherwise.
"""
f = io.BytesIO()
Image.new('RGB', (800, 500), '#000000').save(f, 'PNG')
f.seek(0)
image_black = base64.b64encode(f.read())
images = self.variants.mapped('image_original')
self.assertEqual(len(set(images)), 4)
variant_no_image = self.variants[0]
self.assertFalse(variant_no_image.image)
self.template.image = image_black
# the first has no image variant, all the others do
self.assertFalse(variant_no_image.image_raw_original)
self.assertTrue(all(images[1:]))
# template image is the same as this one, since it has no image variant
self.assertEqual(variant_no_image.image, self.template.image)
# having changed the template image should not have changed these
self.assertEqual(images[1:], self.variants.mapped('image')[1:])
def test_update_images_with_archived_variants(self):
"""Update images after variants have been archived"""
self.variants[1:].write({'active': False})
self.variants[0].image = self.images['red']
self.assertEqual(self.template.image, self.images['red'])
self.assertEqual(self.variants[0].image_raw_original, False)
self.assertEqual(self.variants[0].image, self.images['red'])
@@ -11,4 +11,6 @@ class ProductTemplate(models.Model):
_inherit = "product.template"
email_template_id = fields.Many2one('mail.template', string='Product Email Template',
help='Send a product-specific email once the invoice is paid.')
help='When validating an invoice, an email will be sent to the customer '
'based on this template. The customer will receive an email for each '
'product linked to an email template.')
@@ -10,7 +10,7 @@
<attribute name="invisible">0</attribute>
</xpath>
<xpath expr="//group[@name='email_template_and_project']" position="inside">
<field name="email_template_id" string="Email Template" help="Send a product-specific email once the invoice is paid"
<field name="email_template_id" string="Email Template" help="Send a product-specific email once the invoice is validated"
domain="[('model','=','product.template')]"
context="{
'form_view_ref':'product_email_template.email_template_form_simplified',
@@ -103,11 +103,6 @@ class AccountInvoice(models.Model):
#as there's no invoice line yet, we keep the currency of the PO
self.currency_id = self.purchase_id.currency_id
vendor_ref = self.purchase_id.partner_ref
if vendor_ref:
self.reference = ", ".join([self.reference, vendor_ref]) if (
self.reference and vendor_ref not in self.reference) else vendor_ref
new_lines = self.env['account.invoice.line']
for line in self.purchase_id.order_line - self.invoice_line_ids.mapped('purchase_line_id'):
data = self._prepare_invoice_line_from_po_line(line)
+21 -18
View File
@@ -96,27 +96,27 @@ class AccountInvoice(models.Model):
interim_account_price = valuation_price_unit * line['quantity']
invoice_cur_prec = inv.currency_id.decimal_places
if float_compare(valuation_price_unit, i_line.price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0:
# price with discount and without tax included
price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0)
tax_ids = []
if line['tax_ids']:
#line['tax_ids'] is -2many commands list for account.move.line
tax_ids = [tax_data[id] for tax_data in self.env['account.move.line'].resolve_2many_commands('tax_ids', line['tax_ids'], ['id'])]
taxes = self.env['account.tax'].browse(tax_ids)
price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0, is_refund=inv.type in ('in_refund', 'out_refund'))['total_excluded']
# price with discount and without tax included
price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0)
tax_ids = []
if line['tax_ids']:
#line['tax_ids'] is -2many commands list for account.move.line
tax_ids = [tax_data[id] for tax_data in self.env['account.move.line'].resolve_2many_commands('tax_ids', line['tax_ids'], ['id'])]
taxes = self.env['account.tax'].browse(tax_ids)
price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0, is_refund=inv.type in ('in_refund', 'out_refund'))['total_excluded']
for tax in taxes:
tax_ids.append((4, tax.id, None))
for child in tax.children_tax_ids:
if child.type_tax_use != 'none':
tax_ids.append((4, child.id, None))
for tax in taxes:
tax_ids.append((4, tax.id, None))
for child in tax.children_tax_ids:
if child.type_tax_use != 'none':
tax_ids.append((4, child.id, None))
if float_compare(valuation_price_unit, price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0:
price_before = line.get('price', 0.0)
price_unit_val_dif = price_unit - valuation_price_unit
price_val_dif = price_before - interim_account_price
if inv.currency_id.compare_amounts(i_line.price_unit, valuation_price_unit) != 0 and acc:
if inv.currency_id.compare_amounts(price_unit, valuation_price_unit) != 0 and acc:
# If the unit prices have not changed and we have a
# valuation difference, it means this difference is due to exchange rates,
# so we don't create anything, the exchange rate entries will
@@ -134,9 +134,12 @@ class AccountInvoice(models.Model):
'tax_ids': tax_ids,
}
# We update the original line accordingly
line['price_unit'] = line['price_unit'] - diff_line['price_unit']
line['price'] = inv.currency_id.round(line['quantity'] * line['price_unit'])
line['price_unit'] = inv.currency_id.round(line['price_unit'])
# line['price_unit'] doesn't contain the discount, so use price_unit
# instead. It could make sense to include the discount in line['price_unit'],
# but that doesn't seem a good idea in stable since it is done in
# "invoice_line_move_line_get" of "account.invoice".
line['price_unit'] = inv.currency_id.round(price_unit - diff_line['price_unit'])
line['price'] = inv.currency_id.round(line['price'] - diff_line['price'])
diff_res.append(diff_line)
return diff_res
return []
+3 -1
View File
@@ -344,7 +344,9 @@ class PurchaseOrderLine(models.Model):
for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
qty += move.product_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
template = {
'name': self.name or '',
# truncate to 2000 to avoid triggering index limit error
# TODO: remove index in master?
'name': (self.name or '')[:2000],
'product_id': self.product_id.id,
'product_uom': self.product_uom.id,
'date': self.order_id.date_order,
+1 -1
View File
@@ -132,7 +132,7 @@ class StockWarehouse(models.Model):
@api.multi
def _get_all_routes(self):
routes = super(StockWarehouse, self).get_all_routes_for_wh()
routes = super(StockWarehouse, self)._get_all_routes()
routes |= self.filtered(lambda self: self.buy_to_resupply and self.buy_pull_id and self.buy_pull_id.route_id).mapped('buy_pull_id').mapped('route_id')
return routes
@@ -1165,3 +1165,151 @@ class TestStockValuationWithCOA(AccountingTestCase):
exchange_stock_line = reconciled_lines.filtered(lambda l: l.journal_id == exhange_diff_journal)
self.assertEqual(exchange_stock_line.amount_currency, 0.00)
self.assertAlmostEqual(exchange_stock_line.balance, 119.32)
def test_anglosaxon_valuation_price_total_diff_discount(self):
"""
PO: price unit: 110
Inv: price unit: 100
discount: 10
"""
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.categ_id.property_cost_method = 'fifo'
self.product1.categ_id.property_valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
self.product1.property_account_creditor_price_difference = self.price_diff_account
# Create PO
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.partner_id
with po_form.order_line.new() as po_line:
po_line.product_id = self.product1
po_line.product_qty = 1
po_line.price_unit = 110.0
order = po_form.save()
order.button_confirm()
# Receive the goods
receipt = order.picking_ids[0]
receipt.move_lines.quantity_done = 1
receipt.button_validate()
# Create an invoice with a different price and a discount
invoice = self.env['account.invoice'].create({
'partner_id': order.partner_id.id,
'purchase_id': order.id,
'account_id': order.partner_id.property_account_payable_id.id,
'type': 'in_invoice',
})
invoice.purchase_order_change()
invoice.invoice_line_ids[0].price_unit = 100.0
invoice.invoice_line_ids[0].discount = 10.0
invoice.action_invoice_open()
# Check what was posted in the price difference account
price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
self.assertAlmostEquals(price_diff_aml.credit, 20, "Price difference should be equal to 20 (110-90)")
# Check what was posted in stock input account
input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
self.assertAlmostEquals(sum(input_aml.mapped('debit')), 110, "Total debit value on stock input account should be equal to the original PO price of the product.")
self.assertAlmostEquals(sum(input_aml.mapped('credit')), 110, "Total credit value on stock input account should be equal to the original PO price of the product.")
def test_anglosaxon_valuation_discount(self):
"""
PO: price unit: 100
Inv: price unit: 100
discount: 10
"""
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.categ_id.property_cost_method = 'fifo'
self.product1.categ_id.property_valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
self.product1.property_account_creditor_price_difference = self.price_diff_account
# Create PO
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.partner_id
with po_form.order_line.new() as po_line:
po_line.product_id = self.product1
po_line.product_qty = 1
po_line.price_unit = 100.0
order = po_form.save()
order.button_confirm()
# Receive the goods
receipt = order.picking_ids[0]
receipt.move_lines.quantity_done = 1
receipt.button_validate()
# Create an invoice with a different price and a discount
invoice = self.env['account.invoice'].create({
'partner_id': order.partner_id.id,
'purchase_id': order.id,
'account_id': order.partner_id.property_account_payable_id.id,
'type': 'in_invoice',
})
invoice.purchase_order_change()
invoice.invoice_line_ids[0].discount = 10.0
invoice.action_invoice_open()
# Check what was posted in the price difference account
price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
self.assertEquals(len(price_diff_aml), 1, "Only one line should have been generated in the price difference account.")
self.assertAlmostEquals(price_diff_aml.credit, 10, "Price difference should be equal to 20 (110-90)")
# Check what was posted in stock input account
input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
self.assertAlmostEquals(sum(input_aml.mapped('debit')), 100, "Total debit value on stock input account should be equal to the original PO price of the product.")
self.assertAlmostEquals(sum(input_aml.mapped('credit')), 100, "Total credit value on stock input account should be equal to the original PO price of the product.")
def test_anglosaxon_valuation_price_unit_diff_discount(self):
"""
PO: price unit: 90
Inv: price unit: 100
discount: 10
"""
self.env.user.company_id.anglo_saxon_accounting = True
self.product1.categ_id.property_cost_method = 'fifo'
self.product1.categ_id.property_valuation = 'real_time'
self.product1.product_tmpl_id.invoice_policy = 'delivery'
self.product1.property_account_creditor_price_difference = self.price_diff_account
# Create PO
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.partner_id
with po_form.order_line.new() as po_line:
po_line.product_id = self.product1
po_line.product_qty = 1
po_line.price_unit = 90.0
order = po_form.save()
order.button_confirm()
# Receive the goods
receipt = order.picking_ids[0]
receipt.move_lines.quantity_done = 1
receipt.button_validate()
# Create an invoice with a different price and a discount
invoice = self.env['account.invoice'].create({
'partner_id': order.partner_id.id,
'purchase_id': order.id,
'account_id': order.partner_id.property_account_payable_id.id,
'type': 'in_invoice',
})
invoice.purchase_order_change()
invoice.invoice_line_ids[0].price_unit = 100.0
invoice.invoice_line_ids[0].discount = 10.0
invoice.action_invoice_open()
# Check if something was posted in the price difference account
price_diff_aml = self.env['account.move.line'].search([('account_id','=', self.price_diff_account.id)])
self.assertEquals(len(price_diff_aml), 0, "No line should have been generated in the price difference account.")
# Check what was posted in stock input account
input_aml = self.env['account.move.line'].search([('account_id','=', self.stock_input_account.id)])
self.assertEquals(len(input_aml), 2, "Only two lines should have been generated in stock input account: one when receiving the product, one when making the invoice.")
self.assertAlmostEquals(sum(input_aml.mapped('debit')), 90, "Total debit value on stock input account should be equal to the original PO price of the product.")
self.assertAlmostEquals(sum(input_aml.mapped('credit')), 90, "Total credit value on stock input account should be equal to the original PO price of the product.")
+12 -1
View File
@@ -13,7 +13,18 @@ class VariantController(http.Controller):
ProductTemplate = request.env['product.template']
if 'context' in kw:
ProductTemplate = ProductTemplate.with_context(**kw.get('context'))
return ProductTemplate.browse(int(product_template_id))._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist)
product_template = ProductTemplate.browse(int(product_template_id))
res = product_template._get_combination_info(combination, int(product_id or 0), int(add_qty or 1), pricelist)
if 'parent_combination' in kw:
parent_combination = request.env['product.template.attribute.value'].browse(kw.get('parent_combination'))
if not combination.exists() and product_id:
product = request.env['product.product'].browse(int(product_id))
if product.exists():
combination = product.product_template_attribute_value_ids
res.update({
'is_combination_possible': product_template._is_combination_possible(combination=combination, parent_combination=parent_combination),
})
return res
@http.route(['/sale/create_product_variant'], type='json', auth="user", methods=['POST'])
def create_product_variant(self, product_template_id, product_template_attribute_value_ids, **kwargs):
+1 -1
View File
@@ -43,7 +43,7 @@ class AccountInvoice(models.Model):
inv_type = self.type or self.env.context.get('type', 'out_invoice')
if inv_type == 'out_invoice':
company = self.company_id or self.env.user.company_id
self.comment = company.with_context(lang=self.partner_id.lang).invoice_terms
self.comment = company.with_context(lang=self.partner_id.lang).invoice_terms or (self._origin.company_id == company and self.comment)
@api.multi
def action_invoice_open(self):
+6 -6
View File
@@ -96,11 +96,11 @@ class PaymentTransaction(models.Model):
if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
default_template = self.env['ir.config_parameter'].sudo().get_param('sale.default_email_template')
if default_template:
ctx_company = {'company_id': self.acquirer_id.company_id.id,
'force_company': self.acquirer_id.company_id.id,
'mark_invoice_as_sent': True,
}
for trans in self.filtered(lambda t: t.sale_order_ids):
ctx_company = {'company_id': trans.acquirer_id.company_id.id,
'force_company': trans.acquirer_id.company_id.id,
'mark_invoice_as_sent': True,
}
trans = trans.with_context(ctx_company)
for invoice in trans.invoice_ids:
invoice.message_post_with_template(int(default_template), notif_layout="mail.mail_notification_paynow")
@@ -109,9 +109,9 @@ class PaymentTransaction(models.Model):
@api.multi
def _invoice_sale_orders(self):
if self.env['ir.config_parameter'].sudo().get_param('sale.automatic_invoice'):
ctx_company = {'company_id': self.acquirer_id.company_id.id,
'force_company': self.acquirer_id.company_id.id}
for trans in self.filtered(lambda t: t.sale_order_ids):
ctx_company = {'company_id': trans.acquirer_id.company_id.id,
'force_company': trans.acquirer_id.company_id.id}
trans = trans.with_context(**ctx_company)
trans.sale_order_ids._force_lines_to_invoice_policy_order()
invoices = trans.sale_order_ids._create_invoices()
+2 -2
View File
@@ -1266,7 +1266,7 @@ class SaleOrderLine(models.Model):
result.setdefault(so_line_id, 0.0)
uom = product_uom_map.get(item['product_uom_id'][0])
if so_line.product_uom.category_id == uom.category_id:
qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom)
qty = uom._compute_quantity(item['unit_amount'], so_line.product_uom, rounding_method='HALF-UP')
else:
qty = item['unit_amount']
result[so_line_id] += qty
@@ -1620,7 +1620,7 @@ class SaleOrderLine(models.Model):
new_list_price, self.order_id.pricelist_id.currency_id,
self.order_id.company_id, self.order_id.date_order or fields.Date.today())
discount = (new_list_price - price) / new_list_price * 100
if discount > 0:
if (discount > 0 and new_list_price > 0) or (discount < 0 and new_list_price < 0):
self.discount = discount
def _is_delivery(self):
@@ -52,7 +52,7 @@ class PosSaleReport(models.Model):
LEFT JOIN product_pricelist pp ON (so.pricelist_id = pp.id)
LEFT JOIN uom_uom u on (u.id=sol.product_uom)
LEFT JOIN uom_uom u2 on (u2.id=pt.uom_id)
WHERE so.state != 'cancel'
WHERE so.state in ('sale','done')
"""
return so_str
+77 -71
View File
@@ -1,6 +1,7 @@
odoo.define('sale.VariantMixin', function (require) {
'use strict';
var concurrency = require('web.concurrency');
var core = require('web.core');
var utils = require('web.utils');
var ajax = require('web.ajax');
@@ -8,9 +9,8 @@ var _t = core._t;
var VariantMixin = {
events: {
'click .css_attribute_color input': '_onChangeColorAttribute',
'change .css_attribute_color input': '_onChangeColorAttribute',
'change .main_product:not(.in_cart) input.js_quantity': 'onChangeAddQuantity',
'click button.js_add_cart_json': 'onClickAddCartJSON',
'change [data-attribute_exclusions]': 'onChangeVariant'
},
@@ -36,6 +36,20 @@ var VariantMixin = {
* @param {$.Element} [params.$container] force the used container
*/
onChangeVariant: function (ev, params) {
var $parent = $(ev.target).closest('.js_product');
if (!$parent.data('uniqueId')) {
$parent.data('uniqueId', _.uniqueId());
}
this._throttledGetCombinationInfo($parent.data('uniqueId'))(ev);
},
/**
* @see onChangeVariant
*
* @private
* @param {Event} ev
* @returns {Deferred}
*/
_getCombinationInfo: function (ev) {
var self = this;
if ($(ev.target).hasClass('variant_custom_value')) {
@@ -57,16 +71,21 @@ var VariantMixin = {
var qty = $component.find('input[name="add_qty"]').val();
var $parent = $(ev.target).closest('.js_product');
var combination = this.getSelectedVariantValues($parent);
var parentCombination = $parent.find('ul[data-attribute_exclusions]').data('attribute_exclusions').parent_combination;
var productTemplateId = parseInt($parent.find('.product_template_id').val());
self._checkExclusions($parent, combination);
ajax.jsonRpc(this._getUri('/sale/get_combination_info'), 'call', {
product_template_id: parseInt($parent.find('.product_template_id').val()),
product_id: this._getProductId($parent),
combination: combination,
add_qty: parseInt(qty),
pricelist_id: this.pricelistId || false,
'product_template_id': productTemplateId,
'product_id': this._getProductId($parent),
'combination': combination,
'add_qty': parseInt(qty),
'pricelist_id': this.pricelistId || false,
'parent_combination': parentCombination,
}).then(function (combinationData) {
self._onChangeCombination(ev, $parent, combinationData);
});
@@ -138,10 +157,13 @@ var VariantMixin = {
var $input = $link.closest('.input-group').find("input");
var min = parseFloat($input.data("min") || 0);
var max = parseFloat($input.data("max") || Infinity);
var quantity = ($link.has(".fa-minus").length ? -1 : 1) + parseFloat($input.val() || 0, 10);
var previousQty = parseFloat($input.val() || 0, 10);
var quantity = ($link.has(".fa-minus").length ? -1 : 1) + previousQty;
var newQty = quantity > min ? (quantity < max ? quantity : max) : min;
$input.val(newQty).trigger('change');
if (newQty !== previousQty) {
$input.val(newQty).trigger('change');
}
return false;
},
@@ -332,23 +354,6 @@ var VariantMixin = {
* @param {Array} combination the selected combination of product attribute values
*/
_checkExclusions: function ($parent, combination) {
function areCombinationsEqual(c1, c2) {
return c1.length === c2.length && _.every(c1, function (ptav) {
return c2.indexOf(ptav) > -1;
});
}
function isCombinationInList(c1, list) {
return _.some(list, function (c2) {
return areCombinationsEqual(c1, c2);
});
}
function isPtavInCombination(ptav, combination) {
return combination.indexOf(ptav) > -1;
}
var self = this;
var combinationData = $parent
.find('ul[data-attribute_exclusions]')
@@ -360,17 +365,6 @@ var VariantMixin = {
.attr('title', '')
.data('excluded-by', '');
var disable = false;
// compatibility 12.0
var filteredCombination = combination;
if (combinationData.no_variant_product_template_attribute_value_ids !== undefined) {
var no_variants = combinationData.no_variant_product_template_attribute_value_ids;
filteredCombination = _.filter(combination, function (ptav) {
return !isPtavInCombination(ptav, no_variants);
});
}
// exclusion rules: array of ptav
// for each of them, contains array with the other ptav they exclude
if (combinationData.exclusions) {
@@ -379,11 +373,6 @@ var VariantMixin = {
if (combinationData.exclusions.hasOwnProperty(current_ptav)) {
// for each exclusion of the current attribute:
_.each(combinationData.exclusions[current_ptav], function (excluded_ptav) {
// disable if it excludes any other attribute already in the combination
if (isPtavInCombination(excluded_ptav, combination)) {
disable = true;
}
// disable the excluded input (even when not already selected)
// to give a visual feedback before click
self._disableInput(
@@ -401,9 +390,6 @@ var VariantMixin = {
_.each(combinationData.parent_exclusions, function (exclusions, excluded_by){
// check that the selected combination is in the parent exclusions
_.each(exclusions, function (ptav) {
if (isPtavInCombination(ptav, combination)) {
disable = true;
}
// disable the excluded input (even when not already selected)
// to give a visual feedback before click
@@ -416,29 +402,7 @@ var VariantMixin = {
);
});
});
// archived variants
if (isCombinationInList(filteredCombination, combinationData.archived_combinations)) {
disable = true;
}
// if not using dynamic attributes, exclude variants that are deleted
if (filteredCombination.length && // compatibility 12.0 list view of variants
combinationData.has_dynamic_attributes === false &&
combinationData.existing_combinations !== undefined &&
!isCombinationInList(filteredCombination, combinationData.existing_combinations)
) {
disable = true;
}
$parent.toggleClass('css_not_available', disable);
$parent.find("#add_to_cart").toggleClass('disabled', disable);
$parent
.parents('.modal')
.find('.o_sale_product_configurator_add, .o_sale_product_configurator_edit')
.toggleClass('disabled', disable);
},
/**
* Extracted to a method to be extendable by other modules
*
@@ -447,7 +411,6 @@ var VariantMixin = {
_getProductId: function ($parent) {
return parseInt($parent.find('.product_id').val());
},
/**
* Will disable the input/option that refers to the passed attributeValueId.
* This is used for showing the user that some combinations are not available.
@@ -489,7 +452,6 @@ var VariantMixin = {
$target.data('excluded-by', JSON.stringify(excludedByData));
}
},
/**
* @see onChangeVariant
*
@@ -506,6 +468,10 @@ var VariantMixin = {
$price.text(self._priceToStr(combination.price));
$default_price.text(self._priceToStr(combination.list_price));
var isCombinationPossible = combination.is_combination_possible;
this._toggleDisable($parent, isCombinationPossible);
// compatibility_check to remove in master
// needed for fix in 12.0 in the case of git pull and no server restart
var compatibility_check = combination.list_price - combination.price >= 0.01;
@@ -545,7 +511,8 @@ var VariantMixin = {
combination.display_image,
combination.product_id,
combination.product_template_id,
combination.carousel
combination.carousel,
isCombinationPossible
);
}
@@ -586,7 +553,46 @@ var VariantMixin = {
formatted[0] = utils.insert_thousand_seps(formatted[0]);
return formatted.join(l10n.decimal_point);
},
/**
* Returns a throttled `_getCombinationInfo` with a leading and a trailing
* call, which is memoized per `uniqueId`, and for which previous results
* are dropped.
*
* The uniqueId is needed because on the configurator modal there might be
* multiple elements triggering the rpc at the same time, and we need each
* individual product rpc to be executed, but only once per individual
* product.
*
* The leading execution is to keep good reactivity on the first call, for
* a better user experience. The trailing is because ultimately only the
* information about the last selected combination is useful. All
* intermediary rpc can be ignored and are therefore best not done at all.
*
* The DropMisordered is to make sure slower rpc are ignored if the result
* of a newer rpc has already been received.
*
* @private
* @param {string} uniqueId
* @returns {function}
*/
_throttledGetCombinationInfo: _.memoize(function (uniqueId) {
var dropMisordered = new concurrency.DropMisordered();
var _getCombinationInfo = _.throttle(this._getCombinationInfo.bind(this), 500);
return function (ev) {
return dropMisordered.add(_getCombinationInfo(ev));
};
}),
/**
* Toggles the disabled class depending on the $parent element
* and the possibility of the current combination.
*
* @private
* @param {$.Element} $parent
* @param {boolean} isCombinationPossible
*/
_toggleDisable: function ($parent, isCombinationPossible) {
$parent.toggleClass('css_not_available', !isCombinationPossible);
},
/**
* Updates the product image.
* This will use the productId if available or will fallback to the productTemplateId.
@@ -61,7 +61,7 @@
<div class="o_setting_right_pane">
<label for="module_product_email_template" string="Deliver Content by Email"/>
<div class="text-muted">
Send a product-specific email once the invoice is paid
Send a product-specific email once the invoice is validated
</div>
</div>
</div>
+1
View File
@@ -477,6 +477,7 @@
<field name="price_total" invisible="1"/>
<field name="price_unit"/>
<field name="display_type"/>
<field name="tax_id" invisible="1"/>
<templates>
<t t-name="kanban-box">
<div t-attf-class="oe_kanban_card oe_kanban_global_click {{ record.display_type.raw_value ? 'o_is_' + record.display_type.raw_value : '' }}">
@@ -11,6 +11,7 @@ var optionalProductsMap = {};
var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, {
events: _.extend({}, Dialog.prototype.events, VariantMixin.events, {
'click a.js_add, a.js_remove': '_onAddOrRemoveOption',
'click button.js_add_cart_json': 'onClickAddCartJSON',
'change .in_cart.main_product input.js_quantity': '_onChangeQuantity',
'change .js_raw_price': '_computePriceTotal'
}),
@@ -60,7 +61,6 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, {
this.previousModalHeight = params.previousModalHeight;
this.dialogClass = 'oe_optional_products_modal';
this._productImageField = 'image_medium';
// reset any previously populated properties maps
optionalProductsMap = {};
@@ -153,6 +153,25 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, {
// Public
//--------------------------------------------------------------------------
/**
* Computes and updates the total price, useful when a product is added or
* when the quantity is changed.
* TODO awa: add a container context to avoid global selectors ?
*/
computePriceTotal: function () {
if ($('.js_price_total').length) {
var price = 0;
$('.js_product.in_cart').each(function () {
var quantity = parseInt($('input[name="add_qty"]').first().val());
price += parseFloat($(this).find('.js_raw_price').html()) * quantity;
});
$('.js_price_total .oe_currency_value').html(
this._priceToStr(parseFloat(price))
);
}
ProductConfiguratorMixin.computePriceTotal.apply(this, arguments);
},
/**
* Returns the list of selected products.
* The root product is added on top of the list.
@@ -437,6 +456,8 @@ var OptionalProductsModal = Dialog.extend(ServicesMixin, VariantMixin, {
.text(combination.display_name);
VariantMixin._onChangeCombination.apply(this, arguments);
this.computePriceTotal();
},
/**
* When the quantity of the root product is updated, we need to update
@@ -5,6 +5,11 @@ var FormRenderer = require('web.FormRenderer');
var VariantMixin = require('sale.VariantMixin');
var ProductConfiguratorFormRenderer = FormRenderer.extend(VariantMixin, {
events: _.extend({}, FormRenderer.prototype.events, VariantMixin.events, {
'click button.js_add_cart_json': 'onClickAddCartJSON',
}),
/**
* @override
*/
@@ -73,6 +78,17 @@ var ProductConfiguratorFormRenderer = FormRenderer.extend(VariantMixin, {
}
},
/**
* Toggles the add button depending on the possibility of the current
* combination.
*
* @override
*/
_toggleDisable: function ($parent, isCombinationPossible) {
VariantMixin._toggleDisable.apply(this, arguments);
$parent.parents('.modal').find('.o_sale_product_configurator_add').toggleClass('disabled', !isCombinationPossible);
},
/**
* Will fill the custom values input based on the provided initial configuration.
*

Some files were not shown because too many files have changed in this diff Show More