[MERGE] forward port branch saas-12.3 up to 50e571acf7
closes odoo/odoo#36491 Signed-off-by: Christophe Simonis <chs@odoo.com>
This commit is contained in:
@@ -954,7 +954,7 @@ class AccountJournal(models.Model):
|
||||
@api.depends('company_id')
|
||||
def _belong_to_company(self):
|
||||
for journal in self:
|
||||
journal.belong_to_company = (journal.company_id.id == self.env.company.id)
|
||||
journal.belongs_to_company = (journal.company_id.id == self.env.company.id)
|
||||
|
||||
@api.multi
|
||||
def _search_company_journals(self, operator, value):
|
||||
@@ -1361,20 +1361,27 @@ class AccountTax(models.Model):
|
||||
cached_tax_amounts = {}
|
||||
if handle_price_include:
|
||||
for tax in reversed(taxes):
|
||||
tax_repartition_lines = (
|
||||
is_refund
|
||||
and tax.refund_repartition_line_ids
|
||||
or tax.invoice_repartition_line_ids
|
||||
).filtered(lambda x: x.repartition_type == "tax")
|
||||
sum_repartition_factor = sum(tax_repartition_lines.mapped("factor"))
|
||||
|
||||
if tax.include_base_amount:
|
||||
base = recompute_base(base, incl_fixed_amount, incl_percent_amount, incl_division_amount)
|
||||
incl_fixed_amount = incl_percent_amount = incl_division_amount = 0
|
||||
store_included_tax_total = True
|
||||
if tax.price_include or self._context.get('force_price_include'):
|
||||
if tax.amount_type == 'percent':
|
||||
incl_percent_amount += tax.amount
|
||||
incl_percent_amount += tax.amount * sum_repartition_factor
|
||||
elif tax.amount_type == 'division':
|
||||
incl_division_amount += tax.amount
|
||||
incl_division_amount += tax.amount * sum_repartition_factor
|
||||
elif tax.amount_type == 'fixed':
|
||||
incl_fixed_amount += quantity * tax.amount
|
||||
incl_fixed_amount += quantity * tax.amount * sum_repartition_factor
|
||||
else:
|
||||
# tax.amount_type == other (python)
|
||||
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner)
|
||||
tax_amount = tax._compute_amount(base, price_unit, quantity, product, partner) * sum_repartition_factor
|
||||
incl_fixed_amount += tax_amount
|
||||
# Avoid unecessary re-computation
|
||||
cached_tax_amounts[i] = tax_amount
|
||||
@@ -1393,6 +1400,9 @@ class AccountTax(models.Model):
|
||||
i = 0
|
||||
cumulated_tax_included_amount = 0
|
||||
for tax in taxes:
|
||||
tax_repartition_lines = (is_refund and tax.refund_repartition_line_ids or tax.invoice_repartition_line_ids).filtered(lambda x: x.repartition_type == 'tax')
|
||||
sum_repartition_factor = sum(tax_repartition_lines.mapped('factor'))
|
||||
|
||||
#compute the tax_amount
|
||||
if (self._context.get('force_price_include') or tax.price_include) and total_included_checkpoints.get(i):
|
||||
# We know the total to reach for that tax, so we make a substraction to avoid any rounding issues
|
||||
@@ -1402,11 +1412,12 @@ class AccountTax(models.Model):
|
||||
tax_amount = tax.with_context(force_price_include=False)._compute_amount(
|
||||
base, sign * price_unit, quantity, product, partner)
|
||||
|
||||
# Round the tax_amount
|
||||
# Round the tax_amount multiplied by the computed repartition lines factor.
|
||||
tax_amount = round(tax_amount, prec)
|
||||
factorized_tax_amount = round(tax_amount * sum_repartition_factor, prec)
|
||||
|
||||
if tax.price_include and not total_included_checkpoints.get(i):
|
||||
cumulated_tax_included_amount += tax_amount
|
||||
cumulated_tax_included_amount += factorized_tax_amount
|
||||
|
||||
# If the tax affects the base of subsequent taxes, its tax move lines must
|
||||
# receive the base tags and tag_ids of these taxes, so that the tax report computes
|
||||
@@ -1417,18 +1428,24 @@ class AccountTax(models.Model):
|
||||
subsequent_taxes = taxes[i+1:]
|
||||
subsequent_tags = subsequent_taxes.get_tax_tags(is_refund, 'base')
|
||||
|
||||
# Compute the tax lines
|
||||
tax_repartition_lines = (is_refund and tax.refund_repartition_line_ids or tax.invoice_repartition_line_ids).filtered(lambda x: x.repartition_type == 'tax')
|
||||
sum_factor = sum(tax_repartition_lines.mapped('factor'))
|
||||
repartition_lines_to_treat = len(tax_repartition_lines)
|
||||
total_amount = 0
|
||||
for repartition_line in tax_repartition_lines:
|
||||
if repartition_lines_to_treat != 1 or sum_factor != 1.0:
|
||||
line_amount = round(tax_amount * repartition_line.factor, prec)
|
||||
else:
|
||||
# When the sum of the repartition lines factor is 100%, we need to ensure the whole tax amount has
|
||||
# been spread into the repartition lines.
|
||||
line_amount = round(tax_amount - total_amount, prec)
|
||||
# Compute the tax line amounts by multiplying each factor with the tax amount.
|
||||
# Then, spread the tax rounding to ensure the consistency of each line independently with the factorized
|
||||
# amount. E.g:
|
||||
#
|
||||
# Suppose a tax having 4 x 50% repartition line applied on a tax amount of 0.03 with 2 decimal places.
|
||||
# The factorized_tax_amount will be 0.06 (200% x 0.03). However, each line taken independently will compute
|
||||
# 50% * 0.03 = 0.01 with rounding. It means there is 0.06 - 0.04 = 0.02 as total_rounding_error to dispatch
|
||||
# in lines as 2 x 0.01.
|
||||
repartition_line_amounts = [round(tax_amount * line.factor, prec) for line in tax_repartition_lines]
|
||||
total_rounding_error = round(factorized_tax_amount - sum(repartition_line_amounts), prec)
|
||||
nber_rounding_steps = int(abs(total_rounding_error / currency.rounding))
|
||||
rounding_error = round(nber_rounding_steps and total_rounding_error / nber_rounding_steps or 0.0, prec)
|
||||
|
||||
for repartition_line, line_amount in zip(tax_repartition_lines, repartition_line_amounts):
|
||||
|
||||
if nber_rounding_steps:
|
||||
line_amount += rounding_error
|
||||
nber_rounding_steps -= 1
|
||||
|
||||
taxes_vals.append({
|
||||
'id': tax.id,
|
||||
@@ -1445,16 +1462,14 @@ class AccountTax(models.Model):
|
||||
'tax_ids': subsequent_taxes.ids,
|
||||
})
|
||||
|
||||
total_amount += line_amount
|
||||
if not repartition_line.account_id:
|
||||
total_void += line_amount
|
||||
repartition_lines_to_treat -= 1
|
||||
|
||||
# Affect subsequent taxes
|
||||
if tax.include_base_amount:
|
||||
base += tax_amount
|
||||
base += factorized_tax_amount
|
||||
|
||||
total_included += tax_amount
|
||||
total_included += factorized_tax_amount
|
||||
i += 1
|
||||
|
||||
return {
|
||||
|
||||
@@ -1334,6 +1334,24 @@ class AccountMove(models.Model):
|
||||
raise UserError(message)
|
||||
return True
|
||||
|
||||
def _check_tax_lock_date(self):
|
||||
for move in self:
|
||||
if (
|
||||
move.company_id.tax_lock_date
|
||||
and move.date <= move.company_id.tax_lock_date
|
||||
and any(
|
||||
line.tax_ids
|
||||
or line.tax_line_id
|
||||
or line.tag_ids.filtered(lambda x: x.applicability == "taxes")
|
||||
for line in move.line_ids
|
||||
)
|
||||
):
|
||||
raise UserError(
|
||||
_(
|
||||
"The operation is refused as it would impact an already issued tax statement. Please change the journal entry date or the tax lock date set in the settings ({}) to proceed"
|
||||
).format(move.company_id.tax_lock_date or date.min)
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def _check_move_consistency(self):
|
||||
for move in self:
|
||||
@@ -1342,6 +1360,7 @@ class AccountMove(models.Model):
|
||||
raise UserError(_("Cannot create moves for different companies."))
|
||||
|
||||
self._check_balanced()
|
||||
self._check_tax_lock_date()
|
||||
self._check_fiscalyear_lock_date()
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -1971,6 +1990,7 @@ class AccountMove(models.Model):
|
||||
|
||||
@api.multi
|
||||
def button_cancel(self):
|
||||
self._check_tax_lock_date()
|
||||
AccountMoveLine = self.env['account.move.line']
|
||||
excluded_move_ids = []
|
||||
|
||||
@@ -2033,6 +2053,7 @@ class AccountMove(models.Model):
|
||||
"""
|
||||
if any(not move.is_invoice(include_receipts=True) for move in self):
|
||||
raise UserError(_("Only invoices could be printed."))
|
||||
self._check_tax_lock_date()
|
||||
|
||||
self.filtered(lambda inv: not inv.invoice_sent).write({'invoice_sent': True})
|
||||
if self.user_has_groups('account.group_account_invoice'):
|
||||
@@ -2715,7 +2736,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 * partial_line.amount * rate
|
||||
amount_residual_currency += sign_partial_line * line.currency_id.round(partial_line.amount * rate)
|
||||
|
||||
#computing the `reconciled` field.
|
||||
reconciled = False
|
||||
@@ -2787,24 +2808,6 @@ class AccountMoveLine(models.Model):
|
||||
if control_type_failed or control_account_failed:
|
||||
raise UserError(_('You cannot use this general account in this journal, check the tab \'Entry Controls\' on the related journal.'))
|
||||
|
||||
@api.constrains('tax_ids', 'tax_line_id')
|
||||
def _check_tax_lock_date1(self):
|
||||
for line in self:
|
||||
if line.date <= (line.company_id.tax_lock_date or date.min):
|
||||
raise ValidationError(
|
||||
_("The operation is refused as it would impact an already issued tax statement. " +
|
||||
"Please change the journal entry date or the tax lock date set in the settings ({}) to proceed").format(
|
||||
line.company_id.tax_lock_date or date.min))
|
||||
|
||||
@api.constrains('credit', 'debit', 'date')
|
||||
def _check_tax_lock_date2(self):
|
||||
for line in self:
|
||||
if (line.tax_ids or line.tax_line_id) and line.date <= (line.company_id.tax_lock_date or date.min):
|
||||
raise ValidationError(
|
||||
_("The operation is refused as it would impact an already issued tax statement. " +
|
||||
"Please change the journal entry date or the tax lock date set in the settings ({}) to proceed").format(
|
||||
line.company_id.tax_lock_date or date.min))
|
||||
|
||||
@api.multi
|
||||
def _update_check(self):
|
||||
""" Raise Warning to cause rollback if the move is posted, some entries are reconciled or the move is older than the lock date"""
|
||||
@@ -2817,7 +2820,10 @@ class AccountMoveLine(models.Model):
|
||||
raise UserError(_('You cannot do this modification on a reconciled entry. You can just change some non legal fields or you must unreconcile first.\n%s.') % err_msg)
|
||||
if line.move_id.id not in move_ids:
|
||||
move_ids.add(line.move_id.id)
|
||||
self.env['account.move'].browse(list(move_ids))._check_fiscalyear_lock_date()
|
||||
if move_ids:
|
||||
moves = self.env['account.move'].browse(list(move_ids))
|
||||
moves._check_fiscalyear_lock_date()
|
||||
moves._check_tax_lock_date()
|
||||
return True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
@@ -2976,7 +2982,6 @@ class AccountMoveLine(models.Model):
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
self._update_check()
|
||||
self._check_tax_lock_date2()
|
||||
move_ids = set()
|
||||
for line in self:
|
||||
if line.move_id.id not in move_ids:
|
||||
@@ -3010,6 +3015,11 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
# Suggest default value for debit / credit to balance the journal entry.
|
||||
balance = sum(line['debit'] - line['credit'] for line in move.line_ids)
|
||||
# if we are here, line_ids is in context, so journal_id should also be.
|
||||
journal = self.env['account.journal'].browse(self._context.get('default_journal_id') or self._context['journal_id'])
|
||||
currency = journal.exists() and journal.company_id.currency_id
|
||||
if currency:
|
||||
balance = currency.round(balance)
|
||||
if balance < 0.0:
|
||||
values.update({'debit': -balance})
|
||||
if balance > 0.0:
|
||||
|
||||
@@ -452,7 +452,8 @@ class account_payment(models.Model):
|
||||
for move in rec.move_line_ids.mapped('move_id'):
|
||||
if rec.reconciled_invoice_ids:
|
||||
move.line_ids.remove_move_reconcile()
|
||||
move.button_cancel()
|
||||
if move.state != 'draft':
|
||||
move.button_cancel()
|
||||
move.unlink()
|
||||
rec.write({
|
||||
'state': 'cancelled',
|
||||
|
||||
@@ -361,7 +361,7 @@ class AccountReconcileModel(models.Model):
|
||||
query += ' AND st_line.name NOT ILIKE %s'
|
||||
params += ['%%%s%%' % rule.match_label_param]
|
||||
elif rule.match_label == 'match_regex':
|
||||
query += ' AND st_line.name ~ %s'
|
||||
query += ' AND st_line.name ~* %s'
|
||||
params += [rule.match_label_param]
|
||||
|
||||
# Filter on partners.
|
||||
@@ -621,8 +621,10 @@ class AccountReconcileModel(models.Model):
|
||||
|
||||
if line_residual > total_residual:
|
||||
amount_percentage = (total_residual / line_residual) * 100
|
||||
else:
|
||||
elif total_residual:
|
||||
amount_percentage = (line_residual / total_residual) * 100 if total_residual else 0
|
||||
else:
|
||||
return False
|
||||
return amount_percentage >= self.match_total_amount_param
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -116,7 +116,8 @@ class AccountReconciliation(models.AbstractModel):
|
||||
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)
|
||||
# By definition the commercial partner_id doesn't have a parent_id set
|
||||
query += 'LEFT JOIN res_partner p3 ON p3.name ILIKE st_line.partner_name %s AND p3.parent_id is NULL \n' % (where_partner)
|
||||
query += 'WHERE st_line.id IN %s'
|
||||
|
||||
params += [tuple(st_lines.ids)]
|
||||
@@ -520,17 +521,22 @@ class AccountReconciliation(models.AbstractModel):
|
||||
])
|
||||
|
||||
domain = expression.OR([domain_reconciliation, domain_matching])
|
||||
partner_domain = []
|
||||
if partner_id:
|
||||
domain = expression.AND([domain, [('partner_id', '=', partner_id)]])
|
||||
partner_domain = [('partner_id', '=', partner_id)]
|
||||
domain = expression.AND([domain, partner_domain])
|
||||
|
||||
# Domain factorized for all reconciliation use cases
|
||||
if search_str:
|
||||
str_domain = self._domain_move_lines(search_str=search_str)
|
||||
if not partner_id:
|
||||
str_domain = expression.OR([
|
||||
str_domain,
|
||||
[('partner_id.name', 'ilike', search_str)]
|
||||
])
|
||||
partner_domain = [('partner_id.name', 'ilike', search_str)]
|
||||
|
||||
str_domain = expression.OR([
|
||||
str_domain,
|
||||
partner_domain,
|
||||
])
|
||||
|
||||
domain = expression.AND([
|
||||
domain,
|
||||
str_domain
|
||||
|
||||
@@ -19,3 +19,4 @@ from . import test_templates_consistency
|
||||
from . import test_account_fiscal_year
|
||||
from . import test_account_all_l10n
|
||||
from . import test_reconciliation_matching_rules
|
||||
from . import test_account_move_tax_lock_date
|
||||
|
||||
@@ -69,33 +69,26 @@ class TestAccountMove(InvoiceTestCommon):
|
||||
# lines[3] = 'revenue line 2'
|
||||
lines = self.test_move.line_ids.sorted('debit')
|
||||
|
||||
# Writing not affecting a tax is allowed.
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[0].id, {'credit': 1750.0}), # counterpart line
|
||||
(1, lines[2].id, {'debit': 600.0}), # revenue line 1
|
||||
],
|
||||
})
|
||||
|
||||
self.cr.execute('SAVEPOINT test_misc_tax_lock_date_1')
|
||||
|
||||
# Writing something affecting a tax is not allowed.
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[0].id, {'credit': 2750.0}),
|
||||
(1, lines[3].id, {'debit': 2000.0}),
|
||||
(1, lines[3].id, {'debit': 2100.0}),
|
||||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[3].id, {'tax_ids': [(6, 0, self.company_data['default_tax_purchase'].ids)]}),
|
||||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.write({
|
||||
'line_ids': [
|
||||
(1, lines[0].id, {'credit': 1900.0}),
|
||||
@@ -103,7 +96,7 @@ class TestAccountMove(InvoiceTestCommon):
|
||||
],
|
||||
})
|
||||
|
||||
with self.assertRaises(ValidationError):
|
||||
with self.assertRaises(UserError):
|
||||
self.test_move.unlink()
|
||||
|
||||
self.cr.execute('ROLLBACK TO SAVEPOINT test_misc_tax_lock_date_1')
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from odoo.addons.account.tests.account_test_classes import AccountingTestCase
|
||||
from odoo.exceptions import UserError
|
||||
from datetime import date
|
||||
from calendar import monthrange
|
||||
# from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
from odoo.tests import tagged
|
||||
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestTaxBlockDate(AccountingTestCase):
|
||||
"""
|
||||
Forbid creation, edition and deletion of Journal Items related to taxes with
|
||||
a date prior to the Tax Block Date.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestTaxBlockDate, self).setUp()
|
||||
self.user_id = self.env.user
|
||||
self.company = self.env.company
|
||||
company_id = self.company.id
|
||||
|
||||
last_day_month = date.today()
|
||||
self.last_day_month = last_day_month.replace(day=monthrange(last_day_month.year, last_day_month.month)[1])
|
||||
first_day_month = date.today()
|
||||
self.first_day_month = first_day_month.replace(day=1)
|
||||
middle_day_month = date.today()
|
||||
middle_day_month = middle_day_month.replace(day=15)
|
||||
|
||||
self.sale_journal_id = self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', company_id)], limit=1)[0]
|
||||
self.account_id = self.env['account.account'].search([('internal_type', '=', 'receivable'), ('company_id', '=', company_id)], limit=1)[0]
|
||||
self.other_account_id = self.env['account.account'].search([('internal_type', '!=', 'receivable'), ('company_id', '=', company_id)], limit=1)[0]
|
||||
self.tax_id = self.env['account.tax'].search([('company_id', '=', company_id)], limit=1)[0]
|
||||
|
||||
self.move = {
|
||||
'name': '/',
|
||||
'journal_id': self.sale_journal_id.id,
|
||||
'date': middle_day_month,
|
||||
'line_ids': [(0, 0, {
|
||||
'name': 'foo',
|
||||
'debit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
'tax_ids': [(6, False, [self.tax_id.id])]
|
||||
}), (0, 0, {
|
||||
'name': 'bar',
|
||||
'credit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
})]
|
||||
}
|
||||
|
||||
self.move_no_tax = {
|
||||
'name': '/',
|
||||
'journal_id': self.sale_journal_id.id,
|
||||
'date': middle_day_month,
|
||||
'line_ids': [(0, 0, {
|
||||
'name': 'foo',
|
||||
'debit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
}), (0, 0, {
|
||||
'name': 'bar',
|
||||
'credit': 10,
|
||||
'account_id': self.account_id.id,
|
||||
})]
|
||||
}
|
||||
|
||||
def test_tax_lock_date_post(self):
|
||||
""" Checks that it is impossible to post an entry
|
||||
before the tax lock date.
|
||||
"""
|
||||
# Posting after the lock date is always allowed
|
||||
self.company.tax_lock_date = self.first_day_month
|
||||
# copy() because mail.thread modifies the dictionary given to create
|
||||
move_after_lock = self.env['account.move'].create(self.move.copy())
|
||||
move_after_lock.post()
|
||||
|
||||
# Posting before the tax lock date is allowed only if the move doesn't contain any tax
|
||||
self.user_id.company_id.tax_lock_date = self.last_day_month
|
||||
move_before_lock_no_tax = self.env['account.move'].create(self.move_no_tax)
|
||||
move_before_lock_no_tax.post()
|
||||
|
||||
# Posting before the tax lock date is not allowed for moves impacting the tax report
|
||||
self.user_id.company_id.tax_lock_date = self.last_day_month
|
||||
with self.assertRaises(UserError):
|
||||
self.env['account.move'].create(self.move.copy())
|
||||
|
||||
def test_tax_lock_date_cancel(self):
|
||||
""" Checks that it is not possible to cancel an entry posted before the
|
||||
tax lock date if it impacts the tax report. """
|
||||
self.sale_journal_id.update_posted = True
|
||||
move = self.env['account.move'].create(self.move)
|
||||
move.post()
|
||||
move_no_tax = self.env['account.move'].create(self.move_no_tax)
|
||||
move_no_tax.post()
|
||||
|
||||
self.user_id.company_id.tax_lock_date = self.last_day_month
|
||||
|
||||
move_no_tax.button_cancel()
|
||||
with self.assertRaises(UserError):
|
||||
move.button_cancel()
|
||||
@@ -149,6 +149,24 @@ class TestReconciliation(AccountingTestCase):
|
||||
invoice.post()
|
||||
return invoice
|
||||
|
||||
def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
|
||||
move = self.env['account.move'].with_context(default_type=type).create({
|
||||
'type': type,
|
||||
'partner_id': partner_id,
|
||||
'invoice_date': time.strftime('%Y') + '-07-01',
|
||||
'date': time.strftime('%Y') + '-07-01',
|
||||
'currency_id': currency_id,
|
||||
'invoice_payment_term_id': payment_term_id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'quantity': 1,
|
||||
'price_unit': invoice_amount,
|
||||
|
||||
'name': 'product that cost ' + str(invoice_amount),
|
||||
})],
|
||||
})
|
||||
move.post()
|
||||
return move
|
||||
|
||||
def make_payment(self, invoice_record, bank_journal, amount=0.0, amount_currency=0.0, currency_id=None):
|
||||
bank_stmt = self.acc_bank_stmt_model.create({
|
||||
'journal_id': bank_journal.id,
|
||||
@@ -895,22 +913,6 @@ class TestReconciliationExec(TestReconciliation):
|
||||
self.assertEqual(reversed_bank_line.full_reconcile_id.id, bank_line.full_reconcile_id.id)
|
||||
self.assertEqual(reversed_customer_line.full_reconcile_id.id, customer_line.full_reconcile_id.id)
|
||||
|
||||
def create_invoice_partner(self, type='out_invoice', invoice_amount=50, currency_id=None, partner_id=False, payment_term_id=False):
|
||||
move = self.env['account.move'].with_context(default_type=type).create({
|
||||
'type': type,
|
||||
'partner_id': partner_id,
|
||||
'invoice_date': time.strftime('%Y') + '-07-01',
|
||||
'date': time.strftime('%Y') + '-07-01',
|
||||
'currency_id': currency_id,
|
||||
'invoice_payment_term_id': payment_term_id,
|
||||
'invoice_line_ids': [(0, 0, {
|
||||
'quantity': 1,
|
||||
'price_unit': invoice_amount,
|
||||
'name': 'product that cost ' + str(invoice_amount),
|
||||
})],
|
||||
})
|
||||
move.post()
|
||||
return move
|
||||
|
||||
def test_aged_report(self):
|
||||
AgedReport = self.env['report.account.report_agedpartnerbalance'].with_context(include_nullified_amount=True)
|
||||
@@ -1168,6 +1170,68 @@ class TestReconciliationExec(TestReconciliation):
|
||||
# because they owe us still 50 CC.
|
||||
self.assertEqual(invoice_cust_1.invoice_payment_state, 'not_paid', 'Invoice is in status %s' % invoice_cust_1.state)
|
||||
|
||||
def test_inv_refund_foreign_payment_writeoff_domestic(self):
|
||||
company = self.env.ref('base.main_company')
|
||||
self.env['res.currency.rate'].search([]).unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': time.strftime('%Y') + '-07-01',
|
||||
'rate': 1.0,
|
||||
'currency_id': self.currency_euro_id,
|
||||
'company_id': company.id
|
||||
})
|
||||
self.env['res.currency.rate'].create({
|
||||
'name': time.strftime('%Y') + '-07-01',
|
||||
'rate': 1.113900, # Don't change this !
|
||||
'currency_id': self.currency_usd_id,
|
||||
'company_id': self.env.ref('base.main_company').id
|
||||
})
|
||||
inv1 = self.create_invoice(invoice_amount=480, currency_id=self.currency_usd_id)
|
||||
inv2 = self.create_invoice(type="out_refund", invoice_amount=140, currency_id=self.currency_usd_id)
|
||||
|
||||
payment = self.env['account.payment'].create({
|
||||
'payment_method_id': self.inbound_payment_method.id,
|
||||
'payment_type': 'inbound',
|
||||
'partner_type': 'customer',
|
||||
'partner_id': inv1.partner_id.id,
|
||||
'amount': 287.20,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
payment.post()
|
||||
|
||||
inv1_receivable = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
inv2_receivable = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
pay_receivable = payment.move_line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
|
||||
data_for_reconciliation = [
|
||||
{
|
||||
'type': 'partner',
|
||||
'id': inv1.partner_id.id,
|
||||
'mv_line_ids': (inv1_receivable + inv2_receivable + pay_receivable).ids,
|
||||
'new_mv_line_dicts': [
|
||||
{
|
||||
'credit': 18.04,
|
||||
'debit': 0.00,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'name': 'Total WriteOff (Fees)',
|
||||
'account_id': self.diff_expense_account.id
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
self.env["account.reconciliation.widget"].process_move_lines(data_for_reconciliation)
|
||||
|
||||
self.assertTrue(inv1_receivable.full_reconcile_id.exists())
|
||||
self.assertEquals(inv1_receivable.full_reconcile_id, inv2_receivable.full_reconcile_id)
|
||||
self.assertEquals(inv1_receivable.full_reconcile_id, pay_receivable.full_reconcile_id)
|
||||
|
||||
self.assertTrue(all(l.reconciled for l in inv1_receivable))
|
||||
self.assertTrue(all(l.reconciled for l in inv2_receivable))
|
||||
|
||||
self.assertEquals(inv1.invoice_payment_state, 'paid')
|
||||
self.assertEquals(inv2.invoice_payment_state, 'paid')
|
||||
|
||||
def test_multiple_term_reconciliation_opw_1906665(self):
|
||||
'''Test that when registering a payment to an invoice with multiple
|
||||
payment term lines the reconciliation happens against the line
|
||||
|
||||
@@ -53,3 +53,80 @@ class TestReconciliationWidget(TestReconciliation):
|
||||
|
||||
result = self.env['account.reconciliation.widget'].get_bank_statement_line_data(bank_stmt_line.ids)
|
||||
self.assertEqual(result['lines'][0]['reconciliation_proposition'][0]['amount_str'], '$ 50.00')
|
||||
|
||||
def test_filter_partner1(self):
|
||||
inv1 = self.create_invoice(currency_id=self.currency_euro_id)
|
||||
inv2 = self.create_invoice(currency_id=self.currency_euro_id)
|
||||
|
||||
receivable1 = inv1.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
receivable2 = inv2.line_ids.filtered(lambda l: l.account_id.internal_type == 'receivable')
|
||||
|
||||
bank_stmt = self.acc_bank_stmt_model.create({
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
'name': 'test',
|
||||
})
|
||||
|
||||
bank_stmt_line = self.acc_bank_stmt_line_model.create({
|
||||
'name': 'testLine',
|
||||
'statement_id': bank_stmt.id,
|
||||
'amount': 100,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
})
|
||||
|
||||
# This is like opening the widget, and type "deco" in the filter
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=False,
|
||||
excluded_ids=[],
|
||||
search_str='deco'
|
||||
)
|
||||
mv_lines_ids = [l['id'] for l in mv_lines_rec]
|
||||
|
||||
self.assertTrue(receivable1.id in mv_lines_ids)
|
||||
self.assertTrue(receivable2.id in mv_lines_ids)
|
||||
|
||||
# This is like clicking on the first receivable
|
||||
partner = inv1.partner_id
|
||||
mv_lines_rec = self.env['account.reconciliation.widget'].get_move_lines_for_bank_statement_line(
|
||||
bank_stmt_line.id,
|
||||
partner_id=partner.id,
|
||||
excluded_ids=[receivable1.id],
|
||||
search_str='deco'
|
||||
)
|
||||
mv_lines_ids = [l['id'] for l in mv_lines_rec]
|
||||
|
||||
self.assertFalse(receivable1.id in mv_lines_ids)
|
||||
self.assertTrue(receivable2.id in mv_lines_ids)
|
||||
|
||||
def test_partner_name_with_parent(self):
|
||||
parent_partner = self.env['res.partner'].create({
|
||||
'name': 'test',
|
||||
})
|
||||
child_partner = self.env['res.partner'].create({
|
||||
'name': 'test',
|
||||
'parent_id': parent_partner.id,
|
||||
'type': 'delivery',
|
||||
})
|
||||
self.create_invoice_partner(currency_id=self.currency_euro_id, partner_id=child_partner.id)
|
||||
|
||||
bank_stmt = self.acc_bank_stmt_model.create({
|
||||
'company_id': self.env.ref('base.main_company').id,
|
||||
'journal_id': self.bank_journal_euro.id,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
'name': 'test',
|
||||
})
|
||||
|
||||
bank_stmt_line = self.acc_bank_stmt_line_model.create({
|
||||
'name': 'testLine',
|
||||
'statement_id': bank_stmt.id,
|
||||
'amount': 100,
|
||||
'date': time.strftime('%Y-07-15'),
|
||||
'partner_name': 'test',
|
||||
})
|
||||
|
||||
bkstmt_data = self.env['account.reconciliation.widget'].get_bank_statement_line_data(bank_stmt_line.ids)
|
||||
|
||||
self.assertEqual(len(bkstmt_data['lines']), 1)
|
||||
self.assertEqual(bkstmt_data['lines'][0]['partner_id'], parent_partner.id)
|
||||
|
||||
@@ -348,12 +348,32 @@ class TestTax(AccountTestUsers):
|
||||
'price_include': True,
|
||||
'include_base_amount': True,
|
||||
'sequence': 1,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_2 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_2',
|
||||
'amount_type': 'percent',
|
||||
'amount': 10,
|
||||
'sequence': 2,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_3 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_3',
|
||||
@@ -361,12 +381,32 @@ class TestTax(AccountTestUsers):
|
||||
'amount': 10,
|
||||
'price_include': True,
|
||||
'sequence': 3,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_4 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_4',
|
||||
'amount_type': 'percent',
|
||||
'amount': 10,
|
||||
'sequence': 4,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
tax_5 = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_computation_0_5',
|
||||
@@ -374,21 +414,229 @@ class TestTax(AccountTestUsers):
|
||||
'amount': 10,
|
||||
'price_include': True,
|
||||
'sequence': 5,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
taxes = tax_1 + tax_2 + tax_3 + tax_4 + tax_5
|
||||
res = taxes.compute_all(132.0)
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
154, # 'total_included'
|
||||
100, # 'total_excluded'
|
||||
[
|
||||
# base , amount | seq | amount | incl | incl_base
|
||||
# ---------------------------------------------------
|
||||
(100.0, 10.0), # | 1 | 10% | t | t
|
||||
(110.0, 11.0), # | 2 | 10% | |
|
||||
(110.0, 11.0), # | 3 | 10% | t |
|
||||
(110.0, 11.0), # | 4 | 10% | |
|
||||
(110.0, 11.0), # | 5 | 10% | t |
|
||||
(100.0, 5.0), # | 1 | 10% | t | t
|
||||
(100.0, 5.0), # | 1 | 10% | t | t
|
||||
(110.0, 5.5), # | 2 | 10% | |
|
||||
(110.0, 5.5), # | 2 | 10% | |
|
||||
(110.0, 5.5), # | 3 | 10% | t |
|
||||
(110.0, 5.5), # | 3 | 10% | t |
|
||||
(110.0, 5.5), # | 4 | 10% | |
|
||||
(110.0, 5.5), # | 4 | 10% | |
|
||||
(110.0, 5.5), # | 5 | 10% | t |
|
||||
(110.0, 5.5), # | 5 | 10% | t |
|
||||
# ---------------------------------------------------
|
||||
],
|
||||
res
|
||||
taxes.compute_all(132.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-154, # 'total_included'
|
||||
-100, # 'total_excluded'
|
||||
[
|
||||
# base , amount | seq | amount | incl | incl_base
|
||||
# ---------------------------------------------------
|
||||
(-100.0, -5.0), # | 1 | 10% | t | t
|
||||
(-100.0, -5.0), # | 1 | 10% | t | t
|
||||
(-110.0, -5.5), # | 2 | 10% | |
|
||||
(-110.0, -5.5), # | 2 | 10% | |
|
||||
(-110.0, -5.5), # | 3 | 10% | t |
|
||||
(-110.0, -5.5), # | 3 | 10% | t |
|
||||
(-110.0, -5.5), # | 4 | 10% | |
|
||||
(-110.0, -5.5), # | 4 | 10% | |
|
||||
(-110.0, -5.5), # | 5 | 10% | t |
|
||||
(-110.0, -5.5), # | 5 | 10% | t |
|
||||
# ---------------------------------------------------
|
||||
],
|
||||
taxes.compute_all(-132.0)
|
||||
)
|
||||
|
||||
def test_intracomm_taxes_computation_0(self):
|
||||
''' Test usage of intracomm taxes having e.g.+100%, -100% as repartition lines. '''
|
||||
intracomm_tax = self.env['account.tax'].create({
|
||||
'name': 'test_intracomm_taxes_computation_0_1',
|
||||
'amount_type': 'percent',
|
||||
'amount': 21,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -100.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -100.0}),
|
||||
],
|
||||
})
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
100, # 'total_included'
|
||||
100, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(100.0, 21.0),
|
||||
(100.0, -21.0),
|
||||
# ---------------
|
||||
],
|
||||
intracomm_tax.compute_all(100.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-100, # 'total_included'
|
||||
-100, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(-100.0, -21.0),
|
||||
(-100.0, 21.0),
|
||||
# ---------------
|
||||
],
|
||||
intracomm_tax.compute_all(-100.0)
|
||||
)
|
||||
|
||||
def test_rounding_issues_0(self):
|
||||
''' Test taxes having a complex setup of repartition lines. '''
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'test_rounding_issues_0',
|
||||
'amount_type': 'percent',
|
||||
'amount': 3,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
],
|
||||
})
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
1.09, # 'total_included'
|
||||
1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.02),
|
||||
(1.0, 0.02),
|
||||
(1.0, 0.02),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(1.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-1.09, # 'total_included'
|
||||
-1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.02),
|
||||
(-1.0, -0.02),
|
||||
(-1.0, -0.02),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(-1.0)
|
||||
)
|
||||
|
||||
def test_rounding_issues_1(self):
|
||||
''' Test taxes having a complex setup of repartition lines. '''
|
||||
tax = self.env['account.tax'].create({
|
||||
'name': 'test_advanced_taxes_repartition_lines_computation_1',
|
||||
'amount_type': 'percent',
|
||||
'amount': 3,
|
||||
'invoice_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
],
|
||||
'refund_repartition_line_ids': [
|
||||
(0, 0, {'repartition_type': 'base', 'factor_percent': 100.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -50.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': 25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
(0, 0, {'repartition_type': 'tax', 'factor_percent': -25.0}),
|
||||
],
|
||||
})
|
||||
|
||||
# Test with positive amount.
|
||||
self._check_compute_all_results(
|
||||
1, # 'total_included'
|
||||
1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(1.0, 0.02),
|
||||
(1.0, -0.02),
|
||||
(1.0, 0.01),
|
||||
(1.0, 0.01),
|
||||
(1.0, -0.01),
|
||||
(1.0, -0.01),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(1.0)
|
||||
)
|
||||
|
||||
# Test with negative amount.
|
||||
self._check_compute_all_results(
|
||||
-1, # 'total_included'
|
||||
-1, # 'total_excluded'
|
||||
[
|
||||
# base , amount
|
||||
# ---------------
|
||||
(-1.0, -0.02),
|
||||
(-1.0, 0.02),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, -0.01),
|
||||
(-1.0, 0.01),
|
||||
(-1.0, 0.01),
|
||||
# ---------------
|
||||
],
|
||||
tax.compute_all(-1.0)
|
||||
)
|
||||
|
||||
@@ -176,7 +176,7 @@
|
||||
<p t-if="o.fiscal_position_id.note" name="note">
|
||||
<span t-field="o.fiscal_position_id.note"/>
|
||||
</p>
|
||||
<div id="qrcode" t-if="(o.partner_id.country_id.code in ['BE', 'AT', 'DE', 'FI', 'NL']) and (o.company_id.qr_code) and (o.currency_id.name == 'EUR') and (o.invoice_partner_bank_id.acc_number != False)">
|
||||
<div id="qrcode" t-if="(o.company_id.qr_code) and (o.currency_id.name == 'EUR') and (o.invoice_partner_bank_id.acc_number != False)">
|
||||
<p t-if="(o.invoice_partner_bank_id.qr_code_valid)">
|
||||
<strong class="text-center">Scan me with your banking app.</strong><br /><br />
|
||||
<img class="border border-dark rounded" t-att-src="o.invoice_partner_bank_id.build_qr_code_url(o.amount_residual,(o.ref) if (o.ref) else o.name)"/>
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<field name="model">account.journal</field>
|
||||
<field name="inherit_id" ref="account.view_account_journal_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='loss_account_id']/parent::group" position="attributes">
|
||||
<attribute name="attrs">{'invisible': 0}</attribute>
|
||||
</xpath>
|
||||
<field name="loss_account_id" position="after">
|
||||
<field name="update_posted" groups="base.group_no_one"/>
|
||||
</field>
|
||||
|
||||
@@ -104,6 +104,11 @@ class AccountPayment(models.Model):
|
||||
""" This method is a hook for l10n_xx_check_printing modules to implement actual check printing capabilities """
|
||||
raise UserError(_("You have to choose a check layout. For this, go in Apps, search for 'Checks layout' and install one."))
|
||||
|
||||
|
||||
def set_check_amount_in_words(self):
|
||||
for payment in self:
|
||||
payment.check_amount_in_words = payment.currency_id.amount_to_text(payment.amount) if payment.currency_id else ''
|
||||
|
||||
#######################
|
||||
#CHECK PRINTING METHODS
|
||||
#######################
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<xpath expr="//button[@name='post']" position="before">
|
||||
<button name="print_checks" class="oe_highlight" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'posted')]}" string="Print Check" type="object"/>
|
||||
<button name="unmark_sent" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('state', '!=', 'sent')]}" string="Unmark Sent" type="object"/>
|
||||
<button name="set_check_amount_in_words" attrs="{'invisible': ['|', ('payment_method_code', '!=', 'check_printing'), ('check_amount_in_words', '!=', False)]}" string="Reset Amount in Words" type="object" groups="base.group_no_one"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='amount_div']" position="after">
|
||||
<field name="check_amount_in_words" attrs="{'invisible': [('payment_method_code', '!=', 'check_printing')], 'readonly': [('state', '!=', 'draft')]}" groups="base.group_no_one"/>
|
||||
|
||||
@@ -28,6 +28,8 @@ class AccountTaxPython(models.Model):
|
||||
|
||||
def _compute_amount(self, base_amount, price_unit, quantity=1.0, product=None, partner=None):
|
||||
self.ensure_one()
|
||||
if product and product._name == 'product.template':
|
||||
product = product.product_variant_id
|
||||
if self.amount_type == 'code':
|
||||
company = self.env.company
|
||||
localdict = {'base_amount': base_amount, 'price_unit':price_unit, 'quantity': quantity, 'product':product, 'partner':partner, 'company': company}
|
||||
@@ -39,6 +41,8 @@ class AccountTaxPython(models.Model):
|
||||
def compute_all(self, price_unit, currency=None, quantity=1.0, product=None, partner=None, is_refund=False, handle_price_include=True):
|
||||
taxes = self.filtered(lambda r: r.amount_type != 'code')
|
||||
company = self.env.company
|
||||
if product and product._name == 'product.template':
|
||||
product = product.product_variant_id
|
||||
for tax in self.filtered(lambda r: r.amount_type == 'code'):
|
||||
localdict = self._context.get('tax_computation_context', {})
|
||||
localdict.update({'price_unit': price_unit, 'quantity': quantity, 'product': product, 'partner': partner, 'company': company})
|
||||
|
||||
@@ -109,19 +109,10 @@ class OAuthLogin(Home):
|
||||
|
||||
return response
|
||||
|
||||
@http.route()
|
||||
def web_auth_signup(self, *args, **kw):
|
||||
providers = self.list_providers()
|
||||
response = super(OAuthLogin, self).web_auth_signup(*args, **kw)
|
||||
response.qcontext.update(providers=providers)
|
||||
return response
|
||||
|
||||
@http.route()
|
||||
def web_auth_reset_password(self, *args, **kw):
|
||||
providers = self.list_providers()
|
||||
response = super(OAuthLogin, self).web_auth_reset_password(*args, **kw)
|
||||
response.qcontext.update(providers=providers)
|
||||
return response
|
||||
def get_auth_signup_qcontext(self):
|
||||
result = super(OAuthLogin, self).get_auth_signup_qcontext()
|
||||
result["providers"] = self.list_providers()
|
||||
return result
|
||||
|
||||
|
||||
class OAuthController(http.Controller):
|
||||
|
||||
@@ -31,6 +31,7 @@ class Partner(models.Model):
|
||||
replacement_xml = """
|
||||
<div>
|
||||
<field name="country_enforce_cities" invisible="1"/>
|
||||
<field name="parent_id" invisible="1"/>
|
||||
<field name='city' placeholder="%(placeholder)s" class="o_address_city"
|
||||
attrs="{
|
||||
'invisible': [('country_enforce_cities', '=', True), ('city_id', '!=', False)],
|
||||
|
||||
@@ -448,7 +448,11 @@ class Import(models.TransientModel):
|
||||
'date_format') else []
|
||||
user_date_format = self.env['res.lang']._lang_get(self.env.user.lang).date_format
|
||||
if user_date_format:
|
||||
date_patterns.append(user_date_format)
|
||||
try:
|
||||
to_re(user_date_format)
|
||||
date_patterns.append(user_date_format)
|
||||
except KeyError:
|
||||
pass
|
||||
date_patterns.extend(DATE_PATTERNS)
|
||||
match = check_patterns(date_patterns, preview_values)
|
||||
if match:
|
||||
|
||||
@@ -79,6 +79,12 @@ var CrossTabBus = Longpolling.extend({
|
||||
this._super.apply(this, arguments);
|
||||
this._callLocalStorage('setItem', 'channels', this._channels);
|
||||
},
|
||||
/**
|
||||
* @return {string}
|
||||
*/
|
||||
getTabId: function () {
|
||||
return this._id;
|
||||
},
|
||||
/**
|
||||
* Tells whether this bus is related to the master tab.
|
||||
*
|
||||
|
||||
@@ -1068,11 +1068,13 @@ class Meeting(models.Model):
|
||||
meeting_attendees |= attendee
|
||||
meeting_partners |= partner
|
||||
|
||||
if meeting_attendees:
|
||||
if meeting_attendees and not self._context.get('detaching'):
|
||||
to_notify = meeting_attendees.filtered(lambda a: a.email != current_user.email)
|
||||
to_notify._send_mail_to_attendees('calendar.calendar_template_meeting_invitation')
|
||||
|
||||
if meeting_attendees:
|
||||
meeting.write({'attendee_ids': [(4, meeting_attendee.id) for meeting_attendee in meeting_attendees]})
|
||||
|
||||
if meeting_partners:
|
||||
meeting.message_subscribe(partner_ids=meeting_partners.ids)
|
||||
|
||||
@@ -1420,7 +1422,7 @@ class Meeting(models.Model):
|
||||
# do not copy the id
|
||||
if data.get('id'):
|
||||
del data['id']
|
||||
return meeting_origin.copy(default=data)
|
||||
return meeting_origin.with_context(detaching=True).copy(default=data)
|
||||
|
||||
@api.multi
|
||||
def action_detach_recurring_event(self):
|
||||
|
||||
@@ -7,6 +7,7 @@ from datetime import datetime, timedelta, time
|
||||
from odoo import fields
|
||||
from odoo.tests.common import TransactionCase
|
||||
import pytz
|
||||
import re
|
||||
|
||||
|
||||
class TestCalendar(TransactionCase):
|
||||
@@ -384,3 +385,60 @@ class TestCalendar(TransactionCase):
|
||||
})
|
||||
|
||||
self.assertEqual(str(activity_id.date_deadline), '2018-10-16')
|
||||
|
||||
def test_event_creation_mail(self):
|
||||
"""
|
||||
Check that mail are sent to the attendees on event creation
|
||||
Check that mail are sent to the added attendees on event edit
|
||||
Check that mail are NOT sent to the attendees when detaching a recurring event
|
||||
"""
|
||||
|
||||
def _test_one_mail_per_attendee(self, m, partners):
|
||||
# check that every attendee receive a (single) mail for the event
|
||||
for partner in partners:
|
||||
mail = self.env['mail.mail'].search([
|
||||
('recipient_ids', 'in', partner.id),
|
||||
('subject', 'like', m.name),
|
||||
])
|
||||
self.assertEqual(len(mail), 1)
|
||||
|
||||
partners = [
|
||||
self.env['res.partner'].create({'name':'testuser0','email': u'bob@example.com'}),
|
||||
self.env['res.partner'].create({'name':'testuser1','email': u'alice@example.com'}),
|
||||
]
|
||||
partner_ids = [(6, False, [p.id for p in partners]),]
|
||||
now = fields.Datetime.now()
|
||||
m = self.CalendarEvent.create({
|
||||
'name': "mailTest1",
|
||||
'allday': False,
|
||||
'rrule': u'FREQ=DAILY;INTERVAL=1;COUNT=5',
|
||||
'duration': 0.5,
|
||||
'partner_ids': partner_ids,
|
||||
'start': fields.Datetime.to_string(now + timedelta(days=10)),
|
||||
'stop': fields.Datetime.to_string(now + timedelta(days=15)),
|
||||
})
|
||||
|
||||
# every partner should have 1 mail sent
|
||||
_test_one_mail_per_attendee(self, m, partners)
|
||||
|
||||
# adding more partners to the event
|
||||
partners.extend([
|
||||
self.env['res.partner'].create({'name':'testuser2','email': u'marc@example.com'}),
|
||||
self.env['res.partner'].create({'name':'testuser3','email': u'carl@example.com'}),
|
||||
self.env['res.partner'].create({'name':'testuser4','email': u'alain@example.com'}),
|
||||
])
|
||||
partner_ids = [(6, False, [p.id for p in partners]),]
|
||||
m.write({'partner_ids': partner_ids})
|
||||
|
||||
# more email should be sent
|
||||
_test_one_mail_per_attendee(self, m, partners)
|
||||
|
||||
# calculate virtualid to detach one event
|
||||
virtid = str(m.id) + '-' + ''.join(re.split('[\D]', fields.Datetime.to_string(now + timedelta(days=12))))
|
||||
|
||||
# detaching a virtual event in the chain
|
||||
self.env['calendar.event'].browse(virtid).detach_recurring_event(values={'active':False})
|
||||
|
||||
# since the detach actually create an event in the backend
|
||||
# we check that no mail notifications are sent to the attendees
|
||||
_test_one_mail_per_attendee(self, m, partners)
|
||||
|
||||
@@ -202,7 +202,7 @@
|
||||
|
||||
<page name="page_invitations" string="Invitations" groups="base.group_no_one">
|
||||
<button name="action_sendmail" type="object" string="Send mail" icon="fa-envelope" class="oe_link"/>
|
||||
<field name="attendee_ids" widget="one2many" >
|
||||
<field name="attendee_ids" widget="one2many" mode="tree,kanban">
|
||||
<tree string="Invitation details" editable="top" create="false" delete="false">
|
||||
<field name="partner_id" />
|
||||
<field name="state" />
|
||||
@@ -212,6 +212,27 @@
|
||||
<button name="do_accept" string="Accept" states="needsAction,tentative,declined" type="object" icon="fa-check text-success"/>
|
||||
<button name="do_decline" string="Decline" states="needsAction,tentative,accepted" type="object" icon="fa-times-circle text-danger"/>
|
||||
</tree>
|
||||
<kanban class="o_kanban_mobile" create="false" delete="false">
|
||||
<field name="partner_id" />
|
||||
<field name="state" />
|
||||
<field name="email" widget="email"/>
|
||||
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="d-flex flex-column justify-content-between">
|
||||
<field name="partner_id"/>
|
||||
<field name="email" widget="email"/>
|
||||
<span>Status: <field name="state" /></span>
|
||||
|
||||
<div class="text-right">
|
||||
<button name="do_tentative" states="needsAction,declined,accepted" string="Uncertain" type="object" class="btn fa fa-asterisk"/>
|
||||
<button name="do_accept" states="needsAction,tentative,declined" string="Accept" type="object" class="btn fa fa-check text-success"/>
|
||||
<button name="do_decline" states="needsAction,tentative,accepted" string="Decline" type="object" class="btn fa fa-times-circle text-danger"/>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</page>
|
||||
<page name="page_misc" string="Misc" groups="base.group_no_one">
|
||||
|
||||
@@ -7,7 +7,8 @@ import logging
|
||||
import re
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo import api, fields, models, tools, _, _geoip_resolver
|
||||
import odoo
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.addons.iap import jsonrpc
|
||||
from odoo.addons.crm.models import crm_stage
|
||||
from odoo.exceptions import ValidationError
|
||||
@@ -75,7 +76,7 @@ class CRMRevealRule(models.Model):
|
||||
|
||||
@api.model
|
||||
def _assert_geoip(self):
|
||||
if not _geoip_resolver:
|
||||
if not odoo._geoip_resolver:
|
||||
message = _('Lead Generation requires a GeoIP resolver which could not be found on your system. Please consult https://pypi.org/project/GeoIP/.')
|
||||
self.env['bus.bus'].sendone(
|
||||
(self._cr.dbname, 'res.partner', self.env.user.partner_id.id),
|
||||
|
||||
@@ -49,6 +49,7 @@ class SaleOrder(models.Model):
|
||||
self._remove_delivery_line()
|
||||
|
||||
for order in self:
|
||||
order.carrier_id = carrier.id
|
||||
order._create_delivery_line(carrier, amount, price_unit_in_description=self.carrier_id.invoice_policy == 'real')
|
||||
return True
|
||||
|
||||
|
||||
@@ -74,6 +74,5 @@ class ChooseDeliveryCarrier(models.TransientModel):
|
||||
}
|
||||
|
||||
def button_confirm(self):
|
||||
self.order_id.carrier_id = self.carrier_id
|
||||
self.order_id.delivery_message = self.delivery_message
|
||||
self.order_id.set_delivery_line(self.carrier_id, self.delivery_price)
|
||||
|
||||
@@ -383,9 +383,9 @@ class Challenge(models.Model):
|
||||
|
||||
# the goal is initialised over the limit to make sure we will compute it at least once
|
||||
if line.condition == 'higher':
|
||||
values['current'] = line.target_goal - 1
|
||||
values['current'] = min(line.target_goal - 1, 0)
|
||||
else:
|
||||
values['current'] = line.target_goal + 1
|
||||
values['current'] = max(line.target_goal + 1, 0)
|
||||
|
||||
if challenge.remind_update_delay:
|
||||
values['remind_update_delay'] = challenge.remind_update_delay
|
||||
@@ -743,7 +743,7 @@ class Challenge(models.Model):
|
||||
all_reached = False
|
||||
if goal.definition_condition == 'higher':
|
||||
# can be over 100
|
||||
total_completeness += 100.0 * goal.current / goal.target_goal
|
||||
total_completeness += (100.0 * goal.current / goal.target_goal) if goal.target_goal else 0
|
||||
elif goal.state == 'reached':
|
||||
# for lower goals, can not get percentage so 0 or 100
|
||||
total_completeness += 100
|
||||
|
||||
@@ -196,7 +196,7 @@ class Goal(models.Model):
|
||||
if goal.current >= goal.target_goal:
|
||||
goal.completeness = 100.0
|
||||
else:
|
||||
goal.completeness = round(100.0 * goal.current / goal.target_goal, 2)
|
||||
goal.completeness = round(100.0 * goal.current / goal.target_goal, 2) if goal.target_goal else 0
|
||||
elif goal.current < goal.target_goal:
|
||||
# a goal 'lower than' has only two values possible: 0 or 100%
|
||||
goal.completeness = 100.0
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<record id="goals_from_challenge_act" model="ir.actions.act_window">
|
||||
<field name="res_model">gamification.goal</field>
|
||||
<field name="name">Related Goals</field>
|
||||
<field name="view_mode">kanban,tree</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="context">{'search_default_group_by_definition': True, 'search_default_inprogress': True, 'search_default_challenge_id': active_id, 'default_challenge_id': active_id}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_empty_folder">
|
||||
@@ -61,7 +61,7 @@
|
||||
<label for="user_domain" class="oe_edit_only" string="Assign Challenge To"/>
|
||||
<div>
|
||||
<field name="user_domain" widget="domain" options="{'model': 'res.users'}" />
|
||||
<field name="user_ids" groups="base.group_no_one" widget="many2many_tags" readonly="True" />
|
||||
<field name="user_ids" widget="many2many_tags" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -171,7 +171,7 @@
|
||||
<record id="challenge_list_action" model="ir.actions.act_window">
|
||||
<field name="name">Challenges</field>
|
||||
<field name="res_model">gamification.challenge</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="view_mode">kanban,tree</field>
|
||||
<field name="context">{'search_default_inprogress':True, 'default_inprogress':True}</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
<field name="completeness"/>
|
||||
<field name="state"/>
|
||||
<field name="target_goal"/>
|
||||
<field name="definition_description"/>
|
||||
<field name="definition_condition"/>
|
||||
<field name="definition_suffix"/>
|
||||
<field name="definition_display"/>
|
||||
@@ -143,10 +144,12 @@
|
||||
<field name="last_update"/>
|
||||
<templates>
|
||||
<t t-name="kanban-tooltip">
|
||||
<field name="definition_description"/>
|
||||
<ul class="oe_kanban_tooltip">
|
||||
<li><t t-esc="record.definition_description.value" /></li>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
<div t-attf-class="oe_kanban_card o_kanban_gamification #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
|
||||
<div t-attf-class="oe_kanban_card oe_kanban_global_click o_kanban_gamification #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value < record.last_update.raw_value & record.state.raw_value == 'reached' ? 'oe_kanban_color_5' : ''}">
|
||||
<div class="o_kanban_content">
|
||||
<p><strong><h4 class="oe_goal_name text-center" tooltip="kanban-tooltip"><field name="definition_id" /></h4></strong></p>
|
||||
<div class="float-left">
|
||||
|
||||
@@ -30,14 +30,19 @@ class HrExpense(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_employee_id_domain(self):
|
||||
|
||||
res= [('id', '=', 0)] # Nothing accepted by domain, by default
|
||||
if self.user_has_groups('hr_expense.group_hr_expense_user') or self.user_has_groups('account.group_account_user'):
|
||||
res = [] # Then, domain accepts everything
|
||||
elif self.user_has_groups('hr_expense.group_hr_expense_team_approver') and self.env.user.employee_ids:
|
||||
employee = self.env.user.employee_ids[0]
|
||||
res = ['|', '|', '|', ('department_id.manager_id.id', '=', employee.id),
|
||||
('parent_id.id', '=', employee.id), ('expense_manager_id.id', '=', employee.user_id.id), ('id', '=', employee.id)]
|
||||
user = self.env.user
|
||||
employee = user.employee_ids[0]
|
||||
res = [
|
||||
'|', '|', '|',
|
||||
('department_id.manager_id', '=', employee.id),
|
||||
('parent_id', '=', employee.id),
|
||||
('id', '=', employee.id),
|
||||
('expense_manager_id', '=', user.id),
|
||||
]
|
||||
elif self.env.user.employee_ids:
|
||||
employee = self.env.user.employee_ids[0]
|
||||
res = [('id', '=', employee.id)]
|
||||
|
||||
@@ -187,6 +187,70 @@ class TestAccountEntry(TestExpenseCommon):
|
||||
self.assertEquals(expense.total_amount, 9876.0)
|
||||
self.assertTrue(expense.employee_id in user_demo.employee_ids)
|
||||
|
||||
def test_partial_payment_multiexpense(self):
|
||||
bank_journal = self.env['account.journal'].create({
|
||||
'name': 'Payment Journal',
|
||||
'code': 'PAY',
|
||||
'type': 'bank',
|
||||
'company_id': self.env.company.id,
|
||||
})
|
||||
|
||||
outbound_pay_method = self.env['account.payment.method'].create({
|
||||
'name': 'outbound',
|
||||
'code': 'out',
|
||||
'payment_type': 'outbound',
|
||||
})
|
||||
|
||||
expense = self.env['hr.expense.sheet'].create({
|
||||
'name': 'Expense for John Smith',
|
||||
'employee_id': self.employee.id,
|
||||
})
|
||||
expense_line = self.env['hr.expense'].create({
|
||||
'name': 'Car Travel Expenses',
|
||||
'employee_id': self.employee.id,
|
||||
'product_id': self.product_expense.id,
|
||||
'unit_amount': 200.00,
|
||||
'tax_ids': [(6, 0, [self.tax.id])],
|
||||
'sheet_id': expense.id,
|
||||
'analytic_account_id': self.analytic_account.id,
|
||||
})
|
||||
expense_line.copy({
|
||||
'sheet_id': expense.id
|
||||
})
|
||||
expense.approve_expense_sheets()
|
||||
expense.action_sheet_move_create()
|
||||
|
||||
exp_move_lines = expense.account_move_id.line_ids
|
||||
payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEquals(len(payable_move_lines), 2)
|
||||
|
||||
WizardRegister = self.env['hr.expense.sheet.register.payment.wizard'].with_context(active_ids=expense.ids)
|
||||
|
||||
register_pay1 = WizardRegister.create({
|
||||
'journal_id': bank_journal.id,
|
||||
'payment_method_id': outbound_pay_method.id,
|
||||
'amount': 300,
|
||||
})
|
||||
register_pay1.expense_post_payment()
|
||||
|
||||
exp_move_lines = expense.account_move_id.line_ids
|
||||
payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEquals(len(payable_move_lines.filtered(lambda l: l.reconciled)), 1)
|
||||
|
||||
register_pay2 = WizardRegister.create({
|
||||
'journal_id': bank_journal.id,
|
||||
'payment_method_id': outbound_pay_method.id,
|
||||
'amount': 100,
|
||||
})
|
||||
register_pay2.expense_post_payment()
|
||||
exp_move_lines = expense.account_move_id.line_ids
|
||||
payable_move_lines = exp_move_lines.filtered(lambda l: l.account_id.internal_type == 'payable')
|
||||
self.assertEquals(len(payable_move_lines.filtered(lambda l: l.reconciled)), 2)
|
||||
|
||||
full_reconcile = payable_move_lines.mapped('full_reconcile_id')
|
||||
self.assertEquals(len(full_reconcile), 1)
|
||||
|
||||
|
||||
class TestExpenseRights(TestExpenseCommon):
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -109,7 +109,7 @@ class HrExpenseSheetRegisterPaymentWizard(models.TransientModel):
|
||||
# Reconcile the payment and the expense, i.e. lookup on the payable account move lines
|
||||
account_move_lines_to_reconcile = self.env['account.move.line']
|
||||
for line in payment.move_line_ids + expense_sheet.account_move_id.line_ids:
|
||||
if line.account_id.internal_type == 'payable':
|
||||
if line.account_id.internal_type == 'payable' and not line.reconciled:
|
||||
account_move_lines_to_reconcile |= line
|
||||
account_move_lines_to_reconcile.reconcile()
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import logging
|
||||
import math
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from datetime import datetime, time
|
||||
from pytz import timezone, UTC
|
||||
|
||||
@@ -18,6 +20,9 @@ from odoo.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Used to agglomerate the attendances in order to find the hour_from and hour_to
|
||||
# See _onchange_request_parameters
|
||||
DummyAttendance = namedtuple('DummyAttendance', 'hour_from, hour_to, dayofweek, day_period')
|
||||
|
||||
class HolidaysRequest(models.Model):
|
||||
""" Leave Requests Access specifications
|
||||
@@ -286,12 +291,17 @@ class HolidaysRequest(models.Model):
|
||||
return
|
||||
|
||||
domain = [('calendar_id', '=', self.employee_id.resource_calendar_id.id or self.env.company.resource_calendar_id.id)]
|
||||
attendances = self.env['resource.calendar.attendance'].search(domain, order='dayofweek, day_period DESC')
|
||||
attendances = self.env['resource.calendar.attendance'].read_group(domain, ['ids:array_agg(id)', 'hour_from:min(hour_from)', 'hour_to:max(hour_to)', 'dayofweek', 'day_period'], ['dayofweek', 'day_period'], lazy=False)
|
||||
|
||||
# Must be sorted by dayofweek ASC and day_period DESC
|
||||
attendances = sorted([DummyAttendance(group['hour_from'], group['hour_to'], group['dayofweek'], group['day_period']) for group in attendances], key=lambda att: (att.dayofweek, att.day_period != 'morning'))
|
||||
|
||||
default_value = DummyAttendance(0, 0, 0, 'morning')
|
||||
|
||||
# find first attendance coming after first_day
|
||||
attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.weekday()), attendances[0])
|
||||
attendance_from = next((att for att in attendances if int(att.dayofweek) >= self.request_date_from.weekday()), attendances[0] if attendances else default_value)
|
||||
# find last attendance coming before last_day
|
||||
attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1])
|
||||
attendance_to = next((att for att in reversed(attendances) if int(att.dayofweek) <= self.request_date_to.weekday()), attendances[-1] if attendances else default_value)
|
||||
|
||||
if self.request_unit_half:
|
||||
if self.request_date_from_period == 'am':
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_access_rights
|
||||
from . import test_automatic_leave_dates
|
||||
from . import test_holidays_flow
|
||||
from . import test_hr_leave_type
|
||||
from . import test_accrual_allocations
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import date, datetime
|
||||
|
||||
from odoo.tests.common import Form, tagged
|
||||
|
||||
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
|
||||
|
||||
|
||||
@tagged('prout')
|
||||
class TestAutomaticLeaveDates(TestHrHolidaysBase):
|
||||
def setUp(self):
|
||||
super(TestAutomaticLeaveDates, self).setUp()
|
||||
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Automatic Test',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
'validity_start': False,
|
||||
})
|
||||
|
||||
def test_no_attendances(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'No Attendances',
|
||||
'attendance_ids': [(5, 0, 0)],
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 0)
|
||||
|
||||
def test_single_attendance_on_morning_and_afternoon(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'simple morning + afternoon',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'monday morning',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'monday afternoon',
|
||||
'hour_from': 13,
|
||||
'hour_to': 17,
|
||||
'day_period': 'afternoon',
|
||||
'dayofweek': '0',
|
||||
})]
|
||||
})
|
||||
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
leave_form.request_date_from_period = 'pm'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
def test_multiple_attendance_on_morning(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'multi morning',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'monday morning 1',
|
||||
'hour_from': 8,
|
||||
'hour_to': 10,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'monday morning 2',
|
||||
'hour_from': 10.25,
|
||||
'hour_to': 12.25,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
}),
|
||||
(0, 0, {
|
||||
'name': 'monday afternoon',
|
||||
'hour_from': 13,
|
||||
'hour_to': 17,
|
||||
'day_period': 'afternoon',
|
||||
'dayofweek': '0',
|
||||
})]
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
leave_form.request_date_from_period = 'pm'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, .5)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 4)
|
||||
|
||||
def test_attendance_on_morning(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'Morning only',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'Monday All day',
|
||||
'hour_from': 8,
|
||||
'hour_to': 16,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
})],
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
# Ask for morning
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 8)
|
||||
|
||||
# Ask for afternoon
|
||||
leave_form.request_date_from_period = 'pm'
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 1)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 8)
|
||||
|
||||
def test_attendance_next_day(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'auto next day',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'tuesday morning',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '1',
|
||||
})]
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 2)
|
||||
leave_form.request_date_to = date(2019, 9, 2)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 0)
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 2, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 2, 10, 0, 0))
|
||||
|
||||
def test_attendance_previous_day(self):
|
||||
calendar = self.env['resource.calendar'].create({
|
||||
'name': 'auto next day',
|
||||
'attendance_ids': [(5, 0, 0),
|
||||
(0, 0, {
|
||||
'name': 'monday morning',
|
||||
'hour_from': 8,
|
||||
'hour_to': 12,
|
||||
'day_period': 'morning',
|
||||
'dayofweek': '0',
|
||||
})]
|
||||
})
|
||||
employee = self.employee_emp
|
||||
employee.resource_calendar_id = calendar
|
||||
|
||||
with Form(self.env['hr.leave']) as leave_form:
|
||||
leave_form.employee_id = employee
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.request_date_from = date(2019, 9, 3)
|
||||
leave_form.request_date_to = date(2019, 9, 3)
|
||||
leave_form.request_unit_half = True
|
||||
leave_form.request_date_from_period = 'am'
|
||||
|
||||
|
||||
self.assertEqual(leave_form.number_of_days_display, 0)
|
||||
self.assertEqual(leave_form.number_of_hours_display, 0)
|
||||
self.assertEqual(leave_form.date_from, datetime(2019, 9, 3, 6, 0, 0))
|
||||
self.assertEqual(leave_form.date_to, datetime(2019, 9, 3, 10, 0, 0))
|
||||
@@ -286,6 +286,7 @@
|
||||
<field name="user_email"/>
|
||||
<field name="partner_name"/>
|
||||
<field name="type_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="department_id"/>
|
||||
<field name="message_needaction_counter"/>
|
||||
|
||||
@@ -103,7 +103,6 @@ class LivechatController(http.Controller):
|
||||
@http.route('/im_livechat/feedback', type='json', auth='public', cors="*")
|
||||
def feedback(self, uuid, rate, reason=None, **kwargs):
|
||||
Channel = request.env['mail.channel']
|
||||
Rating = request.env['rating.rating']
|
||||
channel = Channel.sudo().search([('uuid', '=', uuid)], limit=1)
|
||||
if channel:
|
||||
# limit the creation : only ONE rating per session
|
||||
@@ -124,7 +123,11 @@ class LivechatController(http.Controller):
|
||||
# if logged in user, set its partner on rating
|
||||
values['partner_id'] = request.env.user.partner_id.id if request.session.uid else False
|
||||
# create the rating
|
||||
rating = Rating.sudo().create(values)
|
||||
|
||||
channel.write({
|
||||
'rating_ids': [(0, False, values)]
|
||||
})
|
||||
rating = channel.rating_ids[0]
|
||||
else:
|
||||
rating = channel.rating_ids[0]
|
||||
rating.write(values)
|
||||
|
||||
@@ -123,6 +123,14 @@ var LivechatButton = Widget.extend({
|
||||
serverURL: this._serverURL,
|
||||
});
|
||||
var message = new WebsiteLivechatMessage(this, data, options);
|
||||
|
||||
var hasAlreadyMessage = _.some(this._messages, function (msg) {
|
||||
return message.id === msg.id;
|
||||
});
|
||||
if (hasAlreadyMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._livechat) {
|
||||
this._livechat.addMessage(message);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="uae_sale_tax_5_dubai" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Dubai)</field>
|
||||
<field name="name">VAT 5% (Dubai)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -36,11 +36,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_abu_dhabi" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Abu Dhabi)</field>
|
||||
<field name="name">VAT 5% (Abu Dhabi)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -71,11 +71,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_sharjah" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Sharjah)</field>
|
||||
<field name="name">VAT 5% (Sharjah)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -106,11 +106,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_ajman" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Ajman)</field>
|
||||
<field name="name">VAT 5% (Ajman)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -141,11 +141,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_umm_al_quwain" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Umm Al Quwain)</field>
|
||||
<field name="name">VAT 5% (Umm Al Quwain)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -176,11 +176,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_ras_al_khaima" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Ras Al-Khaima)</field>
|
||||
<field name="name">VAT 5% (Ras Al-Khaima)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -211,11 +211,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_5_fujairah" model="account.tax.template">
|
||||
<field name="name">TVA 5% (Fujairah)</field>
|
||||
<field name="name">VAT 5% (Fujairah)</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -277,11 +277,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_sale_tax_0" model="account.tax.template">
|
||||
<field name="name">TVA 0%</field>
|
||||
<field name="name">VAT 0%</field>
|
||||
<field name="type_tax_use">sale</field>
|
||||
<field name="amount">0</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 0%</field>
|
||||
<field name="description">VAT 0%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_0"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -693,11 +693,11 @@
|
||||
</record>
|
||||
<!-- purchase taxes -->
|
||||
<record id="uae_purchase_tax_5" model="account.tax.template">
|
||||
<field name="name">TVA 5%</field>
|
||||
<field name="name">VAT 5%</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="description">VAT 5%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -757,11 +757,11 @@
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_0" model="account.tax.template">
|
||||
<field name="name">TVA 0%</field>
|
||||
<field name="name">VAT 0%</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount">0</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 0%</field>
|
||||
<field name="description">VAT 0%</field>
|
||||
<field name="tax_group_id" ref="ae_tax_group_0"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -817,28 +817,6 @@
|
||||
}),
|
||||
]"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_5_child_global" model="account.tax.template">
|
||||
<field name="name">TVA 5%</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="account_id" ref="uae_account_3726"/>
|
||||
<field name="refund_account_id" ref="uae_account_3726"/>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_5_child_reverse" model="account.tax.template">
|
||||
<field name="name">Reverse Charge TVA +5%(Purchase)</field>
|
||||
<field name="type_tax_use">none</field>
|
||||
<field name="amount">5</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="description">TVA 5%</field>
|
||||
<field name="account_id" ref="uae_account_3726"/>
|
||||
<field name="refund_account_id" ref="uae_account_3726"/>
|
||||
<field name="tax_group_id" ref="ae_tax_group_5"/>
|
||||
<field name="chart_template_id" ref="uae_chart_template_standard"/>
|
||||
</record>
|
||||
<record id="uae_purchase_tax_reverse_charge" model="account.tax.template">
|
||||
<field name="name">Reverse Charge Provision</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
|
||||
@@ -191,3 +191,17 @@ class AccountMove(models.Model):
|
||||
if self.env.context.get('l10n_ch_mark_isr_as_sent'):
|
||||
self.filtered(lambda inv: not inv.l10n_ch_isr_sent).write({'l10n_ch_isr_sent': True})
|
||||
return super(AccountMove, self.with_context(mail_post_autofollow=True)).message_post(**kwargs)
|
||||
|
||||
def _get_reference_ch_invoice(self):
|
||||
""" This sets ISR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
`Payment Reference` of the invoice when invoice's journal is using Switzerland's communication standard
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.l10n_ch_isr_number_spaced
|
||||
|
||||
def _get_reference_ch_partner(self):
|
||||
""" This sets ISR reference number which is generated based on customer's `Bank Account` and set it as
|
||||
`Payment Reference` of the invoice when invoice's journal is using Switzerland's communication standard
|
||||
"""
|
||||
self.ensure_one()
|
||||
return self.l10n_ch_isr_number_spaced
|
||||
|
||||
@@ -14,6 +14,7 @@ class AccountJournal(models.Model):
|
||||
|
||||
# creation of bank journals by giving the account number, allow craetion of the
|
||||
l10n_ch_postal = fields.Char(related='bank_account_id.l10n_ch_postal', readonly=False)
|
||||
invoice_reference_model = fields.Selection(selection_add=[('ch', 'Switzerland')])
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
|
||||
@@ -445,7 +445,6 @@
|
||||
<field name="description">P_IVA21_SP_IN</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
@@ -551,7 +550,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -718,7 +716,6 @@
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/>
|
||||
<field name="name">IVA 4% Adquisición de servicios extracomunitarios</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_4"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -770,7 +767,6 @@
|
||||
<field name="description">P_IVA10_SP_EX</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición de servicios extracomunitarios</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_10"/>
|
||||
@@ -824,7 +820,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_12_13'), ref('mod_303_14_15_purchase'), ref('mod_303_28_29'), ref('mod_303_40_41')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
@@ -874,7 +869,6 @@
|
||||
<field name="amount" eval="4"/>
|
||||
<field name="description">P_IVA4_IC_BC</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_36_37'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 4% Adquisición Intracomunitario. Bienes corrientes</field>
|
||||
@@ -930,7 +924,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 4% Adquisición Intracomunitario. Bienes de inversión</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_4"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -979,7 +972,6 @@
|
||||
<field name="amount" eval="10"/>
|
||||
<field name="description">P_IVA10_IC_BC</field>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_36_37'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición Intracomunitario. Bienes corrientes</field>
|
||||
@@ -1034,7 +1026,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición Intracomunitario. Bienes de inversión</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_38_39'), ref('mod_303_40_41'), ref('mod_303_10_11'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -3077,7 +3068,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 10% Adquisición de servicios intracomunitarios</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_10"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -3129,7 +3119,6 @@
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="name">IVA 4% Adquisición de servicios intracomunitarios</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_10_11'), ref('mod_303_14_15_purchase'), ref('mod_303_36_37'), ref('mod_303_40_41')])]"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_4"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
(0,0, {
|
||||
@@ -3403,7 +3392,6 @@
|
||||
<field name="description">P_IVA4_ISP</field>
|
||||
<field name="name">IVA 4% Compra con Inversión del Sujeto Pasivo Nacional</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="4"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
@@ -3455,7 +3443,6 @@
|
||||
<field name="description">P_IVA10_ISP</field>
|
||||
<field name="name">IVA 10% Compra con Inversión del Sujeto Pasivo Nacional</field>
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="10"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
@@ -3509,7 +3496,6 @@
|
||||
<field name="type_tax_use">purchase</field>
|
||||
<field name="amount_type">percent</field>
|
||||
<field name="amount" eval="21"/>
|
||||
<field name="tag_ids" eval="[(6, False, [ref('mod_303_28_29'), ref('mod_303_40_41'), ref('mod_303_12_13'), ref('mod_303_14_15_purchase')])]"/>
|
||||
<field name="chart_template_id" ref="l10n_es.account_chart_template_common"/>
|
||||
<field name="tax_group_id" ref="tax_group_iva_21"/>
|
||||
<field name="invoice_repartition_line_ids" eval="[(5, 0, 0),
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
<xpath expr="//table[@name='invoice_line_table']" position="before">
|
||||
<t t-if="o.company_id.country_id.code == 'IN'">
|
||||
<!-- Tag wise Tax Calculation -->
|
||||
<t t-set="tag_cgst_id" t-value="env.ref('l10n_in.cgst_tag_tax').id"/>
|
||||
<t t-set="tag_sgst_id" t-value="env.ref('l10n_in.sgst_tag_tax').id"/>
|
||||
<t t-set="tag_igst_id" t-value="env.ref('l10n_in.igst_tag_tax').id"/>
|
||||
<t t-set="tag_cess_id" t-value="env.ref('l10n_in.cess_tag_tax').id"/>
|
||||
<t t-set="tax_map" t-value="o.invoice_line_ids.mapped('tax_ids.tag_ids.id')"/>
|
||||
<t t-set="tag_cgst_id" t-value="env.ref('l10n_in.tax_report_line_cgst').id"/>
|
||||
<t t-set="tag_sgst_id" t-value="env.ref('l10n_in.tax_report_line_sgst').id"/>
|
||||
<t t-set="tag_igst_id" t-value="env.ref('l10n_in.tax_report_line_igst').id"/>
|
||||
<t t-set="tag_cess_id" t-value="env.ref('l10n_in.tax_report_line_cess').id"/>
|
||||
<t t-set="tax_map" t-value="o.tax_line_ids.mapped('tag_ids.tax_report_line_ids').ids"/>
|
||||
<t t-set="is_igst" t-value="tag_igst_id in tax_map"/>
|
||||
<t t-set="is_cgst" t-value="tag_cgst_id in tax_map"/>
|
||||
<t t-set="is_cess" t-value="tag_cess_id in tax_map"/>
|
||||
@@ -43,21 +43,21 @@
|
||||
<t t-if="o.company_id.country_id.code == 'IN'">
|
||||
<td class="text-right" t-if="is_cgst">
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_cgst_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_cgst_id in tax_data['report_line_ids']">
|
||||
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-right" t-if="is_cgst">
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_sgst_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_sgst_id in tax_data['report_line_ids']">
|
||||
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
</td>
|
||||
<td class="text-right" t-if="is_igst">
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_igst_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_igst_id in tax_data['report_line_ids']">
|
||||
<t t-esc="tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
@@ -65,7 +65,7 @@
|
||||
<td class="text-right" t-if="is_cess">
|
||||
<t t-set="total_cess" t-value="0.0"/>
|
||||
<t t-foreach="tax_datas[line.id]" t-as="tax_data">
|
||||
<t t-if="tag_cess_id in tax_data['tag_ids']">
|
||||
<t t-if="tag_cess_id in tax_data['report_line_ids']">
|
||||
<t t-set="total_cess" t-value="total_cess + tax_data['amount']" t-options="{'widget': 'monetary', 'display_currency': o.currency_id}"/>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
@@ -31,10 +31,10 @@ class FetchmailServer(models.Model):
|
||||
l10n_it_is_pec = fields.Boolean('PEC server', help="If PEC Server, only mail from '...@pec.fatturapa.it' will be processed.")
|
||||
l10n_it_last_uid = fields.Integer(string='Last message UID', default=1)
|
||||
|
||||
@api.constrains('l10n_it_is_pec', 'type')
|
||||
@api.constrains('l10n_it_is_pec', 'server_type')
|
||||
def _check_pec(self):
|
||||
for record in self:
|
||||
if record.l10n_it_is_pec and record.type != 'imap':
|
||||
if record.l10n_it_is_pec and record.server_type != 'imap':
|
||||
raise ValidationError(_("PEC mail server must be of type IMAP."))
|
||||
|
||||
@api.multi
|
||||
@@ -43,7 +43,7 @@ class FetchmailServer(models.Model):
|
||||
|
||||
MailThread = self.env['mail.thread']
|
||||
for server in self.filtered(lambda s: s.l10n_it_is_pec):
|
||||
_logger.info('start checking for new emails on %s PEC server %s', server.type, server.name)
|
||||
_logger.info('start checking for new emails on %s PEC server %s', server.server_type, server.name)
|
||||
|
||||
count, failed = 0, 0
|
||||
imap_server = None
|
||||
@@ -81,14 +81,14 @@ class FetchmailServer(models.Model):
|
||||
self._attachment_invoice(msg_txt)
|
||||
new_max_uid = max(new_max_uid, int(uid))
|
||||
except Exception:
|
||||
_logger.info('Failed to process mail from %s server %s.', server.type, server.name, exc_info=True)
|
||||
_logger.info('Failed to process mail from %s server %s.', server.server_type, server.name, exc_info=True)
|
||||
failed += 1
|
||||
self._cr.commit()
|
||||
count += 1
|
||||
server.write({'l10n_it_last_uid': new_max_uid})
|
||||
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.type, server.name, (count - failed), failed)
|
||||
_logger.info("Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", count, server.server_type, server.name, (count - failed), failed)
|
||||
except Exception:
|
||||
_logger.info("General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True)
|
||||
_logger.info("General failure when trying to fetch mail from %s server %s.", server.server_type, server.name, exc_info=True)
|
||||
finally:
|
||||
if imap_server:
|
||||
imap_server.close()
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
from . import models
|
||||
|
||||
def _post_init_hook(cr, registry):
|
||||
_preserve_tag_on_taxes(cr, registry)
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
env.ref('l10n_lu.lu_2011_chart_1').process_coa_translations()
|
||||
|
||||
def _preserve_tag_on_taxes(cr, registry):
|
||||
from odoo.addons.account.models.chart_template import preserve_existing_tags_on_taxes
|
||||
preserve_existing_tags_on_taxes(cr, registry, 'l10n_lu')
|
||||
|
||||
@@ -43,5 +43,5 @@ Notes:
|
||||
'data/account.chart.template.csv',
|
||||
'data/account_chart_template_data.xml',
|
||||
],
|
||||
'post_init_hook': '_preserve_tag_on_taxes',
|
||||
'post_init_hook': '_post_init_hook',
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<odoo>
|
||||
<!-- account chart -->
|
||||
<record id="l10n_kzc_temp_chart" model="account.chart.template">
|
||||
<field name="property_account_receivable_id" ref="pcg_3488"/>
|
||||
<field name="property_account_payable_id" ref="pcg_4488"/>
|
||||
<field name="property_account_receivable_id" ref="pcg_34211"/>
|
||||
<field name="property_account_payable_id" ref="pcg_4411"/>
|
||||
<field name="property_account_income_categ_id" ref="pcg_7111"/>
|
||||
<field name="property_account_expense_categ_id" ref="pcg_1486"/>
|
||||
<field name="property_account_expense_categ_id" ref="pcg_6111"/>
|
||||
<field name="income_currency_exchange_account_id" ref="pcg_733"/>
|
||||
<field name="expense_currency_exchange_account_id" ref="pcg_633"/>
|
||||
</record>
|
||||
|
||||
@@ -1190,13 +1190,15 @@
|
||||
<record id="pcg_34211" model="account.account.template">
|
||||
<field name="name">Clients - catégorie A</field>
|
||||
<field name="code">34211</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_receivable"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_34212" model="account.account.template">
|
||||
<field name="name">Clients - catégorie B</field>
|
||||
<field name="code">34212</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_receivable"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_3423" model="account.account.template">
|
||||
@@ -1370,7 +1372,7 @@
|
||||
<record id="pcg_3488" model="account.account.template">
|
||||
<field name="name">Divers débiteurs</field>
|
||||
<field name="code">3488</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_receivable"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_assets"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
@@ -1515,7 +1517,8 @@
|
||||
<record id="pcg_4411" model="account.account.template">
|
||||
<field name="name">Fournisseurs</field>
|
||||
<field name="code">4411</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_payable"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_4413" model="account.account.template">
|
||||
@@ -1761,7 +1764,7 @@
|
||||
<record id="pcg_4488" model="account.account.template">
|
||||
<field name="name">Divers créanciers</field>
|
||||
<field name="code">4488</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_payable"/>
|
||||
<field name="user_type_id" ref="account.data_account_type_current_liabilities"/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
<field name="reconcile" eval='True'/>
|
||||
</record>
|
||||
@@ -2065,6 +2068,12 @@
|
||||
<field name="user_type_id" ref="account.data_account_type_expenses"/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_61263" model="account.account.template">
|
||||
<field name="name">Achats des prestations de service</field>
|
||||
<field name="code">61263</field>
|
||||
<field name="user_type_id" ref="account.data_account_type_expenses"/>
|
||||
<field name="chart_template_id" ref="l10n_kzc_temp_chart"/>
|
||||
</record>
|
||||
<record id="pcg_6128" model="account.account.template">
|
||||
<field name="name">Achats de matières et de fournitures des exercices antérieurs</field>
|
||||
<field name="code">6128</field>
|
||||
|
||||
@@ -12,6 +12,19 @@ _logger = logging.getLogger(__name__)
|
||||
class AccountChartTemplate(models.Model):
|
||||
_inherit = 'account.chart.template'
|
||||
|
||||
def load_for_current_company(self, sale_tax_rate, purchase_tax_rate):
|
||||
res = super(AccountChartTemplate, self).load_for_current_company(sale_tax_rate, purchase_tax_rate)
|
||||
# Copy chart of account translations when loading chart of account
|
||||
for chart_template in self.filtered('spoken_languages'):
|
||||
external_id = self.env['ir.model.data'].search([
|
||||
('model', '=', 'account.chart.template'),
|
||||
('res_id', '=', chart_template.id),
|
||||
], order='id', limit=1)
|
||||
module = external_id and self.env.ref('base.module_' + external_id.module)
|
||||
if module and module.state == 'installed':
|
||||
chart_template.process_coa_translations()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def process_translations(self, langs, in_field, in_ids, out_ids):
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,7 @@ This is the latest UK Odoo localisation necessary to run Odoo accounting for UK
|
||||
- InfoLogic UK counties listing
|
||||
- a few other adaptations""",
|
||||
'author': 'SmartMode LTD',
|
||||
'website': 'http://www.smartmode.co.uk',
|
||||
'website': 'https://www.odoo.com/page/accounting',
|
||||
'depends': [
|
||||
'account',
|
||||
'base_iban',
|
||||
|
||||
@@ -117,9 +117,11 @@ var LunchKanbanController = KanbanController.extend({
|
||||
* @private
|
||||
*/
|
||||
_updateSearchPanel: function () {
|
||||
var locationId = this.model.getCurrentLocationId();
|
||||
var domain = this.controlPanelDomain.concat([['is_available_at', 'in', [locationId]]]);
|
||||
return this._searchPanel.update({searchDomain: domain});
|
||||
var self = this;
|
||||
this.model.getLocationDomain().then(function (locationDomain) {
|
||||
var domain = self.controlPanelDomain.concat(locationDomain);
|
||||
return self._searchPanel.update({searchDomain: domain});
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
@@ -12,6 +12,7 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
init: function () {
|
||||
this.locationId = false;
|
||||
this.userId = false;
|
||||
this._promInitLocation = null;
|
||||
|
||||
this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -26,15 +27,23 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
getCurrentLocationId: function () {
|
||||
return this.locationId;
|
||||
},
|
||||
/**
|
||||
* @return {Promise} resolved with the location domain
|
||||
*/
|
||||
getLocationDomain: function () {
|
||||
var self = this;
|
||||
return this._initUserLocation().then(function () {
|
||||
return self._buildLocationDomainLeaf() ? [self._buildLocationDomainLeaf()]: [];
|
||||
});
|
||||
},
|
||||
load: function () {
|
||||
var self = this;
|
||||
var args = arguments;
|
||||
var _super = this._super;
|
||||
|
||||
return this._getUserLocation().then(function (locationId) {
|
||||
return this._initUserLocation().then(function () {
|
||||
var params = args[0];
|
||||
self.locationId = locationId;
|
||||
self._addOrUpdate(params.domain, ['is_available_at', 'in', [locationId]]);
|
||||
self._addOrUpdate(params.domain, self._buildLocationDomainLeaf());
|
||||
|
||||
return _super.apply(self, args);
|
||||
});
|
||||
@@ -42,7 +51,7 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
reload: function (id, options) {
|
||||
var domain = options && options.domain || this.localData[id].domain;
|
||||
|
||||
this._addOrUpdate(domain, ['is_available_at', 'in', [this.locationId]]);
|
||||
this._addOrUpdate(domain, this._buildLocationDomainLeaf());
|
||||
options = _.extend(options, {domain: domain});
|
||||
|
||||
return this._super.apply(this, arguments);
|
||||
@@ -53,19 +62,35 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
_addOrUpdate: function (domain, subDomain) {
|
||||
var key = subDomain[0];
|
||||
var index = _.findIndex(domain, function (val) {
|
||||
return val[0] === key;
|
||||
});
|
||||
if (subDomain && subDomain.length) {
|
||||
var key = subDomain[0];
|
||||
var index = _.findIndex(domain, function (val) {
|
||||
return val[0] === key;
|
||||
});
|
||||
|
||||
if (index < 0) {
|
||||
domain.push(subDomain);
|
||||
} else {
|
||||
domain[index] = subDomain;
|
||||
if (index < 0) {
|
||||
domain.push(subDomain);
|
||||
} else {
|
||||
domain[index] = subDomain;
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
|
||||
return domain;
|
||||
},
|
||||
/**
|
||||
* Builds the domain leaf corresponding to the current user's location
|
||||
*
|
||||
* @private
|
||||
* @return {Array}
|
||||
*/
|
||||
_buildLocationDomainLeaf: function () {
|
||||
var locationId = this.getCurrentLocationId();
|
||||
if (locationId) {
|
||||
return ['is_available_at', 'in', [locationId]];
|
||||
}
|
||||
},
|
||||
_getUserLocation: function () {
|
||||
return this._rpc({
|
||||
route: '/lunch/user_location_get',
|
||||
@@ -74,18 +99,34 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
},
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Gets the user location once.
|
||||
* Can be triggered from anywhere
|
||||
* Useful to inject the location domain in the search panel
|
||||
*
|
||||
* @private
|
||||
* @return {Promise}
|
||||
*/
|
||||
_initUserLocation: function () {
|
||||
var self = this;
|
||||
if (!this._promInitLocation) {
|
||||
this._promInitLocation = new Promise(function (resolve) {
|
||||
self._getUserLocation().then(function (locationId) {
|
||||
self.locationId = locationId;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
return this._promInitLocation;
|
||||
},
|
||||
_updateLocation: function (locationId) {
|
||||
this.locationId = locationId;
|
||||
return Promise.resolve();
|
||||
},
|
||||
_updateUser: function (userId) {
|
||||
var self = this;
|
||||
|
||||
this.userId = userId;
|
||||
|
||||
return this._getUserLocation().then(function (locationId) {
|
||||
self.locationId = locationId;
|
||||
});
|
||||
this._promInitLocation = null;
|
||||
return this._initUserLocation();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,26 @@ var LunchKanbanView = KanbanView.extend({
|
||||
Renderer: LunchKanbanRenderer,
|
||||
}),
|
||||
display_name: _lt('Lunch Kanban'),
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// Private
|
||||
//--------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Injects an additional domain in the search panel
|
||||
*
|
||||
* @private
|
||||
* @override
|
||||
*/
|
||||
_createSearchPanel: function (parent) {
|
||||
var self = this;
|
||||
var model = this.getModel(parent);
|
||||
var _super = this._super.bind(this);
|
||||
return model.getLocationDomain().then(function (domain) {
|
||||
self.loadParams.domain = self.loadParams.domain.concat(domain);
|
||||
return _super(parent);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
view_registry.add('lunch_kanban', LunchKanbanView);
|
||||
|
||||
@@ -211,6 +211,56 @@ QUnit.module('LunchKanbanView', {
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('search panel domain location', async function (assert) {
|
||||
assert.expect(10);
|
||||
const locationId = this.data['lunch.location'].records[0].id;
|
||||
const regularInfos = _.extend({}, this.regularInfos);
|
||||
|
||||
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) {
|
||||
assert.step(route);
|
||||
|
||||
if (route.startsWith('/lunch')) {
|
||||
return mockLunchRPC({
|
||||
infos: regularInfos,
|
||||
userLocation: locationId,
|
||||
}).apply(this, arguments);
|
||||
}
|
||||
if (args.method === 'search_panel_select_multi_range') {
|
||||
assert.deepEqual(args.kwargs.search_domain, [["is_available_at", "in", [locationId]]],
|
||||
'The initial domain of the search panel must contain the user location');
|
||||
}
|
||||
if (route === '/web/dataset/search_read') {
|
||||
assert.deepEqual(args.domain, [["is_available_at", "in", [locationId]]],
|
||||
'The domain for fetching actual data should be correct');
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
});
|
||||
assert.verifySteps([
|
||||
'/lunch/user_location_get',
|
||||
'/web/dataset/call_kw/product/search_panel_select_multi_range',
|
||||
'/web/dataset/call_kw/product/search_panel_select_multi_range',
|
||||
'/web/dataset/search_read',
|
||||
'/lunch/infos',
|
||||
'/web/dataset/call_kw/ir.model.data/xmlid_to_res_id',
|
||||
])
|
||||
|
||||
kanban.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('non-empty cart', async function (assert) {
|
||||
assert.expect(17);
|
||||
|
||||
|
||||
@@ -144,7 +144,7 @@ class MailController(http.Controller):
|
||||
@http.route('/mail/read_followers', type='json', auth='user')
|
||||
def read_followers(self, follower_ids, res_model):
|
||||
followers = []
|
||||
is_editable = request.env.user.has_group('base.group_no_one')
|
||||
is_editable = request.env['mail.followers'].user_has_groups('base.group_no_one')
|
||||
partner_id = request.env.user.partner_id
|
||||
follower_id = None
|
||||
follower_recs = request.env['mail.followers'].sudo().browse(follower_ids)
|
||||
|
||||
@@ -62,9 +62,16 @@ var DocumentThread = Thread.extend({
|
||||
* shared between tabs, and restored on F5.
|
||||
*
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, it should
|
||||
* not notify other tabs from updating document thread state. This happens
|
||||
* in case the `close` operation comes from local storage event.
|
||||
*/
|
||||
close: function () {
|
||||
close: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (options && options.skipCrossTabSync) {
|
||||
return;
|
||||
}
|
||||
this.call('mail_service', 'updateDocumentThreadState', this._id, {
|
||||
name: this.getName(),
|
||||
windowState: 'closed',
|
||||
@@ -75,9 +82,16 @@ var DocumentThread = Thread.extend({
|
||||
* shared between tabs, and restored on F5.
|
||||
*
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, it should
|
||||
* not notify other tabs from updating document thread state. This happens
|
||||
* in case the `detach` operation comes from local storage event.
|
||||
*/
|
||||
detach: function () {
|
||||
detach: function (options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (options && options.skipCrossTabSync) {
|
||||
return;
|
||||
}
|
||||
var windowState = this._folded ? 'folded' : 'open';
|
||||
this.call('mail_service', 'updateDocumentThreadState', this._id, {
|
||||
name: this.getName(),
|
||||
@@ -105,9 +119,16 @@ var DocumentThread = Thread.extend({
|
||||
* shared between tabs, and restored on F5.
|
||||
*
|
||||
* @override
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, it should
|
||||
* not notify other tabs from updating document thread state. This happens
|
||||
* in case the `detach` operation comes from local storage event.
|
||||
*/
|
||||
fold: function () {
|
||||
fold: function (folded, options) {
|
||||
this._super.apply(this, arguments);
|
||||
if (options && options.skipCrossTabSync) {
|
||||
return;
|
||||
}
|
||||
var windowState = this._folded ? 'folded' : 'open';
|
||||
this.call('mail_service', 'updateDocumentThreadState', this._id, {
|
||||
name: this.getName(),
|
||||
|
||||
@@ -148,7 +148,11 @@ var SearchableThread = Thread.extend({
|
||||
if (cache.messages[index] !== message) {
|
||||
cache.messages.splice(index, 0, message);
|
||||
}
|
||||
if (!message.isMyselfAuthor() && options.incrementUnread) {
|
||||
if (
|
||||
!message.isMyselfAuthor() &&
|
||||
options.incrementUnread &&
|
||||
message.getType() !== 'notification'
|
||||
) {
|
||||
this._incrementUnreadCounter();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,17 +21,23 @@ MailManager.include({
|
||||
|
||||
start: function () {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
this._documentThreadWindowStates = {};
|
||||
this._tabId = this.call('bus_service', 'getTabId');
|
||||
// retrieve the open DocumentThreads from the localStorage
|
||||
var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
|
||||
if (!state) {
|
||||
this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, {});
|
||||
} else {
|
||||
this.isReady().then(this._updateDocumentThreadWindows.bind(this, state));
|
||||
var localStorageLength = this.call('local_storage', 'length');
|
||||
for (var i = 0; i < localStorageLength; i++) {
|
||||
var key = this.call('local_storage', 'key', i);
|
||||
if (key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
|
||||
// key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
|
||||
var documentThreadId = key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
|
||||
this._documentThreadWindowStates[documentThreadId] =
|
||||
this.call('local_storage', 'getItem', key);
|
||||
}
|
||||
}
|
||||
this.isReady().then(this._updateDocumentThreadWindows.bind(this, this._documentThreadWindowStates));
|
||||
// listen to localStorage changes to synchronize DocumentThread's
|
||||
// windows between tabs
|
||||
window.addEventListener('storage', this._onStorage.bind(this));
|
||||
this.call('local_storage', 'onStorage', this, this._onStorage);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -60,7 +66,10 @@ MailManager.include({
|
||||
options.postedFromDocumentThread
|
||||
) {
|
||||
var key = this.DOCUMENT_THREAD_MESSAGE_KEY;
|
||||
this.call('local_storage', 'setItem', key, data);
|
||||
this.call('local_storage', 'setItem', key, {
|
||||
messageData: data,
|
||||
tabId: this._tabId,
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
@@ -144,12 +153,11 @@ MailManager.include({
|
||||
* @param {string} state.windowState ('closed', 'folded' or 'open')
|
||||
*/
|
||||
updateDocumentThreadState: function (threadID, state) {
|
||||
var states = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
|
||||
states = _.omit(states, function (state) {
|
||||
return state.windowState === 'closed';
|
||||
this._documentThreadWindowStates[threadID] = state;
|
||||
this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY + '/' + threadID, {
|
||||
state: state,
|
||||
tabId: this._tabId,
|
||||
});
|
||||
states[threadID] = state;
|
||||
this.call('local_storage', 'setItem', this.DOCUMENT_THREAD_STATE_KEY, states);
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -194,10 +202,17 @@ MailManager.include({
|
||||
resModel: info[0],
|
||||
});
|
||||
if (state.windowState === 'closed') {
|
||||
documentThread.close();
|
||||
documentThread.close({
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
} else {
|
||||
documentThread.fold(state.windowState === 'folded');
|
||||
self.openThreadWindow(documentThread.getID(), { keepFoldState: true });
|
||||
documentThread.fold(state.windowState === 'folded', {
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
self.openThreadWindow(documentThread.getID(), {
|
||||
keepFoldState: true,
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
@@ -213,13 +228,27 @@ MailManager.include({
|
||||
* @param {StorageEvent} ev
|
||||
*/
|
||||
_onStorage: function (ev) {
|
||||
if (ev.key === this.DOCUMENT_THREAD_STATE_KEY) {
|
||||
var state = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_STATE_KEY);
|
||||
this._updateDocumentThreadWindows(state);
|
||||
var value;
|
||||
try {
|
||||
value = JSON.parse(ev.newValue);
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
if (ev.key.indexOf(this.DOCUMENT_THREAD_STATE_KEY) === 0) {
|
||||
if (value.tabId === this._tabId) {
|
||||
return;
|
||||
}
|
||||
// key format: DOCUMENT_THREAD_STATE_KEY + '/' + threadId
|
||||
var documentThreadId = ev.key.substring(this.DOCUMENT_THREAD_STATE_KEY.length+1);
|
||||
var param = {};
|
||||
param[documentThreadId] = value.state;
|
||||
this._updateDocumentThreadWindows(param);
|
||||
} else if (ev.key === this.DOCUMENT_THREAD_MESSAGE_KEY) {
|
||||
var message = this.call('local_storage', 'getItem', this.DOCUMENT_THREAD_MESSAGE_KEY);
|
||||
if (message) {
|
||||
this.addMessage(message);
|
||||
if (value.tabId === this._tabId) {
|
||||
return;
|
||||
}
|
||||
if (value.messageData) {
|
||||
this.addMessage(value.messageData);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -94,6 +94,8 @@ MailManager.include({
|
||||
* is not open yet, and do nothing otherwise.
|
||||
* @param {boolean} [options.keepFoldState=false] if set to true, keep the
|
||||
* fold state of the thread
|
||||
* @param {boolean} [options.skipCrossTabSync=false] if set, thread
|
||||
* should not notify other tabs from new document thread chat window state.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
openThreadWindow: function (threadID, options) {
|
||||
@@ -132,7 +134,9 @@ MailManager.include({
|
||||
// thread window could not be open, which may happen due to
|
||||
// access error while fetching messages to the document.
|
||||
// abort opening the thread window in this case.
|
||||
thread.close();
|
||||
thread.close({
|
||||
skipCrossTabSync: true,
|
||||
});
|
||||
thread.isCreatingWindow = false;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ odoo.define('mail.ThreadWindow', function (require) {
|
||||
|
||||
var AbstractThreadWindow = require('mail.AbstractThreadWindow');
|
||||
var BasicComposer = require('mail.composer.Basic');
|
||||
var ExtendedComposer = require('mail.composer.Extended');
|
||||
|
||||
var config = require('web.config');
|
||||
var core = require('web.core');
|
||||
|
||||
var QWeb = core.qweb;
|
||||
@@ -65,19 +67,27 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
if (!this.hasThread()) {
|
||||
this._startWithoutThread();
|
||||
} else if (this.needsComposer()) {
|
||||
var basicComposer = new BasicComposer(this, {
|
||||
var composer;
|
||||
var composerParams = {
|
||||
mentionPartnersRestricted: this._thread.getType() !== 'document_thread',
|
||||
isMini: true,
|
||||
thread: this._thread,
|
||||
});
|
||||
basicComposer.on('post_message', this, this._postMessage);
|
||||
basicComposer.once('input_focused', this, function () {
|
||||
};
|
||||
if (
|
||||
config.device.isMobile &&
|
||||
this._thread.isMassMailing()
|
||||
) {
|
||||
composer = new ExtendedComposer(this, composerParams);
|
||||
} else {
|
||||
composer = new BasicComposer(this, _.extend({ isMini: true }, composerParams));
|
||||
}
|
||||
composer.on('post_message', this, this._postMessage);
|
||||
composer.once('input_focused', this, function () {
|
||||
var commands = this._thread.getCommands();
|
||||
var partners = this._thread.getMentionPartnerSuggestions();
|
||||
basicComposer.mentionSetCommands(commands);
|
||||
basicComposer.mentionSetPrefetchedPartners(partners);
|
||||
composer.mentionSetCommands(commands);
|
||||
composer.mentionSetPrefetchedPartners(partners);
|
||||
});
|
||||
composerDef = basicComposer.replace(this.$('.o_thread_composer'));
|
||||
composerDef = composer.replace(this.$('.o_thread_composer'));
|
||||
composerDef.then(function () {
|
||||
self.$input = self.$('.o_composer_text_field');
|
||||
});
|
||||
@@ -129,24 +139,6 @@ var ThreadWindow = AbstractThreadWindow.extend({
|
||||
isPassive: function () {
|
||||
return this._passive;
|
||||
},
|
||||
/**
|
||||
* States whether the input of the thread window should be displayed or not.
|
||||
* This is based on the type of the thread:
|
||||
*
|
||||
* Do not display the input in the following cases:
|
||||
*
|
||||
* - no thread related to this window
|
||||
* - window of a mailbox (temp: let us have mailboxes in window mode)
|
||||
* - window of a thread with mass mailing
|
||||
*
|
||||
* Any other threads show the input in the window.
|
||||
*
|
||||
* @override
|
||||
* @returns {boolean}
|
||||
*/
|
||||
needsComposer: function () {
|
||||
return this._super() && !this._thread.isMassMailing();
|
||||
},
|
||||
/**
|
||||
* Turn the thread window in active mode, so that when the bottom of the
|
||||
* thread is visible, it is automatically marked as read.
|
||||
|
||||
@@ -8,7 +8,12 @@ var _t = core._t;
|
||||
function parseAndTransform(htmlString, transformFunction) {
|
||||
var openToken = "OPEN" + Date.now();
|
||||
var string = htmlString.replace(/</g, openToken);
|
||||
var children = $('<div>').html(string).contents();
|
||||
var children;
|
||||
try {
|
||||
children = $('<div>').html(string).contents();
|
||||
} catch (e) {
|
||||
children = $('<div>').html('<pre>' + string + '</pre>').contents();
|
||||
}
|
||||
return _parseAndTransform(children, transformFunction)
|
||||
.replace(new RegExp(openToken, "g"), "<");
|
||||
}
|
||||
|
||||
@@ -251,6 +251,9 @@
|
||||
.o_composer_send .o_composer_button_send, .o_chatter_avatar {
|
||||
display: none !important;
|
||||
}
|
||||
.o_attachment {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_file_drop_zone_container {
|
||||
@@ -292,6 +295,10 @@
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.o_attachment_wrap {
|
||||
padding-right: 0px;
|
||||
}
|
||||
|
||||
.o_attachments_previews {
|
||||
> .o_attachment {
|
||||
width: 50%;
|
||||
|
||||
@@ -183,6 +183,7 @@
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.o_mail_body_long {
|
||||
|
||||
@@ -13,6 +13,9 @@ QUnit.module('Discuss in mobile', {
|
||||
beforeEach: function () {
|
||||
this.services = mailTestUtils.getMailServices();
|
||||
this.data = {
|
||||
'mail.channel': {
|
||||
fields: {},
|
||||
},
|
||||
'mail.message': {
|
||||
fields: {},
|
||||
},
|
||||
@@ -80,27 +83,65 @@ QUnit.test('mobile basic rendering', async function (assert) {
|
||||
discuss.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('on_{attach/detach}_callback', function (assert) {
|
||||
QUnit.test('on_{attach/detach}_callback', async function (assert) {
|
||||
assert.expect(2);
|
||||
var done = assert.async();
|
||||
|
||||
createDiscuss({
|
||||
var discuss = await createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
}).then(function (discuss) {
|
||||
try {
|
||||
discuss.on_attach_callback();
|
||||
assert.ok(true, 'should not crash on attach callback');
|
||||
discuss.on_detach_callback();
|
||||
assert.ok(true, 'should not crash on detach callback');
|
||||
} finally {
|
||||
discuss.destroy();
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
discuss.on_attach_callback();
|
||||
assert.ok(true, 'should not crash on attach callback');
|
||||
discuss.on_detach_callback();
|
||||
assert.ok(true, 'should not crash on detach callback');
|
||||
discuss.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('extended composer in mass mailing channel', async function (assert) {
|
||||
assert.expect(4);
|
||||
|
||||
this.data.initMessaging = {
|
||||
channel_slots: {
|
||||
channel_channel: [{
|
||||
id: 1,
|
||||
channel_type: "channel",
|
||||
name: "General",
|
||||
mass_mailing: true,
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
var discuss = await createDiscuss({
|
||||
id: 1,
|
||||
context: {},
|
||||
params: {},
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
});
|
||||
|
||||
await testUtils.dom.click(discuss.$('.o_mail_mobile_tab[data-type="multi_user_channel"]'));
|
||||
await testUtils.dom.click(discuss.$('.o_mail_preview[data-preview-id="1"]'));
|
||||
assert.containsOnce(
|
||||
$,
|
||||
'.o_thread_window',
|
||||
"should display a chat window");
|
||||
assert.strictEqual(
|
||||
$('.o_thread_window').data('thread-id'),
|
||||
1,
|
||||
"chat window should be from channel General");
|
||||
assert.containsOnce(
|
||||
$('.o_thread_window'),
|
||||
'.o_thread_composer',
|
||||
"chat window should have a composer");
|
||||
assert.ok(
|
||||
$('.o_thread_window .o_thread_composer').hasClass('o_thread_composer_extended'),
|
||||
"chat window composer should be extended composer");
|
||||
|
||||
discuss.destroy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -201,7 +201,7 @@ QUnit.test('post messages in a document thread window', async function (assert)
|
||||
testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) {
|
||||
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem' &&
|
||||
ev.data.args[0] === 'mail.document_threads_last_message') {
|
||||
assert.deepEqual(ev.data.args[1], newMessage,
|
||||
assert.deepEqual(ev.data.args[1].messageData, newMessage,
|
||||
"should write sent message in local storage, to share info with other tabs");
|
||||
}
|
||||
}, true);
|
||||
@@ -227,7 +227,7 @@ QUnit.test('post messages in a document thread window', async function (assert)
|
||||
});
|
||||
|
||||
QUnit.test('open, fold, unfold and close a document thread window', async function (assert) {
|
||||
assert.expect(8);
|
||||
assert.expect(24);
|
||||
var messagingMenu = new MessagingMenu();
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
services: this.services,
|
||||
@@ -237,7 +237,13 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi
|
||||
await testUtils.nextTick();
|
||||
testUtils.mock.intercept(messagingMenu, 'call_service', function (ev) {
|
||||
if (ev.data.service === 'local_storage' && ev.data.method === 'setItem') {
|
||||
assert.step(JSON.stringify(ev.data.args));
|
||||
const key = ev.data.args[0];
|
||||
const state = ev.data.args[1].state;
|
||||
assert.strictEqual(key, 'mail.document_threads_state/some.res.model_1');
|
||||
assert.ok(state);
|
||||
assert.ok(state.name);
|
||||
assert.ok(state.windowState);
|
||||
assert.step(`${key}: { name: "${state.name}", windowState: '${state.windowState}' }`);
|
||||
}
|
||||
}, true);
|
||||
await messagingMenu.appendTo($('#qunit-fixture'));
|
||||
@@ -263,10 +269,10 @@ QUnit.test('open, fold, unfold and close a document thread window', async functi
|
||||
await testUtils.nextTick();
|
||||
|
||||
assert.verifySteps([
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]',
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"folded"}}]',
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"open"}}]',
|
||||
'["mail.document_threads_state",{"some.res.model_1":{"name":"Some Record","windowState":"closed"}}]',
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'folded' }`,
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'open' }`,
|
||||
`mail.document_threads_state/some.res.model_1: { name: "Some Record", windowState: 'closed' }`,
|
||||
]);
|
||||
|
||||
messagingMenu.destroy();
|
||||
|
||||
@@ -427,6 +427,88 @@ QUnit.test('update messaging preview on receiving a new message in channel previ
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('new message of type "notification" are not considered as unread messages', async function (assert) {
|
||||
assert.expect(8);
|
||||
|
||||
const messagingMenu = new MessagingMenu();
|
||||
testUtils.mock.addMockEnvironment(messagingMenu, {
|
||||
data: this.data,
|
||||
services: this.services,
|
||||
});
|
||||
await messagingMenu.appendTo($('#qunit-fixture'));
|
||||
// open messaging menu
|
||||
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||
assert.containsOnce(
|
||||
messagingMenu,
|
||||
'.o_mail_preview',
|
||||
"should display a single channel preview");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_name')
|
||||
.text()
|
||||
.trim(),
|
||||
"general",
|
||||
"should display channel preview of 'general' channel");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_counter')
|
||||
.text()
|
||||
.trim(),
|
||||
"",
|
||||
"should have unread counter of 0 for 'general' channel (no unread messages)");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_last_message_preview')
|
||||
.text()
|
||||
.replace(/\s/g, ""),
|
||||
"Me:test",
|
||||
"should display author name and inline body of currently last message in the channel");
|
||||
// close messaging menu
|
||||
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||
|
||||
// simulate receiving a new message on the channel 'general' (id 1)
|
||||
// of type 'notification'
|
||||
const data = {
|
||||
id: 100,
|
||||
author_id: [42, "Someone"],
|
||||
body: "<p>Left #general</p>",
|
||||
channel_ids: [1],
|
||||
message_type: 'notification',
|
||||
};
|
||||
const notification = [[false, 'mail.channel', 1], data];
|
||||
messagingMenu.call('bus_service', 'trigger', 'notification', [notification]);
|
||||
await testUtils.nextTick();
|
||||
// open messaging menu
|
||||
await testUtils.dom.click(messagingMenu.$('.dropdown-toggle'));
|
||||
assert.containsOnce(
|
||||
messagingMenu,
|
||||
'.o_mail_preview',
|
||||
"should still display a single channel preview");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_name')
|
||||
.text()
|
||||
.trim(),
|
||||
"general",
|
||||
"should still display channel preview of 'general' channel");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_preview_counter')
|
||||
.text()
|
||||
.trim(),
|
||||
"",
|
||||
"should still have unread counter of 0 for 'general' channel (no unread messages)");
|
||||
assert.strictEqual(
|
||||
messagingMenu
|
||||
.$('.o_last_message_preview')
|
||||
.text()
|
||||
.replace(/\s/g, ""),
|
||||
"Someone:Left#general",
|
||||
"should display author name and inline body of notification message (last message of channel)");
|
||||
|
||||
messagingMenu.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('preview of inbox message not linked to document + mark as read', async function (assert) {
|
||||
assert.expect(17);
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@
|
||||
<templates>
|
||||
<t t-name="kanban-tooltip">
|
||||
<ul class="oe_kanban_tooltip">
|
||||
<li><b>Category:</b> <field name="category_id"/></li>
|
||||
<li><b>Request to:</b> <field name="user_id"/></li>
|
||||
<li t-if="record.category_id.raw_value"><b>Category:</b> <t t-esc="record.category_id.value"/></li>
|
||||
<li t-if="record.user_id.raw_value"><b>Request to:</b> <t t-esc="record.user_id.value"/></li>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
@@ -427,8 +427,8 @@
|
||||
<templates>
|
||||
<t t-name="kanban-tooltip">
|
||||
<ul class="oe_kanban_tooltip">
|
||||
<li><b>Serial Number:</b> <field name="serial_no"/></li>
|
||||
<li><b>Model Number:</b> <field name="model"/></li>
|
||||
<li t-if="record.serial_no.raw_value"><b>Serial Number:</b> <t t-esc="record.serial_no.value"/></li>
|
||||
<li t-if="record.model.raw_value"><b>Model Number:</b> <t t-esc="record.model.value"/></li>
|
||||
</ul>
|
||||
</t>
|
||||
<t t-name="kanban-box">
|
||||
|
||||
@@ -338,6 +338,7 @@ class MrpProduction(models.Model):
|
||||
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_raw_ids | order.move_finished_ids)
|
||||
else:
|
||||
order.post_visible = order.is_locked and any((x.quantity_done > 0 and x.state not in ['done', 'cancel']) for x in order.move_finished_ids)
|
||||
order.post_visible &= all(wo.state in ['done', 'cancel'] for wo in order.workorder_ids) or all(m.product_id.tracking == 'none' for m in order.move_raw_ids)
|
||||
|
||||
@api.multi
|
||||
@api.depends('workorder_ids.state', 'move_finished_ids', 'move_finished_ids.quantity_done', 'is_locked')
|
||||
@@ -619,6 +620,8 @@ class MrpProduction(models.Model):
|
||||
@api.multi
|
||||
def open_produce_product(self):
|
||||
self.ensure_one()
|
||||
if self.bom_id.type == 'phantom':
|
||||
raise UserError(_('You cannot produce a MO with a bom kit product.'))
|
||||
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
|
||||
return action
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ class ResConfigSettings(models.TransientModel):
|
||||
module_mrp_plm = fields.Boolean("Product Lifecycle Management (PLM)")
|
||||
module_mrp_workorder = fields.Boolean("Work Orders")
|
||||
module_quality_control = fields.Boolean("Quality")
|
||||
module_mrp_subcontracting = fields.Boolean("Subcontracting")
|
||||
group_mrp_routings = fields.Boolean("MRP Work Orders",
|
||||
implied_group='mrp.group_mrp_routings')
|
||||
|
||||
|
||||
@@ -233,6 +233,11 @@ class StockMove(models.Model):
|
||||
res = super(StockMove, self)._should_be_assigned()
|
||||
return bool(res and not (self.production_id or self.raw_material_production_id))
|
||||
|
||||
def _key_assign_picking(self):
|
||||
keys = super(StockMove, self)._key_assign_picking()
|
||||
return keys + (self.created_production_id,)
|
||||
|
||||
|
||||
def _compute_kit_quantities(self, product_id, kit_qty, kit_bom, filters):
|
||||
""" Computes the quantity delivered or received when a kit is sold or purchased.
|
||||
A ratio 'qty_processed/qty_needed' is computed for each component, and the lowest one is kept
|
||||
@@ -266,4 +271,4 @@ class StockMove(models.Model):
|
||||
# doesn't make sense.
|
||||
return min(qty_ratios) // 1
|
||||
else:
|
||||
return 0.0
|
||||
return 0.0
|
||||
@@ -44,17 +44,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6 col-12 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="module_mrp_subcontracting"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="module_mrp_subcontracting"/>
|
||||
<div class="text-muted">
|
||||
Subcontract the production of some products
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Products</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
def _create_subcontracting_rules(cr, registry):
|
||||
""" This hook is used to add the default subcontracting rules on every warehouse/company. It is
|
||||
necessary if the mrp_subcontracting module is installed after some warehouses
|
||||
were already created.
|
||||
"""
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
warehouse_ids = env['stock.warehouse'].search(['|', ('subcontracting_mto_pull_id', '=', False), ('subcontracting_pull_id', '=', False)])
|
||||
for warehouse_id in warehouse_ids:
|
||||
warehouse_id.write({'subcontracting_to_resupply': warehouse_id.subcontracting_to_resupply})
|
||||
company_ids = env['res.company'].search([('subcontracting_location_id', '!=', False)])
|
||||
for company_id in company_ids:
|
||||
wh = env['stock.warehouse'].search([('company_id', '=', company_id.id), ('active', '=', True)], limit=1)
|
||||
production_location = wh._get_production_location()
|
||||
rsoo_rule = env['stock.rule'].search([
|
||||
('location_src_id', '=', company_id.subcontracting_location_id.id),
|
||||
('location_id', '=', production_location.id),
|
||||
('picking_type_id', '=', wh.manu_type_id.id)
|
||||
])
|
||||
if not rsoo_rule:
|
||||
company_id._create_resupply_subcontractor_rules()
|
||||
@@ -1,19 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': "mrp_subcontracting",
|
||||
'version': '0.1',
|
||||
'summary': "Subcontract Productions",
|
||||
'description': "",
|
||||
'website': 'https://www.odoo.com/page/manufacturing',
|
||||
'category': 'Manufacturing/Manufacturing',
|
||||
'depends': ['mrp', 'contacts'],
|
||||
'data': [
|
||||
'data/mrp_subcontracting_data.xml',
|
||||
'views/stock_warehouse_views.xml',
|
||||
'views/stock_picking_views.xml',
|
||||
'views/mrp_bom_views.xml',
|
||||
],
|
||||
'post_init_hook': '_create_subcontracting_rules',
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="route_resupply_subcontractor_mto" model='stock.location.route'>
|
||||
<field name="name">Resupply Subcontractor on Order</field>
|
||||
<field name="company_id"></field>
|
||||
<field name="sequence">5</field>
|
||||
</record>
|
||||
<function model="res.company" name="create_missing_subcontracting_location" />
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import mrp_bom
|
||||
from . import res_company
|
||||
from . import res_partner
|
||||
from . import stock_move
|
||||
from . import stock_picking
|
||||
from . import stock_warehouse
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class MrpBom(models.Model):
|
||||
_inherit = 'mrp.bom'
|
||||
|
||||
type = fields.Selection(selection_add=[('subcontract', 'Subcontracting')])
|
||||
subcontractor_ids = fields.One2many('res.partner', 'bom_id', domain=[('type', '=', 'subcontractor')], string='Subcontractors')
|
||||
|
||||
def _bom_subcontract_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type='subcontract', subcontractor=False):
|
||||
domain = self._bom_find_domain(product_tmpl=product_tmpl, product=product, picking_type=picking_type, company_id=company_id, bom_type=bom_type)
|
||||
if subcontractor:
|
||||
domain += ['|', ('subcontractor_ids', 'in', subcontractor.id), ('subcontractor_ids', '=', False)]
|
||||
else:
|
||||
domain += [('subcontractor_ids', '=', False)]
|
||||
return self.search(domain, order='subcontractor_ids, sequence, product_id', limit=1)
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
subcontracting_location_id = fields.Many2one('stock.location')
|
||||
|
||||
def _create_per_company_locations(self):
|
||||
super(ResCompany, self)._create_per_company_locations()
|
||||
self._create_subcontracting_location()
|
||||
|
||||
def _create_subcontracting_location(self):
|
||||
for company in self:
|
||||
parent_location = self.env.ref('stock.stock_location_locations_partner', raise_if_not_found=False)
|
||||
subcontracting_location = self.env['stock.location'].create({
|
||||
'name': _('%s: Subcontracting Location') % company.name,
|
||||
'usage': 'internal',
|
||||
'location_id': parent_location.id,
|
||||
'company_id': company.id,
|
||||
})
|
||||
company.write({'subcontracting_location_id': subcontracting_location.id})
|
||||
|
||||
@api.model
|
||||
def create_missing_subcontracting_location(self):
|
||||
company_without_subcontracting_loc = self.env['res.company'].search(
|
||||
[('subcontracting_location_id', '=', False)])
|
||||
for company in company_without_subcontracting_loc:
|
||||
company._create_subcontracting_location()
|
||||
|
||||
def _create_resupply_subcontractor_rules(self):
|
||||
self.ensure_one()
|
||||
wh = self.env['stock.warehouse'].search([('company_id', '=', self.id), ('active', '=', True)], limit=1)
|
||||
subcontracting_location = self.subcontracting_location_id
|
||||
production_location = wh._get_production_location()
|
||||
resupply_route = wh._find_global_route('mrp_subcontracting.route_resupply_subcontractor_mto', _('Resupply Subcontractor on Order'))
|
||||
resupply_route.rule_ids = [(0, 0, {
|
||||
'name': '%s → %s' % (subcontracting_location.name, production_location.name),
|
||||
'action': 'pull',
|
||||
'picking_type_id': wh.subcontracting_type_id,
|
||||
'location_src_id': subcontracting_location,
|
||||
'location_id': production_location,
|
||||
})],
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class ResPartner(models.Model):
|
||||
_inherit = 'res.partner'
|
||||
|
||||
type = fields.Selection(selection_add=[('subcontractor', 'Subcontractor')])
|
||||
bom_id = fields.Many2one('mrp.bom', domain=[('type', '=', 'subcontract')])
|
||||
|
||||
def _get_name(self):
|
||||
name = super(ResPartner, self)._get_name()
|
||||
if self.type == 'subcontractor':
|
||||
subcontractor = _('Subcontractor')
|
||||
if name.endswith(', '):
|
||||
name = "%s%s" % (name, subcontractor)
|
||||
else:
|
||||
name = '%s, %s' % (name, subcontractor)
|
||||
return name
|
||||
|
||||
@api.onchange('type')
|
||||
def _onchange_partner_type(self):
|
||||
if self.type == 'subcontractor':
|
||||
company = self.company_id or self.parent_id.company_id
|
||||
subcontracting_location = company.subcontracting_location_id
|
||||
self.update({
|
||||
'property_stock_customer': subcontracting_location,
|
||||
'property_stock_supplier': subcontracting_location
|
||||
})
|
||||
else:
|
||||
self.update({
|
||||
'property_stock_customer': self.env.ref('stock.stock_location_customers'),
|
||||
'property_stock_supplier': self.env.ref('stock.stock_location_suppliers')
|
||||
})
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
if vals.get('type') == 'subcontractor':
|
||||
if not vals.get('property_stock_supplier') and not vals.get('property_stock_customer'):
|
||||
company = vals.get('company_id') and self.env['res.company'].browse(vals['company_id']) or \
|
||||
self.browse(vals.get('parent_id')).company_id
|
||||
subcontracting_location = company.subcontracting_location_id
|
||||
vals['property_stock_supplier'] = subcontracting_location
|
||||
vals['property_stock_customer'] = subcontracting_location
|
||||
return super(ResPartner, self).create(vals_list)
|
||||
|
||||
def write(self, vals):
|
||||
if vals.get('type') == 'subcontractor':
|
||||
subcontracting_location = self.env.company.subcontracting_location_id
|
||||
if not vals.get('property_stock_supplier') and not vals.get('property_stock_customer'):
|
||||
vals['property_stock_supplier'] = subcontracting_location.id
|
||||
vals['property_stock_customer'] = subcontracting_location.id
|
||||
return super(ResPartner, self).write(vals)
|
||||
@@ -1,41 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import models, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class StockMove(models.Model):
|
||||
_inherit = 'stock.move'
|
||||
|
||||
def _get_subcontract_bom(self):
|
||||
self.ensure_one()
|
||||
bom = self.env['mrp.bom']._bom_subcontract_find(
|
||||
product=self.product_id,
|
||||
picking_type=self.picking_type_id,
|
||||
company_id=self.company_id.id,
|
||||
bom_type='subcontract',
|
||||
subcontractor=self.picking_id.partner_id,
|
||||
)
|
||||
return bom
|
||||
|
||||
def _action_confirm(self, merge=True, merge_into=False):
|
||||
res = super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into)
|
||||
subcontract_details_per_picking = defaultdict(list)
|
||||
for move in res:
|
||||
if not move.picking_id:
|
||||
continue
|
||||
if not move.picking_id._is_subcontract():
|
||||
continue
|
||||
bom = move._get_subcontract_bom()
|
||||
if not bom:
|
||||
error_message = _('Please define a BoM of type subcontracting for the product "%s". If you don\'t want to subcontract the product "%s", do not select a partner of type subcontractor.')
|
||||
error_message += '\n\n'
|
||||
error_message += _('If there is well a BoM of type subcontracting defined, check if you have set the correct subcontractors on it.')
|
||||
raise UserError(error_message % (move.product_id.name, move.product_id.name))
|
||||
subcontract_details_per_picking[move.picking_id].append((move, bom))
|
||||
for picking, subcontract_details in subcontract_details_per_picking.items():
|
||||
picking._subcontracted_produce(subcontract_details)
|
||||
return res
|
||||
@@ -1,133 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StockPicking(models.Model):
|
||||
_inherit = 'stock.picking'
|
||||
|
||||
display_action_record_components = fields.Boolean(compute='_compute_display_action_record_components')
|
||||
display_view_subcontracted_move_lines = fields.Boolean(compute='_compute_display_view_subcontracted_move_lines')
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_display_action_record_components(self):
|
||||
for picking in self:
|
||||
# Hide if not encoding state
|
||||
if picking.state in ('draft', 'cancel', 'done'):
|
||||
continue
|
||||
if not picking._is_subcontract():
|
||||
continue
|
||||
# Hide if no move is tracked
|
||||
subcontracted_productions = picking._get_subcontracted_productions()
|
||||
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
|
||||
subcontracted_moves |= subcontracted_productions.mapped('move_finished_ids')
|
||||
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
|
||||
continue
|
||||
# Hide if the production is to close
|
||||
if not subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done')):
|
||||
continue
|
||||
picking.display_action_record_components = True
|
||||
|
||||
@api.depends('state')
|
||||
def _compute_display_view_subcontracted_move_lines(self):
|
||||
for picking in self:
|
||||
# Hide if not encoding state
|
||||
if picking.state in ('draft', 'cancel'):
|
||||
continue
|
||||
if not picking._is_subcontract():
|
||||
continue
|
||||
# Hide until state done if no move is tracked, if tracked until something was produced
|
||||
subcontracted_productions = picking._get_subcontracted_productions()
|
||||
subcontracted_moves = subcontracted_productions.mapped('move_raw_ids')
|
||||
subcontracted_moves |= subcontracted_productions.mapped('move_finished_ids')
|
||||
if all(subcontracted_move.has_tracking == 'none' for subcontracted_move in subcontracted_moves):
|
||||
if picking.state != 'done':
|
||||
continue
|
||||
# Hide if nothing was produced
|
||||
if all(subcontracted_move.quantity_done == 0 for subcontracted_move in subcontracted_moves):
|
||||
continue
|
||||
picking.display_view_subcontracted_move_lines = True
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Action methods
|
||||
# -------------------------------------------------------------------------
|
||||
def action_done(self):
|
||||
for picking in self:
|
||||
subcontracted_productions = picking._get_subcontracted_productions()
|
||||
for subcontracted_production in subcontracted_productions:
|
||||
subcontracted_production.button_mark_done()
|
||||
return super(StockPicking, self).action_done()
|
||||
|
||||
def action_view_subcontracted_move_lines(self):
|
||||
""" Returns a list view with the move lines of the subcontracted products. To find them, we
|
||||
look on the origin moves of the move lines of the picking if there is a manufacturing order.
|
||||
"""
|
||||
self.ensure_one()
|
||||
subcontracted_productions = self._get_subcontracted_productions()
|
||||
subcontracted_move_lines = self.env['stock.move.line']
|
||||
for subcontracted_production in subcontracted_productions:
|
||||
subcontracted_move_lines |= subcontracted_production.move_raw_ids.mapped('move_line_ids')
|
||||
subcontracted_move_lines |= subcontracted_production.move_finished_ids.mapped('move_line_ids')
|
||||
action = self.env.ref('stock.stock_move_line_action').read()[0]
|
||||
action['context'] = {}
|
||||
action['domain'] = [('id', 'in', subcontracted_move_lines.ids)]
|
||||
return action
|
||||
|
||||
def action_record_components(self):
|
||||
self.ensure_one()
|
||||
subcontracted_productions = self._get_subcontracted_productions()
|
||||
to_register = subcontracted_productions.filtered(lambda mo: mo.state not in ('to_close', 'done'))
|
||||
if to_register:
|
||||
mo = to_register[0]
|
||||
action = self.env.ref('mrp.act_mrp_product_produce').read()[0]
|
||||
action['context'] = dict(self.env.context, active_id=mo.id)
|
||||
return action
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Subcontract helpers
|
||||
# -------------------------------------------------------------------------
|
||||
def _is_subcontract(self):
|
||||
self.ensure_one()
|
||||
if self.partner_id.type == 'subcontractor' and \
|
||||
self.picking_type_id.code == 'incoming':
|
||||
return True
|
||||
return False
|
||||
|
||||
def _get_subcontracted_productions(self):
|
||||
self.ensure_one()
|
||||
return self.move_lines.mapped('move_orig_ids.production_id')
|
||||
|
||||
def _prepare_subcontract_mo_vals(self, subcontract_move, bom):
|
||||
subcontract_move.ensure_one()
|
||||
product = subcontract_move.product_id
|
||||
warehouse = subcontract_move.warehouse_id or self.picking_type_id.warehouse_id
|
||||
vals = {
|
||||
'product_id': product.id,
|
||||
'product_uom_id': subcontract_move.product_uom.id,
|
||||
'bom_id': bom.id,
|
||||
'location_src_id': subcontract_move.picking_id.partner_id.property_stock_supplier.id,
|
||||
'location_dest_id': subcontract_move.location_id.id,
|
||||
'product_qty': subcontract_move.product_uom_qty,
|
||||
'picking_type_id': warehouse.subcontracting_type_id.id
|
||||
}
|
||||
return vals
|
||||
|
||||
def _subcontracted_produce(self, subcontract_details):
|
||||
self.ensure_one()
|
||||
for move, bom in subcontract_details:
|
||||
mo = self.env['mrp.production'].create(self._prepare_subcontract_mo_vals(move, bom))
|
||||
self.env['stock.move'].create(mo._get_moves_raw_values())
|
||||
mo.action_confirm()
|
||||
|
||||
# Link the finished to the receipt move.
|
||||
finished_move = mo.move_finished_ids.filtered(lambda m: m.product_id == move.product_id)
|
||||
finished_move.write({'move_dest_ids': [(4, move.id, False)]})
|
||||
mo.action_assign()
|
||||
|
||||
# Only skip the produce wizard if no moves is tracked
|
||||
moves = mo.mapped('move_raw_ids') + mo.mapped('move_finished_ids')
|
||||
if any(move.has_tracking != 'none' for move in moves):
|
||||
continue
|
||||
for m in moves:
|
||||
m.quantity_done = m.product_uom_qty
|
||||
@@ -1,139 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
|
||||
|
||||
class StockWarehouse(models.Model):
|
||||
_inherit = 'stock.warehouse'
|
||||
|
||||
subcontracting_to_resupply = fields.Boolean(
|
||||
'Resupply Subcontractors', default=True,
|
||||
help="Resupply subcontractors with components")
|
||||
|
||||
subcontracting_mto_pull_id = fields.Many2one(
|
||||
'stock.rule', 'Subcontracting MTO Rule')
|
||||
subcontracting_pull_id = fields.Many2one(
|
||||
'stock.rule', 'Subcontracting MTS Rule'
|
||||
)
|
||||
|
||||
subcontracting_route_id = fields.Many2one('stock.location.route', 'Resupply Subcontractor', ondelete='restrict')
|
||||
|
||||
subcontracting_type_id = fields.Many2one(
|
||||
'stock.picking.type', 'Subcontracting Operation Type',
|
||||
domain=[('code', '=', 'mrp_operation')])
|
||||
|
||||
def get_rules_dict(self):
|
||||
result = super(StockWarehouse, self).get_rules_dict()
|
||||
subcontract_location_id = self._get_subcontracting_location()
|
||||
for warehouse in self:
|
||||
result[warehouse.id].update({
|
||||
'subcontract': [
|
||||
self.Routing(warehouse.lot_stock_id, subcontract_location_id, warehouse.out_type_id, 'pull'),
|
||||
]
|
||||
})
|
||||
return result
|
||||
|
||||
def _get_routes_values(self):
|
||||
routes = super(StockWarehouse, self)._get_routes_values()
|
||||
routes.update({
|
||||
'subcontracting_route_id': {
|
||||
'routing_key': 'subcontract',
|
||||
'depends': ['subcontracting_to_resupply'],
|
||||
'route_create_values': {
|
||||
'product_categ_selectable': False,
|
||||
'warehouse_selectable': True,
|
||||
'product_selectable': False,
|
||||
'company_id': self.company_id.id,
|
||||
'sequence': 10,
|
||||
'name': self._format_routename(name=_('Resupply Subcontractor'))
|
||||
},
|
||||
'route_update_values': {
|
||||
'active': self.subcontracting_to_resupply,
|
||||
},
|
||||
'rules_values': {
|
||||
'active': self.subcontracting_to_resupply,
|
||||
}
|
||||
}
|
||||
})
|
||||
return routes
|
||||
|
||||
def _get_global_route_rules_values(self):
|
||||
rules = super(StockWarehouse, self)._get_global_route_rules_values()
|
||||
subcontract_location_id = self._get_subcontracting_location()
|
||||
production_location_id = self._get_production_location()
|
||||
rules.update({
|
||||
'subcontracting_mto_pull_id': {
|
||||
'depends': ['subcontracting_to_resupply'],
|
||||
'create_values': {
|
||||
'procure_method': 'make_to_order',
|
||||
'company_id': self.company_id.id,
|
||||
'action': 'pull',
|
||||
'auto': 'manual',
|
||||
'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id,
|
||||
'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, 'MTO'),
|
||||
'location_id': subcontract_location_id.id,
|
||||
'location_src_id': self.lot_stock_id.id,
|
||||
'picking_type_id': self.out_type_id.id
|
||||
},
|
||||
'update_values': {
|
||||
'active': self.subcontracting_to_resupply
|
||||
}
|
||||
},
|
||||
'subcontracting_pull_id': {
|
||||
'depends': ['subcontracting_to_resupply'],
|
||||
'create_values': {
|
||||
'procure_method': 'make_to_order',
|
||||
'company_id': self.company_id.id,
|
||||
'action': 'pull',
|
||||
'auto': 'manual',
|
||||
'route_id': self._find_global_route('mrp_subcontracting.route_resupply_subcontractor_mto',
|
||||
_('Resupply Subcontractor on Order')).id,
|
||||
'name': self._format_rulename(self.lot_stock_id, subcontract_location_id, False),
|
||||
'location_id': production_location_id.id,
|
||||
'location_src_id': subcontract_location_id.id,
|
||||
'picking_type_id': self.out_type_id.id
|
||||
},
|
||||
'update_values': {
|
||||
'active': self.subcontracting_to_resupply
|
||||
}
|
||||
},
|
||||
})
|
||||
return rules
|
||||
|
||||
def _get_picking_type_create_values(self, max_sequence):
|
||||
data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
|
||||
data.update({
|
||||
'subcontracting_type_id': {
|
||||
'name': _('Subcontracting'),
|
||||
'code': 'mrp_operation',
|
||||
'use_create_lots': True,
|
||||
'use_existing_lots': True,
|
||||
'sequence': next_sequence + 2
|
||||
},
|
||||
})
|
||||
return data, max_sequence + 4
|
||||
|
||||
def _get_sequence_values(self):
|
||||
values = super(StockWarehouse, self)._get_sequence_values()
|
||||
values.update({
|
||||
'subcontracting_type_id': {'name': self.name + ' ' + _('Sequence subcontracting'), 'prefix': self.code + '/SBC/', 'padding': 5},
|
||||
})
|
||||
return values
|
||||
|
||||
def _get_picking_type_update_values(self):
|
||||
data = super(StockWarehouse, self)._get_picking_type_update_values()
|
||||
subcontract_location_id = self._get_subcontracting_location()
|
||||
production_location_id = self._get_production_location()
|
||||
data.update({
|
||||
'subcontracting_type_id': {
|
||||
'active': False,
|
||||
'default_location_src_id': subcontract_location_id.id,
|
||||
'default_location_dest_id': production_location_id.id,
|
||||
},
|
||||
})
|
||||
return data
|
||||
|
||||
def _get_subcontracting_location(self):
|
||||
return self.company_id.subcontracting_location_id
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import test_subcontracting
|
||||
|
||||
@@ -1,526 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo.tests import Form
|
||||
from odoo.tests.common import SavepointCase, TransactionCase
|
||||
|
||||
from odoo.tests import tagged
|
||||
|
||||
@tagged('post_install', '-at_install')
|
||||
class TestSubcontractingBasic(TransactionCase):
|
||||
def test_partner_1(self):
|
||||
""" Checks the `display_name` of subcontracted partners. """
|
||||
main_partner = self.env['res.partner'].create({'name': 'main_partner'})
|
||||
|
||||
# When a name is set to the subcontracted partner.
|
||||
subcontractor_partner1 = self.env['res.partner'].create({
|
||||
'name': 'subcontractor_partner',
|
||||
'type': 'subcontractor',
|
||||
'parent_id': main_partner.id,
|
||||
})
|
||||
self.assertEqual(subcontractor_partner1.display_name, "main_partner, subcontractor_partner, Subcontractor")
|
||||
|
||||
# When a name is not set to the subcontracted partner.
|
||||
subcontractor_partner1 = self.env['res.partner'].create({
|
||||
'name': '',
|
||||
'type': 'subcontractor',
|
||||
'parent_id': main_partner.id,
|
||||
})
|
||||
self.assertEqual(subcontractor_partner1.display_name, "main_partner, Subcontractor")
|
||||
|
||||
def test_partner_2(self):
|
||||
""" Open an existing partner and add a contact of type subcontractor (simple form view).
|
||||
Checks the customer and supplier locations are set to the subcontracting one onces saved.
|
||||
"""
|
||||
that_company = self.env['res.partner'].create({
|
||||
'is_company': True,
|
||||
'name': "That Company",
|
||||
})
|
||||
partner_form = Form(that_company)
|
||||
with partner_form.child_ids.new() as partner:
|
||||
partner.name = 'That Guy'
|
||||
partner.type = 'subcontractor'
|
||||
partner_form.save()
|
||||
subcontractor = that_company.child_ids
|
||||
self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
|
||||
self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
|
||||
|
||||
def test_partner_3(self):
|
||||
""" Create a new partner, select a parent company, and choose type subcontractor (debug
|
||||
active). Checks the onchange correctly set the customer and supplier locations to the
|
||||
subcontracting ones. Change them to another one, save, checks they are rightly applied.
|
||||
"""
|
||||
# FIXME sle: this field should not be writeable without the debug mode but it is. ask xmo
|
||||
group_no_one = self.env.ref('base.group_no_one')
|
||||
self.env.user.write({'groups_id': [(3, group_no_one.id)]})
|
||||
partner_form = Form(self.env['res.partner'])
|
||||
partner_form.name = 'subcontractor partner'
|
||||
self.assertNotEqual(partner_form.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
|
||||
self.assertNotEqual(partner_form.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
|
||||
partner_form.type = 'subcontractor'
|
||||
self.assertEqual(partner_form.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
|
||||
self.assertEqual(partner_form.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
|
||||
subcontracting_location2 = self.env.user.company_id.subcontracting_location_id.copy()
|
||||
partner_form.property_stock_supplier = subcontracting_location2
|
||||
partner_form.property_stock_customer = subcontracting_location2
|
||||
partner = partner_form.save()
|
||||
self.assertEqual(partner.property_stock_supplier, subcontracting_location2)
|
||||
self.assertEqual(partner.property_stock_customer, subcontracting_location2)
|
||||
|
||||
def test_partner_4(self):
|
||||
""" Create a new partner, select a parent company, and choose type subcontractor (debug
|
||||
not active). Checks the customer and supplier locations are set to the subcontracting one
|
||||
onces saved.
|
||||
"""
|
||||
that_company = self.env['res.partner'].create({
|
||||
'is_company': True,
|
||||
'name': "That Company",
|
||||
})
|
||||
partner_form = Form(self.env['res.partner'])
|
||||
partner_form.name = 'That Guy'
|
||||
partner_form.parent_id = that_company
|
||||
partner_form.type = 'subcontractor'
|
||||
subcontractor = partner_form.save()
|
||||
self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
|
||||
self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
|
||||
|
||||
def test_partner_5(self):
|
||||
""" Create a new partner with a child partner of type contact,
|
||||
then write the type 'subcontractor' on it
|
||||
"""
|
||||
that_company = self.env['res.partner'].create({
|
||||
'is_company': True,
|
||||
'name': "That Company",
|
||||
})
|
||||
partner_form = Form(that_company)
|
||||
with partner_form.child_ids.new() as partner:
|
||||
partner.name = 'That Guy'
|
||||
partner.type = 'contact'
|
||||
partner_form.save()
|
||||
|
||||
# We write a new type on the created child partner
|
||||
subcontractor = that_company.child_ids
|
||||
self.assertEquals(len(that_company.child_ids), 1)
|
||||
subcontractor.type = 'subcontractor'
|
||||
|
||||
self.assertEquals(subcontractor.property_stock_supplier, self.env.user.company_id.subcontracting_location_id)
|
||||
self.assertEquals(subcontractor.property_stock_customer, self.env.user.company_id.subcontracting_location_id)
|
||||
|
||||
def test_subcontracting_location_1(self):
|
||||
""" Checks the creation and presence of the subcontracting location. """
|
||||
self.assertTrue(self.env.user.company_id.subcontracting_location_id)
|
||||
self.assertTrue(self.env.user.company_id.subcontracting_location_id.active)
|
||||
company2 = self.env['res.company'].create({'name': 'Test Company'})
|
||||
self.assertTrue(company2.subcontracting_location_id)
|
||||
|
||||
|
||||
class TestSubcontractingFlows(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestSubcontractingFlows, cls).setUpClass()
|
||||
# 1: Create a subcontracting partner
|
||||
main_partner = cls.env['res.partner'].create({'name': 'main_partner'})
|
||||
cls.subcontractor_partner1 = cls.env['res.partner'].create({
|
||||
'name': 'subcontractor_partner',
|
||||
'type': 'subcontractor',
|
||||
'parent_id': main_partner.id,
|
||||
})
|
||||
|
||||
# 2. Create a BOM of subcontracting type
|
||||
cls.comp1 = cls.env['product.product'].create({
|
||||
'name': 'Component1',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.comp2 = cls.env['product.product'].create({
|
||||
'name': 'Component2',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
cls.finished = cls.env['product.product'].create({
|
||||
'name': 'finished',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
bom_form = Form(cls.env['mrp.bom'])
|
||||
bom_form.type = 'subcontract'
|
||||
bom_form.product_tmpl_id = cls.finished.product_tmpl_id
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = cls.comp1
|
||||
bom_line.product_qty = 1
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = cls.comp2
|
||||
bom_line.product_qty = 1
|
||||
cls.bom = bom_form.save()
|
||||
|
||||
# Create a BoM for cls.comp2
|
||||
cls.comp2comp = cls.env['product.product'].create({
|
||||
'name': 'component for Component2',
|
||||
'type': 'product',
|
||||
'categ_id': cls.env.ref('product.product_category_all').id,
|
||||
})
|
||||
bom_form = Form(cls.env['mrp.bom'])
|
||||
bom_form.product_tmpl_id = cls.comp2.product_tmpl_id
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = cls.comp2comp
|
||||
bom_line.product_qty = 1
|
||||
cls.comp2_bom = bom_form.save()
|
||||
|
||||
def test_flow_1(self):
|
||||
""" Don't tick any route on the components and trigger the creation of the subcontracting
|
||||
manufacturing order through a receipt picking. Create a reordering rule in the
|
||||
subcontracting locations for a component and run the scheduler to resupply. Checks if the
|
||||
resupplying actually works
|
||||
"""
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# Nothing should be tracked
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
# Check the created manufacturing order
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
self.assertEqual(len(mo.picking_ids), 0)
|
||||
wh = picking_receipt.picking_type_id.warehouse_id
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# Create a RR
|
||||
pg1 = self.env['procurement.group'].create({})
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'xxx',
|
||||
'product_id': self.comp1.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 0,
|
||||
'location_id': self.env.user.company_id.subcontracting_location_id.id,
|
||||
'group_id': pg1.id,
|
||||
})
|
||||
|
||||
# Run the scheduler and check the created picking
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
|
||||
self.assertEqual(len(picking), 1)
|
||||
self.assertEquals(picking.picking_type_id, wh.out_type_id)
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
picking_receipt.button_validate()
|
||||
self.assertEquals(mo.state, 'done')
|
||||
|
||||
# Now that the picking is done, the details stat button should be visible
|
||||
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
# Available quantities should be negative at the subcontracting location for each components
|
||||
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
|
||||
self.assertEquals(avail_qty_comp1, -1)
|
||||
self.assertEquals(avail_qty_comp2, -1)
|
||||
self.assertEquals(avail_qty_finished, 1)
|
||||
|
||||
def test_flow_2(self):
|
||||
""" Tick "Resupply Subcontractor on Order" on the components and trigger the creation of
|
||||
the subcontracting manufacturing order through a receipt picking. Checks if the resupplying
|
||||
actually works.
|
||||
"""
|
||||
# Tick "resupply subconractor on order"
|
||||
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
|
||||
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
|
||||
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# Nothing should be tracked
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
# Pickings should directly be created
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertEqual(len(mo.picking_ids), 1)
|
||||
self.assertEquals(mo.state, 'to_close')
|
||||
self.assertEqual(len(mo.picking_ids.move_lines), 2)
|
||||
|
||||
picking = mo.picking_ids
|
||||
wh = picking.picking_type_id.warehouse_id
|
||||
|
||||
# The picking should be a delivery order
|
||||
self.assertEquals(picking.picking_type_id, wh.out_type_id)
|
||||
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# No manufacturing order for `self.comp2`
|
||||
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
|
||||
self.assertEqual(len(comp2mo), 0)
|
||||
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
picking_receipt.button_validate()
|
||||
self.assertEquals(mo.state, 'done')
|
||||
|
||||
# Now that the picking is done, the details stat button should be visible
|
||||
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
# Available quantities should be negative at the subcontracting location for each components
|
||||
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
|
||||
self.assertEquals(avail_qty_comp1, -1)
|
||||
self.assertEquals(avail_qty_comp2, -1)
|
||||
self.assertEquals(avail_qty_finished, 1)
|
||||
|
||||
def test_flow_3(self):
|
||||
""" Tick "Resupply Subcontractor on Order" and "MTO" on the components and trigger the
|
||||
creation of the subcontracting manufacturing order through a receipt picking. Checks if the
|
||||
resupplying actually works. One of the component has also "manufacture" set and a BOM
|
||||
linked. Checks that an MO is created for this one.
|
||||
"""
|
||||
# Tick "resupply subconractor on order"
|
||||
resupply_sub_on_order_route = self.env['stock.location.route'].search([('name', '=', 'Resupply Subcontractor on Order')])
|
||||
(self.comp1 + self.comp2).write({'route_ids': [(4, resupply_sub_on_order_route.id, None)]})
|
||||
|
||||
# Tick "manufacture" and MTO on self.comp2
|
||||
mto_route = self.env['stock.location.route'].search([('name', '=', 'Replenish on Order (MTO)')])
|
||||
manufacture_route = self.env['stock.location.route'].search([('name', '=', 'Manufacture')])
|
||||
self.comp2.write({'route_ids': [(4, manufacture_route.id, None)]})
|
||||
self.comp2.write({'route_ids': [(4, mto_route.id, None)]})
|
||||
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# Nothing should be tracked
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
# Pickings should directly be created
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom.id)])
|
||||
self.assertEqual(len(mo.picking_ids), 1)
|
||||
self.assertEquals(mo.state, 'to_close')
|
||||
self.assertEqual(len(mo.picking_ids.move_lines), 2)
|
||||
|
||||
# The picking should be a delivery order
|
||||
wh = picking_receipt.picking_type_id.warehouse_id
|
||||
self.assertEquals(mo.picking_ids.picking_type_id, wh.out_type_id)
|
||||
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# As well as a manufacturing order for `self.comp2`
|
||||
comp2mo = self.env['mrp.production'].search([('bom_id', '=', self.comp2_bom.id)])
|
||||
self.assertEqual(len(comp2mo), 1)
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
picking_receipt.button_validate()
|
||||
self.assertEquals(mo.state, 'done')
|
||||
|
||||
# Now that the picking is done, the details stat button should be visible
|
||||
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
# Available quantities should be negative at the subcontracting location for each components
|
||||
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished, wh.lot_stock_id)
|
||||
self.assertEquals(avail_qty_comp1, -1)
|
||||
self.assertEquals(avail_qty_comp2, -1)
|
||||
self.assertEquals(avail_qty_finished, 1)
|
||||
|
||||
def test_flow_4(self):
|
||||
""" Check that the correct BoM is chosen accordingly to the partner
|
||||
"""
|
||||
# We create a second partner of type subcontractor
|
||||
main_partner_2 = self.env['res.partner'].create({'name': 'main_partner'})
|
||||
subcontractor_partner2 = self.env['res.partner'].create({
|
||||
'name': 'subcontractor_partner',
|
||||
'type': 'subcontractor',
|
||||
'parent_id': main_partner_2.id,
|
||||
})
|
||||
|
||||
# We create a different BoM for the same product
|
||||
comp3 = self.env['product.product'].create({
|
||||
'name': 'Component1',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
bom_form = Form(self.env['mrp.bom'])
|
||||
bom_form.type = 'subcontract'
|
||||
bom_form.product_tmpl_id = self.finished.product_tmpl_id
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = self.comp1
|
||||
bom_line.product_qty = 1
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = comp3
|
||||
bom_line.product_qty = 1
|
||||
bom2 = bom_form.save()
|
||||
|
||||
# We assign the second BoM to the new partner
|
||||
self.bom.write({'subcontractor_ids': [(4, self.subcontractor_partner1.id, None)]})
|
||||
bom2.write({'subcontractor_ids': [(4, subcontractor_partner2.id, None)]})
|
||||
|
||||
# Create a receipt picking from the subcontractor1
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt1 = picking_form.save()
|
||||
picking_receipt1.action_confirm()
|
||||
|
||||
# Create a receipt picking from the subcontractor2
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = subcontractor_partner2
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt2 = picking_form.save()
|
||||
picking_receipt2.action_confirm()
|
||||
|
||||
mo_pick1 = picking_receipt1.move_lines.mapped('move_orig_ids.production_id')
|
||||
mo_pick2 = picking_receipt2.move_lines.mapped('move_orig_ids.production_id')
|
||||
self.assertEquals(len(mo_pick1), 1)
|
||||
self.assertEquals(len(mo_pick2), 1)
|
||||
self.assertEquals(mo_pick1.bom_id, self.bom)
|
||||
self.assertEquals(mo_pick2.bom_id, bom2)
|
||||
|
||||
|
||||
class TestSubcontractingTracking(TransactionCase):
|
||||
def setUp(self):
|
||||
super(TestSubcontractingTracking, self).setUp()
|
||||
# 1: Create a subcontracting partner
|
||||
main_company_1 = self.env['res.partner'].create({'name': 'main_partner'})
|
||||
self.subcontractor_partner1 = self.env['res.partner'].create({
|
||||
'name': 'Subcontractor 1',
|
||||
'type': 'subcontractor',
|
||||
'parent_id': main_company_1.id,
|
||||
})
|
||||
|
||||
# 2. Create a BOM of subcontracting type
|
||||
# 2.1. Comp1 has tracking by lot
|
||||
self.comp1_sn = self.env['product.product'].create({
|
||||
'name': 'Component1',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
'tracking': 'serial'
|
||||
})
|
||||
self.comp2 = self.env['product.product'].create({
|
||||
'name': 'Component2',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
})
|
||||
|
||||
# 2.2. Finished prodcut has tracking by serial number
|
||||
self.finished_lot = self.env['product.product'].create({
|
||||
'name': 'finished',
|
||||
'type': 'product',
|
||||
'categ_id': self.env.ref('product.product_category_all').id,
|
||||
'tracking': 'lot'
|
||||
})
|
||||
bom_form = Form(self.env['mrp.bom'])
|
||||
bom_form.type = 'subcontract'
|
||||
bom_form.product_tmpl_id = self.finished_lot.product_tmpl_id
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = self.comp1_sn
|
||||
bom_line.product_qty = 1
|
||||
with bom_form.bom_line_ids.new() as bom_line:
|
||||
bom_line.product_id = self.comp2
|
||||
bom_line.product_qty = 1
|
||||
self.bom_tracked = bom_form.save()
|
||||
|
||||
def test_flow_tracked_1(self):
|
||||
""" This test mimics test_flow_1 but with a BoM that has tracking included in it.
|
||||
"""
|
||||
# Create a receipt picking from the subcontractor
|
||||
picking_form = Form(self.env['stock.picking'])
|
||||
picking_form.picking_type_id = self.env.ref('stock.picking_type_in')
|
||||
picking_form.partner_id = self.subcontractor_partner1
|
||||
with picking_form.move_ids_without_package.new() as move:
|
||||
move.product_id = self.finished_lot
|
||||
move.product_uom_qty = 1
|
||||
picking_receipt = picking_form.save()
|
||||
picking_receipt.action_confirm()
|
||||
|
||||
# We should be able to call the 'record_components' button
|
||||
self.assertTrue(picking_receipt.display_action_record_components)
|
||||
self.assertFalse(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
# Check the created manufacturing order
|
||||
mo = self.env['mrp.production'].search([('bom_id', '=', self.bom_tracked.id)])
|
||||
self.assertEqual(len(mo), 1)
|
||||
self.assertEquals(mo.state, 'confirmed')
|
||||
self.assertEqual(len(mo.picking_ids), 0)
|
||||
wh = picking_receipt.picking_type_id.warehouse_id
|
||||
self.assertEquals(mo.picking_type_id, wh.subcontracting_type_id)
|
||||
self.assertFalse(mo.picking_type_id.active)
|
||||
|
||||
# Create a RR
|
||||
pg1 = self.env['procurement.group'].create({})
|
||||
self.env['stock.warehouse.orderpoint'].create({
|
||||
'name': 'xxx',
|
||||
'product_id': self.comp1_sn.id,
|
||||
'product_min_qty': 0,
|
||||
'product_max_qty': 0,
|
||||
'location_id': self.env.user.company_id.subcontracting_location_id.id,
|
||||
'group_id': pg1.id,
|
||||
})
|
||||
|
||||
# Run the scheduler and check the created picking
|
||||
self.env['procurement.group'].run_scheduler()
|
||||
picking = self.env['stock.picking'].search([('group_id', '=', pg1.id)])
|
||||
self.assertEqual(len(picking), 1)
|
||||
self.assertEquals(picking.picking_type_id, wh.out_type_id)
|
||||
|
||||
lot_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.finished_lot.id
|
||||
})
|
||||
serial_id = self.env['stock.production.lot'].create({
|
||||
'name': 'lot1',
|
||||
'product_id': self.comp1_sn.id
|
||||
})
|
||||
produce_form = Form(self.env['mrp.product.produce'].with_context({
|
||||
'active_id': mo.id,
|
||||
'active_ids': [mo.id],
|
||||
}))
|
||||
produce_form.finished_lot_id = lot_id
|
||||
produce_form.raw_workorder_line_ids._records[0]['lot_id'] = serial_id.id
|
||||
wiz_produce = produce_form.save()
|
||||
wiz_produce.do_produce()
|
||||
|
||||
# We should not be able to call the 'record_components' button
|
||||
self.assertFalse(picking_receipt.display_action_record_components)
|
||||
# We should see the move lines stat button
|
||||
self.assertTrue(picking_receipt.display_view_subcontracted_move_lines)
|
||||
|
||||
picking_receipt.move_lines.quantity_done = 1
|
||||
picking_receipt.move_lines.move_line_ids.lot_name = 'lot00001test'
|
||||
picking_receipt.button_validate()
|
||||
self.assertEquals(mo.state, 'done')
|
||||
|
||||
# Available quantities should be negative at the subcontracting location for each components
|
||||
avail_qty_comp1 = self.env['stock.quant']._get_available_quantity(self.comp1_sn, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_comp2 = self.env['stock.quant']._get_available_quantity(self.comp2, self.subcontractor_partner1.property_stock_supplier, allow_negative=True)
|
||||
avail_qty_finished = self.env['stock.quant']._get_available_quantity(self.finished_lot, wh.lot_stock_id)
|
||||
self.assertEquals(avail_qty_comp1, -1)
|
||||
self.assertEquals(avail_qty_comp2, -1)
|
||||
self.assertEquals(avail_qty_finished, 1)
|
||||
@@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="mrp_bom_form_view" model="ir.ui.view">
|
||||
<field name="name">mrp.bom.form.view</field>
|
||||
<field name="model">mrp.bom</field>
|
||||
<field name="inherit_id" ref="mrp.mrp_bom_form_view" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='type']" position="after">
|
||||
<field name="subcontractor_ids" widget="many2many_tags" context="{'default_type': 'subcontractor'}" attrs="{'invisible': [('type', '!=', 'subcontract')]}"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="stock_picking_form_view" model="ir.ui.view">
|
||||
<field name="name">stock.picking.form.view</field>
|
||||
<field name="model">stock.picking</field>
|
||||
<field name="inherit_id" ref="stock.view_picking_form" />
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='button_box']" position="inside">
|
||||
<field name="display_view_subcontracted_move_lines" invisible="1" />
|
||||
<button string="Subcontracted Moves" type="object"
|
||||
name="action_view_subcontracted_move_lines"
|
||||
class="oe_stat_button" icon="fa-exchange"
|
||||
attrs="{'invisible': [('display_view_subcontracted_move_lines', '=', False)]}" />
|
||||
</xpath>
|
||||
|
||||
<xpath expr="//button[@name='button_validate'][hasclass('o_btn_validate')]" position="before">
|
||||
<field name="display_action_record_components" invisible="1" />
|
||||
<button name="action_record_components" class="oe_highlight" attrs="{'invisible': [('display_action_record_components', '=', False)]}" string="Record components" type="object"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="view_warehouse_inherit_mrp_subcontracting" model="ir.ui.view">
|
||||
<field name="name">Stock Warehouse Inherit Subcontracting</field>
|
||||
<field name="model">stock.warehouse</field>
|
||||
<field name="inherit_id" ref="mrp.view_warehouse_inherit_mrp"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='manufacture_to_resupply']" position="before">
|
||||
<field name="subcontracting_to_resupply" />
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -138,7 +138,7 @@
|
||||
<div t-if="thanks_msg and payment_tx_id.acquirer_id.provider == 'transfer' and reference">
|
||||
<b>Communication: </b><span t-esc='reference'/>
|
||||
</div>
|
||||
<div t-if="(payment_tx_id.partner_id.country_id.code in ['BE', 'AT', 'DE', 'FI', 'NL']) and (payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.currency_id.name == 'EUR')">
|
||||
<div t-if="(payment_tx_id.acquirer_id.qr_code == True) and (payment_tx_id.currency_id.name == 'EUR')">
|
||||
<div t-if="payment_tx_id.acquirer_id.journal_id.bank_account_id.qr_code_valid">
|
||||
<h3>Or scan me with your banking app.</h3>
|
||||
<img class="border border-dark rounded" t-att-src="payment_tx_id.acquirer_id.journal_id.bank_account_id.build_qr_code_url(payment_tx_id.amount,payment_tx_id.reference)"/>
|
||||
|
||||
@@ -238,7 +238,12 @@ class AuthorizeAPI():
|
||||
etree.SubElement(root_profile, "customerProfileId").text = res['profile_id']
|
||||
etree.SubElement(root_profile, "customerPaymentProfileId").text = res['payment_profile_id']
|
||||
response_profile = self._authorize_request(root_profile)
|
||||
res['name'] = response_profile.find('paymentProfile/payment/creditCard/cardNumber').text
|
||||
card_number = response_profile.find('paymentProfile/payment/creditCard/cardNumber')
|
||||
if card_number:
|
||||
res['name'] = card_number.text
|
||||
else:
|
||||
res['name'] = response_profile.find('paymentProfile/payment/bankAccount/accountNumber').text
|
||||
|
||||
return res
|
||||
|
||||
def credit(self, token, amount, transaction_id):
|
||||
|
||||
@@ -44,9 +44,9 @@ class AcquirerSips(models.Model):
|
||||
provider = fields.Selection(selection_add=[('sips', 'Sips')])
|
||||
sips_merchant_id = fields.Char('Merchant ID', help="Used for production only", required_if_provider='sips', groups='base.group_user')
|
||||
sips_secret = fields.Char('Secret Key', size=64, required_if_provider='sips', groups='base.group_user')
|
||||
sips_test_url = fields.Char("Test url", required_if_provider='sips', groups='base.group_no_one', default='https://payment-webinit.sips-atos.com/paymentInit')
|
||||
sips_prod_url = fields.Char("Production url", required_if_provider='sips', groups='base.group_no_one', default='https://payment-webinit.simu.sips-atos.com/paymentInit')
|
||||
sips_version = fields.Char("Interface Version", required_if_provider='sips', groups='base.group_no_one', default='HP_2.3')
|
||||
sips_test_url = fields.Char("Test url", required_if_provider='sips', default='https://payment-webinit.simu.sips-atos.com/paymentInit')
|
||||
sips_prod_url = fields.Char("Production url", required_if_provider='sips', default='https://payment-webinit.sips-atos.com/paymentInit')
|
||||
sips_version = fields.Char("Interface Version", required_if_provider='sips', default='HP_2.3')
|
||||
|
||||
def _sips_generate_shasign(self, values):
|
||||
""" Generate the shasign for incoming or outgoing communications.
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
<group attrs="{'invisible': [('provider', '!=', 'sips')]}">
|
||||
<field name="sips_merchant_id"/>
|
||||
<field name="sips_secret"/>
|
||||
<field name="sips_test_url"/>
|
||||
<field name="sips_prod_url"/>
|
||||
<field name="sips_version"/>
|
||||
<field name="sips_test_url" groups='base.group_no_one'/>
|
||||
<field name="sips_prod_url" groups='base.group_no_one'/>
|
||||
<field name="sips_version" groups='base.group_no_one'/>
|
||||
</group>
|
||||
</xpath>
|
||||
</field>
|
||||
|
||||
@@ -25,3 +25,12 @@ class AccountTax(models.Model):
|
||||
'You must close the POS sessions before modifying the tax.'
|
||||
))
|
||||
return super(AccountTax, self).write(vals)
|
||||
|
||||
def get_real_tax_amount(self):
|
||||
tax_list = []
|
||||
for tax in self:
|
||||
tax_repartition_lines = tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
|
||||
total_factor = sum(tax_repartition_lines.mapped('factor'))
|
||||
real_amount = tax.amount * total_factor
|
||||
tax_list.append({'id': tax.id, 'amount': real_amount})
|
||||
return tax_list
|
||||
|
||||
@@ -122,8 +122,7 @@ body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_order
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
}
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div,
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
|
||||
body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div, body .pos-customer_facing_display .pos_orderlines .pos_orderlines_item.pos_orderlines_header > div:last-child {
|
||||
border-left-width: 0;
|
||||
text-align: center;
|
||||
font-size: 70%;
|
||||
@@ -423,7 +422,7 @@ body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .p
|
||||
text-align: center;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-company_logo {
|
||||
display: none!important;
|
||||
display: none !important;
|
||||
}
|
||||
body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details {
|
||||
-webkit-box-flex: 1 0 100%;
|
||||
@@ -439,6 +438,7 @@ body .pos-customer_facing_display .pos-payment_info .pos-payment_info_details .p
|
||||
font-size: 6.5vw;
|
||||
}
|
||||
}
|
||||
|
||||
body .pos-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -460,7 +460,7 @@ body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-gro
|
||||
}
|
||||
body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group > .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu > a {
|
||||
position: relative;
|
||||
padding: 14px 0!important;
|
||||
padding: 14px 0 !important;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
body.editor_enable #oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group > .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu > a:before {
|
||||
@@ -470,6 +470,7 @@ body.editor_enable #oe_manipulators .no_parent_options .snippet-option-pos_palet
|
||||
body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pos-palette_01 .pos-payment_info {
|
||||
background: #3E3E3E;
|
||||
color: #f6f6f6;
|
||||
@@ -506,6 +507,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_01:before {
|
||||
text-shadow: 0 0 1px #090909;
|
||||
}
|
||||
|
||||
.pos-palette_02 .pos-payment_info {
|
||||
background: #364152;
|
||||
color: #e6e7e8;
|
||||
@@ -542,6 +544,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_02:before {
|
||||
text-shadow: 0 0 1px #130d09;
|
||||
}
|
||||
|
||||
.pos-palette_03 .pos-payment_info {
|
||||
background: #1BA39C;
|
||||
color: #f6f6f6;
|
||||
@@ -578,6 +581,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_03:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_04 .pos-payment_info {
|
||||
background: #0b7b6c;
|
||||
color: #f6f6f6;
|
||||
@@ -614,6 +618,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_04:before {
|
||||
text-shadow: 0 0 1px #101113;
|
||||
}
|
||||
|
||||
.pos-palette_05 .pos-payment_info {
|
||||
background: #E26868;
|
||||
color: #f6f6f6;
|
||||
@@ -650,6 +655,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_05:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_06 .pos-payment_info {
|
||||
background: #9E373B;
|
||||
color: #f6f6f6;
|
||||
@@ -686,6 +692,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_06:before {
|
||||
text-shadow: 0 0 1px #090909;
|
||||
}
|
||||
|
||||
.pos-palette_07 .pos-payment_info {
|
||||
background: #ce9934;
|
||||
color: white;
|
||||
@@ -722,6 +729,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_07:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_08 .pos-payment_info {
|
||||
background: #a48c77;
|
||||
color: #f6f6f6;
|
||||
@@ -758,6 +766,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_08:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_09 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #014d43, #127e71);
|
||||
color: #f6f6f6;
|
||||
@@ -794,6 +803,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_09:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_10 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #e2316c, #ea4c89);
|
||||
color: white;
|
||||
@@ -830,6 +840,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_10:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_11 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #362b3d, #5b4a63);
|
||||
color: white;
|
||||
@@ -866,6 +877,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_11:before {
|
||||
text-shadow: 0 0 1px #131313;
|
||||
}
|
||||
|
||||
.pos-palette_12 .pos-payment_info {
|
||||
background: #434343;
|
||||
color: #e6e6e6;
|
||||
@@ -902,6 +914,7 @@ body.editor_enable #oe_manipulators .no_parent_options .o_parent_editor_header {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > .pos-palette_12:before {
|
||||
text-shadow: 0 0 1px #a4a4a4;
|
||||
}
|
||||
|
||||
.pos-palette_13 .pos-payment_info {
|
||||
background: linear-gradient(30deg, #1a1b1f, #3d3f45);
|
||||
color: white;
|
||||
|
||||
@@ -248,6 +248,19 @@ exports.PosModel = Backbone.Model.extend({
|
||||
return self.taxes_by_id[child_tax_id];
|
||||
});
|
||||
});
|
||||
return new Promise(function (resolve, reject) {
|
||||
var tax_ids = _.pluck(self.taxes, 'id');
|
||||
rpc.query({
|
||||
model: 'account.tax',
|
||||
method: 'get_real_tax_amount',
|
||||
args: [tax_ids],
|
||||
}).then(function (taxes) {
|
||||
_.each(taxes, function (tax) {
|
||||
self.taxes_by_id[tax.id].amount = tax.amount;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},{
|
||||
model: 'pos.session',
|
||||
@@ -828,7 +841,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
self.flush_mutex.exec(function () {
|
||||
var flushed = self._flush_orders(self.db.get_orders(), opts);
|
||||
|
||||
flushed.then(resolve, resolve);
|
||||
flushed.then(resolve, reject);
|
||||
|
||||
return flushed;
|
||||
});
|
||||
@@ -912,13 +925,14 @@ exports.PosModel = Backbone.Model.extend({
|
||||
});
|
||||
|
||||
return server_ids;
|
||||
}).catch(function(){
|
||||
}).catch(function(error){
|
||||
var pending = self.db.get_orders().length;
|
||||
if (self.get('failed')) {
|
||||
self.set('synch', { state: 'error', pending: pending });
|
||||
} else {
|
||||
self.set('synch', { state: 'disconnected', pending: pending });
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -981,6 +995,7 @@ exports.PosModel = Backbone.Model.extend({
|
||||
self.set('failed',error);
|
||||
}
|
||||
console.error('Failed to send orders:', orders);
|
||||
throw error;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -99,7 +99,8 @@ body {
|
||||
@include flex-display;
|
||||
@include flex-direction(row);
|
||||
|
||||
.pos-customer_products, .pos-payment_info {
|
||||
.pos-customer_products,
|
||||
.pos-payment_info {
|
||||
height: 100%;
|
||||
padding: 2%;
|
||||
@include flex-display;
|
||||
@@ -139,11 +140,14 @@ body {
|
||||
box-shadow: none;
|
||||
animation: none;
|
||||
|
||||
> div, > div:last-child {
|
||||
border-left-width: 0;
|
||||
text-align: center;
|
||||
font-size: 70%;
|
||||
font-weight: normal;
|
||||
> div {
|
||||
&,
|
||||
&:last-child {
|
||||
border-left-width: 0;
|
||||
text-align: center;
|
||||
font-size: 70%;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
> div:last-child {
|
||||
@@ -194,7 +198,8 @@ body {
|
||||
@include flex-direction(column);
|
||||
@include justify-content(space-between);
|
||||
|
||||
.pos-adv, .pos-company_logo {
|
||||
.pos-adv,
|
||||
.pos-company_logo {
|
||||
@include pos-bg;
|
||||
}
|
||||
|
||||
@@ -212,7 +217,8 @@ body {
|
||||
}
|
||||
|
||||
.pos-payment_info_details{
|
||||
.pos-total, .pos-paymentlines {
|
||||
.pos-total,
|
||||
.pos-paymentlines {
|
||||
@include flex-direction(row);
|
||||
@include flex-display;
|
||||
@include flex-wrap(wrap);
|
||||
@@ -221,6 +227,7 @@ body {
|
||||
|
||||
> div {
|
||||
@include flex(1 0 48%);
|
||||
|
||||
&:nth-child(even) {
|
||||
font-weight: bold;
|
||||
font-size: 120%;
|
||||
@@ -433,7 +440,7 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group> .dropdown.oe_options.btn-group.snippet-option-pos_palette > ul {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options > .btn-group> .dropdown.oe_options.btn-group.snippet-option-pos_palette > .dropdown-menu {
|
||||
min-width: 100px;
|
||||
max-width: 100px;
|
||||
|
||||
@@ -441,7 +448,7 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> li > a {
|
||||
> a {
|
||||
position: relative;
|
||||
padding: 14px 0!important;
|
||||
margin-bottom: 1px;
|
||||
@@ -453,7 +460,8 @@ body {
|
||||
}
|
||||
|
||||
#oe_manipulators .no_parent_options {
|
||||
.snippet-option-pos_palette, .o_parent_editor_header {
|
||||
.snippet-option-pos_palette,
|
||||
.o_parent_editor_header {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -462,7 +470,7 @@ body {
|
||||
|
||||
|
||||
// =========== PALETTE GENERATOR ===========
|
||||
.palette_variant (
|
||||
@mixin palette-variant (
|
||||
$bg-info: $color-gray-dark,
|
||||
$text-info: $color-gray-lighter,
|
||||
$bg-products: $color-gray-lighter,
|
||||
@@ -497,7 +505,7 @@ body {
|
||||
}
|
||||
|
||||
// Editor dropdown buttons
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > ul > li > & {
|
||||
#oe_manipulators .oe_overlay > .oe_overlay_options .snippet-option-pos_palette > .dropdown-menu > & {
|
||||
background: $bg-products!important;
|
||||
|
||||
&:after {
|
||||
@@ -511,7 +519,7 @@ body {
|
||||
}
|
||||
|
||||
&:before {
|
||||
text-shadow: 0 0 1px (#fff - $bg-products);
|
||||
text-shadow: 0 0 1px invert($bg-products);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,6 +139,7 @@ class Pricelist(models.Model):
|
||||
self.ensure_one()
|
||||
if not date:
|
||||
date = self._context.get('date') or fields.Date.context_today(self)
|
||||
date = fields.Date.to_date(date) # handwritten query below does not work with non-dates (e.g. datetimes)
|
||||
if not uom_id and self._context.get('uom'):
|
||||
uom_id = self._context['uom']
|
||||
if uom_id:
|
||||
@@ -329,10 +330,10 @@ class Pricelist(models.Model):
|
||||
return res[partner_id].id
|
||||
|
||||
def _get_partner_pricelist_multi_search_domain_hook(self):
|
||||
return []
|
||||
return [('active', '=', True)]
|
||||
|
||||
def _get_partner_pricelist_multi_filter_hook(self):
|
||||
return self
|
||||
return self.filtered('active')
|
||||
|
||||
def _get_partner_pricelist_multi(self, partner_ids, company_id=None):
|
||||
""" Retrieve the applicable pricelist for given partners in a given company.
|
||||
|
||||
@@ -413,13 +413,20 @@ class ProductTemplate(models.Model):
|
||||
|
||||
Product = self.env['product.product']
|
||||
templates = self.browse([])
|
||||
domain_no_variant = [('product_variant_ids', '=', False)]
|
||||
while True:
|
||||
domain = templates and [('product_tmpl_id', 'not in', templates.ids)] or []
|
||||
args = args if args is not None else []
|
||||
products_ns = Product._name_search(name, args+domain, operator=operator, name_get_uid=name_get_uid)
|
||||
products = Product.browse([x[0] for x in products_ns])
|
||||
templates |= products.mapped('product_tmpl_id')
|
||||
if (not products) or (limit and (len(templates) > limit)):
|
||||
current_round_templates = self.browse([])
|
||||
if not products:
|
||||
domain_template = args + domain_no_variant + (templates and [('id', 'not in', templates.ids)] or [])
|
||||
template_ns = super(ProductTemplate, self)._name_search(name=name, args=domain_template, operator=operator, limit=limit, name_get_uid=name_get_uid)
|
||||
current_round_templates |= self.browse([ns[0] for ns in template_ns])
|
||||
templates |= current_round_templates
|
||||
if (not products and not current_round_templates) or (limit and (len(templates) > limit)):
|
||||
break
|
||||
|
||||
searched_ids = set(templates.ids)
|
||||
|
||||
@@ -817,3 +817,25 @@ class TestVariantsArchive(common.TestProductCommon):
|
||||
"We should have re-activated one variant only.")
|
||||
|
||||
Product._revert_method('unlink')
|
||||
|
||||
def test_name_search_dynamic_attributes(self):
|
||||
dynamic_attr = self.env['product.attribute'].create({
|
||||
'name': 'Dynamic',
|
||||
'create_variant': 'dynamic',
|
||||
'value_ids': [(0, False, {'name': 'ValueDynamic'})],
|
||||
})
|
||||
template = self.env['product.template'].create({
|
||||
'name': 'cimanyd'
|
||||
})
|
||||
self.assertEqual(len(template.product_variant_ids), 1)
|
||||
|
||||
template.write({
|
||||
'attribute_line_ids': [(0, False, {
|
||||
'attribute_id': dynamic_attr.id,
|
||||
'value_ids': [(4, dynamic_attr.value_ids[0].id, False)],
|
||||
})]
|
||||
})
|
||||
self.assertEqual(len(template.product_variant_ids), 0)
|
||||
|
||||
name_searched = self.env['product.template'].name_search(name='cima')
|
||||
self.assertIn(template.id, [ng[0] for ng in name_searched])
|
||||
|
||||
@@ -405,8 +405,6 @@ class PurchaseOrder(models.Model):
|
||||
'default_type': 'in_invoice',
|
||||
'default_purchase_id': self.id,
|
||||
}
|
||||
if self.user_id:
|
||||
result['context']['default_user_id'] = self.user_id.id
|
||||
# choose the view_mode accordingly
|
||||
if len(self.invoice_ids) > 1 and not create_bill:
|
||||
result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user