[MERGE] forward port branch saas-12.4 up to 618ea1ab67

This commit is contained in:
Christophe Simonis
2019-06-18 10:24:25 +02:00
139 changed files with 1509 additions and 568 deletions
+68 -23
View File
@@ -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
+11 -4
View File
@@ -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')
+93 -31
View File
@@ -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'})
+45 -74
View File
@@ -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
+25 -21
View File
@@ -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()
+13 -7
View File
@@ -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")
+2 -3
View File
@@ -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"/>
+4 -3
View File
@@ -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>
+218 -14
View File
@@ -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')]}">
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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="['&amp;', ('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)
+3 -3
View File
@@ -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',
+1
View File
@@ -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>
+1 -1
View File
@@ -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>
+7 -7
View File
@@ -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
+2 -2
View File
@@ -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'])
+13 -5
View File
@@ -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':
+30 -4
View File
@@ -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">
+5 -6
View File
@@ -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"/>
+62 -38
View File
@@ -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>
+1 -1
View File
@@ -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});
+2
View File
@@ -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]
+6 -4
View File
@@ -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,
+18 -3
View File
@@ -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();
});
+8 -1
View File
@@ -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',
+1
View File
@@ -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.")
+5 -3
View File
@@ -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
+2 -1
View File
@@ -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>
+1 -1
View File
@@ -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 &gt; 0) and (qty_done&gt;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'">
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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)
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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):
+13
View File
@@ -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>
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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>
+5 -1
View File
@@ -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):
+2 -2
View File
@@ -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"/>
+1
View File
@@ -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
+1
View File
@@ -443,6 +443,7 @@
optional="hide"
groups="analytic.group_analytic_tags"
widget="many2many_tags"
options="{'color_field': 'color'}"
/>
<field
name="product_uom_qty"
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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")
+21 -3
View File
@@ -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,
})
+2
View File
@@ -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>
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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()
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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',
},
[
{
+1 -3
View File
@@ -378,7 +378,7 @@
<separator/>
<filter name="late" string="Late" domain="[('scheduled_date','&lt;', 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"/>
+1
View File
@@ -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">
+1 -1
View File
@@ -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>',
+5 -5
View File
@@ -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"/>
+1 -2
View File
@@ -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():
+10
View File
@@ -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 -1
View File
@@ -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);
+1 -1
View File
@@ -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">&lt;!DOCTYPE html&gt;
@@ -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);
+6 -1
View File
@@ -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)
+32 -33
View File
@@ -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>
+2 -1
View File
@@ -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
+1 -1
View File
@@ -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))
+48
View File
@@ -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">&amp;times;</button>
Following content will appear on all events
</div>
<div class="oe_structure" id="oe_structure_website_event_layout_1"/>
</div>
</t>
+4 -1
View File
@@ -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