[MERGE] forward port branch saas-12.2 up to 995aa1685e
This commit is contained in:
+40
@@ -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).
|
||||
@@ -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'})
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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', '<', 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)
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,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')])
|
||||
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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']))
|
||||
|
||||
|
||||
@@ -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"},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')})
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
'''
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user