[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:
Christophe Simonis
2019-09-11 09:39:33 +00:00
181 changed files with 2494 additions and 1780 deletions
+38 -23
View File
@@ -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 {
+31 -21
View File
@@ -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:
+2 -1
View File
@@ -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
+12 -6
View File
@@ -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
+1
View File
@@ -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()
+80 -16
View File
@@ -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)
+255 -7
View File
@@ -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)
)
+1 -1
View File
@@ -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})
+4 -13
View File
@@ -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)],
+5 -1
View File
@@ -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:
+6
View File
@@ -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.
*
+4 -2
View File
@@ -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):
+58
View File
@@ -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)
+22 -1
View File
@@ -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),
+1
View File
@@ -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)
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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">
+5 -2
View File
@@ -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 &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; 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 &lt; record.last_update.raw_value &amp; record.state.raw_value == 'failed' ? 'oe_kanban_color_2' : ''} #{record.end_date.raw_value &lt; record.last_update.raw_value &amp; 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">
+9 -4
View File
@@ -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)]
+64
View File
@@ -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()
+13 -3
View File
@@ -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':
+1
View File
@@ -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"/>
+5 -2
View File
@@ -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>
+14
View File
@@ -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
+1
View File
@@ -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):
-14
View File
@@ -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),
+9 -9
View File
@@ -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>
+6 -6
View File
@@ -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()
+7
View File
@@ -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')
+1 -1
View File
@@ -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',
}
+3 -3
View File
@@ -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>
+14 -5
View File
@@ -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):
"""
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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.
+6 -1
View File
@@ -8,7 +8,12 @@ var _t = core._t;
function parseAndTransform(htmlString, transformFunction) {
var openToken = "OPEN" + Date.now();
var string = htmlString.replace(/&lt;/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"), "&lt;");
}
@@ -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%;
+1
View File
@@ -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">
+3
View File
@@ -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
-1
View File
@@ -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')
+6 -1
View File
@@ -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">
-25
View File
@@ -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()
-19
View File
@@ -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):
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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;
+17 -2
View File
@@ -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);
}
}
};
+3 -2
View File
@@ -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.
+8 -1
View File
@@ -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)
+22
View File
@@ -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])
-2
View File
@@ -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