[MERGE] forward port branch saas-12.4 up to 618ea1ab67
This commit is contained in:
+68
-23
@@ -17,6 +17,11 @@ file_filter = addons/account_analytic_default/i18n/<lang>.po
|
||||
source_file = addons/account_analytic_default/i18n/account_analytic_default.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.account_analytic_default_hr_expense]
|
||||
file_filter = addons/account_analytic_default_hr_expense/i18n/<lang>.po
|
||||
source_file = addons/account_analytic_default_hr_expense/i18n/account_analytic_default_hr_expense.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.account_bank_statement_import]
|
||||
file_filter = addons/account_bank_statement_import/i18n/<lang>.po
|
||||
source_file = addons/account_bank_statement_import/i18n/account_bank_statement_import.pot
|
||||
@@ -32,6 +37,16 @@ file_filter = addons/account_check_printing/i18n/<lang>.po
|
||||
source_file = addons/account_check_printing/i18n/account_check_printing.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.account_facturx]
|
||||
file_filter = addons/account_facturx/i18n/<lang>.po
|
||||
source_file = addons/account_facturx/i18n/account_facturx.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.account_lock]
|
||||
file_filter = addons/account_lock/i18n/<lang>.po
|
||||
source_file = addons/account_lock/i18n/account_lock.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.account_payment]
|
||||
file_filter = addons/account_payment/i18n/<lang>.po
|
||||
source_file = addons/account_payment/i18n/account_payment.pot
|
||||
@@ -42,11 +57,6 @@ file_filter = addons/account_tax_python/i18n/<lang>.po
|
||||
source_file = addons/account_tax_python/i18n/account_tax_python.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.account_test]
|
||||
file_filter = addons/account_test/i18n/<lang>.po
|
||||
source_file = addons/account_test/i18n/account_test.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.account_voucher]
|
||||
file_filter = addons/account_voucher/i18n/<lang>.po
|
||||
source_file = addons/account_voucher/i18n/account_voucher.pot
|
||||
@@ -82,11 +92,6 @@ file_filter = addons/auth_password_policy/i18n/<lang>.po
|
||||
source_file = addons/auth_password_policy/i18n/auth_password_policy.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.auth_password_policy_signup]
|
||||
file_filter = addons/auth_password_policy_signup/i18n/<lang>.po
|
||||
source_file = addons/auth_password_policy_signup/i18n/auth_password_policy_signup.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.auth_signup]
|
||||
file_filter = addons/auth_signup/i18n/<lang>.po
|
||||
source_file = addons/auth_signup/i18n/auth_signup.pot
|
||||
@@ -277,6 +282,11 @@ file_filter = addons/hr_attendance/i18n/<lang>.po
|
||||
source_file = addons/hr_attendance/i18n/hr_attendance.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_attendance_presence]
|
||||
file_filter = addons/hr_attendance_presence/i18n/<lang>.po
|
||||
source_file = addons/hr_attendance_presence/i18n/hr_attendance_presence.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_contract]
|
||||
file_filter = addons/hr_contract/i18n/<lang>.po
|
||||
source_file = addons/hr_contract/i18n/hr_contract.pot
|
||||
@@ -317,16 +327,6 @@ file_filter = addons/hr_org_chart/i18n/<lang>.po
|
||||
source_file = addons/hr_org_chart/i18n/hr_org_chart.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_payroll]
|
||||
file_filter = addons/hr_payroll/i18n/<lang>.po
|
||||
source_file = addons/hr_payroll/i18n/hr_payroll.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_payroll_account]
|
||||
file_filter = addons/hr_payroll_account/i18n/<lang>.po
|
||||
source_file = addons/hr_payroll_account/i18n/hr_payroll_account.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.hr_presence]
|
||||
file_filter = addons/hr_presence/i18n/<lang>.po
|
||||
source_file = addons/hr_presence/i18n/hr_presence.pot
|
||||
@@ -447,14 +447,19 @@ file_filter = addons/mrp/i18n/<lang>.po
|
||||
source_file = addons/mrp/i18n/mrp.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.mrp_account]
|
||||
file_filter = addons/mrp_account/i18n/<lang>.po
|
||||
source_file = addons/mrp_account/i18n/mrp_account.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.mrp_bom_cost]
|
||||
file_filter = addons/mrp_bom_cost/i18n/<lang>.po
|
||||
source_file = addons/mrp_bom_cost/i18n/mrp_bom_cost.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.mrp_byproduct]
|
||||
file_filter = addons/mrp_byproduct/i18n/<lang>.po
|
||||
source_file = addons/mrp_byproduct/i18n/mrp_byproduct.pot
|
||||
[odoo-master.mrp_subcontracting]
|
||||
file_filter = addons/mrp_subcontracting/i18n/<lang>.po
|
||||
source_file = addons/mrp_subcontracting/i18n/mrp_subcontracting.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.note]
|
||||
@@ -482,6 +487,11 @@ file_filter = addons/partner_autocomplete/i18n/<lang>.po
|
||||
source_file = addons/partner_autocomplete/i18n/partner_autocomplete.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.partner_autocomplete_address_extended]
|
||||
file_filter = addons/partner_autocomplete_address_extended/i18n/<lang>.po
|
||||
source_file = addons/partner_autocomplete_address_extended/i18n/partner_autocomplete_address_extended.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.payment]
|
||||
file_filter = addons/payment/i18n/<lang>.po
|
||||
source_file = addons/payment/i18n/payment.pot
|
||||
@@ -632,6 +642,11 @@ file_filter = addons/purchase_mrp/i18n/<lang>.po
|
||||
source_file = addons/purchase_mrp/i18n/purchase_mrp.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.purchase_mrp_subcontracting]
|
||||
file_filter = addons/purchase_mrp_subcontracting/i18n/<lang>.po
|
||||
source_file = addons/purchase_mrp_subcontracting/i18n/purchase_mrp_subcontracting.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.purchase_requisition]
|
||||
file_filter = addons/purchase_requisition/i18n/<lang>.po
|
||||
source_file = addons/purchase_requisition/i18n/purchase_requisition.pot
|
||||
@@ -667,6 +682,16 @@ file_filter = addons/sale/i18n/<lang>.po
|
||||
source_file = addons/sale/i18n/sale.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.sale_coupon]
|
||||
file_filter = addons/sale_coupon/i18n/<lang>.po
|
||||
source_file = addons/sale_coupon/i18n/sale_coupon.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.sale_coupon_delivery]
|
||||
file_filter = addons/sale_coupon_delivery/i18n/<lang>.po
|
||||
source_file = addons/sale_coupon_delivery/i18n/sale_coupon_delivery.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.sale_crm]
|
||||
file_filter = addons/sale_crm/i18n/<lang>.po
|
||||
source_file = addons/sale_crm/i18n/sale_crm.pot
|
||||
@@ -692,6 +717,11 @@ file_filter = addons/sale_mrp/i18n/<lang>.po
|
||||
source_file = addons/sale_mrp/i18n/sale_mrp.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.sale_product_configurator]
|
||||
file_filter = addons/sale_product_configurator/i18n/<lang>.po
|
||||
source_file = addons/sale_product_configurator/i18n/sale_product_configurator.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.sale_purchase]
|
||||
file_filter = addons/sale_purchase/i18n/<lang>.po
|
||||
source_file = addons/sale_purchase/i18n/sale_purchase.pot
|
||||
@@ -952,6 +982,16 @@ file_filter = addons/website_sale_comparison/i18n/<lang>.po
|
||||
source_file = addons/website_sale_comparison/i18n/website_sale_comparison.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.website_sale_coupon]
|
||||
file_filter = addons/website_sale_coupon/i18n/<lang>.po
|
||||
source_file = addons/website_sale_coupon/i18n/website_sale_coupon.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.website_sale_coupon_delivery]
|
||||
file_filter = addons/website_sale_coupon_delivery/i18n/<lang>.po
|
||||
source_file = addons/website_sale_coupon_delivery/i18n/website_sale_coupon_delivery.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.website_sale_delivery]
|
||||
file_filter = addons/website_sale_delivery/i18n/<lang>.po
|
||||
source_file = addons/website_sale_delivery/i18n/website_sale_delivery.pot
|
||||
@@ -972,6 +1012,11 @@ file_filter = addons/website_sale_management/i18n/<lang>.po
|
||||
source_file = addons/website_sale_management/i18n/website_sale_management.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.website_sale_product_configurator]
|
||||
file_filter = addons/website_sale_product_configurator/i18n/<lang>.po
|
||||
source_file = addons/website_sale_product_configurator/i18n/website_sale_product_configurator.pot
|
||||
source_lang = en
|
||||
|
||||
[odoo-master.website_sale_slides]
|
||||
file_filter = addons/website_sale_slides/i18n/<lang>.po
|
||||
source_file = addons/website_sale_slides/i18n/website_sale_slides.pot
|
||||
|
||||
@@ -1097,10 +1097,17 @@ class AccountTax(models.Model):
|
||||
for record in self:
|
||||
record._check_repartition_lines(record.invoice_repartition_line_ids)
|
||||
record._check_repartition_lines(record.refund_repartition_line_ids)
|
||||
sum_inv_tax = sum(record.mapped('invoice_repartition_line_ids.factor_percent'))
|
||||
sum_refund_tax = sum(record.mapped('refund_repartition_line_ids.factor_percent'))
|
||||
if float_compare(sum_inv_tax, sum_refund_tax, precision_digits=4) != 0:
|
||||
raise ValidationError(_("You should impact the same total percentage of the tax amount for invoices and refunds."))
|
||||
|
||||
if len(record.invoice_repartition_line_ids) != len(record.refund_repartition_line_ids):
|
||||
raise ValidationError(_("Invoice and credit note repartition should have the same number of lines."))
|
||||
|
||||
index = 0
|
||||
while index < len(record.invoice_repartition_line_ids):
|
||||
inv_rep_ln = record.invoice_repartition_line_ids[index]
|
||||
ref_rep_ln = record.refund_repartition_line_ids[index]
|
||||
if inv_rep_ln.repartition_type != ref_rep_ln.repartition_type or inv_rep_ln.factor_percent != ref_rep_ln.factor_percent:
|
||||
raise ValidationError(_("Invoice and credit note repartitions should match (same percentages, in the same order)."))
|
||||
index += 1
|
||||
|
||||
@api.one
|
||||
@api.constrains('children_tax_ids', 'type_tax_use')
|
||||
|
||||
@@ -444,6 +444,7 @@ class AccountBankStatementLine(models.Model):
|
||||
""" Prepare the dict of values to balance the move.
|
||||
|
||||
:param recordset move: the account.move to link the move line
|
||||
:param dict move: a dict of vals of a account.move which will be created later
|
||||
:param float amount: the amount of transaction that wasn't already reconciled
|
||||
"""
|
||||
company_currency = self.journal_id.company_id.currency_id
|
||||
@@ -451,19 +452,23 @@ class AccountBankStatementLine(models.Model):
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
amount_currency = False
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
if isinstance(move, dict):
|
||||
amount_sum = sum(x[2].get('amount_currency', 0) for x in move['line_ids'])
|
||||
else:
|
||||
amount_sum = sum(x.amount_currency for x in move.line_ids)
|
||||
# We have several use case here to compare the currency and amount currency of counterpart line to balance the move:
|
||||
if st_line_currency != company_currency and st_line_currency == statement_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency B
|
||||
# counterpart line must have currency B and correct amount is inverse of already existing lines
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])
|
||||
amount_currency = -amount_sum
|
||||
elif st_line_currency != company_currency and statement_currency == company_currency:
|
||||
# company in currency A, statement in currency A and transaction in currency B
|
||||
# counterpart line must have currency B and correct amount is inverse of already existing lines
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])
|
||||
amount_currency = -amount_sum
|
||||
elif st_line_currency != company_currency and st_line_currency != statement_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency C
|
||||
# counterpart line must have currency B and use rate between B and C to compute correct amount
|
||||
amount_currency = -sum([x.amount_currency for x in move.line_ids])/st_line_currency_rate
|
||||
amount_currency = -amount_sum/st_line_currency_rate
|
||||
elif st_line_currency == company_currency and statement_currency != company_currency:
|
||||
# company in currency A, statement in currency B and transaction in currency A
|
||||
# counterpart line must have currency B and amount is computed using the rate between A and B
|
||||
@@ -472,9 +477,8 @@ class AccountBankStatementLine(models.Model):
|
||||
# last case is company in currency A, statement in currency A and transaction in currency A
|
||||
# and in this case counterpart line does not need any second currency nor amount_currency
|
||||
|
||||
return {
|
||||
aml_dict = {
|
||||
'name': self.name,
|
||||
'move_id': move.id,
|
||||
'partner_id': self.partner_id and self.partner_id.id or False,
|
||||
'account_id': amount >= 0 \
|
||||
and self.statement_id.journal_id.default_credit_account_id.id \
|
||||
@@ -485,22 +489,72 @@ class AccountBankStatementLine(models.Model):
|
||||
'currency_id': statement_currency != company_currency and statement_currency.id or (st_line_currency != company_currency and st_line_currency.id or False),
|
||||
'amount_currency': amount_currency,
|
||||
}
|
||||
if isinstance(move, self.env['account.move'].__class__):
|
||||
aml_dict['move_id'] = move.id
|
||||
return aml_dict
|
||||
|
||||
@api.multi
|
||||
def fast_counterpart_creation(self):
|
||||
"""This function is called when confirming a bank statement and will allow to automatically process lines without
|
||||
going in the bank reconciliation widget. By setting an account_id on bank statement lines, it will create a journal
|
||||
entry using that account to counterpart the bank account
|
||||
"""
|
||||
payment_list = []
|
||||
move_list = []
|
||||
account_type_receivable = self.env.ref('account.data_account_type_receivable')
|
||||
already_done_stmt_line_ids = [a['statement_line_id'][0] for a in self.env['account.move.line'].read_group([('statement_line_id', 'in', self.ids)], ['statement_line_id'], ['statement_line_id'])]
|
||||
managed_st_line = []
|
||||
for st_line in self:
|
||||
# Technical functionality to automatically reconcile by creating a new move line
|
||||
if st_line.account_id and not st_line.journal_entry_ids.ids:
|
||||
vals = {
|
||||
if st_line.account_id and not st_line.id in already_done_stmt_line_ids:
|
||||
managed_st_line.append(st_line.id)
|
||||
# Create payment vals
|
||||
total = st_line.amount
|
||||
payment_methods = (total > 0) and st_line.journal_id.inbound_payment_method_ids or st_line.journal_id.outbound_payment_method_ids
|
||||
currency = st_line.journal_id.currency_id or st_line.company_id.currency_id
|
||||
partner_type = 'customer' if st_line.account_id.user_type_id == account_type_receivable else 'supplier'
|
||||
payment_list.append({
|
||||
'payment_method_id': payment_methods and payment_methods[0].id or False,
|
||||
'payment_type': total > 0 and 'inbound' or 'outbound',
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'partner_type': partner_type,
|
||||
'journal_id': st_line.statement_id.journal_id.id,
|
||||
'payment_date': st_line.date,
|
||||
'state': 'reconciled',
|
||||
'currency_id': currency.id,
|
||||
'amount': abs(total),
|
||||
'communication': st_line._get_communication(payment_methods[0] if payment_methods else False),
|
||||
'name': st_line.statement_id.name or _("Bank Statement %s") % st_line.date,
|
||||
})
|
||||
|
||||
# Create move and move line vals
|
||||
move_vals = st_line._prepare_reconciliation_move(st_line.statement_id.name)
|
||||
aml_dict = {
|
||||
'name': st_line.name,
|
||||
'debit': st_line.amount < 0 and -st_line.amount or 0.0,
|
||||
'credit': st_line.amount > 0 and st_line.amount or 0.0,
|
||||
'account_id': st_line.account_id.id,
|
||||
'partner_id': st_line.partner_id.id,
|
||||
'statement_line_id': st_line.id,
|
||||
}
|
||||
st_line.process_reconciliation(new_aml_dicts=[vals])
|
||||
st_line._prepare_move_line_for_currency(aml_dict, st_line.date or fields.Date.context_today())
|
||||
move_vals['line_ids'] = [(0, 0, aml_dict)]
|
||||
balance_line = self._prepare_reconciliation_move_line(
|
||||
move_vals, -aml_dict['debit'] if st_line.amount < 0 else aml_dict['credit'])
|
||||
move_vals['line_ids'].append((0, 0, balance_line))
|
||||
move_list.append(move_vals)
|
||||
|
||||
# Creates
|
||||
payment_ids = self.env['account.payment'].create(payment_list)
|
||||
for payment_id, move_vals in zip(payment_ids, move_list):
|
||||
for line in move_vals['line_ids']:
|
||||
line[2]['payment_id'] = payment_id.id
|
||||
move_ids = self.env['account.move'].create(move_list)
|
||||
move_ids.post()
|
||||
|
||||
for move, st_line, payment in zip(move_ids, self.browse(managed_st_line), payment_ids):
|
||||
st_line.write({'move_name': move.name})
|
||||
payment.write({'payment_reference': move.name})
|
||||
|
||||
def _get_communication(self, payment_method_id):
|
||||
return self.name or ''
|
||||
@@ -628,8 +682,6 @@ class AccountBankStatementLine(models.Model):
|
||||
# Create move line(s). Either matching an existing journal entry (eg. invoice), in which
|
||||
# case we reconcile the existing and the new move lines together, or being a write-off.
|
||||
if counterpart_aml_dicts or new_aml_dicts:
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
|
||||
# Create the move
|
||||
self.sequence = self.statement_id.line_ids.ids.index(self.id) + 1
|
||||
@@ -652,32 +704,12 @@ class AccountBankStatementLine(models.Model):
|
||||
|
||||
# Complete dicts to create both counterpart move lines and write-offs
|
||||
to_create = (counterpart_aml_dicts + new_aml_dicts)
|
||||
company = self.company_id
|
||||
date = self.date or fields.Date.today()
|
||||
for aml_dict in to_create:
|
||||
aml_dict['move_id'] = move.id
|
||||
aml_dict['partner_id'] = self.partner_id.id
|
||||
aml_dict['statement_line_id'] = self.id
|
||||
if st_line_currency.id != company_currency.id:
|
||||
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
|
||||
aml_dict['currency_id'] = st_line_currency.id
|
||||
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
|
||||
# Statement is in company currency but the transaction is in foreign currency
|
||||
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
|
||||
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
|
||||
elif self.currency_id and st_line_currency_rate:
|
||||
# Statement is in foreign currency and the transaction is in another one
|
||||
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
|
||||
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
|
||||
else:
|
||||
# Statement is in foreign currency and no extra currency is given for the transaction
|
||||
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
|
||||
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
|
||||
elif statement_currency.id != company_currency.id:
|
||||
# Statement is in foreign currency but the transaction is in company currency
|
||||
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
|
||||
aml_dict['amount_currency'] = prorata_factor * self.amount
|
||||
aml_dict['currency_id'] = statement_currency.id
|
||||
self._prepare_move_line_for_currency(aml_dict, date)
|
||||
|
||||
# Create write-offs
|
||||
for aml_dict in new_aml_dicts:
|
||||
@@ -727,6 +759,36 @@ class AccountBankStatementLine(models.Model):
|
||||
counterpart_moves.assert_balanced()
|
||||
return counterpart_moves
|
||||
|
||||
@api.multi
|
||||
def _prepare_move_line_for_currency(self, aml_dict, date):
|
||||
self.ensure_one()
|
||||
company_currency = self.journal_id.company_id.currency_id
|
||||
statement_currency = self.journal_id.currency_id or company_currency
|
||||
st_line_currency = self.currency_id or statement_currency
|
||||
st_line_currency_rate = self.currency_id and (self.amount_currency / self.amount) or False
|
||||
company = self.company_id
|
||||
|
||||
if st_line_currency.id != company_currency.id:
|
||||
aml_dict['amount_currency'] = aml_dict['debit'] - aml_dict['credit']
|
||||
aml_dict['currency_id'] = st_line_currency.id
|
||||
if self.currency_id and statement_currency.id == company_currency.id and st_line_currency_rate:
|
||||
# Statement is in company currency but the transaction is in foreign currency
|
||||
aml_dict['debit'] = company_currency.round(aml_dict['debit'] / st_line_currency_rate)
|
||||
aml_dict['credit'] = company_currency.round(aml_dict['credit'] / st_line_currency_rate)
|
||||
elif self.currency_id and st_line_currency_rate:
|
||||
# Statement is in foreign currency and the transaction is in another one
|
||||
aml_dict['debit'] = statement_currency._convert(aml_dict['debit'] / st_line_currency_rate, company_currency, company, date)
|
||||
aml_dict['credit'] = statement_currency._convert(aml_dict['credit'] / st_line_currency_rate, company_currency, company, date)
|
||||
else:
|
||||
# Statement is in foreign currency and no extra currency is given for the transaction
|
||||
aml_dict['debit'] = st_line_currency._convert(aml_dict['debit'], company_currency, company, date)
|
||||
aml_dict['credit'] = st_line_currency._convert(aml_dict['credit'], company_currency, company, date)
|
||||
elif statement_currency.id != company_currency.id:
|
||||
# Statement is in foreign currency but the transaction is in company currency
|
||||
prorata_factor = (aml_dict['debit'] - aml_dict['credit']) / self.amount_currency
|
||||
aml_dict['amount_currency'] = prorata_factor * self.amount
|
||||
aml_dict['currency_id'] = statement_currency.id
|
||||
|
||||
def _check_invoice_state(self, invoice):
|
||||
if invoice.state == 'in_payment' and all([payment.state == 'reconciled' for payment in invoice.mapped('payment_move_line_ids.payment_id')]):
|
||||
invoice.write({'state': 'paid'})
|
||||
|
||||
@@ -93,11 +93,12 @@ class AccountInvoice(models.Model):
|
||||
('type', 'in', [TYPE2JOURNAL[ty] for ty in inv_types if ty in TYPE2JOURNAL]),
|
||||
('company_id', '=', company_id),
|
||||
]
|
||||
journal_with_currency = False
|
||||
if self._context.get('default_currency_id'):
|
||||
currency_clause = [('currency_id', '=', self._context.get('default_currency_id'))]
|
||||
journal_with_currency = self.env['account.journal'].search(domain + currency_clause, limit=1)
|
||||
return journal_with_currency or self.env['account.journal'].search(domain, limit=1)
|
||||
company_currency_id = self.env['res.company'].browse(company_id).currency_id.id
|
||||
currency_id = self._context.get('default_currency_id') or company_currency_id
|
||||
currency_clause = [('currency_id', '=', currency_id)]
|
||||
if currency_id == company_currency_id:
|
||||
currency_clause = ['|', ('currency_id', '=', False)] + currency_clause
|
||||
return self.env['account.journal'].search(domain + currency_clause, limit=1)
|
||||
|
||||
@api.model
|
||||
def _default_currency(self):
|
||||
@@ -1555,80 +1556,22 @@ class AccountInvoice(models.Model):
|
||||
This function is used in order to fix account.invoice.tax objects generated
|
||||
for refunds when tax amounts have been modified manually.
|
||||
|
||||
For example, a tax with the following tax repartition...
|
||||
- INVOICE:
|
||||
1) 30%
|
||||
2) 70%
|
||||
- REFUND
|
||||
3) 40%
|
||||
4) 60%
|
||||
|
||||
... will group line 1) with line 3), and line 2) with lines 3) and 4).
|
||||
|
||||
:return: A dictionnary, with invoice repartition line ids as keys, and lists
|
||||
of corresponding refund repartition line ids as values.
|
||||
:return: A dictionnary, with invoice repartition line ids as keys, and refund
|
||||
repartition lines as values
|
||||
"""
|
||||
rslt = {}
|
||||
for tax in taxes:
|
||||
inv_index = 0
|
||||
ref_index = 0
|
||||
|
||||
tax_inv_lines = tax.invoice_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
|
||||
tax_ref_lines = tax.refund_repartition_line_ids.filtered(lambda x: x.repartition_type == 'tax')
|
||||
|
||||
amount_to_match = tax_inv_lines[inv_index].factor_percent
|
||||
while inv_index < len(tax_inv_lines) or ref_index < len(tax_ref_lines):
|
||||
inv_rep_line = inv_index < len(tax_inv_lines) and tax_inv_lines[inv_index] or tax_inv_lines[-1]
|
||||
ref_rep_line = ref_index < len(tax_ref_lines) and tax_ref_lines[ref_index] or tax_ref_lines[-1]
|
||||
|
||||
rslt_list = rslt.get(inv_rep_line.id, [])
|
||||
if ref_rep_line.id not in rslt_list:
|
||||
rslt_list.append(ref_rep_line.id)
|
||||
rslt[inv_rep_line.id] = rslt_list
|
||||
|
||||
if amount_to_match > 0:
|
||||
amount_to_match -= ref_rep_line.factor_percent
|
||||
ref_index += 1
|
||||
elif amount_to_match < 0:
|
||||
amount_to_match += inv_rep_line.factor_percent
|
||||
inv_index +=1
|
||||
else:
|
||||
inv_index += 1
|
||||
if inv_index < len(tax_inv_lines):
|
||||
amount_to_match = tax_inv_lines[inv_index].factor_percent
|
||||
index = 0
|
||||
while(index < len(tax.invoice_repartition_line_ids)):
|
||||
# _validate_repartition_lines constraint on taxes ensure invoice and refund repartition are equal, and in the same order
|
||||
inv_rep_ln = tax.invoice_repartition_line_ids[index]
|
||||
ref_rep_ln = tax.refund_repartition_line_ids[index]
|
||||
if inv_rep_ln.repartition_type == 'tax':
|
||||
rslt[inv_rep_ln.id] = ref_rep_ln
|
||||
index += 1
|
||||
|
||||
return rslt
|
||||
|
||||
def _group_tax_lines_by_repartition(self):
|
||||
self.ensure_one()
|
||||
rslt = {}
|
||||
for tax_line in self.tax_line_ids:
|
||||
rslt[tax_line.tax_repartition_line_id.id] = tax_line
|
||||
return rslt
|
||||
|
||||
@api.model
|
||||
def _fix_refund_tax_lines(self, invoice, refund):
|
||||
""" Modifies the tax_line_ids of a draft refund invoice in order to make it
|
||||
match the manual tax modifications that were made on its original invoice.
|
||||
"""
|
||||
invoice_tax_lines_map = invoice._group_tax_lines_by_repartition()
|
||||
refund_tax_lines_map = refund._group_tax_lines_by_repartition()
|
||||
refund_repartition_map = self._create_refund_repartition_mapping(invoice.mapped('invoice_line_ids.invoice_line_tax_ids'))
|
||||
computed_tax_values = invoice.get_taxes_values()
|
||||
|
||||
for tax_data in computed_tax_values.values():
|
||||
rep_line_id = tax_data['tax_repartition_line_id']
|
||||
|
||||
matching_invoice_tax = invoice_tax_lines_map.get(rep_line_id)
|
||||
|
||||
if matching_invoice_tax:
|
||||
ref_rep_line_id = refund_repartition_map[rep_line_id][0]
|
||||
refund_tax_line = refund_tax_lines_map.get(ref_rep_line_id)
|
||||
manual_difference = matching_invoice_tax.amount - tax_data['amount']
|
||||
|
||||
if not invoice.currency_id.is_zero(manual_difference):
|
||||
refund_tax_line.amount += manual_difference
|
||||
|
||||
@api.model
|
||||
def _prepare_refund(self, invoice, date_invoice=None, date=None, description=None, journal_id=None):
|
||||
""" Prepare the dict of values to create the new credit note from the invoice.
|
||||
@@ -1677,6 +1620,35 @@ class AccountInvoice(models.Model):
|
||||
values['date'] = date
|
||||
if description:
|
||||
values['name'] = description
|
||||
|
||||
# Treat refund tax lines.
|
||||
# We copy them from the invoice and replace their account_id and
|
||||
# tag_ids based on the refund repartition of the corresponding taxes.
|
||||
tax_rep_ln_mapping = {}
|
||||
for invoice_tax_entry in invoice.tax_line_ids:
|
||||
if not invoice_tax_entry.tax_repartition_line_id.id in tax_rep_ln_mapping:
|
||||
tax_rep_ln_mapping[invoice_tax_entry.tax_id.id] = self._create_refund_repartition_mapping(invoice_tax_entry.tax_id)
|
||||
|
||||
tax_line_vals = []
|
||||
for invoice_tax_entry in invoice.tax_line_ids:
|
||||
ref_rep_ln = tax_rep_ln_mapping[invoice_tax_entry.tax_id.id][invoice_tax_entry.tax_repartition_line_id.id]
|
||||
tax_line_vals.append({
|
||||
'name': invoice_tax_entry.name,
|
||||
'tax_id': invoice_tax_entry.tax_id.id,
|
||||
'tax_repartition_line_id': ref_rep_ln.id,
|
||||
'account_id': ref_rep_ln.account_id.id or invoice_tax_entry.account_id.id, # If the refund repartition line has no account set, we use the one from the original invoice
|
||||
'account_analytic_id': invoice_tax_entry.account_analytic_id.id,
|
||||
'analytic_tag_ids': [(6, 0, invoice_tax_entry.analytic_tag_ids.ids)],
|
||||
'amount': invoice_tax_entry.amount,
|
||||
'amount_rounding': invoice_tax_entry.amount_rounding,
|
||||
'manual': invoice_tax_entry.manual,
|
||||
'sequence': invoice_tax_entry.sequence,
|
||||
'base': invoice_tax_entry.base,
|
||||
'tax_ids': [(6, 0, invoice_tax_entry.tax_ids.ids)],
|
||||
'tag_ids': [(6, 0, ref_rep_ln.tag_ids.ids)],
|
||||
})
|
||||
values['tax_line_ids'] = [(0, 0, tax_line_val) for tax_line_val in tax_line_vals]
|
||||
|
||||
return values
|
||||
|
||||
@api.multi
|
||||
@@ -1692,7 +1664,6 @@ class AccountInvoice(models.Model):
|
||||
message = _("This customer invoice credit note has been created from: <a href=# data-oe-model=account.invoice data-oe-id=%d>%s</a><br>Reason: %s") % (invoice.id, invoice.number, description)
|
||||
else:
|
||||
message = _("This vendor bill credit note has been created from: <a href=# data-oe-model=account.invoice data-oe-id=%d>%s</a><br>Reason: %s") % (invoice.id, invoice.number, description)
|
||||
self._fix_refund_tax_lines(invoice, refund_invoice)
|
||||
|
||||
refund_invoice.message_post(body=message)
|
||||
new_invoices += refund_invoice
|
||||
|
||||
@@ -19,7 +19,7 @@ from lxml import etree
|
||||
class AccountMove(models.Model):
|
||||
_name = "account.move"
|
||||
_description = "Journal Entries"
|
||||
_order = 'date desc, id desc'
|
||||
_order = 'date desc, name desc, id desc'
|
||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||
|
||||
@api.model
|
||||
@@ -34,10 +34,11 @@ class AccountMove(models.Model):
|
||||
def name_get(self):
|
||||
result = []
|
||||
for move in self:
|
||||
name = move.name
|
||||
if self.env.context.get('name_groupby'):
|
||||
name = "**{date}**, {name} {partner}".format(date=format_date(self.env, move.date), name=move.name, partner=move.partner_id.name or "")
|
||||
if move.state == 'draft':
|
||||
name = '* ' + str(move.id)
|
||||
else:
|
||||
name = move.name
|
||||
result.append((move.id, name))
|
||||
return result
|
||||
|
||||
@@ -87,12 +88,14 @@ class AccountMove(models.Model):
|
||||
if line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
amount = abs(line.debit - line.credit)
|
||||
total_amount += amount
|
||||
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
|
||||
total_reconciled += partial_line.amount
|
||||
precision_currency = move.currency_id or move.company_id.currency_id
|
||||
if float_is_zero(total_amount, precision_rounding=precision_currency.rounding):
|
||||
move.matched_percentage = 1.0
|
||||
else:
|
||||
for line in move.line_ids:
|
||||
if line.account_id.user_type_id.type in ('receivable', 'payable'):
|
||||
for partial_line in (line.matched_debit_ids + line.matched_credit_ids):
|
||||
total_reconciled += partial_line.amount
|
||||
move.matched_percentage = total_reconciled / total_amount
|
||||
|
||||
@api.multi
|
||||
@@ -433,6 +436,7 @@ class AccountMove(models.Model):
|
||||
@api.multi
|
||||
def _reverse_move(self, date=None, journal_id=None):
|
||||
self.ensure_one()
|
||||
date = date or fields.Date.today()
|
||||
with self.env.norecompute():
|
||||
reversed_move = self.copy(default={
|
||||
'date': date,
|
||||
@@ -510,7 +514,7 @@ class AccountMove(models.Model):
|
||||
class AccountMoveLine(models.Model):
|
||||
_name = "account.move.line"
|
||||
_description = "Journal Item"
|
||||
_order = "date desc, id desc"
|
||||
_order = "date desc, move_name desc, id desc"
|
||||
|
||||
@api.onchange('debit', 'credit', 'tax_ids', 'analytic_account_id', 'analytic_tag_ids')
|
||||
def onchange_tax_ids_create_aml(self):
|
||||
@@ -614,6 +618,7 @@ class AccountMoveLine(models.Model):
|
||||
record.parent_state = record.move_id.state
|
||||
|
||||
name = fields.Char(string="Label")
|
||||
move_name = fields.Char(string='Number', related='move_id.name', store=True, index=True)
|
||||
quantity = fields.Float(digits=dp.get_precision('Product Unit of Measure'),
|
||||
help="The optional quantity expressed by this line, eg: number of product sold. The quantity is not a legal requirement but is very useful for some reports.")
|
||||
product_uom_id = fields.Many2one('uom.uom', string='Unit of Measure')
|
||||
@@ -677,8 +682,8 @@ class AccountMoveLine(models.Model):
|
||||
tax_audit = fields.Char(string="Tax Audit String", compute="_compute_tax_audit", store=True, help="Computed field, listing the tax grids impacted by this line, and the amount it applies to each of them.")
|
||||
|
||||
_sql_constraints = [
|
||||
('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry !'),
|
||||
('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry !'),
|
||||
('credit_debit1', 'CHECK (credit*debit=0)', 'Wrong credit or debit value in accounting entry! Credit or debit should be zero.'),
|
||||
('credit_debit2', 'CHECK (credit+debit>=0)', 'Wrong credit or debit value in accounting entry! Credit and debit should be positive.'),
|
||||
]
|
||||
|
||||
@api.model
|
||||
@@ -813,22 +818,21 @@ class AccountMoveLine(models.Model):
|
||||
In case of full reconciliation, all moves belonging to the reconciliation will belong to the same account_full_reconcile object.
|
||||
"""
|
||||
# Get first all aml involved
|
||||
part_recs = self.env['account.partial.reconcile'].search(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)])
|
||||
amls = self
|
||||
todo = set(part_recs)
|
||||
todo = self.env['account.partial.reconcile'].search_read(['|', ('debit_move_id', 'in', self.ids), ('credit_move_id', 'in', self.ids)], ['debit_move_id', 'credit_move_id'])
|
||||
amls = set(self.ids)
|
||||
seen = set()
|
||||
while todo:
|
||||
partial_rec = todo.pop()
|
||||
seen.add(partial_rec)
|
||||
for aml in [partial_rec.debit_move_id, partial_rec.credit_move_id]:
|
||||
if aml not in amls:
|
||||
amls += aml
|
||||
for x in aml.matched_debit_ids | aml.matched_credit_ids:
|
||||
if x not in seen:
|
||||
todo.add(x)
|
||||
partial_rec_ids = [x.id for x in seen]
|
||||
aml_ids = [rec['debit_move_id'][0] for rec in todo if rec['debit_move_id']] + [rec['credit_move_id'][0] for rec in todo if rec['credit_move_id']]
|
||||
amls |= set(aml_ids)
|
||||
seen |= set([rec['id'] for rec in todo])
|
||||
todo = self.env['account.partial.reconcile'].search_read(['&', '|', ('credit_move_id', 'in', aml_ids), ('debit_move_id', 'in', aml_ids), '!', ('id', 'in', list(seen))], ['debit_move_id', 'credit_move_id'])
|
||||
|
||||
partial_rec_ids = list(seen)
|
||||
if not amls:
|
||||
return
|
||||
else:
|
||||
amls = self.browse(list(amls))
|
||||
|
||||
# If we have multiple currency, we can only base ourselve on debit-credit to see if it is fully reconciled
|
||||
currency = set([a.currency_id for a in amls if a.currency_id.id != False])
|
||||
multiple_currency = False
|
||||
@@ -1532,7 +1536,7 @@ class AccountMoveLine(models.Model):
|
||||
|
||||
@api.multi
|
||||
def open_reconcile_view(self):
|
||||
[action] = self.env.ref('account.action_account_moves_all_a').read()
|
||||
[action] = self.env.ref('account.action_account_moves_all').read()
|
||||
ids = []
|
||||
for aml in self:
|
||||
if aml.account_id.reconcile:
|
||||
|
||||
@@ -96,7 +96,7 @@ class AccountInvoiceReport(models.Model):
|
||||
sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id,
|
||||
sub.fiscal_position_id, sub.user_id, sub.company_id, sub.nbr, sub.invoice_id, sub.type, sub.state,
|
||||
sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id,
|
||||
sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, sub.amount_total as amount_total,
|
||||
sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, sub.amount_total / COALESCE(cr.rate, 1) as amount_total,
|
||||
COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id
|
||||
"""
|
||||
return select_str
|
||||
@@ -114,8 +114,7 @@ class AccountInvoiceReport(models.Model):
|
||||
ai.partner_bank_id,
|
||||
SUM ((invoice_type.sign_qty * ail.quantity) / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) AS product_qty,
|
||||
SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total,
|
||||
ai.amount_total_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) *
|
||||
count(*) * invoice_type.sign AS amount_total,
|
||||
SUM(ail.price_total * invoice_type.sign_qty) AS amount_total,
|
||||
SUM(ABS(ail.price_subtotal_signed)) / CASE
|
||||
WHEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1)) <> 0::numeric
|
||||
THEN SUM(ail.quantity / COALESCE(u.factor,1) * COALESCE(u2.factor,1))
|
||||
|
||||
@@ -281,7 +281,6 @@ var StatementModel = BasicModel.extend({
|
||||
if (last && !this._isValid(last)) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
prop = this._formatQuickCreate(line);
|
||||
line.reconciliation_proposition.push(prop);
|
||||
line.createForm = _.pick(prop, this.quickCreateFields);
|
||||
@@ -493,13 +492,6 @@ var StatementModel = BasicModel.extend({
|
||||
this._blurProposition(handle);
|
||||
var focus = this._formatQuickCreate(line, _.pick(reconcileModel, fields));
|
||||
focus.reconcileModelId = reconcileModelId;
|
||||
if (!line.reconciliation_proposition.every(function(prop) {return prop.to_check == focus.to_check;})) {
|
||||
new CrashManager().show_warning({data: {
|
||||
exception_type: _t("Incorrect Operation"),
|
||||
message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.")
|
||||
}});
|
||||
return Promise.resolve();
|
||||
}
|
||||
line.reconciliation_proposition.push(focus);
|
||||
if (reconcileModel.has_second_line) {
|
||||
var second = {};
|
||||
@@ -619,19 +611,16 @@ var StatementModel = BasicModel.extend({
|
||||
var self = this;
|
||||
var line = this.getLine(handle);
|
||||
var prop = _.last(_.filter(line.reconciliation_proposition, '__focus'));
|
||||
if ('to_check' in values && values.to_check === false) {
|
||||
// check if we have another line with to_check and if yes don't change value of this proposition
|
||||
prop.to_check = line.reconciliation_proposition.some(function(rec_prop, index) {
|
||||
return rec_prop.id !== prop.id && rec_prop.to_check;
|
||||
});
|
||||
}
|
||||
if (!prop) {
|
||||
prop = this._formatQuickCreate(line);
|
||||
line.reconciliation_proposition.push(prop);
|
||||
}
|
||||
if ('to_check' in values && !line.reconciliation_proposition.slice(0,-1).every(function(prop) {return prop.to_check == values.to_check;})) {
|
||||
new CrashManager().show_warning({data: {
|
||||
exception_type: _t("Incorrect Operation"),
|
||||
message: _t("You cannot mix items with and without the 'To Check' checkbox ticked.")
|
||||
}});
|
||||
// FIXME: model should not be tied to the DOM !
|
||||
$('.create_to_check input:visible').prop('checked', !values.to_check).change();
|
||||
return Promise.resolve();
|
||||
}
|
||||
_.each(values, function (value, fieldName) {
|
||||
if (fieldName === 'analytic_tag_ids') {
|
||||
switch (value.operation) {
|
||||
@@ -683,7 +672,6 @@ var StatementModel = BasicModel.extend({
|
||||
prop.__tax_to_recompute = true;
|
||||
}
|
||||
line.createForm = _.pick(prop, this.quickCreateFields);
|
||||
|
||||
// If you check/uncheck the force_tax_included box, reset the createForm amount.
|
||||
if(prop.base_amount)
|
||||
line.createForm.amount = prop.base_amount;
|
||||
@@ -744,16 +732,8 @@ var StatementModel = BasicModel.extend({
|
||||
"new_aml_dicts": _.map(_.filter(props, function (prop) {
|
||||
return isNaN(prop.id) && prop.display;
|
||||
}), self._formatToProcessReconciliation.bind(self, line)),
|
||||
"to_check": line.to_check,
|
||||
};
|
||||
line.reconciliation_proposition.some(function(prop) {
|
||||
if (prop.to_check) {
|
||||
values_dict['to_check'] = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (line.reconciliation_proposition[0].to_check) {
|
||||
values_dict['to_check'] = true;
|
||||
}
|
||||
|
||||
// If the lines are not fully balanced, create an unreconciled amount.
|
||||
// line.st_line.currency_id is never false here because its equivalent to
|
||||
@@ -870,7 +850,12 @@ var StatementModel = BasicModel.extend({
|
||||
var formatOptions = {
|
||||
currency_id: line.st_line.currency_id,
|
||||
};
|
||||
line.to_check = false;
|
||||
_.each(line.reconciliation_proposition, function (prop) {
|
||||
if (prop.to_check) {
|
||||
// If one of the proposition is to_check, set the global to_check flag to true
|
||||
line.to_check = true;
|
||||
}
|
||||
if (prop.is_tax) {
|
||||
if (!_.find(line.reconciliation_proposition, {'id': prop.link}).__tax_to_recompute) {
|
||||
reconciliation_proposition.push(prop);
|
||||
@@ -1025,6 +1010,7 @@ var StatementModel = BasicModel.extend({
|
||||
prop.label = prop.name;
|
||||
prop.account_id = self._formatNameGet(prop.account_id || line.account_id);
|
||||
prop.is_partially_reconciled = prop.amount_str !== prop.total_amount_str;
|
||||
prop.to_check = !!prop.to_check;
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -1160,7 +1146,7 @@ var StatementModel = BasicModel.extend({
|
||||
'link': values.link,
|
||||
'display': true,
|
||||
'invalid': true,
|
||||
'to_check': values.to_check,
|
||||
'to_check': !!values.to_check,
|
||||
'__tax_to_recompute': true,
|
||||
'is_tax': values.is_tax,
|
||||
'__focus': '__focus' in values ? values.__focus : true,
|
||||
@@ -1406,11 +1392,8 @@ var ManualModel = StatementModel.extend({
|
||||
return self.loadData(lines);
|
||||
});
|
||||
default:
|
||||
var partner_ids = context.partner_ids;
|
||||
var account_ids = context.account_ids || self.account_ids;
|
||||
if (partner_ids && !account_ids) account_ids = [];
|
||||
if (!partner_ids && account_ids) partner_ids = [];
|
||||
account_ids = null; // TOFIX: REMOVE ME
|
||||
var partner_ids = context.partner_ids || null;
|
||||
var account_ids = context.account_ids || self.account_ids || null;
|
||||
return self._rpc({
|
||||
model: 'account.reconciliation.widget',
|
||||
method: 'get_all_data_for_manual_reconciliation',
|
||||
@@ -1627,6 +1610,7 @@ var ManualModel = StatementModel.extend({
|
||||
prop.debit = prop.debit !== 0 ? 0 : tmp_value;
|
||||
prop.amount = -prop.amount;
|
||||
prop.journal_id = self._formatNameGet(prop.journal_id || line.journal_id);
|
||||
prop.to_check = !!prop.to_check;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@@ -344,7 +344,7 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
update: function (state) {
|
||||
var self = this;
|
||||
// isValid
|
||||
var to_check_checked = !!(state.reconciliation_proposition[0] && state.reconciliation_proposition[0].to_check);
|
||||
var to_check_checked = !!(state.to_check);
|
||||
this.$('caption .o_buttons button.o_validate').toggleClass('d-none', !!state.balance.type && !to_check_checked);
|
||||
this.$('caption .o_buttons button.o_reconcile').toggleClass('d-none', state.balance.type <= 0 || to_check_checked);
|
||||
this.$('caption .o_buttons .o_no_valid').toggleClass('d-none', state.balance.type >= 0);
|
||||
@@ -474,9 +474,13 @@ var LineRenderer = Widget.extend(FieldManagerMixin, {
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
if (state.to_check) {
|
||||
// Set the to_check field to true if global to_check is set
|
||||
self.$('.create_to_check input').prop('checked', state.to_check).change();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
this.$('.create .add_line').toggle(!!state.balance.amount_currency);
|
||||
},
|
||||
|
||||
@@ -477,7 +477,7 @@ var auto_reconciliation = {
|
||||
};
|
||||
|
||||
var data_for_manual_reconciliation_widget = {
|
||||
'[[],null]': {
|
||||
'[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]': {
|
||||
'customers': [
|
||||
{'account_id': 287, 'partner_name': "Agrolait", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-14 12:30:31", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 8, 'account_name': "101200 Account Receivable", 'mode': "customers"},
|
||||
{'account_id': 7, 'partner_name': "Camptocamp", 'reconciliation_proposition': [], 'currency_id': 3, 'max_date': "2017-02-13 14:24:55", 'last_time_entries_checked': null, 'account_code': "101200", 'partner_id': 12, 'account_name': "101200 Account Receivable", 'mode': "customers"}
|
||||
@@ -744,7 +744,7 @@ QUnit.module('account', {
|
||||
"name": "INV/2017/0003",
|
||||
"analytic_tag_ids": [[6, null, []]]
|
||||
}],
|
||||
payment_aml_ids: [], new_aml_dicts: []}]],
|
||||
payment_aml_ids: [], new_aml_dicts: [], to_check: false}]],
|
||||
"Should call process_bank_statement_line with ids");
|
||||
var def = testUtils.makeTestPromise();
|
||||
def.abort = function () {};
|
||||
@@ -782,8 +782,10 @@ QUnit.module('account', {
|
||||
// line is send back to server.
|
||||
testUtils.mock.intercept(clientAction, 'call_service', function (event) {
|
||||
assert.deepEqual(event.data.args[1].args,
|
||||
[[5],[{partner_id: 8, counterpart_aml_dicts: [],
|
||||
payment_aml_ids: [], new_aml_dicts: [{
|
||||
[[5],[{partner_id: 8, to_check: false, counterpart_aml_dicts: [],
|
||||
payment_aml_ids: [],
|
||||
to_check: false,
|
||||
new_aml_dicts: [{
|
||||
account_id: 287,
|
||||
credit: 1175,
|
||||
debit: 0,
|
||||
@@ -834,7 +836,7 @@ QUnit.module('account', {
|
||||
// We also create a line which is the open balance.
|
||||
testUtils.mock.intercept(clientAction, 'call_service', function (event) {
|
||||
assert.deepEqual(event.data.args[1].args,
|
||||
[[5],[{partner_id: 8,
|
||||
[[5],[{partner_id: 8, to_check: false,
|
||||
counterpart_aml_dicts: [{
|
||||
counterpart_aml_id: 109,
|
||||
credit: 650,
|
||||
@@ -884,6 +886,7 @@ QUnit.module('account', {
|
||||
}],
|
||||
payment_aml_ids: [],
|
||||
new_aml_dicts: [],
|
||||
to_check: false,
|
||||
}]
|
||||
], "should call process_bank_statement_line with partial reconcile values");
|
||||
}
|
||||
@@ -1408,6 +1411,7 @@ QUnit.module('account', {
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
to_check: false,
|
||||
new_aml_dicts: []}]
|
||||
], "should call process_bank_statement_line with partial reconcile values");
|
||||
}
|
||||
@@ -1500,6 +1504,7 @@ QUnit.module('account', {
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
to_check: false,
|
||||
new_aml_dicts: [
|
||||
{account_id: 282,
|
||||
credit: 0,
|
||||
@@ -1609,6 +1614,7 @@ QUnit.module('account', {
|
||||
|
||||
payment_aml_ids: [],
|
||||
partner_id: 8,
|
||||
to_check: false,
|
||||
new_aml_dicts: []
|
||||
}]
|
||||
], "should call process_bank_statement_line with correct counterpart_aml_dicts");
|
||||
@@ -1662,7 +1668,7 @@ QUnit.module('account', {
|
||||
assert.expect(5);
|
||||
|
||||
// tweak the data to fit our needs
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[[],null]']);
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'] = _.extend({}, this.params.data_for_manual_reconciliation_widget['[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]']);
|
||||
this.params.data_for_manual_reconciliation_widget['[283, null, "", 0, 6]'].accounts[0].reconciliation_proposition = [
|
||||
{account_id: 283, account_type: "other", amount_currency_str: "", currency_id: false, date_maturity: "2017-03-18", date: "2017-02-16",
|
||||
total_amount_str: "$ 500.00", partner_id: 8, account_name: "101000 Current Assets", name: "INV/2017/0987", partner_name: "Agrolait",
|
||||
@@ -1772,7 +1778,7 @@ QUnit.module('account', {
|
||||
QUnit.test('Tax on account receivable', async function(assert){
|
||||
assert.expect(21);
|
||||
|
||||
this.params.data_for_manual_reconciliation_widget['[[],null]'].accounts = [];
|
||||
this.params.data_for_manual_reconciliation_widget['[null,[282,283,284,285,286,287,288,308,499,500,501,502,503,504]]'].accounts = [];
|
||||
var clientAction = new ReconciliationClientAction.ManualAction(null, this.params.options);
|
||||
testUtils.mock.addMockEnvironment(clientAction, {
|
||||
data: this.params.data,
|
||||
|
||||
@@ -13,6 +13,8 @@ class TestBankStatementReconciliation(AccountingTestCase):
|
||||
self.bsl_model = self.env['account.bank.statement.line']
|
||||
self.reconciliation_widget = self.env['account.reconciliation.widget']
|
||||
self.partner = self.env['res.partner'].create({'name': 'test'})
|
||||
self.currency_usd_id = self.env.ref("base.USD").id
|
||||
self.currency_euro_id = self.env.ref("base.EUR").id
|
||||
|
||||
def test_reconciliation_proposition(self):
|
||||
rcv_mv_line = self.create_invoice(100)
|
||||
@@ -108,3 +110,31 @@ class TestBankStatementReconciliation(AccountingTestCase):
|
||||
})
|
||||
|
||||
return bank_stmt_line
|
||||
|
||||
def test_confirm_statement_usd(self):
|
||||
company = self.env.ref('base.main_company')
|
||||
self.cr.execute("UPDATE res_company SET currency_id = %s WHERE id = %s", [self.currency_euro_id, company.id])
|
||||
self.env['res.currency.rate'].search([]).unlink()
|
||||
self.env['res.currency.rate'].create({
|
||||
'currency_id': self.currency_usd_id,
|
||||
'rate': 2.0,
|
||||
'name': '2001-01-01',
|
||||
})
|
||||
bank_journal_usd = self.env['account.journal'].create({
|
||||
'name': 'Bank US',
|
||||
'type': 'bank',
|
||||
'code': 'BNK68',
|
||||
'currency_id': self.currency_usd_id,
|
||||
})
|
||||
statement = self.bs_model.create({
|
||||
'journal_id': bank_journal_usd.id,
|
||||
'balance_end_real': 100,
|
||||
'line_ids': [(0, 0, {
|
||||
'name': '_',
|
||||
'partner_id': self.partner.id,
|
||||
'amount': 100,
|
||||
'account_id': bank_journal_usd.default_debit_account_id.id,
|
||||
})],
|
||||
})
|
||||
statement.button_open()
|
||||
statement.button_confirm_bank()
|
||||
|
||||
@@ -189,7 +189,13 @@ class TestInvoiceTaxes(AccountingTestCase):
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 100,
|
||||
'factor_percent': 10,
|
||||
'repartition_type': 'tax',
|
||||
'tag_ids': [(4, ref_tax_tag.id, 0)],
|
||||
}),
|
||||
|
||||
(0,0, {
|
||||
'factor_percent': 90,
|
||||
'repartition_type': 'tax',
|
||||
'account_id': account_1.id,
|
||||
'tag_ids': [(4, ref_tax_tag.id, 0)],
|
||||
@@ -219,13 +225,13 @@ class TestInvoiceTaxes(AccountingTestCase):
|
||||
refund.action_invoice_open()
|
||||
refund_move = refund.move_id
|
||||
|
||||
self.assertEqual(len(refund_move.line_ids), 3, "There should be 4 account move lines created for the refund: payable, base and tax line")
|
||||
self.assertEqual(len(refund_move.line_ids), 4, "There should be 4 account move lines created for the refund: payable, base and 2 tax lines")
|
||||
ref_base_line = refund_move.line_ids.filtered(lambda x: not x.tax_repartition_line_id and x.account_id.user_type_id.type != 'receivable')
|
||||
self.assertEqual(len(ref_base_line), 1, "There should be only one base line generated")
|
||||
self.assertEqual(abs(ref_base_line.balance), 100, "Base amount should be 100")
|
||||
self.assertEqual(ref_base_line.tag_ids, ref_base_tag, "Base line should have received base tag")
|
||||
ref_tax_line = refund_move.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
|
||||
self.assertEqual(len(ref_tax_line), 1, "There should be only one tax line")
|
||||
self.assertEqual(ref_tax_line.account_id, account_1, "Tax line should have been made on account 1")
|
||||
self.assertEqual(abs(ref_tax_line.balance), 42, "Tax line should have been made on account 1")
|
||||
self.assertEqual(ref_tax_line.tag_ids, ref_tax_tag, "Tax line should have the right tag")
|
||||
ref_tax_lines = refund_move.line_ids.filtered(lambda x: x.tax_repartition_line_id.repartition_type == 'tax')
|
||||
self.assertEqual(len(ref_tax_lines), 2, "There should be two refund tax lines")
|
||||
self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == ref_base_line.account_id).balance), 4.2, "Refund tax line on base account should amount to 4.2 (10% of 42)")
|
||||
self.assertEqual(abs(ref_tax_lines.filtered(lambda x: x.account_id == account_1).balance), 37.8, "Refund tax line on account 1 should amount to 37.8 (90% of 42)")
|
||||
self.assertEqual(ref_tax_lines.mapped('tag_ids'), ref_tax_tag, "Refund tax lines should have the right tag")
|
||||
|
||||
@@ -129,11 +129,10 @@ class TestPayment(AccountingTestCase):
|
||||
})
|
||||
payment.post()
|
||||
self.assertRecordValues(payment.move_line_ids, [
|
||||
{'account_id': self.account_usd.id, 'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
|
||||
{'account_id': self.transfer_account.id, 'debit': 32.70, 'credit': 0.0, 'amount_currency': 50, 'currency_id': self.currency_usd_id},
|
||||
{'account_id': self.transfer_account.id, 'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
|
||||
{'account_id': self.account_eur.id, 'debit': 32.70, 'credit': 0.0, 'amount_currency': 0, 'currency_id': False},
|
||||
{'account_id': self.account_usd.id, 'debit': 0.0, 'credit': 32.70, 'amount_currency': -50, 'currency_id': self.currency_usd_id},
|
||||
{'account_id': self.transfer_account.id, 'debit': 32.70, 'credit': 0.0, 'amount_currency': 50,
|
||||
'currency_id': self.currency_usd_id},
|
||||
])
|
||||
|
||||
def test_payment_chf_journal_usd(self):
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
<menuitem
|
||||
name="Analytic Items"
|
||||
parent="account.menu_finance_entries_accounting_entries"
|
||||
parent="account.menu_finance_entries_accounting_miscellaneous"
|
||||
action="analytic.account_analytic_line_action_entries"
|
||||
id="menu_action_analytic_lines_tree"
|
||||
sequence="35"
|
||||
|
||||
@@ -354,13 +354,13 @@
|
||||
domain="[('company_id', '=', parent.company_id)]"
|
||||
context="{'default_partner_id': parent.partner_id}"
|
||||
optional="show"/>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||
<field name="analytic_tag_ids" groups="analytic.group_analytic_tags" widget="many2many_tags" options="{'color_field': 'color'}" optional="show"/>
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" string="UoM" groups="uom.group_uom" optional="show"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="discount" string="Disc.%" groups="base.group_no_one" optional="show"/>
|
||||
<field name="discount" string="Disc.%" optional="hide"/>
|
||||
<field name="invoice_line_tax_ids" widget="many2many_tags" options="{'no_create': True}" context="{'type':parent.type, 'tree_view_ref': 'account.account_tax_view_tree', 'search_view_ref': 'account.account_tax_view_search'}"
|
||||
domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]" optional="show"/>
|
||||
domain="[('type_tax_use','=','purchase'),('company_id', '=', parent.company_id)]"/>
|
||||
<field name="price_subtotal" string="Amount"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
</tree>
|
||||
@@ -539,10 +539,9 @@
|
||||
<field name="quantity"/>
|
||||
<field name="uom_id" string="UoM" groups="uom.group_uom" optional="show"/>
|
||||
<field name="price_unit" string="Price"/>
|
||||
<field name="discount" groups="base.group_no_one" string="Disc.%" optional="show"/>
|
||||
<field name="discount" string="Disc.%" optional="hide"/>
|
||||
<field name="invoice_line_tax_ids" widget="many2many_tags" options="{'no_create': True}" context="{'type':parent.type, 'tree_view_ref': 'account.account_tax_view_tree', 'search_view_ref': 'account.account_tax_view_search'}"
|
||||
domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]"
|
||||
optional="hide"/>
|
||||
domain="[('type_tax_use','=','sale'),('company_id', '=', parent.company_id)]"/>
|
||||
<field name="price_subtotal" string="Subtotal" groups="account.group_show_line_subtotals_tax_excluded"/>
|
||||
<field name="price_total" string="Total" groups="account.group_show_line_subtotals_tax_included"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
<menuitem id="menu_finance_payables" name="Vendors" parent="menu_finance" sequence="3"/>
|
||||
<menuitem id="menu_finance_entries" name="Accounting" parent="menu_finance" sequence="4" groups="account.group_account_user"/>
|
||||
<!-- Adviser sub-menus -->
|
||||
<menuitem id="menu_finance_entries_accounting_entries" name="Accounting Entries" parent="account.menu_finance_entries"/>
|
||||
<menuitem id="menu_finance_entries_accounting_miscellaneous" name="Miscellaneous" parent="account.menu_finance_entries" sequence="1"/>
|
||||
<menuitem id="menu_finance_entries_accounting_journals" name="Journals" parent="account.menu_finance_entries" sequence="2"/>
|
||||
<menuitem id="menu_finance_entries_accounting_ledgers" name="Ledgers" parent="account.menu_finance_entries" sequence="3"/>
|
||||
<menuitem id="menu_finance_entries_management" name="Management" parent="menu_finance_entries" sequence="10" groups="account.group_account_invoice"/>
|
||||
<menuitem id="menu_finance_entries_actions" name="Actions" parent="account.menu_finance_entries"/>
|
||||
<menuitem id="menu_finance_entries_actions" name="Actions" parent="account.menu_finance_entries" sequence="11"/>
|
||||
<menuitem id="menu_finance_entries_generate_entries" name="Generate Entries" parent="account.menu_finance_entries"/>
|
||||
|
||||
<menuitem id="menu_finance_reports" name="Reporting" parent="menu_finance" sequence="20" groups="account.group_account_manager"/>
|
||||
@@ -34,4 +36,3 @@
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
<record id="view_move_line_tree" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tree</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field eval="1" name="priority"/>
|
||||
<field eval="100" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Journal Items" create="false" editable="bottom">
|
||||
<field name="date"/>
|
||||
@@ -154,17 +154,111 @@
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="parent_state" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tree_grouped" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tree.grouped</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field eval="1" name="priority"/>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Journal Items" create="false" editable="bottom" expand="context.get('group_by') in ('move_id', 'account_id') and 1 or 0">
|
||||
<field name="date" optional="show" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="ref" optional="hide"/>
|
||||
<field name="move_id" optional="show" readonly="1"/>
|
||||
<field name="name"/>
|
||||
<field name="statement_id" invisible="1"/>
|
||||
<field name="account_id" options="{'no_open': True, 'no_create': True}" domain="[('company_id', '=', company_id)]" groups="account.group_account_user" attrs="{'readonly':[('parent_state','=','posted')]}" optional="show"/>
|
||||
<field name="partner_id" optional="show"/>
|
||||
<field name="reconciled" invisible="1"/>
|
||||
<field name="full_reconcile_id" string="Matching" optional="show" readonly="1"/>
|
||||
<field name="reconcile_model_id" invisible="1"/>
|
||||
<field name="date_maturity" optional="hide"/>
|
||||
<field name="debit" sum="Total Debit" readonly="1"/>
|
||||
<field name="credit" sum="Total Credit" readonly="1"/>
|
||||
<field name="amount_currency" readonly="True" groups="base.group_multi_currency"/>
|
||||
<field name="currency_id" readonly="True" invisible="1" />
|
||||
<field name="company_currency_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="parent_state" invisible="1"/>
|
||||
<field name="tax_ids" widget="many2many_tags" width_factor="0.5" optional="hide" attrs="{'readonly':[('parent_state','=','posted')]}"/>
|
||||
<field name="tag_ids" widget="many2many_tags" width_factor="0.5" optional="hide"/>
|
||||
<groupby name="move_id">
|
||||
<field name="state" invisible="1"/>
|
||||
<button name="edit" type="edit" string="Edit"/>
|
||||
<button name="action_post" states="draft" string="Post" type="object" groups="account.group_account_invoice"/>
|
||||
<button name="%(action_view_account_move_reversal)d" states="posted" string="Reverse Entry" type="action" groups="account.group_account_invoice"/>
|
||||
<button name="action_duplicate" string="Duplicate" type="object" groups="account.group_account_invoice"/>
|
||||
<button name="edit" type="edit" icon="fa-edit" title="Edit"/>
|
||||
<button name="action_post" states="draft" icon="fa-check" title="Post" type="object" groups="account.group_account_invoice"/>
|
||||
<button name="%(action_view_account_move_reversal)d" states="posted" title="Reverse" icon="fa-refresh" type="action" groups="account.group_account_invoice"/>
|
||||
<button name="action_duplicate" icon="fa-files-o" title="Duplicate" type="object" groups="account.group_account_invoice"/>
|
||||
</groupby>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tree_grouped_sales_purchases" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tree.grouped.sales.purchase</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_tree_grouped"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="move_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="partner_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="full_reconcile_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="tag_ids" position="attributes">
|
||||
<attribute name="optional">show</attribute>
|
||||
<attribute name="string">Tax Grid</attribute>
|
||||
<attribute name="domain">[('applicability', '=', 'taxes')]</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tree_grouped_bank_cash" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tree.grouped.bank.cash</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_tree_grouped"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="move_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="ref" position="attributes"><attribute name="optional">show</attribute></field>
|
||||
<field name="full_reconcile_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tree_grouped_misc" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tree.grouped.misc</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_tree_grouped"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="date" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="move_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tree_grouped_general" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tree.grouped.misc</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_tree_grouped"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="account_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
<record id="view_move_line_tree_grouped_partner" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tree.grouped.partner</field>
|
||||
<field name="model">account.move.line</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="inherit_id" ref="account.view_move_line_tree_grouped"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="partner_id" position="attributes"><attribute name="optional">hide</attribute></field>
|
||||
<field name="date_maturity" position="attributes"><attribute name="optional">show</attribute></field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_move_line_tax_audit_tree" model="ir.ui.view">
|
||||
<field name="name">account.move.line.tax.audit.tree</field>
|
||||
<field name="model">account.move.line</field>
|
||||
@@ -172,11 +266,16 @@
|
||||
<field name="inherit_id" ref="account.view_move_line_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="full_reconcile_id" position="after">
|
||||
<field name="tax_line_id"/>
|
||||
<field name="tax_line_id" string="Tax"/>
|
||||
<field name="tax_base_amount" sum="Total Base Amount"/>
|
||||
<field name="tax_audit"/>
|
||||
<field name="move_id"/>
|
||||
</field>
|
||||
<field name="date_maturity" position="replace"/>
|
||||
<field name="analytic_account_id" position="replace"/>
|
||||
<field name="full_reconcile_id" position="replace"/>
|
||||
<field name="debit" position="replace"/>
|
||||
<field name="credit" position="replace"/>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -201,6 +300,13 @@
|
||||
<separator/>
|
||||
<filter string="Unposted" name="unposted" domain="[('move_id.state','=','draft')]" help="Unposted Journal Items"/>
|
||||
<filter name="posted" string="Posted" domain="[('move_id.state','=','posted')]" help="Posted Journal Items"/>
|
||||
<filter string="To Check" name="to_check" domain="[('move_id.to_check', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Sales" name="sales" domain="[('move_id.journal_id.type','=','sale')]" context="{'default_journal_type': 'sale'}"/>
|
||||
<filter string="Purchases" name="purchases" domain="[('move_id.journal_id.type','=','purchase')]" context="{'default_journal_type': 'purchase'}"/>
|
||||
<filter string="Bank" name="bank" domain="[('move_id.journal_id.type', '=', 'bank')]" context="{'default_journal_type': 'bank'}"/>
|
||||
<filter string="Cash" name="cash" domain="[('move_id.journal_id.type', '=', 'cash')]" context="{'default_journal_type': 'cash'}"/>
|
||||
<filter string="Miscellaneous Operations" domain="[('move_id.journal_id.type','=','general')]" name="misc_filter" context="{'default_journal_type': 'general'}"/>
|
||||
<separator/>
|
||||
<filter string="Unreconciled" domain="[('full_reconcile_id', '=', False), ('balance','!=', 0), ('account_id.reconcile','=',True)]" help="Journal items where matching number isn't set" name="unreconciled"/>
|
||||
<separator/>
|
||||
@@ -216,9 +322,9 @@
|
||||
<field name="reconcile_model_id"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Journal Entry" name="group_by_move" domain="[]" context="{'group_by':'move_id'}"/>
|
||||
<filter string="Partner" name="partner" domain="[]" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Account" name="group_by_account" domain="[]" context="{'group_by':'account_id'}"/>
|
||||
<filter string="Partner" name="group_by_partner" domain="[]" context="{'group_by':'partner_id'}"/>
|
||||
<filter string="Journal" name="journal" domain="[]" context="{'group_by':'journal_id'}"/>
|
||||
<filter string="Account" name="account" context="{'group_by':'account_id'}"/>
|
||||
<filter string="Date" name="groupby_date" domain="[]" context="{'group_by':'date'}"/>
|
||||
</group>
|
||||
</search>
|
||||
@@ -451,10 +557,58 @@
|
||||
view_id="account.view_move_line_tax_audit_tree"/>
|
||||
|
||||
<record id="action_account_moves_all_a" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'search_default_posted':1}</field>
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_sales':1, 'name_groupby':1}</field>
|
||||
<field name="name">Journal Items</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree"/>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_sales" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'sales', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_sales':1, 'name_groupby':1}</field>
|
||||
<field name="name">Sales</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped_sales_purchases"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_purchase" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'purchase', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_purchases':1, 'name_groupby':1}</field>
|
||||
<field name="name">Purchase</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped_sales_purchases"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_bank_cash" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'bank', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_bank':1, 'search_default_cash':1, 'name_groupby':1}</field>
|
||||
<field name="name">Bank and Cash</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped_bank_cash"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_journal_misc" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_move': 1, 'group_by':'move_id', 'search_default_posted':1, 'search_default_misc_filter':1, 'name_groupby':1}</field>
|
||||
<field name="name">Miscellaneous</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped_misc"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_ledger_general" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_account': 1, 'group_by':'account_id', 'search_default_posted':1}</field>
|
||||
<field name="name">General Ledger</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped_general"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_ledger_partner" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_group_by_partner': 1, 'group_by':'partner_id', 'search_default_posted':1, 'search_default_payable':1, 'search_default_receivable':1, 'search_default_unreconciled':1}</field>
|
||||
<field name="name">Partner Ledger</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree_grouped_partner"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
@@ -465,6 +619,14 @@
|
||||
<field name="view_id" ref="view_move_line_tree"/>
|
||||
</record>
|
||||
|
||||
<record id="action_account_moves_all" model="ir.actions.act_window">
|
||||
<field name="context">{'journal_type':'general', 'search_default_posted':1}</field>
|
||||
<field name="name">Journal Items</field>
|
||||
<field name="res_model">account.move.line</field>
|
||||
<field name="view_id" ref="view_move_line_tree"/>
|
||||
<field name="view_mode">tree,pivot,graph,form,kanban</field>
|
||||
</record>
|
||||
|
||||
<record id="action_move_line_graph" model="ir.actions.act_window">
|
||||
<field name="name">Journal Items</field>
|
||||
<field name="view_mode">graph,pivot</field>
|
||||
@@ -520,15 +682,57 @@
|
||||
|
||||
<menuitem
|
||||
id="menu_action_account_moves_all"
|
||||
action="action_account_moves_all_a"
|
||||
parent="account.menu_finance_entries_accounting_entries"
|
||||
groups="group_account_manager"
|
||||
action="action_account_moves_all"
|
||||
parent="account.menu_finance_entries_accounting_miscellaneous"
|
||||
groups="base.group_no_one"
|
||||
sequence="10"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_action_move_journal_line_form"
|
||||
action="action_move_journal_line"
|
||||
parent="account.menu_finance_entries_accounting_entries"
|
||||
parent="account.menu_finance_entries_accounting_miscellaneous"
|
||||
groups="account.group_account_user"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_action_account_moves_journal_sales"
|
||||
action="action_account_moves_journal_sales"
|
||||
parent="account.menu_finance_entries_accounting_journals"
|
||||
groups="account.group_account_user"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_action_account_moves_journal_purchase"
|
||||
action="action_account_moves_journal_purchase"
|
||||
parent="account.menu_finance_entries_accounting_journals"
|
||||
groups="account.group_account_user"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_action_account_moves_journal_bank_cash"
|
||||
action="action_account_moves_journal_bank_cash"
|
||||
parent="account.menu_finance_entries_accounting_journals"
|
||||
groups="account.group_account_user"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_action_account_moves_journal_misc"
|
||||
action="action_account_moves_journal_misc"
|
||||
parent="account.menu_finance_entries_accounting_journals"
|
||||
groups="account.group_account_user"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_action_account_moves_ledger_general"
|
||||
action="action_account_moves_ledger_general"
|
||||
parent="account.menu_finance_entries_accounting_ledgers"
|
||||
groups="account.group_account_user"
|
||||
sequence="1"/>
|
||||
|
||||
<menuitem
|
||||
id="menu_action_account_moves_ledger_partner"
|
||||
action="action_account_moves_ledger_partner"
|
||||
parent="account.menu_finance_entries_accounting_ledgers"
|
||||
groups="account.group_account_user"
|
||||
sequence="1"/>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
_description = 'Account Invoice Send'
|
||||
|
||||
is_email = fields.Boolean('Email', default=lambda self: self.env.company.invoice_is_email)
|
||||
invoice_without_email = fields.Text(compute='_compute_invoice_without_email', string='invoice(s) that will not be sent')
|
||||
is_print = fields.Boolean('Print', default=lambda self: self.env.company.invoice_is_print)
|
||||
printed = fields.Boolean('Is Printed', default=False)
|
||||
invoice_ids = fields.Many2many('account.invoice', 'account_invoice_account_invoice_send_rel', string='Invoices')
|
||||
@@ -45,6 +46,22 @@ class AccountInvoiceSend(models.TransientModel):
|
||||
self.composer_id.template_id = self.template_id.id
|
||||
self.composer_id.onchange_template_id_wrapper()
|
||||
|
||||
@api.onchange('is_email')
|
||||
def _compute_invoice_without_email(self):
|
||||
for wizard in self:
|
||||
if wizard.is_email and len(wizard.invoice_ids) > 1:
|
||||
invoices = self.env['account.invoice'].search([
|
||||
('id', 'in', self.env.context.get('active_ids')),
|
||||
('partner_id.email', '=', False)
|
||||
])
|
||||
if invoices:
|
||||
wizard.invoice_without_email = "%s\n%s" % (
|
||||
_("The following invoice(s) will not be sent by email, because the customers don't have email address."),
|
||||
"\n".join([i.reference for i in invoices])
|
||||
)
|
||||
else:
|
||||
wizard.invoice_without_email = False
|
||||
|
||||
@api.multi
|
||||
def _send_email(self):
|
||||
if self.is_email:
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
<field name="is_email" />
|
||||
<b><label for="is_email"/></b>
|
||||
</div>
|
||||
<div class="text-left d-inline-block mr8" attrs="{'invisible': ['|', ('is_email','=', False), ('invoice_without_email', '=', False)]}">
|
||||
<field name="invoice_without_email" class="mr4"/>
|
||||
</div>
|
||||
<div name="mail_form" attrs="{'invisible': [('is_email', '=', False)]}">
|
||||
<!-- visible wizard -->
|
||||
<div attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
<div class="o_row">
|
||||
<field name="end_type" attrs="{'required': [('recurrency', '=', True)]}"/>
|
||||
<field name="count" attrs="{'invisible': [('end_type', '!=', 'count')], 'required': [('recurrency', '=', True)]}"/>
|
||||
<field name="final_date" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date')]}"/>
|
||||
<field name="final_date" attrs="{'invisible': [('end_type', '!=', 'end_date')], 'required': [('end_type', '=', 'end_date'), ('recurrency', '=', True)]}"/>
|
||||
</div>
|
||||
</group>
|
||||
<group attrs="{'invisible': [('rrule_type', '!=', 'weekly')]}" name="weekdays">
|
||||
@@ -181,10 +181,10 @@
|
||||
attrs="{'required': [('month_by', '=', 'date'), ('rrule_type', '=', 'monthly')],
|
||||
'invisible': [('month_by', '!=', 'date')]}"/>
|
||||
<field name="byday" string="The"
|
||||
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
'invisible': [('month_by', '!=', 'day')]}"/>
|
||||
<field name="week_list" nolabel="1"
|
||||
attrs="{'required': [('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
attrs="{'required': [('recurrency', '=', True), ('month_by', '=', 'day'), ('rrule_type', '=', 'monthly')],
|
||||
'invisible': [('month_by', '!=', 'day')]}"/>
|
||||
</div>
|
||||
</group>
|
||||
|
||||
@@ -282,7 +282,7 @@ class Lead(models.Model):
|
||||
return {}
|
||||
if user_id and self._context.get('team_id'):
|
||||
team = self.env['crm.team'].browse(self._context['team_id'])
|
||||
if user_id in team.member_ids.ids:
|
||||
if user_id in team.member_ids.ids or user_id == team.user_id.id:
|
||||
return {}
|
||||
team_id = self.env['crm.team']._get_default_team_id(user_id=user_id)
|
||||
return {'team_id': team_id}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<filter name="opportunity" string="Opportunities" domain="[('lead_type','=','opportunity')]" help="Show only opportunity" groups="crm.group_use_lead"/>
|
||||
<separator/>
|
||||
<filter string="Won" name="won"
|
||||
domain="['&', ('stage_id.is_won', '=', True)]"/>
|
||||
domain="[('stage_id.is_won', '=', True)]"/>
|
||||
<separator/>
|
||||
<filter string="Completion Date" name="completion_date" date="date" default_period="last_365_days"/>
|
||||
<group expand="1" string="Group By">
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<record id="rank_doctor" model="gamification.karma.rank">
|
||||
<field name="description_motivational" type="html">
|
||||
<div class="media align-items-center">
|
||||
<div class="media-body">Reach the next rank and gain a week-end with a unicorn !</div>
|
||||
<div class="media-body">You have reached the last rank. Congratulations!</div>
|
||||
<img class="ml-3 img img-fluid" style="max-height: 72px;" src="/gamification/static/img/rank_misc_unicorn.png"/>
|
||||
</div>
|
||||
</field>
|
||||
|
||||
@@ -81,7 +81,7 @@ class SyncEvent(object):
|
||||
self.OP = Delete((self.OE.status and "OE") or (self.GG.status and "GG"),
|
||||
'The event has been deleted from one side, we delete on other side !')
|
||||
#If event is not deleted !
|
||||
elif self.OE.status and (self.GG.status or not is_owner):
|
||||
elif self.OE.status and (self.GG.status or (not is_owner and self.GG.update)):
|
||||
if abs(self.OE.update - self.GG.update) > timedelta(seconds=1):
|
||||
if self.OE.update < self.GG.update:
|
||||
tmpSrc = 'GG'
|
||||
|
||||
@@ -35,7 +35,7 @@ class GoogleDrive(models.Model):
|
||||
if not modifiers.get('invisible') and not modifiers.get('column_invisible'):
|
||||
display_fields.append(node.get('name'))
|
||||
fields = " ".join(display_fields)
|
||||
domain = domain.replace("'", r"\'").replace('"', "'")
|
||||
domain = domain.replace("'", r"\'").replace('"', "'").replace('True', 'true').replace('False', 'false')
|
||||
if groupbys:
|
||||
fields = "%s %s" % (groupbys, fields)
|
||||
formula = '=oe_read_group("%s";"%s";"%s";"%s")' % (model, fields, groupbys, domain)
|
||||
|
||||
@@ -21,7 +21,7 @@ class User(models.Model):
|
||||
department_id = fields.Many2one(related='employee_id.department_id', readonly=False, related_sudo=False)
|
||||
address_id = fields.Many2one(related='employee_id.address_id', readonly=False, related_sudo=False)
|
||||
work_location = fields.Char(related='employee_id.work_location', readonly=False, related_sudo=False)
|
||||
parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False)
|
||||
employee_parent_id = fields.Many2one(related='employee_id.parent_id', related_sudo=False)
|
||||
coach_id = fields.Many2one(related='employee_id.coach_id', readonly=False, related_sudo=False)
|
||||
address_home_id = fields.Many2one(related='employee_id.address_home_id', readonly=False, related_sudo=False)
|
||||
is_address_home_a_company = fields.Boolean(related='employee_id.is_address_home_a_company', readonly=False, related_sudo=False)
|
||||
@@ -68,7 +68,7 @@ class User(models.Model):
|
||||
'child_ids',
|
||||
'employee_id',
|
||||
'employee_ids',
|
||||
'parent_id',
|
||||
'employee_parent_id',
|
||||
]
|
||||
|
||||
hr_writable_fields = [
|
||||
@@ -96,7 +96,7 @@ class User(models.Model):
|
||||
'marital',
|
||||
'mobile_phone',
|
||||
'notes',
|
||||
'parent_id',
|
||||
'employee_parent_id',
|
||||
'passport_id',
|
||||
'permit_no',
|
||||
'employee_phone',
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="department_id"/>
|
||||
<field name="job_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
<field name="work_location"/>
|
||||
</group>
|
||||
<group name="managers" string="Managers">
|
||||
<field name="parent_id"/>
|
||||
<field name="employee_parent_id"/>
|
||||
<field name="coach_id"/>
|
||||
</group>
|
||||
</div>
|
||||
|
||||
@@ -53,10 +53,10 @@ class HrEmployeeBase(models.AbstractModel):
|
||||
@api.multi
|
||||
def attendance_manual(self, next_action, entered_pin=None):
|
||||
self.ensure_one()
|
||||
if not (entered_pin is None) or self.env.user.has_group('hr_attendance.group_hr_attendance_use_pin') and (self.user_id and self.user_id == self.env.user):
|
||||
if entered_pin != self.pin:
|
||||
return {'warning': _('Wrong PIN')}
|
||||
return self._attendance_action(next_action)
|
||||
can_check_without_pin = not self.env.user.has_group('hr_attendance.group_hr_attendance_use_pin') or (self.user_id == self.env.user and entered_pin is None)
|
||||
if can_check_without_pin or entered_pin is not None and entered_pin == self.sudo().pin:
|
||||
return self._attendance_action(next_action)
|
||||
return {'warning': _('Wrong PIN')}
|
||||
|
||||
@api.multi
|
||||
def _attendance_action(self, next_action):
|
||||
@@ -73,14 +73,14 @@ class HrEmployeeBase(models.AbstractModel):
|
||||
action_message['next_action'] = next_action
|
||||
|
||||
if employee.user_id:
|
||||
modified_attendance = employee.sudo(employee.user_id.id).attendance_action_change()
|
||||
modified_attendance = employee.sudo(employee.user_id.id)._attendance_action_change()
|
||||
else:
|
||||
modified_attendance = employee.attendance_action_change()
|
||||
modified_attendance = employee._attendance_action_change()
|
||||
action_message['attendance'] = modified_attendance.read()[0]
|
||||
return {'action': action_message}
|
||||
|
||||
@api.multi
|
||||
def attendance_action_change(self):
|
||||
def _attendance_action_change(self):
|
||||
""" Check In/Check Out action
|
||||
Check In: create a new attendance record
|
||||
Check Out: modify check_out field of appropriate attendance record
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo.tests import new_test_user
|
||||
from odoo.tests.common import TransactionCase
|
||||
|
||||
|
||||
@@ -8,12 +9,75 @@ class TestHrAttendance(TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestHrAttendance, self).setUp()
|
||||
self.test_employee = self.browse_ref('hr.employee_al')
|
||||
self.user = new_test_user(self.env, login='fru', groups='base.group_user,hr_attendance.group_hr_attendance_use_pin')
|
||||
self.user_no_pin = new_test_user(self.env, login='gru', groups='base.group_user')
|
||||
self.test_employee = self.env['hr.employee'].create({
|
||||
'name': "François Russie",
|
||||
'user_id': self.user.id,
|
||||
'pin': '1234',
|
||||
})
|
||||
self.employee_kiosk = self.env['hr.employee'].create({
|
||||
'name': "Machiavel",
|
||||
'pin': '5678',
|
||||
})
|
||||
|
||||
def test_employee_state(self):
|
||||
# Make sure the attendance of the employee will display correctly
|
||||
assert self.test_employee.attendance_state == 'checked_out'
|
||||
self.test_employee.attendance_action_change()
|
||||
self.test_employee._attendance_action_change()
|
||||
assert self.test_employee.attendance_state == 'checked_in'
|
||||
self.test_employee.attendance_action_change()
|
||||
self.test_employee._attendance_action_change()
|
||||
assert self.test_employee.attendance_state == 'checked_out'
|
||||
|
||||
def test_checkin_self_without_pin(self):
|
||||
""" Employee can check in/out without pin with his own account """
|
||||
employee = self.test_employee.sudo(self.user)
|
||||
employee.sudo(self.user).attendance_manual({}, entered_pin=None)
|
||||
self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in without pin")
|
||||
employee.attendance_manual({}, entered_pin=None)
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out without pin")
|
||||
|
||||
def test_checkin_self_with_pin(self):
|
||||
""" Employee can check in/out with pin with his own account """
|
||||
employee = self.test_employee.sudo(self.user)
|
||||
employee.attendance_manual({}, entered_pin='1234')
|
||||
self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in with his pin")
|
||||
employee.attendance_manual({}, entered_pin='1234')
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out with his pin")
|
||||
|
||||
def test_checkin_self_wrong_pin(self):
|
||||
""" Employee cannot check in/out with wrong pin with his own account """
|
||||
employee = self.test_employee.sudo(self.user)
|
||||
action = employee.attendance_manual({}, entered_pin='9999')
|
||||
self.assertNotEqual(employee.attendance_state, 'checked_in', "He should not be able to check in with a wrong pin")
|
||||
self.assertTrue(action.get('warning'))
|
||||
|
||||
def test_checkin_kiosk_with_pin(self):
|
||||
""" Employee can check in/out with his pin in kiosk """
|
||||
employee = self.employee_kiosk.sudo(self.user)
|
||||
employee.attendance_manual({}, entered_pin='5678')
|
||||
self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in with his pin")
|
||||
employee.attendance_manual({}, entered_pin='5678')
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out with his pin")
|
||||
|
||||
def test_checkin_kiosk_with_wrong_pin(self):
|
||||
""" Employee cannot check in/out with wrong pin in kiosk """
|
||||
employee = self.employee_kiosk.sudo(self.user)
|
||||
action = employee.attendance_manual({}, entered_pin='8888')
|
||||
self.assertNotEqual(employee.attendance_state, 'checked_in', "He should not be able to check in with a wrong pin")
|
||||
self.assertTrue(action.get('warning'))
|
||||
|
||||
def test_checkin_kiosk_without_pin(self):
|
||||
""" Employee cannot check in/out without his pin in kiosk """
|
||||
employee = self.employee_kiosk.sudo(self.user)
|
||||
action = employee.attendance_manual({}, entered_pin=None)
|
||||
self.assertNotEqual(employee.attendance_state, 'checked_in', "He should not be able to check in with no pin")
|
||||
self.assertTrue(action.get('warning'))
|
||||
|
||||
def test_checkin_kiosk_no_pin_mode(self):
|
||||
""" Employee can check in/out without pin in kiosk when user has not group `use_pin` """
|
||||
employee = self.employee_kiosk.sudo(self.user_no_pin)
|
||||
employee.attendance_manual({}, entered_pin=None)
|
||||
self.assertEqual(employee.attendance_state, 'checked_in', "He should be able to check in with his pin")
|
||||
employee.attendance_manual({}, entered_pin=None)
|
||||
self.assertEqual(employee.attendance_state, 'checked_out', "He should be able to check out with his pin")
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
<field name="res_model">hr.employee</field>
|
||||
<field name="view_mode">kanban,tree,activity</field>
|
||||
<field name="view_id" ref="hr_employees_view_kanban"/>
|
||||
<field name="target">fullscreen</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create a new employee
|
||||
|
||||
@@ -800,12 +800,12 @@ class HrExpenseSheet(models.Model):
|
||||
return self.employee_id.parent_id.user_id
|
||||
elif self.employee_id.department_id.manager_id.user_id:
|
||||
return self.employee_id.department_id.manager_id.user_id
|
||||
return self.env.user
|
||||
return self.env['res.users']
|
||||
|
||||
def activity_update(self):
|
||||
for expense_report in self.filtered(lambda hol: hol.state == 'submit'):
|
||||
self.activity_schedule(
|
||||
'hr_expense.mail_act_expense_approval',
|
||||
user_id=expense_report.sudo()._get_responsible_for_approval().id)
|
||||
user_id=expense_report.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
self.filtered(lambda hol: hol.state == 'approve').activity_feedback(['hr_expense.mail_act_expense_approval'])
|
||||
self.filtered(lambda hol: hol.state == 'cancel').activity_unlink(['hr_expense.mail_act_expense_approval'])
|
||||
|
||||
@@ -265,6 +265,11 @@ class HolidaysRequest(models.Model):
|
||||
self.request_unit_hours = False
|
||||
self.request_unit_custom = False
|
||||
|
||||
@api.onchange('user_id')
|
||||
def _onchange_user_id(self):
|
||||
if not self.out_of_office_message:
|
||||
self.out_of_office_message = self.user_id.out_of_office_message
|
||||
|
||||
@api.onchange('request_date_from_period', 'request_hour_from', 'request_hour_to',
|
||||
'request_date_from', 'request_date_to',
|
||||
'employee_id')
|
||||
@@ -575,6 +580,10 @@ class HolidaysRequest(models.Model):
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
# Allow an employee to always write his own out of office message
|
||||
if len(self) == 1 and values.keys() == {'out_of_office_message'} and self.employee_id.user_id == self.env.user:
|
||||
return super(HolidaysRequest, self.sudo()).write(values)
|
||||
|
||||
employee_id = values.get('employee_id', False)
|
||||
if not self.env.context.get('leave_fast_create') and values.get('state'):
|
||||
self._check_approval_update(values['state'])
|
||||
@@ -788,8 +797,7 @@ class HolidaysRequest(models.Model):
|
||||
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
|
||||
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
|
||||
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
|
||||
if not all(self._ids):
|
||||
return
|
||||
|
||||
for holiday in self:
|
||||
val_type = holiday.holiday_status_id.validation_type
|
||||
|
||||
@@ -829,7 +837,7 @@ class HolidaysRequest(models.Model):
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
self.ensure_one()
|
||||
responsible = self.env.user
|
||||
responsible = self.env['res.users']
|
||||
|
||||
if self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
|
||||
if self.holiday_status_id.responsible_id:
|
||||
@@ -854,13 +862,13 @@ class HolidaysRequest(models.Model):
|
||||
holiday.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_approval',
|
||||
note=note,
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id)
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif holiday.state == 'validate1':
|
||||
holiday.activity_feedback(['hr_holidays.mail_act_leave_approval'])
|
||||
holiday.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_second_approval',
|
||||
note=note,
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id)
|
||||
user_id=holiday.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif holiday.state == 'validate':
|
||||
to_do |= holiday
|
||||
elif holiday.state == 'refuse':
|
||||
|
||||
@@ -525,7 +525,7 @@ class HolidaysAllocation(models.Model):
|
||||
def _check_approval_update(self, state):
|
||||
""" Check if target state is achievable. """
|
||||
current_employee = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1)
|
||||
if not current_employee or not all(self._ids):
|
||||
if not current_employee:
|
||||
return
|
||||
is_officer = self.env.user.has_group('hr_holidays.group_hr_holidays_user')
|
||||
is_manager = self.env.user.has_group('hr_holidays.group_hr_holidays_manager')
|
||||
@@ -564,7 +564,7 @@ class HolidaysAllocation(models.Model):
|
||||
|
||||
def _get_responsible_for_approval(self):
|
||||
self.ensure_one()
|
||||
responsible = self.env.user
|
||||
responsible = self.env['res.users']
|
||||
|
||||
if self.validation_type == 'hr' or (self.validation_type == 'both' and self.state == 'validate1'):
|
||||
if self.holiday_status_id.responsible_id:
|
||||
@@ -586,13 +586,13 @@ class HolidaysAllocation(models.Model):
|
||||
allocation.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_allocation_approval',
|
||||
note=note,
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id)
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif allocation.state == 'validate1':
|
||||
allocation.activity_feedback(['hr_holidays.mail_act_leave_allocation_approval'])
|
||||
allocation.activity_schedule(
|
||||
'hr_holidays.mail_act_leave_allocation_second_approval',
|
||||
note=note,
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id)
|
||||
user_id=allocation.sudo()._get_responsible_for_approval().id or self.env.user.id)
|
||||
elif allocation.state == 'validate':
|
||||
to_do |= allocation
|
||||
elif allocation.state == 'refuse':
|
||||
|
||||
@@ -4,26 +4,29 @@
|
||||
from datetime import datetime
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from odoo.tests.common import tagged, users, warmup
|
||||
from odoo.tests.common import tagged, users, warmup, Form
|
||||
from odoo.addons.hr_holidays.tests.common import TestHrHolidaysBase
|
||||
|
||||
|
||||
@tagged('out_of_office')
|
||||
class TestOutOfOffice(TestHrHolidaysBase):
|
||||
|
||||
def test_leave_ooo(self):
|
||||
leave_type = self.env['hr.leave.type'].create({
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.leave_type = self.env['hr.leave.type'].create({
|
||||
'name': 'Legal Leaves',
|
||||
'time_type': 'leave',
|
||||
'allocation_type': 'no',
|
||||
})
|
||||
|
||||
def test_leave_ooo(self):
|
||||
self.assertNotEqual(self.employee_hruser.user_id.im_status, 'leave_offline', 'user should not be on leave')
|
||||
self.assertNotEqual(self.employee_hruser.user_id.partner_id.im_status, 'leave_offline', 'user should not be on leave')
|
||||
leave_date_end = (datetime.today() + relativedelta(days=3))
|
||||
leave = self.env['hr.leave'].create({
|
||||
'name': 'Christmas',
|
||||
'employee_id': self.employee_hruser.id,
|
||||
'holiday_status_id': leave_type.id,
|
||||
'holiday_status_id': self.leave_type.id,
|
||||
'date_from': (datetime.today() - relativedelta(days=1)),
|
||||
'date_to': leave_date_end,
|
||||
'out_of_office_message': 'contact tde in case of problems',
|
||||
@@ -51,6 +54,29 @@ class TestOutOfOffice(TestHrHolidaysBase):
|
||||
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_date_end'], leave_date_end)
|
||||
self.assertEqual(infos[0]['direct_partner'][0]['out_of_office_message'], 'contact tde in case of problems')
|
||||
|
||||
def test_leave_ooo_use_default(self):
|
||||
""" Out of office message from Preferences should be used as default value """
|
||||
self.user_hruser.out_of_office_message = 'contact xdo in case of problems'
|
||||
|
||||
leave_form = Form(self.env['hr.leave'].sudo(self.user_hruser), view='hr_holidays.hr_leave_view_form')
|
||||
leave_form.date_from = datetime.today() - relativedelta(days=1)
|
||||
leave_form.date_to = datetime.today()
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave = leave_form.save()
|
||||
self.assertEqual(leave.out_of_office_message, 'contact xdo in case of problems')
|
||||
|
||||
def test_leave_ooo_overwrite_default(self):
|
||||
""" Out of office message default value should be overwrittable """
|
||||
self.user_hruser.out_of_office_message = 'contact xdo in case of problems'
|
||||
|
||||
leave_form = Form(self.env['hr.leave'].sudo(self.user_hruser), view='hr_holidays.hr_leave_view_form')
|
||||
leave_form.date_from = datetime.today() - relativedelta(days=1)
|
||||
leave_form.date_to = datetime.today()
|
||||
leave_form.holiday_status_id = self.leave_type
|
||||
leave_form.out_of_office_message = 'contact tde in case of problems'
|
||||
leave = leave_form.save()
|
||||
self.assertEqual(leave.out_of_office_message, 'contact tde in case of problems')
|
||||
|
||||
|
||||
@tagged('out_of_office')
|
||||
class TestOutOfOfficePerformance(TestHrHolidaysBase):
|
||||
|
||||
@@ -277,6 +277,7 @@
|
||||
</div>
|
||||
|
||||
<field name="name" attrs="{'readonly': [('state', 'not in', ('draft', 'confirm'))]}"/>
|
||||
<field name="user_id" invisible="1"/>
|
||||
<field name="out_of_office_message"/>
|
||||
</group>
|
||||
<group name="col_right">
|
||||
|
||||
@@ -47,7 +47,7 @@ def _guess_mimetype(ext=False, default='text/html'):
|
||||
return ext is not False and exts.get(ext, default) or exts
|
||||
|
||||
|
||||
def slugify_one(s, max_length=None):
|
||||
def slugify_one(s, max_length=0):
|
||||
""" Transform a string to a slug that can be used in a url path.
|
||||
This method will first try to do the job with python-slugify if present.
|
||||
Otherwise it will process string by stripping leading and ending spaces,
|
||||
@@ -65,13 +65,12 @@ def slugify_one(s, max_length=None):
|
||||
except TypeError:
|
||||
pass
|
||||
uni = unicodedata.normalize('NFKD', s).encode('ascii', 'ignore').decode('ascii')
|
||||
slug_str = re.sub('[\W_]', ' ', uni).strip().lower()
|
||||
slug_str = re.sub('[-\s]+', '-', slug_str)
|
||||
|
||||
return slug_str[:max_length]
|
||||
slug_str = re.sub(r'[\W_]', ' ', uni).strip().lower()
|
||||
slug_str = re.sub(r'[-\s]+', '-', slug_str)
|
||||
return slug_str[:max_length] if max_length > 0 else slug_str
|
||||
|
||||
|
||||
def slugify(s, max_length=None, path=False):
|
||||
def slugify(s, max_length=0, path=False):
|
||||
if not path:
|
||||
return slugify_one(s, max_length=max_length)
|
||||
else:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Translation of Odoo Server.
|
||||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * l10n_cn_small_business
|
||||
#
|
||||
|
||||
@@ -1004,7 +1004,7 @@
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_3b_total_input_tax_nd" model="account.tax.report.line">
|
||||
<field name="name">III.B. Total input tax non-deductible </field>
|
||||
<field name="name">III.B. Total input tax non-deductible</field>
|
||||
<field name="sequence">2</field>
|
||||
<field name="parent_id" ref="account_tax_report_line_3_assessment_deducible_tax"/>
|
||||
<field name="country_id" ref="base.lu"/>
|
||||
@@ -1080,7 +1080,7 @@
|
||||
</record>
|
||||
|
||||
<record id="account_tax_report_line_3a_5_taxe_declaree_comme_debiteur_voir_points_ii_e" model="account.tax.report.line">
|
||||
<field name="name">III.A.5. Taxe déclarée comme débiteur (voir points II.E et F) </field>
|
||||
<field name="name">III.A.5. Taxe déclarée comme débiteur (voir points II.E et F)</field>
|
||||
<field name="tag_name">III.A.5. Taxe déclarée comme débiteur (voir points II.E et F)</field>
|
||||
<field name="sequence">5</field>
|
||||
<field name="parent_id" ref="account_tax_report_line_3a_total_de_la_taxe_en_amont"/>
|
||||
|
||||
@@ -2077,212 +2077,230 @@
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_04_new' model='account.account.tag'>
|
||||
<field name='name'>401.04 Ventas y/o servicios gravados realizados en zona fronteriza norte</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_05_new' model='account.account.tag'>
|
||||
<field name='name'>401.05 Ventas y/o servicios gravados realizados en zona fronteriza norte de contado</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_06_new' model='account.account.tag'>
|
||||
<field name='name'>401.06 Ventas y/o servicios gravados realizados en zona fronteriza norte a crédito</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_04' model='account.account.tag'>
|
||||
<field name='name'>401.04 Ventas y/o servicios gravados al 0%</field>
|
||||
<field name='name'>401.07 Ventas y/o servicios gravados al 0%</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_05' model='account.account.tag'>
|
||||
<field name='name'>401.05 Ventas y/o servicios gravados al 0% de contado</field>
|
||||
<field name='name'>401.08 Ventas y/o servicios gravados al 0% de contado</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_06' model='account.account.tag'>
|
||||
<field name='name'>401.06 Ventas y/o servicios gravados al 0% a crédito</field>
|
||||
<field name='name'>401.09 Ventas y/o servicios gravados al 0% a crédito</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_07' model='account.account.tag'>
|
||||
<field name='name'>401.07 Ventas y/o servicios exentos</field>
|
||||
<field name='name'>401.10 Ventas y/o servicios exentos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_08' model='account.account.tag'>
|
||||
<field name='name'>401.08 Ventas y/o servicios exentos de contado</field>
|
||||
<field name='name'>401.11 Ventas y/o servicios exentos de contado</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_09' model='account.account.tag'>
|
||||
<field name='name'>401.09 Ventas y/o servicios exentos a crédito</field>
|
||||
<field name='name'>401.12 Ventas y/o servicios exentos a crédito</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_10' model='account.account.tag'>
|
||||
<field name='name'>401.10 Ventas y/o servicios gravados a la tasa general nacionales partes relacionadas</field>
|
||||
<field name='name'>401.13 Ventas y/o servicios gravados a la tasa general nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_11' model='account.account.tag'>
|
||||
<field name='name'>401.11 Ventas y/o servicios gravados a la tasa general extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.14 Ventas y/o servicios gravados a la tasa general extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_12' model='account.account.tag'>
|
||||
<field name='name'>401.12 Ventas y/o servicios gravados al 0% nacionales partes relacionadas</field>
|
||||
<field name='name'>401.15 Ventas y/o servicios gravados al 0% nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_13' model='account.account.tag'>
|
||||
<field name='name'>401.13 Ventas y/o servicios gravados al 0% extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.16 Ventas y/o servicios gravados al 0% extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_14' model='account.account.tag'>
|
||||
<field name='name'>401.14 Ventas y/o servicios exentos nacionales partes relacionadas</field>
|
||||
<field name='name'>401.17 Ventas y/o servicios exentos nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_15' model='account.account.tag'>
|
||||
<field name='name'>401.15 Ventas y/o servicios exentos extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.18 Ventas y/o servicios exentos extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_16' model='account.account.tag'>
|
||||
<field name='name'>401.16 Ingresos por servicios administrativos</field>
|
||||
<field name='name'>401.19 Ingresos por servicios administrativos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_17' model='account.account.tag'>
|
||||
<field name='name'>401.17 Ingresos por servicios administrativos nacionales partes relacionadas</field>
|
||||
<field name='name'>401.20 Ingresos por servicios administrativos nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_18' model='account.account.tag'>
|
||||
<field name='name'>401.18 Ingresos por servicios administrativos extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.21 Ingresos por servicios administrativos extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_19' model='account.account.tag'>
|
||||
<field name='name'>401.19 Ingresos por servicios profesionales</field>
|
||||
<field name='name'>401.22 Ingresos por servicios profesionales</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_20' model='account.account.tag'>
|
||||
<field name='name'>401.20 Ingresos por servicios profesionales nacionales partes relacionadas</field>
|
||||
<field name='name'>401.23 Ingresos por servicios profesionales nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_21' model='account.account.tag'>
|
||||
<field name='name'>401.21 Ingresos por servicios profesionales extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.24 Ingresos por servicios profesionales extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_22' model='account.account.tag'>
|
||||
<field name='name'>401.22 Ingresos por arrendamiento</field>
|
||||
<field name='name'>401.25 Ingresos por arrendamiento</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_23' model='account.account.tag'>
|
||||
<field name='name'>401.23 Ingresos por arrendamiento nacionales partes relacionadas</field>
|
||||
<field name='name'>401.26 Ingresos por arrendamiento nacionales partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_24' model='account.account.tag'>
|
||||
<field name='name'>401.24 Ingresos por arrendamiento extranjeros partes relacionadas</field>
|
||||
<field name='name'>401.27 Ingresos por arrendamiento extranjeros partes relacionadas</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_25' model='account.account.tag'>
|
||||
<field name='name'>401.25 Ingresos por exportación</field>
|
||||
<field name='name'>401.28 Ingresos por exportación</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_26' model='account.account.tag'>
|
||||
<field name='name'>401.26 Ingresos por comisiones</field>
|
||||
<field name='name'>401.29 Ingresos por comisiones</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_27' model='account.account.tag'>
|
||||
<field name='name'>401.27 Ingresos por maquila</field>
|
||||
<field name='name'>401.30 Ingresos por maquila</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_28' model='account.account.tag'>
|
||||
<field name='name'>401.28 Ingresos por coordinados</field>
|
||||
<field name='name'>401.31 Ingresos por coordinados</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_29' model='account.account.tag'>
|
||||
<field name='name'>401.29 Ingresos por regalías</field>
|
||||
<field name='name'>401.32 Ingresos por regalías</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_30' model='account.account.tag'>
|
||||
<field name='name'>401.30 Ingresos por asistencia técnica</field>
|
||||
<field name='name'>401.33 Ingresos por asistencia técnica</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_31' model='account.account.tag'>
|
||||
<field name='name'>401.31 Ingresos por donativos</field>
|
||||
<field name='name'>401.34 Ingresos por donativos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_32' model='account.account.tag'>
|
||||
<field name='name'>401.32 Ingresos por intereses (actividad propia)</field>
|
||||
<field name='name'>401.35 Ingresos por intereses (actividad propia)</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_33' model='account.account.tag'>
|
||||
<field name='name'>401.33 Ingresos de copropiedad</field>
|
||||
<field name='name'>401.36 Ingresos de copropiedad</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_34' model='account.account.tag'>
|
||||
<field name='name'>401.34 Ingresos por fideicomisos</field>
|
||||
<field name='name'>401.37 Ingresos por fideicomisos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_35' model='account.account.tag'>
|
||||
<field name='name'>401.35 Ingresos por factoraje financiero</field>
|
||||
<field name='name'>401.38 Ingresos por factoraje financiero</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_36' model='account.account.tag'>
|
||||
<field name='name'>401.36 Ingresos por arrendamiento financiero</field>
|
||||
<field name='name'>401.39 Ingresos por arrendamiento financiero</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_37' model='account.account.tag'>
|
||||
<field name='name'>401.37 Ingresos de extranjeros con establecimiento en el país</field>
|
||||
<field name='name'>401.40 Ingresos de extranjeros con establecimiento en el país</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
</record>
|
||||
<record id='account_tag_401_38' model='account.account.tag'>
|
||||
<field name='name'>401.38 Otros ingresos propios</field>
|
||||
<field name='name'>401.41 Otros ingresos propios</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>A</field>
|
||||
@@ -2293,20 +2311,26 @@
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_02_new' model='account.account.tag'>
|
||||
<field name='name'>402.02 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios en zona fronteriza norte</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_02' model='account.account.tag'>
|
||||
<field name='name'>402.02 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios al 0%</field>
|
||||
<field name='name'>402.03 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios al 0%</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_03' model='account.account.tag'>
|
||||
<field name='name'>402.03 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios exentos</field>
|
||||
<field name='name'>402.04 Devoluciones, descuentos o bonificaciones sobre ventas y/o servicios exentos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
</record>
|
||||
<record id='account_tag_402_04' model='account.account.tag'>
|
||||
<field name='name'>402.04 Devoluciones, descuentos o bonificaciones de otros ingresos</field>
|
||||
<field name='name'>402.05 Devoluciones, descuentos o bonificaciones de otros ingresos</field>
|
||||
<field name='color'>4</field>
|
||||
<field name='applicability'>accounts</field>
|
||||
<field name='nature'>D</field>
|
||||
|
||||
@@ -69,7 +69,7 @@ class LunchProductReport(models.Model):
|
||||
self._cr.execute("""
|
||||
CREATE or REPLACE view %s AS (
|
||||
SELECT
|
||||
row_number() over () AS id,
|
||||
row_number() over (ORDER BY user_id,product.id) AS id,
|
||||
product.id AS product_id,
|
||||
product.name,
|
||||
product.category_id,
|
||||
|
||||
@@ -40,7 +40,7 @@ var LunchKanbanModel = KanbanModel.extend({
|
||||
});
|
||||
},
|
||||
reload: function (id, options) {
|
||||
var domain = options.domain || this.localData[id].domain;
|
||||
var domain = options && options.domain || this.localData[id].domain;
|
||||
|
||||
this._addOrUpdate(domain, ['is_available_at', 'in', [this.locationId]]);
|
||||
options = _.extend(options, {domain: domain});
|
||||
|
||||
@@ -172,6 +172,8 @@ class MailMail(models.Model):
|
||||
messages._notify_failure_update() # notify user that we have a failure
|
||||
(notifications - failed).sudo().write({
|
||||
'email_status': 'sent',
|
||||
'failure_type': '',
|
||||
'failure_reason': '',
|
||||
})
|
||||
if not failure_type or failure_type == 'RECIPIENT': # if we have another error, we want to keep the mail.
|
||||
mail_to_delete_ids = [mail.id for mail in self if mail.auto_delete]
|
||||
|
||||
@@ -750,8 +750,9 @@ class Message(models.Model):
|
||||
WHERE message.message_type = %%s AND (message.subtype_id IS NULL OR subtype.internal IS TRUE) AND message.id = ANY (%%s)''' % (self._table), ('comment', self.ids,))
|
||||
if self._cr.fetchall():
|
||||
raise AccessError(
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
|
||||
(self._description, operation))
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
|
||||
+ ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)
|
||||
)
|
||||
|
||||
# Read mail_message.ids to have their values
|
||||
message_values = dict((message_id, {}) for message_id in self.ids)
|
||||
@@ -956,8 +957,9 @@ class Message(models.Model):
|
||||
if not self.browse(messages_to_check).exists():
|
||||
return
|
||||
raise AccessError(
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') %
|
||||
(self._description, operation))
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
|
||||
+ ' - ({} {}, {} {})'.format(_('Records:'), list(messages_to_check)[:6], _('User:'), self._uid)
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _get_record_name(self, values):
|
||||
|
||||
@@ -538,6 +538,7 @@ var BasicComposer = Widget.extend({
|
||||
files: ev.currentTarget.files,
|
||||
submitForm: true
|
||||
});
|
||||
ev.target.value = "";
|
||||
},
|
||||
/**
|
||||
* @private
|
||||
|
||||
@@ -88,7 +88,7 @@ var ChatterComposer = BasicComposer.extend({
|
||||
// partner_id
|
||||
var def;
|
||||
if (namesToFind.length > 0) {
|
||||
def = this._rpc({
|
||||
def = self._rpc({
|
||||
route: '/mail/get_partner_info',
|
||||
params: {
|
||||
model: self._model,
|
||||
|
||||
@@ -1374,12 +1374,12 @@ QUnit.test('chatter: discard changes on message post with post_refresh "recipien
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('chatter: discard changes on opening full-composer', async function (assert) {
|
||||
QUnit.test('chatter: discard changes on opening full-composer and open missing partner info popup', async function (assert) {
|
||||
// When we open the full-composer, any following operations by the user
|
||||
// will reload the record (even closing the full-composer). Therefore,
|
||||
// we should warn the user when we open the full-composer if the record
|
||||
// is dirty (= has some unsaved changes).
|
||||
assert.expect(2);
|
||||
assert.expect(3);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
@@ -1396,11 +1396,24 @@ QUnit.test('chatter: discard changes on opening full-composer', async function (
|
||||
' \'post_refresh\': \'always\'}"/>' +
|
||||
'</div>' +
|
||||
'</form>',
|
||||
archs: {
|
||||
'res.partner,false,form':
|
||||
'<form string="partners">' +
|
||||
'<field name="name"/>' +
|
||||
'</form>',
|
||||
},
|
||||
res_id: 2,
|
||||
session: {},
|
||||
mockRPC: function (route, args) {
|
||||
if (route === "/mail/get_suggested_recipients") {
|
||||
return Promise.resolve({2: []});
|
||||
return Promise.resolve({2: [[
|
||||
false,
|
||||
'pikapika@pikachu.com',
|
||||
''
|
||||
]]});
|
||||
}
|
||||
if (route === "/mail/get_partner_info") {
|
||||
return Promise.resolve([{full_name: "pikapika@pikachu.com", partner_id: false}]);
|
||||
}
|
||||
return this._super(route, args);
|
||||
},
|
||||
@@ -1421,6 +1434,8 @@ QUnit.test('chatter: discard changes on opening full-composer', async function (
|
||||
assert.strictEqual($modal.find('.modal-body').text(),
|
||||
"The record has been modified, your changes will be discarded. Do you want to proceed?",
|
||||
"should warn the user that any unsaved changes will be lost");
|
||||
await testUtils.dom.click($modal.find('.modal-footer .btn.btn-primary'));
|
||||
assert.strictEqual($modal.length, 1, "should have a modal opened");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
@@ -44,12 +44,19 @@ class Partner(models.Model):
|
||||
'member_lines.account_invoice_line.invoice_id.partner_id',
|
||||
'free_member',
|
||||
'member_lines.date_to', 'member_lines.date_from',
|
||||
'associate_member.membership_state')
|
||||
'associate_member')
|
||||
def _compute_membership_state(self):
|
||||
values = self._membership_state()
|
||||
for partner in self:
|
||||
partner.membership_state = values[partner.id]
|
||||
|
||||
# Do not depend directly on "associate_member.membership_state" or we might end up in an
|
||||
# infinite loop. Since we still need this dependency somehow, we explicitly search for the
|
||||
# "parent members" and trigger a recompute.
|
||||
parent_members = self.search([('associate_member', 'in', self.ids)]) - self
|
||||
if parent_members:
|
||||
parent_members._recompute_todo(self._fields['membership_state'])
|
||||
|
||||
@api.depends('member_lines.account_invoice_line.invoice_id.state',
|
||||
'member_lines.account_invoice_line.invoice_id.invoice_line_ids',
|
||||
'member_lines.account_invoice_line.invoice_id.payment_ids',
|
||||
|
||||
@@ -249,6 +249,7 @@ class MrpBomLine(models.Model):
|
||||
'mrp.bom', 'Parent BoM',
|
||||
index=True, ondelete='cascade', required=True)
|
||||
parent_product_tmpl_id = fields.Many2one('product.template', 'Parent Product Template', related='bom_id.product_tmpl_id')
|
||||
valid_product_attribute_value_ids = fields.Many2many('product.attribute.value', related='bom_id.product_tmpl_id.valid_product_attribute_value_ids')
|
||||
attribute_value_ids = fields.Many2many(
|
||||
'product.attribute.value', string='Apply on Variants',
|
||||
help="BOM Product Variants needed form apply this line.")
|
||||
|
||||
@@ -602,7 +602,7 @@ class MrpProduction(models.Model):
|
||||
move_raw.write({
|
||||
'group_id': production.procurement_group_id.id,
|
||||
'unit_factor': move_raw.product_uom_qty / production.product_qty,
|
||||
'reference': self.name, # set reference when MO name is different than 'New'
|
||||
'reference': production.name, # set reference when MO name is different than 'New'
|
||||
})
|
||||
production._generate_finished_moves()
|
||||
production.move_raw_ids._adjust_procure_method()
|
||||
@@ -861,8 +861,10 @@ class MrpProduction(models.Model):
|
||||
raise UserError(_('Work order %s is still running') % wo.name)
|
||||
self._check_lots()
|
||||
self.post_inventory()
|
||||
moves_to_cancel = (self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
moves_to_cancel._action_cancel()
|
||||
# Moves without quantity done are not posted => set them as done instead of canceling. In
|
||||
# case the user edits the MO later on and sets some consumed quantity on those, we do not
|
||||
# want the move lines to be canceled.
|
||||
(self.move_raw_ids | self.move_finished_ids).filtered(lambda x: x.state not in ('done', 'cancel')).write({'state': 'done'})
|
||||
self.write({'date_finished': fields.Datetime.now()})
|
||||
return True
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@
|
||||
<field name="product_qty"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<field name="parent_product_tmpl_id" invisible="1" />
|
||||
<field name="valid_product_attribute_value_ids" invisible="1"/>
|
||||
<field name="product_uom_id" options="{'no_open':True,'no_create':True}" groups="uom.group_uom"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
|
||||
<field name="attribute_value_ids" widget="many2many_tags" domain="[('id', 'in', valid_product_attribute_value_ids)]" options="{'no_create': True}" attrs="{'column_invisible': [('parent.product_id', '!=', False)]}" groups="product.group_product_variant"/>
|
||||
<field name="operation_id" groups="mrp.group_mrp_routings" attrs="{'column_invisible': [('parent.type','not in', ('normal', 'phantom'))]}" domain="[('routing_id', '=', parent.routing_id)]" options="{'no_quick_create':True,'no_create_edit':True}"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<field name="finished_lots_exist" invisible="1"/>
|
||||
</group>
|
||||
</group>
|
||||
<field name="move_line_ids" attrs="{'invisible': [('parent.state', '=', 'draft')], 'readonly': [('is_locked', '=', True)]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
|
||||
<field name="move_line_ids" attrs="{'invisible': [('parent.state', '=', 'draft')], 'readonly': ['|', ('is_locked', '=', True), ('state', '=', 'cancel')]}" context="{'default_workorder_id': workorder_id, 'default_product_uom_id': product_uom, 'default_product_id': product_id, 'default_location_id': location_id, 'default_location_dest_id': location_dest_id, 'default_production_id': production_id or raw_material_production_id}">
|
||||
<tree editable="bottom" decoration-success="product_uom_qty==qty_done" decoration-danger="(product_uom_qty > 0) and (qty_done>product_uom_qty)">
|
||||
<field name="lot_id" attrs="{'column_invisible': [('parent.has_tracking', '=', 'none')]}" domain="[('product_id', '=', parent.product_id)]" context="{'default_product_id': parent.product_id}"/>
|
||||
<field name="lot_produced_ids" widget="many2many_tags" options="{'no_open': True, 'no_create': True}" domain="[('id', 'in', parent.order_finished_lot_ids)]" invisible="not context.get('final_lots')"/>
|
||||
|
||||
@@ -93,6 +93,9 @@
|
||||
<input type="checkbox" name="o_payment_form_save_token" data-remove-me=""/>
|
||||
Save my payment data
|
||||
</label>
|
||||
<t t-if="acq.save_token == 'always'">
|
||||
<input type="checkbox" name="o_payment_form_save_token" checked="'checked'" class="o_hidden" data-remove-me=""/>
|
||||
</t>
|
||||
</div>
|
||||
</t>
|
||||
<t t-else="acq.payment_flow == 's2s'">
|
||||
|
||||
@@ -170,7 +170,7 @@ class AcquirerAdyen(models.Model):
|
||||
'sessionValidity': tmp_date.isoformat('T')[:19] + "Z",
|
||||
'resURL': urls.url_join(base_url, AdyenController._return_url),
|
||||
'merchantReturnData': json.dumps({'return_url': '%s' % values.pop('return_url')}) if values.get('return_url', '') else False,
|
||||
'shopperEmail': values.get('partner_email', ''),
|
||||
'shopperEmail': values.get('partner_email') or values.get('billing_partner_email') or '',
|
||||
})
|
||||
values['merchantSig'] = self._adyen_generate_merchant_sig_sha256('in', values)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class AcquirerPaypal(models.Model):
|
||||
else:
|
||||
percentage = self.fees_int_var
|
||||
fixed = self.fees_int_fixed
|
||||
fees = (percentage / 100.0 * amount + fixed) / (1 - percentage / 100.0)
|
||||
fees = (percentage / 100.0 * amount) + fixed / (1 - percentage / 100.0)
|
||||
return fees
|
||||
|
||||
@api.multi
|
||||
|
||||
@@ -224,7 +224,8 @@ class PosSession(models.Model):
|
||||
st_values = {
|
||||
'journal_id': journal.id,
|
||||
'user_id': self.env.user.id,
|
||||
'name': pos_name
|
||||
'name': pos_name,
|
||||
'balance_start': self.env["account.bank.statement"]._get_opening_balance(journal.id)
|
||||
}
|
||||
|
||||
statements.append(ABS.with_context(ctx).sudo(uid).create(st_values).id)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
<record id="pos_session_action_from_crm_team" model="ir.actions.act_window">
|
||||
<field name="name">Open Sessions</field>
|
||||
<field name="binding_model_id" ref="crm.model_crm_team"/>
|
||||
<field name="binding_model_id" ref="sales_team.model_crm_team"/>
|
||||
<field name="res_model">pos.session</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_open_sessions': True, 'search_default_crm_team_id': active_id}</field>
|
||||
|
||||
@@ -348,7 +348,7 @@ class ProductTemplate(models.Model):
|
||||
''' Store the initial standard price in order to be able to retrieve the cost of a product template for a given date'''
|
||||
templates = super(ProductTemplate, self).create(vals_list)
|
||||
if "create_product_product" not in self._context:
|
||||
templates.with_context(create_from_tmpl=True).create_variant_ids()
|
||||
templates.create_variant_ids()
|
||||
|
||||
# This is needed to set given values to first variant after creation
|
||||
for template, vals in zip(templates, vals_list):
|
||||
|
||||
@@ -38,3 +38,16 @@
|
||||
margin-top: 6px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.o_form_project_tasks.o_form_editable .oe_title {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.o_form_project_tasks.o_form_editable .oe_title .o_task_name {
|
||||
/*
|
||||
* should be (coming from addons/web/static/src/scss/form_view.scss):
|
||||
* max-width: map-get($container-max-widths, md) - (2 * $o-horizontal-padding);
|
||||
*/
|
||||
max-width: 688px;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@
|
||||
<div class="row">
|
||||
<div t-attf-class="col {{'col-lg-6' if not task.description else 'col-lg-12'}}">
|
||||
<ul class="list-group">
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center oe_attachments py-1 px-2" t-foreach='task.attachment_ids' t-as='attachment' t-attf-href="/web/content/#{attachment.id}" target="_blank" data-no-post-process="">
|
||||
<a class="list-group-item list-group-item-action d-flex align-items-center oe_attachments py-1 px-2" t-foreach='task.attachment_ids' t-as='attachment' t-attf-href="/web/content/#{attachment.id}?download=true" target="_blank" data-no-post-process="">
|
||||
<div class='oe_attachment_embedded o_image o_image_small mr-2 mr-lg-3' t-att-title="attachment.name" t-att-data-mimetype="attachment.mimetype" t-attf-data-src="/web/image/#{attachment.id}/50x40"/>
|
||||
<div class='oe_attachment_name text-truncate'><t t-esc='attachment.name'/></div>
|
||||
</a>
|
||||
|
||||
@@ -95,9 +95,9 @@ class AccountInvoice(models.Model):
|
||||
self.partner_id = self.purchase_id.partner_id.id
|
||||
|
||||
vendor_ref = self.purchase_id.partner_ref
|
||||
if vendor_ref:
|
||||
self.reference = ", ".join([self.reference, vendor_ref]) if (
|
||||
self.reference and vendor_ref not in self.reference) else vendor_ref
|
||||
if vendor_ref and (not self.reference or (
|
||||
vendor_ref + ", " not in self.reference and not self.reference.endswith(vendor_ref))):
|
||||
self.reference = ", ".join([self.reference, vendor_ref]) if self.reference else vendor_ref
|
||||
|
||||
if not self.invoice_line_ids:
|
||||
#as there's no invoice line yet, we keep the currency of the PO
|
||||
|
||||
@@ -384,7 +384,7 @@
|
||||
<field name="amount_total" sum="Total amount" widget="monetary"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="state"/>
|
||||
<field name="invoice_status" attrs="{'column_invisible': [('parent.state', 'not in', ('purchase', 'done'))]}"/>
|
||||
<field name="invoice_status" invisible="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
@@ -188,11 +188,15 @@ class StockRule(models.Model):
|
||||
price_unit = seller.currency_id._convert(
|
||||
price_unit, line.order_id.currency_id, line.order_id.company_id, fields.Date.today())
|
||||
|
||||
return {
|
||||
res = {
|
||||
'product_qty': line.product_qty + procurement_uom_po_qty,
|
||||
'price_unit': price_unit,
|
||||
'move_dest_ids': [(4, x.id) for x in values.get('move_dest_ids', [])]
|
||||
}
|
||||
orderpoint_id = values.get('orderpoint_id')
|
||||
if orderpoint_id:
|
||||
res['orderpoint_id'] = orderpoint_id.id
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _prepare_purchase_order_line(self, product_id, product_qty, product_uom, company_id, values, po):
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
<form string="Operations">
|
||||
<group>
|
||||
<group>
|
||||
<field name="type"/>
|
||||
<field name="product_id"/>
|
||||
<field name="name"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
@@ -86,7 +87,6 @@
|
||||
<field name="product_uom_qty"/>
|
||||
<field name="product_uom" groups="uom.group_uom"/>
|
||||
</div>
|
||||
<field name="type" invisible="1"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="tax_id" widget="many2many_tags" domain="[('type_tax_use','=','sale')]"/>
|
||||
<field name="invoiced" invisible="1"/>
|
||||
@@ -135,7 +135,7 @@
|
||||
<field name="fees_lines">
|
||||
<form string="Fees">
|
||||
<group>
|
||||
<field name="product_id"/>
|
||||
<field name="product_id" domain="[('type','=','service')]" required="True"/>
|
||||
<field name="name"/>
|
||||
<field name="product_uom_category_id" invisible="1"/>
|
||||
<label for="product_uom_qty"/>
|
||||
|
||||
@@ -1246,6 +1246,7 @@ class SaleOrderLine(models.Model):
|
||||
:param additional_domain: domain to restrict AAL to include in computation (required since timesheet is an AAL with a project ...)
|
||||
"""
|
||||
result = {}
|
||||
|
||||
# avoid recomputation if no SO lines concerned
|
||||
if not self:
|
||||
return result
|
||||
|
||||
@@ -443,6 +443,7 @@
|
||||
optional="hide"
|
||||
groups="analytic.group_analytic_tags"
|
||||
widget="many2many_tags"
|
||||
options="{'color_field': 'color'}"
|
||||
/>
|
||||
<field
|
||||
name="product_uom_qty"
|
||||
|
||||
@@ -92,10 +92,10 @@ class ProductTemplate(models.Model):
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
res = super(ProductTemplate, self)._onchange_type()
|
||||
if self.type == 'service':
|
||||
if self.type == 'service' and not self.invoice_policy:
|
||||
self.invoice_policy = 'order'
|
||||
self.service_type = 'timesheet'
|
||||
elif self.type == 'consu' and self.service_policy == 'ordered_timesheet':
|
||||
elif self.type == 'consu' and not self.invoice_policy and self.service_policy == 'ordered_timesheet':
|
||||
self.invoice_policy = 'order'
|
||||
return res
|
||||
|
||||
|
||||
@@ -128,8 +128,8 @@ class ProjectTask(models.Model):
|
||||
def _get_default_partner(self):
|
||||
partner = False
|
||||
if 'default_project_id' in self.env.context: # partner from SO line is prior on one from project
|
||||
project = self.env['project.project'].browse(self.env.context['default_project_id'])
|
||||
partner = project.sale_line_id.order_partner_id
|
||||
project_sudo = self.env['project.project'].browse(self.env.context['default_project_id']).sudo()
|
||||
partner = project_sudo.sale_line_id.order_partner_id
|
||||
if not partner:
|
||||
partner = super(ProjectTask, self)._get_default_partner()
|
||||
return partner
|
||||
@@ -212,7 +212,7 @@ class ProjectTask(models.Model):
|
||||
@api.multi
|
||||
@api.constrains('sale_line_id')
|
||||
def _check_sale_line_type(self):
|
||||
for task in self:
|
||||
for task in self.sudo():
|
||||
if task.sale_line_id:
|
||||
if not task.sale_line_id.is_service or task.sale_line_id.is_expense:
|
||||
raise ValidationError(_('You cannot link the order item %s - %s to this task because it is a re-invoiced expense.' % (task.sale_line_id.order_id.id, task.sale_line_id.product_id.name)))
|
||||
|
||||
@@ -92,12 +92,16 @@ class Project(models.Model):
|
||||
#
|
||||
# Time Repartition (per employee per billable types)
|
||||
#
|
||||
user_ids = self.env['project.task'].sudo().search_read([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'])
|
||||
user_ids = self.env['project.task'].sudo().read_group([('project_id', 'in', self.ids), ('user_id', '!=', False)], ['user_id'], ['user_id'])
|
||||
user_ids = [user_id['user_id'][0] for user_id in user_ids]
|
||||
employee_ids = self.env['res.users'].sudo().search_read([('id', 'in', user_ids)], ['employee_ids'])
|
||||
# flatten the list of list
|
||||
employee_ids = list(itertools.chain.from_iterable([employee_id['employee_ids'] for employee_id in employee_ids]))
|
||||
employees = self.env['hr.employee'].sudo().browse(employee_ids) | self.env['account.analytic.line'].search([('project_id', 'in', self.ids)]).mapped('employee_id')
|
||||
|
||||
aal_employee_ids = self.env['account.analytic.line'].read_group([('project_id', 'in', self.ids), ('employee_id', '!=', False)], ['employee_id'], ['employee_id'])
|
||||
employee_ids.extend(list(map(lambda x: x['employee_id'][0], aal_employee_ids)))
|
||||
|
||||
employees = self.env['hr.employee'].sudo().browse(employee_ids)
|
||||
repartition_domain = [('project_id', 'in', self.ids), ('employee_id', '!=', False), ('timesheet_invoice_type', '!=', False)] # force billable type
|
||||
# repartition data, without timesheet on cancelled so
|
||||
repartition_data = self.env['account.analytic.line'].read_group(repartition_domain + ['|', ('so_line', '=', False), ('so_line.state', '!=', 'cancel')], ['employee_id', 'timesheet_invoice_type', 'unit_amount'], ['employee_id', 'timesheet_invoice_type'], lazy=False)
|
||||
@@ -348,8 +352,14 @@ class Project(models.Model):
|
||||
def _plan_prepare_actions(self, values):
|
||||
actions = []
|
||||
if len(self) == 1:
|
||||
task_order_line_ids = []
|
||||
# retrieve all the sale order line that we will need later below
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman') or self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
task_order_line_ids = self.env['project.task'].read_group([('project_id', '=', self.id), ('sale_line_id', '!=', False)], ['sale_line_id'], ['sale_line_id'])
|
||||
task_order_line_ids = [ol['sale_line_id'][0] for ol in task_order_line_ids]
|
||||
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman'):
|
||||
if not self.sale_line_id and not self.tasks.mapped('sale_line_id'):
|
||||
if not self.sale_line_id and not task_order_line_ids:
|
||||
actions.append({
|
||||
'label': _("Create a Sales Order"),
|
||||
'type': 'action',
|
||||
@@ -358,14 +368,19 @@ class Project(models.Model):
|
||||
})
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
to_invoice_amount = values['dashboard']['profit'].get('to_invoice', False) # plan project only takes services SO line with timesheet into account
|
||||
sale_orders = self.tasks.mapped('sale_line_id.order_id').filtered(lambda so: so.invoice_status == 'to invoice')
|
||||
if to_invoice_amount and sale_orders:
|
||||
if len(sale_orders) == 1:
|
||||
|
||||
sale_order_ids = self.env['sale.order.line'].read_group([('id', 'in', task_order_line_ids)], ['order_id'], ['order_id'])
|
||||
sale_order_ids = [s['order_id'][0] for s in sale_order_ids]
|
||||
sale_order_ids = self.env['sale.order'].search_read([('id', 'in', sale_order_ids), ('invoice_status', '=', 'to invoice')], ['id'])
|
||||
sale_order_ids = list(map(lambda x: x['id'], sale_order_ids))
|
||||
|
||||
if to_invoice_amount and sale_order_ids:
|
||||
if len(sale_order_ids) == 1:
|
||||
actions.append({
|
||||
'label': _("Create Invoice"),
|
||||
'type': 'action',
|
||||
'action_id': 'sale.action_view_sale_advance_payment_inv',
|
||||
'context': json.dumps({'active_ids': sale_orders.ids, 'active_model': 'project.project'}),
|
||||
'context': json.dumps({'active_ids': sale_order_ids, 'active_model': 'project.project'}),
|
||||
})
|
||||
else:
|
||||
actions.append({
|
||||
@@ -401,9 +416,13 @@ class Project(models.Model):
|
||||
# if only one project, add it in the context as default value
|
||||
tasks_domain = [('project_id', 'in', self.ids)]
|
||||
tasks_context = self.env.context
|
||||
tasks_projects = self.env['project.task'].sudo().search(tasks_domain).mapped('project_id')
|
||||
if len(tasks_projects) == 1:
|
||||
tasks_context = {**tasks_context, 'default_project_id': tasks_projects.id}
|
||||
|
||||
# filter out all the projects that have no tasks
|
||||
task_projects_ids = self.env['project.task'].read_group([('project_id', 'in', self.ids)], ['project_id'], ['project_id'])
|
||||
task_projects_ids = [p['project_id'][0] for p in task_projects_ids]
|
||||
|
||||
if len(task_projects_ids) == 1:
|
||||
tasks_context = {**tasks_context, 'default_project_id': task_projects_ids[0]}
|
||||
stat_buttons.append({
|
||||
'name': _('Tasks'),
|
||||
'count': sum(self.mapped('task_count')),
|
||||
@@ -416,8 +435,13 @@ class Project(models.Model):
|
||||
})
|
||||
|
||||
if self.env.user.has_group('sales_team.group_sale_salesman_all_leads'):
|
||||
sale_orders = self.mapped('sale_line_id.order_id') | self.mapped(
|
||||
'tasks.sale_order_id')
|
||||
# read all the sale orders linked to the projects' tasks
|
||||
task_so_ids = self.env['project.task'].search_read([
|
||||
('project_id', 'in', self.ids), ('sale_order_id', '!=', False)
|
||||
], ['sale_order_id'])
|
||||
task_so_ids = [o['sale_order_id'][0] for o in task_so_ids]
|
||||
|
||||
sale_orders = self.mapped('sale_line_id.order_id') | self.env['sale.order'].browse(task_so_ids)
|
||||
if sale_orders:
|
||||
stat_buttons.append({
|
||||
'name': _('Sales Orders'),
|
||||
@@ -429,15 +453,20 @@ class Project(models.Model):
|
||||
context={'create': False, 'edit': False, 'delete': False}
|
||||
)
|
||||
})
|
||||
invoices = sale_orders.mapped('invoice_ids').filtered(lambda inv: inv.type == 'out_invoice')
|
||||
if invoices:
|
||||
|
||||
invoice_ids = self.env['sale.order'].search_read([('id', 'in', sale_orders.ids)], ['invoice_ids'])
|
||||
invoice_ids = list(itertools.chain(*[i['invoice_ids'] for i in invoice_ids]))
|
||||
invoice_ids = self.env['account.invoice'].search_read([('id', 'in', invoice_ids), ('type', '=', 'out_invoice')], ['id'])
|
||||
invoice_ids = list(map(lambda x: x['id'], invoice_ids))
|
||||
|
||||
if invoice_ids:
|
||||
stat_buttons.append({
|
||||
'name': _('Invoices'),
|
||||
'count': len(invoices),
|
||||
'count': len(invoice_ids),
|
||||
'icon': 'fa fa-pencil-square-o',
|
||||
'action': _to_action_data(
|
||||
action=self.env.ref('account.action_invoice_tree1'),
|
||||
domain=[('id', 'in', invoices.ids), ('type', '=', 'out_invoice')],
|
||||
domain=[('id', 'in', invoice_ids), ('type', '=', 'out_invoice')],
|
||||
context={'create': False, 'delete': False}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -365,6 +365,12 @@ class SaleOrderLine(models.Model):
|
||||
map_so_project_templates[(so_line.order_id.id, so_line.product_id.project_template_id.id)] = project
|
||||
else:
|
||||
map_so_project[so_line.order_id.id] = project
|
||||
elif not project:
|
||||
# Attach subsequent SO lines to the created project
|
||||
so_line.project_id = (
|
||||
map_so_project_templates.get((so_line.order_id.id, so_line.product_id.project_template_id.id))
|
||||
or map_so_project.get(so_line.order_id.id)
|
||||
)
|
||||
if so_line.product_id.service_tracking == 'task_in_project':
|
||||
if not project:
|
||||
if so_line.product_id.project_template_id:
|
||||
|
||||
@@ -363,8 +363,8 @@ class TestSaleService(TestCommonSaleTimesheetNoChart):
|
||||
# check each line has or no generate something
|
||||
self.assertTrue(so_line1.project_id, "Line1 should have create a project based on template A")
|
||||
self.assertTrue(so_line2.project_id, "Line2 should have create an empty project")
|
||||
self.assertFalse(so_line3.project_id, "Line3 should not have create a project, since line1 already create a project based on template A")
|
||||
self.assertFalse(so_line4.project_id, "Line4 should not have create a project, since line1 already create an empty project")
|
||||
self.assertEqual(so_line3.project_id, so_line1.project_id, "Line3 should reuse project of line1")
|
||||
self.assertEqual(so_line4.project_id, so_line2.project_id, "Line4 should reuse project of line2")
|
||||
self.assertTrue(so_line4.task_id, "Line4 should have create a new task, even if no project created.")
|
||||
self.assertTrue(so_line5.project_id, "Line5 should have create a project based on template B")
|
||||
|
||||
|
||||
@@ -18,25 +18,43 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_view_search_sale_timesheet" model="ir.ui.view">
|
||||
<field name="name">product.template.search.timesheet</field>
|
||||
<field name="model">product.template</field>
|
||||
<field name="inherit_id" ref="product.product_template_search_view"/>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//filter[@name='consumable']" position="after">
|
||||
<separator/>
|
||||
<filter string="Time-based services" name="product_time_based" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'timesheet')]"/>
|
||||
<filter string="Fixed price services" name="product_service_fixed" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'order'), ('service_type', '=', 'timesheet')]"/>
|
||||
<filter string="Milestone services" name="product_service_milestone" domain="[('type', '=', 'service'), ('invoice_policy', '=', 'delivery'), ('service_type', '=', 'manual')]"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_action_time_based" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'timesheet'}</field>
|
||||
<field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
|
||||
<field name="context">{'search_default_product_time_based':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'timesheet', 'default_service_policy': 'delivered_timesheet'}</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_action_fixed" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order', 'default_service_type': 'timesheet'}</field>
|
||||
<field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
|
||||
<field name="context">{'search_default_product_service_fixed':1, 'default_type': 'service', 'default_invoice_policy': 'order', 'default_service_type': 'timesheet', 'default_service_policy': 'ordered_timesheet'}</field>
|
||||
</record>
|
||||
|
||||
<record id="product_template_action_milestone" model="ir.actions.act_window">
|
||||
<field name="name">Products</field>
|
||||
<field name="res_model">product.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="context">{'search_default_product_service_milestone':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'manual'}</field>
|
||||
<field name="search_view_id" ref="sale_timesheet.product_template_view_search_sale_timesheet"/>
|
||||
<field name="context">{'search_default_product_service_milestone':1, 'default_type': 'service', 'default_invoice_policy': 'delivery', 'default_service_type': 'manual', 'default_service_policy': 'delivered_manual'}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
||||
@@ -61,20 +61,42 @@
|
||||
<field name="groups_id" eval="[(4, ref('base.group_user'))]"/>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<button type="object" name="action_view_so"
|
||||
class="oe_stat_button" icon="fa-dollar"
|
||||
attrs="{'invisible': [('sale_order_id', '=', False)]}"
|
||||
string="Sales Order"/>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
</xpath>
|
||||
<field name="partner_id" position="after">
|
||||
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False)]}" options='{"no_create": True}' context="{'create': False, 'edit': False, 'delete': False}"/>
|
||||
<field name="sale_line_id" string="Sales Order Item" attrs="{'invisible': ['|', ('partner_id', '=', False)]}" options='{"no_open": True}' readonly="1" context="{'create': False, 'edit': False, 'delete': False}"/>
|
||||
<field name="billable_type" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form_inherit_sale_line_editable" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit.sale.line.editable.salesman</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="view_sale_service_inherit_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='sale_line_id']" position="attributes">
|
||||
<attribute name="options">{"no_create": True}</attribute>
|
||||
<attribute name="readonly">0</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_form_sale_order" model="ir.ui.view">
|
||||
<field name="name">project.task.form.inherit.sale.order</field>
|
||||
<field name="model">project.task</field>
|
||||
<field name="inherit_id" ref="project.view_task_form2"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//button[@name='toggle_active']" position="before">
|
||||
<button type="object" name="action_view_so"
|
||||
class="oe_stat_button" icon="fa-dollar"
|
||||
attrs="{'invisible': [('sale_order_id', '=', False)]}"
|
||||
string="Sales Order"/>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
</xpath>
|
||||
</field>
|
||||
<field name="groups_id" eval="[(4, ref('sales_team.group_sale_salesman'))]"/>
|
||||
</record>
|
||||
|
||||
<record id="project_task_view_search" model="ir.ui.view">
|
||||
<field name="name">project.task.search.inherit</field>
|
||||
<field name="model">project.task</field>
|
||||
|
||||
@@ -6,6 +6,14 @@ from odoo import models, fields, api, _
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = 'ir.actions.report'
|
||||
|
||||
@api.multi
|
||||
def retrieve_attachment(self, record):
|
||||
# Override this method in order to force to re-render the pdf in case of
|
||||
# using snailmail
|
||||
if self.env.context.get('snailmail_layout'):
|
||||
return False
|
||||
return super(IrActionsReport, self).retrieve_attachment(record)
|
||||
|
||||
@api.model
|
||||
def get_paperformat(self):
|
||||
# force the right format (euro/A4) when sending letters, only if we are not using the l10n_DE layout
|
||||
|
||||
@@ -25,7 +25,7 @@ class TestPingenSend(AccountingTestCase):
|
||||
self.data = {
|
||||
'data': json.dumps({
|
||||
'speed': 1,
|
||||
'color': 2,
|
||||
'color': 1,
|
||||
'duplex': 0,
|
||||
'send': True,
|
||||
})
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
<!--
|
||||
Resource: stock.location
|
||||
-->
|
||||
</data>
|
||||
<data noupdate="1">
|
||||
<record id="stock_location_locations" model="stock.location">
|
||||
<field name="name">Physical Locations</field>
|
||||
<field name="usage">view</field>
|
||||
|
||||
@@ -200,7 +200,7 @@ class Route(models.Model):
|
||||
@api.onchange('warehouse_selectable')
|
||||
def _onchange_warehouse_selectable(self):
|
||||
if not self.warehouse_selectable:
|
||||
self.warehouse_ids = []
|
||||
self.warehouse_ids = [(5, 0, 0)]
|
||||
|
||||
def toggle_active(self):
|
||||
for route in self:
|
||||
|
||||
@@ -1136,7 +1136,7 @@ class StockMove(models.Model):
|
||||
.filtered(lambda p: p.quant_ids and len(p.quant_ids) > 1):
|
||||
if len(result_package.quant_ids.mapped('location_id')) > 1:
|
||||
raise UserError(_('You cannot move the same package content more than once in the same transfer or split the same package into two location.'))
|
||||
picking = moves_todo and moves_todo[0].picking_id or False
|
||||
picking = moves_todo.mapped('picking_id')
|
||||
moves_todo.write({'state': 'done', 'date': fields.Datetime.now()})
|
||||
moves_todo.mapped('move_dest_ids')._action_assign()
|
||||
|
||||
|
||||
@@ -647,5 +647,5 @@ class QuantPackage(models.Model):
|
||||
for quant in self._get_contained_quants():
|
||||
if quant.product_id not in res:
|
||||
res[quant.product_id] = 0
|
||||
res[quant.product_id] += quant.qty
|
||||
res[quant.product_id] += quant.quantity
|
||||
return res
|
||||
|
||||
@@ -5,7 +5,7 @@ var tour = require('web_tour.tour');
|
||||
|
||||
tour.register('stock', {
|
||||
test: true,
|
||||
url: '/web#action=stock.action_stock_config_settings',
|
||||
url: '/web?debug=1#action=stock.action_stock_config_settings',
|
||||
},
|
||||
[
|
||||
{
|
||||
|
||||
@@ -378,7 +378,7 @@
|
||||
<separator/>
|
||||
<filter name="late" string="Late" domain="[('scheduled_date','<', time.strftime('%%Y-%%m-%%d %%H:%%M:%%S')), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Pickings that are late on scheduled time"/>
|
||||
<separator/>
|
||||
<filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False)]" help="Remaining parts of picking partially processed"/>
|
||||
<filter name="backorder" string="Backorders" domain="[('backorder_id','!=', False), ('state', 'in', ('assigned', 'waiting', 'confirmed'))]" help="Remaining parts of picking partially processed"/>
|
||||
<separator/>
|
||||
<filter string="My Activities" name="activities_my"
|
||||
domain="[('activity_ids.user_id', '=', uid)]"/>
|
||||
@@ -557,7 +557,6 @@ action = action_values
|
||||
'default_picking_type_id': active_id,
|
||||
'contact_display': 'partner_address',
|
||||
'search_default_late': 1,
|
||||
'search_default_confirmed': 1,
|
||||
}
|
||||
</field>
|
||||
<field name="search_view_id" ref="view_picking_internal_search"/>
|
||||
@@ -579,7 +578,6 @@ action = action_values
|
||||
'default_picking_type_id': active_id,
|
||||
'contact_display': 'partner_address',
|
||||
'search_default_backorder': 1,
|
||||
'search_default_confirmed': 1,
|
||||
}
|
||||
</field>
|
||||
<field name="search_view_id" ref="view_picking_internal_search"/>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
<link href="/survey/static/src/css/survey_print.css" rel="stylesheet" type="text/css"/>
|
||||
<link href="/survey/static/src/css/survey_result.css" rel="stylesheet" type="text/css"></link>
|
||||
<link rel="stylesheet" type="text/scss" href="/survey/static/src/scss/survey_templates.scss"/>
|
||||
</template>
|
||||
|
||||
<template id="survey_report_assets_pdf" inherit_id="web.report_assets_pdf">
|
||||
|
||||
@@ -751,7 +751,7 @@ class TestMailPerformancePost(TransactionCase):
|
||||
]
|
||||
self.attachements = self.env['ir.attachment'].sudo(self.env.user).create(self.vals) #-> 163-> 165 query
|
||||
attachement_ids = self.attachements.ids
|
||||
with self.assertQueryCount(emp=163): # com runbot 156 // test_mail only: 133
|
||||
with self.assertQueryCount(emp=175): # com runbot 156 // test_mail only: 133
|
||||
self.cr.sql_log = self.warm and self.cr.sql_log_count
|
||||
record.with_context({}).message_post(
|
||||
body='<p>Test body <img src="cid:cid1"> <img src="cid:cid2"></p>',
|
||||
|
||||
@@ -57,17 +57,17 @@
|
||||
<field name="uom_type">smaller</field>
|
||||
</record>
|
||||
|
||||
<record id="uom_time_day" model="uom.uom">
|
||||
<field name="name">Day(s)</field>
|
||||
<field name="category_id" ref="uom_categ_time"/>
|
||||
<field name="factor" eval="1.0"/>
|
||||
</record>
|
||||
<record id="uom_time_week" model="uom.uom">
|
||||
<field name="name">Week(s)</field>
|
||||
<field name="category_id" ref="uom_categ_time"/>
|
||||
<field name="factor" eval="7.0"/>
|
||||
<field name="uom_type">bigger</field>
|
||||
</record>
|
||||
<record id="uom_time_day" model="uom.uom">
|
||||
<field name="name">Day(s)</field>
|
||||
<field name="category_id" ref="uom_categ_time"/>
|
||||
<field name="factor" eval="1.0"/>
|
||||
</record>
|
||||
<record id="uom_time_hour" model="uom.uom">
|
||||
<field name="name">Hour(s)</field>
|
||||
<field name="category_id" ref="uom_categ_time"/>
|
||||
|
||||
@@ -84,9 +84,8 @@ class UoM(models.Model):
|
||||
self._cr.execute("""
|
||||
SELECT C.id AS category_id, count(U.id) AS uom_count
|
||||
FROM uom_category C
|
||||
LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference'
|
||||
LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference' AND U.active = 't'
|
||||
WHERE C.id IN %s
|
||||
AND U.active = 't'
|
||||
GROUP BY C.id
|
||||
""", (tuple(category_ids),))
|
||||
for uom_data in self._cr.dictfetchall():
|
||||
|
||||
@@ -68,6 +68,16 @@ class TestUom(TransactionCase):
|
||||
'name': 'Custom UoM category',
|
||||
})
|
||||
|
||||
# at first we can not create a non reference in custom category
|
||||
with self.assertRaises(ValidationError):
|
||||
self.env['uom.uom'].create({
|
||||
'name': 'Bigger UoM of my category',
|
||||
'factor_inv': 42,
|
||||
'uom_type': 'bigger',
|
||||
'rounding': 0.5,
|
||||
'category_id': category.id
|
||||
})
|
||||
|
||||
# create the reference
|
||||
self.env['uom.uom'].create({
|
||||
'name': 'Reference UoM of my category',
|
||||
|
||||
@@ -372,7 +372,7 @@ ActionManager.include({
|
||||
Widget: View,
|
||||
});
|
||||
} else if (config.isDebug('assets')) {
|
||||
console.error("View type '" + viewType + "' is not present in the view registry.");
|
||||
console.log("View type '" + viewType + "' is not present in the view registry.");
|
||||
}
|
||||
});
|
||||
return views;
|
||||
|
||||
@@ -1526,6 +1526,7 @@ var AbstractFieldBinary = AbstractField.extend({
|
||||
*/
|
||||
_clearFile: function (){
|
||||
var self = this;
|
||||
this.$('.o_input_file').val('');
|
||||
this.set_filename('');
|
||||
if (!this.isDestroyed()) {
|
||||
this._setValue(false).then(function() {
|
||||
|
||||
@@ -14,6 +14,7 @@ var core = require('web.core');
|
||||
var ListModel = require('web.ListModel');
|
||||
var ListRenderer = require('web.ListRenderer');
|
||||
var ListController = require('web.ListController');
|
||||
var pyUtils = require('web.py_utils');
|
||||
|
||||
var _lt = core._lt;
|
||||
|
||||
@@ -41,7 +42,8 @@ var ListView = BasicView.extend({
|
||||
var selectedRecords = []; // there is no selected records by default
|
||||
|
||||
var mode = this.arch.attrs.editable && !params.readonly ? "edit" : "readonly";
|
||||
var expandGroups = !!JSON.parse(this.arch.attrs.expand || "0");
|
||||
var pyevalContext = py.dict.fromJSON(_.pick(params.context, function(value, key, object) {return !_.isUndefined(value)}) || {});
|
||||
var expandGroups = !!JSON.parse(pyUtils.py_eval(this.arch.attrs.expand || "0", {'context': pyevalContext}));
|
||||
|
||||
this.groupbys = {};
|
||||
this.arch.children.forEach(function (child) {
|
||||
|
||||
@@ -65,8 +65,9 @@ var Model = AbstractModel.extend({
|
||||
/**
|
||||
* reload
|
||||
*/
|
||||
reload: function (_id, _params) {
|
||||
// FIXME: update this._state or something?
|
||||
reload: function (_id, params) {
|
||||
_.extend(this._state, _.pick(params, ['domain', 'context']));
|
||||
|
||||
return this._fetch();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.o_boxed_footer, .o_boxed_header, .o_report_layout_boxed {
|
||||
font-family: 'Lato' 'Noto';
|
||||
font-family: 'Lato', 'Noto';
|
||||
color: gray('700');
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
@@ -1998,6 +1998,36 @@ QUnit.module('basic_fields', {
|
||||
list.destroy();
|
||||
});
|
||||
|
||||
QUnit.test("binary fields input value is empty whean clearing after uploading", async function (assert) {
|
||||
assert.expect(2);
|
||||
|
||||
var form = await createView({
|
||||
View: FormView,
|
||||
model: 'partner',
|
||||
data: this.data,
|
||||
arch: '<form string="Partners">' +
|
||||
'<field name="document" filename="foo"/>' +
|
||||
'<field name="foo"/>' +
|
||||
'</form>',
|
||||
res_id: 1,
|
||||
});
|
||||
|
||||
await testUtils.form.clickEdit(form);
|
||||
|
||||
// // We need to convert the input type since we can't programmatically set the value of a file input
|
||||
form.$('.o_input_file').attr('type', 'text').val('coucou.txt');
|
||||
|
||||
assert.strictEqual(form.$('.o_input_file').val(), 'coucou.txt',
|
||||
"input value should be changed to \"coucou.txt\"");
|
||||
|
||||
await testUtils.dom.click(form.$('.o_field_binary_file > .o_clear_file_button'));
|
||||
|
||||
assert.strictEqual(form.$('.o_input_file').val(), '',
|
||||
"input value should be empty");
|
||||
|
||||
form.destroy();
|
||||
});
|
||||
|
||||
QUnit.test('field text in editable list view', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@
|
||||
</template>
|
||||
|
||||
<template id="conditional_assets_tests" name="Tests Assets Bundle">
|
||||
<t t-call-assets="web.assets_tests" t-if="'tests' in debug" defer_load="True" />
|
||||
<t t-call-assets="web.assets_tests" t-if="'tests' in debug or test_mode_enabled" defer_load="True" />
|
||||
</template>
|
||||
|
||||
<template id="web.layout" name="Web layout"><!DOCTYPE html>
|
||||
|
||||
@@ -539,7 +539,7 @@ var FileWidget = SearchableMediaWidget.extend({
|
||||
* @param {boolean} isURL
|
||||
* @param {boolean} isImage
|
||||
*/
|
||||
_updateAddUrlUi(emptyValue, isURL, isImage) {
|
||||
_updateAddUrlUi: function (emptyValue, isURL, isImage) {
|
||||
this.$addUrlButton.toggleClass('btn-secondary', emptyValue)
|
||||
.toggleClass('btn-primary', !emptyValue)
|
||||
.prop('disabled', !isURL);
|
||||
|
||||
@@ -99,7 +99,7 @@ class Website(Home):
|
||||
if request.env['res.users'].browse(request.uid).has_group('base.group_user'):
|
||||
redirect = b'/web?' + request.httprequest.query_string
|
||||
else:
|
||||
redirect = '/'
|
||||
redirect = '/my'
|
||||
return http.redirect_with_hash(redirect)
|
||||
return response
|
||||
|
||||
@@ -471,3 +471,8 @@ class WebsiteBinary(http.Controller):
|
||||
if unique:
|
||||
kw['unique'] = unique
|
||||
return Binary().content_image(**kw)
|
||||
|
||||
@http.route(['/favicon.ico'], type='http', auth='public', website=True)
|
||||
def favicon(self, **kw):
|
||||
# when opening a pdf in chrome, chrome tries to open the default favicon url
|
||||
return self.content_image(model='website', id=str(request.website.id), field='favicon', **kw)
|
||||
|
||||
@@ -1,27 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<!-- Default Menu to store module menus for new website -->
|
||||
<record id="main_menu" model="website.menu">
|
||||
<field name="name">Default Main Menu</field>
|
||||
<field name="url">/default-main-menu</field>
|
||||
</record>
|
||||
<record id="menu_home" model="website.menu">
|
||||
<field name="name">Home</field>
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
</record>
|
||||
<record id="menu_contactus" model="website.menu">
|
||||
<field name="name">Contact us</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
</record>
|
||||
|
||||
<data>
|
||||
<!-- split between ir_ui_view & website_page record to keep external_id on ir_ui_view -->
|
||||
|
||||
<!-- Homepage -->
|
||||
<record id="homepage" model="ir.ui.view">
|
||||
<field name="name">Home</field>
|
||||
<field name="type">qweb</field>
|
||||
@@ -35,13 +15,7 @@
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="homepage_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/</field>
|
||||
<field name="view_id" ref="homepage"/>
|
||||
</record>
|
||||
|
||||
<!-- Contact Us page -->
|
||||
<record id="contactus" model="ir.ui.view">
|
||||
<field name="name">Contact Us</field>
|
||||
<field name="type">qweb</field>
|
||||
@@ -93,13 +67,7 @@
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
<record id="contactus_page" model="website.page">
|
||||
<field name="url">/contactus</field>
|
||||
<field name="website_published">True</field>
|
||||
<field name="view_id" ref="contactus"/>
|
||||
</record>
|
||||
|
||||
<!-- About Us page -->
|
||||
<record id="aboutus" model="ir.ui.view">
|
||||
<field name="name">About us</field>
|
||||
<field name="type">qweb</field>
|
||||
@@ -145,12 +113,43 @@
|
||||
</t>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
|
||||
<data noupdate="1">
|
||||
<record id="homepage_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/</field>
|
||||
<field name="view_id" ref="homepage"/>
|
||||
</record>
|
||||
<record id="contactus_page" model="website.page">
|
||||
<field name="url">/contactus</field>
|
||||
<field name="website_published">True</field>
|
||||
<field name="view_id" ref="contactus"/>
|
||||
</record>
|
||||
<record id="aboutus_page" model="website.page">
|
||||
<field name="website_published">True</field>
|
||||
<field name="url">/aboutus</field>
|
||||
<field name="view_id" ref="aboutus"/>
|
||||
</record>
|
||||
|
||||
<!-- Default Menu to store module menus for new website -->
|
||||
<record id="main_menu" model="website.menu">
|
||||
<field name="name">Default Main Menu</field>
|
||||
<field name="url">/default-main-menu</field>
|
||||
</record>
|
||||
<record id="menu_home" model="website.menu">
|
||||
<field name="name">Home</field>
|
||||
<field name="url">/</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">10</field>
|
||||
</record>
|
||||
<record id="menu_contactus" model="website.menu">
|
||||
<field name="name">Contact us</field>
|
||||
<field name="url">/contactus</field>
|
||||
<field name="parent_id" ref="website.main_menu"/>
|
||||
<field name="sequence" type="int">60</field>
|
||||
</record>
|
||||
|
||||
<record id="default_website" model="website">
|
||||
<field name="name">My Website</field>
|
||||
<field name="domain"></field>
|
||||
|
||||
@@ -82,7 +82,8 @@ class View(models.Model):
|
||||
# original tree. Indeed, the order of children 'id' fields
|
||||
# must remain the same so that the inheritance is applied
|
||||
# in the same order in the copied tree.
|
||||
inherit_child.copy({'inherit_id': website_specific_view.id, 'key': inherit_child.key})
|
||||
child = inherit_child.copy({'inherit_id': website_specific_view.id, 'key': inherit_child.key})
|
||||
inherit_child.inherit_children_ids.write({'inherit_id': child.id})
|
||||
inherit_child.unlink()
|
||||
else:
|
||||
# Trigger COW on inheriting views
|
||||
|
||||
@@ -154,7 +154,7 @@ class Website(models.Model):
|
||||
|
||||
self._get_languages.clear_cache(self)
|
||||
if 'company_id' in values and 'user_id' not in values:
|
||||
public_user_to_change_websites = self.filtered(lambda w: w.user_id.company_id.id != values['company_id'])
|
||||
public_user_to_change_websites = self.filtered(lambda w: w.sudo().user_id.company_id.id != values['company_id'])
|
||||
if public_user_to_change_websites:
|
||||
company = self.env['res.company'].browse(values['company_id'])
|
||||
super(Website, public_user_to_change_websites).write(dict(values, user_id=company._get_public_user().id))
|
||||
|
||||
@@ -797,6 +797,54 @@ class TestCowViewSaving(common.TransactionCase):
|
||||
all_title_updated = specific_view.website_meta_title == self.base_view.website_meta_title == "A bug got fixed by updating this field"
|
||||
self.assertEqual(all_title_updated, True, "Update on top level generic views should also be applied on specific views")
|
||||
|
||||
def test_multiple_inherit_level(self):
|
||||
""" Test multi-level inheritance:
|
||||
Base
|
||||
|
|
||||
---> Extension (Website-specific)
|
||||
|
|
||||
---> Extension 2 (Website-specific)
|
||||
"""
|
||||
View = self.env['ir.ui.view']
|
||||
|
||||
self.inherit_view.website_id = 1
|
||||
inherit_view_2 = View.create({
|
||||
'name': 'Extension 2',
|
||||
'mode': 'extension',
|
||||
'inherit_id': self.inherit_view.id,
|
||||
'arch': '<div position="inside">, extended content 2</div>',
|
||||
'key': 'website.extension_view_2',
|
||||
'website_id': 1,
|
||||
})
|
||||
|
||||
total_views = View.search_count([])
|
||||
|
||||
# id | name | content | website_id | inherit | key
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# 1 | Base | base content | / | / | website.base_view
|
||||
# 2 | Extension | , extended content | 1 | 1 | website.extension_view
|
||||
# 3 | Extension 2 | , extended content 2 | 1 | 2 | website.extension_view_2
|
||||
|
||||
self.base_view.with_context(website_id=1).write({'arch': '<div>modified content</div>'})
|
||||
|
||||
# 2 views are created, one is deleted
|
||||
self.assertEqual(View.search_count([]), total_views + 1)
|
||||
self.assertFalse(self.inherit_view.exists())
|
||||
self.assertTrue(inherit_view_2.exists())
|
||||
|
||||
# Verify the inheritance
|
||||
base_specific = View.search([('key', '=', self.base_view.key), ('website_id', '=', 1)]).with_context(load_all_views=True)
|
||||
extend_specific = View.search([('key', '=', 'website.extension_view'), ('website_id', '=', 1)])
|
||||
self.assertEqual(extend_specific.inherit_id, base_specific)
|
||||
self.assertEqual(inherit_view_2.inherit_id, extend_specific)
|
||||
|
||||
# id | name | content | website_id | inherit | key
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# 1 | Base | base content | / | / | website.base_view
|
||||
# 4 | Base | modified content | 1 | / | website.base_view
|
||||
# 5 | Extension | , extended content | 1 | 4 | website.extension_view
|
||||
# 3 | Extension 2 | , extended content 2 | 1 | 5 | website.extension_view_2
|
||||
|
||||
|
||||
class Crawler(HttpCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -218,6 +218,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<t t-raw="0"/>
|
||||
<div class="alert alert-info alert-dismissable mt16 css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status">
|
||||
<button type="button" class="close" data-dismiss="alert" aria-label="Close">&times;</button>
|
||||
Following content will appear on all events
|
||||
</div>
|
||||
<div class="oe_structure" id="oe_structure_website_event_layout_1"/>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
@@ -59,7 +59,10 @@ class MailMessage(models.Model):
|
||||
if self.user_has_groups('base.group_public'):
|
||||
self.env.cr.execute('SELECT id FROM "%s" WHERE website_published IS FALSE AND id = ANY (%%s)' % (self._table), (self.ids,))
|
||||
if self.env.cr.fetchall():
|
||||
raise AccessError(_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation))
|
||||
raise AccessError(
|
||||
_('The requested operation cannot be completed due to security restrictions. Please contact your system administrator.\n\n(Document type: %s, Operation: %s)') % (self._description, operation)
|
||||
+ ' - ({} {}, {} {})'.format(_('Records:'), self.ids[:6], _('User:'), self._uid)
|
||||
)
|
||||
return super(MailMessage, self).check_access_rule(operation=operation)
|
||||
|
||||
@api.multi
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user