From 3e576b368d72da6826a537c8e99c09c70ff9a819 Mon Sep 17 00:00:00 2001
From: Parthiv Patel
Date: Sun, 20 Jun 2021 11:03:19 +0000
Subject: [PATCH] [PATCH] Upstream patch
---
addons/account/models/account_move.py | 68 ++--
.../models/account_partial_reconcile.py | 47 +--
.../account/models/account_reconcile_model.py | 3 +-
addons/account/models/account_tax_report.py | 23 +-
.../account/report/account_invoice_report.py | 6 +-
addons/account/tests/common.py | 1 +
.../tests/test_account_invoice_report.py | 4 +-
.../tests/test_account_move_reconcile.py | 304 ++++++++++++++++++
addons/account/tests/test_tax_report.py | 28 ++
.../views/account_account_tag_views.xml | 6 +-
addons/account/views/account_move_views.xml | 6 +-
addons/account/views/account_payment_view.xml | 6 +-
.../wizard/account_payment_register.py | 2 +-
.../account_edi/models/account_edi_format.py | 3 +-
.../models/account_edi_format.py | 24 +-
addons/base_vat/models/res_partner.py | 9 +-
addons/crm/models/crm_lead.py | 75 ++---
addons/crm/tests/test_crm_lead.py | 56 ++++
addons/crm/views/crm_lead_views.xml | 6 +-
addons/digest/data/digest_data.xml | 10 +-
addons/event/views/event_views.xml | 12 +-
.../fleet/views/fleet_vehicle_cost_views.xml | 6 +-
addons/fleet/views/fleet_vehicle_views.xml | 6 +-
addons/hr/models/hr_employee_base.py | 2 +-
addons/hr/views/hr_employee_views.xml | 6 +-
.../hr_contract/views/hr_contract_views.xml | 6 +-
addons/hr_expense/models/hr_expense.py | 21 +-
addons/hr_expense/views/hr_expense_views.xml | 12 +-
.../wizard/account_payment_register.py | 9 +
.../views/hr_leave_allocation_views.xml | 6 +-
addons/hr_holidays/views/hr_leave_views.xml | 6 +-
addons/hr_recruitment/data/digest_data.xml | 2 +-
.../data/hr_recruitment_data.xml | 4 +-
.../views/hr_recruitment_views.xml | 6 +-
addons/hw_drivers/tools/helpers.py | 4 +-
.../l10n_it_edi/data/account_invoice_demo.xml | 10 +-
.../l10n_it_edi/data/invoice_it_template.xml | 4 +-
.../l10n_it_edi/models/account_edi_format.py | 166 ++++++++--
addons/l10n_it_edi/models/account_invoice.py | 90 +-----
addons/l10n_it_edi/models/ir_mail_server.py | 22 +-
.../l10n_it_edi/tests/test_ir_mail_server.py | 19 +-
addons/link_tracker/models/link_tracker.py | 7 +-
addons/mail/models/mail_activity.py | 22 ++
.../src/components/message/message_tests.js | 71 ++++
addons/mail/views/res_partner_views.xml | 6 +-
.../maintenance/views/maintenance_views.xml | 12 +-
.../data/mailing_data_templates.xml | 2 +-
addons/mrp/models/mrp_production.py | 8 +-
addons/mrp/models/stock_move.py | 4 +-
addons/mrp/views/mrp_bom_views.xml | 6 +-
addons/mrp/views/mrp_production_views.xml | 6 +-
addons/mrp/views/mrp_unbuild_views.xml | 6 +-
addons/mrp_account/models/product.py | 2 +-
addons/mrp_account/models/stock_move.py | 5 +
addons/note/views/note_views.xml | 6 +-
addons/payment_ingenico/controllers/main.py | 2 +-
addons/point_of_sale/controllers/main.py | 4 +-
addons/point_of_sale/models/pos_order.py | 2 +-
addons/point_of_sale/models/pos_session.py | 22 +-
addons/point_of_sale/models/stock_picking.py | 2 +-
addons/point_of_sale/static/src/css/pos.css | 9 +-
.../static/src/css/pos_receipts.css | 2 +-
addons/point_of_sale/static/src/js/Chrome.js | 2 +-
.../src/js/Misc/AbstractReceiptScreen.js | 6 +-
.../ClientListScreen/ClientDetailsEdit.js | 6 +-
.../ControlButtons/ReprintReceiptButton.js | 10 +-
.../OrderManagementScreen.js | 7 +-
.../ReprintReceiptScreen.js | 19 +-
.../js/Screens/PaymentScreen/PaymentScreen.js | 7 +-
.../js/Screens/ReceiptScreen/ReceiptScreen.js | 5 +-
.../static/src/js/custom_hooks.js | 8 +
addons/point_of_sale/static/src/js/models.js | 2 +-
.../OrderManagementScreen.xml | 5 +-
.../xml/Screens/ProductScreen/Orderline.xml | 2 +-
.../Screens/ReceiptScreen/OrderReceipt.xml | 4 +-
.../tests/test_point_of_sale_flow.py | 68 ++++
.../controllers/portal_rating.py | 2 +-
.../static/src/js/Screens/BillScreen.js | 3 +
.../static/src/js/Screens/TipScreen.js | 3 +-
addons/product/views/product_views.xml | 6 +-
addons/project/views/project_views.xml | 6 +-
addons/purchase/models/purchase.py | 10 +-
addons/purchase/tests/test_purchase.py | 33 ++
addons/purchase/views/purchase_views.xml | 6 +-
.../views/purchase_requisition_views.xml | 6 +-
addons/repair/views/repair_views.xml | 6 +-
addons/sale/models/account_move.py | 13 +-
addons/sale/models/sale.py | 19 +-
addons/sale/security/sale_security.xml | 12 +-
addons/sale/tests/test_sale_to_invoice.py | 28 ++
addons/sale/views/sale_views.xml | 6 +-
addons/sale_coupon/models/coupon_program.py | 5 +-
addons/sale_mrp/models/account_move.py | 4 +-
addons/sale_stock/models/stock.py | 15 +-
addons/sale_timesheet/models/project.py | 4 +-
.../sale_timesheet/models/project_overview.py | 13 +-
addons/stock/models/stock_move.py | 17 +-
addons/stock/models/stock_picking.py | 2 +-
addons/stock/static/img/replenishment.svg | 2 +-
addons/stock/views/stock_move_views.xml | 6 +
addons/stock/views/stock_picking_views.xml | 6 +-
addons/stock_account/models/product.py | 2 +-
addons/stock_account/models/stock_move.py | 3 +
.../stock_valuation_layer_revaluation.py | 6 +-
...tock_valuation_layer_revaluation_views.xml | 2 +-
addons/stock_landed_costs/models/product.py | 11 +
.../views/stock_landed_cost_views.xml | 6 +-
.../views/stock_picking_batch_views.xml | 6 +-
addons/survey/controllers/main.py | 2 +-
addons/survey/tests/__init__.py | 1 +
addons/survey/tests/common.py | 12 +
.../tests/test_survey_flow_with_conditions.py | 126 ++++++++
addons/survey/views/survey_survey_views.xml | 6 +-
addons/test_mail/tests/test_mail_activity.py | 28 ++
addons/uom/views/uom_uom_views.xml | 8 +-
addons/web/models/ir_http.py | 6 +
addons/web/static/lib/owl/owl.js | 53 ++-
addons/web/static/src/img/logo.png | Bin 14890 -> 10503 bytes
addons/web/static/src/js/core/time.js | 4 +-
.../web/static/src/js/fields/basic_fields.js | 4 +-
.../web/static/src/js/fields/field_utils.js | 2 +-
.../src/js/views/calendar/calendar_model.js | 4 +-
.../src/js/views/calendar/calendar_popover.js | 5 +-
.../src/js/views/graph/graph_renderer.js | 2 +-
.../src/js/views/kanban/kanban_record.js | 2 +-
.../static/src/js/views/pivot/pivot_model.js | 11 +-
addons/web/static/src/xml/base.xml | 2 +-
addons/web/static/tests/core/time_tests.js | 15 +
.../web/static/tests/views/calendar_tests.js | 51 +++
.../static/shapes/Blocks/01_001.svg | 26 ++
.../static/shapes/Blocks/02_001.svg | 25 ++
.../static/src/scss/web_editor.frontend.scss | 2 +-
.../static/src/scss/web_editor.variables.scss | 2 +
addons/web_editor/views/snippets.xml | 4 +-
addons/web_tour/static/src/js/tour_service.js | 2 +-
addons/website/models/website.py | 10 +-
.../src/js/backend/res_config_settings.js | 4 +-
addons/website/static/src/js/content/menu.js | 2 +-
addons/website/static/src/js/menu/edit.js | 9 +-
.../src/snippets/s_image_gallery/options.js | 2 +-
.../static/src/snippets/s_timeline/000.scss | 1 +
addons/website/views/snippets/s_popup.xml | 1 +
.../static/src/scss/website_blog.scss | 8 +-
.../views/event_track_views.xml | 6 +-
.../static/src/scss/website_forum.scss | 14 +-
addons/website_membership/controllers/main.py | 5 +-
.../static/src/js/website_sale_backend.js | 2 +-
addons/website_slides/controllers/mail.py | 2 +-
addons/website_slides/controllers/main.py | 2 +-
addons/website_slides/data/mail_data.xml | 4 +-
addons/website_slides/models/slide_slide.py | 2 +-
.../static/src/img/banner_default.svg | 239 +-------------
.../static/src/img/banner_default_all.svg | 113 +------
.../views/slide_slide_views.xml | 7 +-
.../views/website_slides_templates_course.xml | 4 +-
155 files changed, 1673 insertions(+), 901 deletions(-)
create mode 100644 addons/survey/tests/test_survey_flow_with_conditions.py
create mode 100644 addons/web_editor/static/shapes/Blocks/01_001.svg
create mode 100644 addons/web_editor/static/shapes/Blocks/02_001.svg
diff --git a/addons/account/models/account_move.py b/addons/account/models/account_move.py
index 9f6c87c2ca..e0f2c0f5b3 100644
--- a/addons/account/models/account_move.py
+++ b/addons/account/models/account_move.py
@@ -367,9 +367,12 @@ class AccountMove(models.Model):
if self.invoice_date:
if not self.invoice_payment_term_id and (not self.invoice_date_due or self.invoice_date_due < self.invoice_date):
self.invoice_date_due = self.invoice_date
- if self.date != self.invoice_date: # Don't flag date as dirty if not needed
+ if (
+ self.is_sale_document() and self.date != self.invoice_date
+ or self.is_purchase_document() and not self.date
+ ):
self.date = self.invoice_date
- self._onchange_currency()
+ self._onchange_currency()
@api.onchange('journal_id')
def _onchange_journal(self):
@@ -795,7 +798,7 @@ class AccountMove(models.Model):
'amount_currency': diff_amount_currency,
'partner_id': self.partner_id.id,
'move_id': self.id,
- 'currency_id': self.currency_id,
+ 'currency_id': self.currency_id.id,
'company_id': self.company_id.id,
'company_currency_id': self.company_id.currency_id.id,
'is_rounding_line': True,
@@ -2338,13 +2341,17 @@ class AccountMove(models.Model):
if cancel:
reverse_moves.with_context(move_reverse_cancel=cancel)._post(soft=False)
for move, reverse_move in zip(self, reverse_moves):
- accounts = move.mapped('line_ids.account_id') \
- .filtered(lambda account: account.reconcile or account.internal_type == 'liquidity')
- for account in accounts:
- (move.line_ids + reverse_move.line_ids)\
- .filtered(lambda line: line.account_id == account and not line.reconciled)\
- .with_context(move_reverse_cancel=cancel)\
- .reconcile()
+ lines = move.line_ids.filtered(
+ lambda x: (x.account_id.reconcile or x.account_id.internal_type == 'liquidity')
+ and not x.reconciled
+ )
+ for line in lines:
+ counterpart_lines = reverse_move.line_ids.filtered(
+ lambda x: x.account_id == line.account_id
+ and x.currency_id == line.currency_id
+ and not x.reconciled
+ )
+ (line + counterpart_lines).with_context(move_reverse_cancel=cancel).reconcile()
return reverse_moves
@@ -3540,6 +3547,8 @@ class AccountMoveLine(models.Model):
@api.onchange('product_uom_id')
def _onchange_uom_id(self):
''' Recompute the 'price_unit' depending of the unit of measure. '''
+ if self.display_type in ('line_section', 'line_note'):
+ return
taxes = self._get_computed_taxes()
if taxes and self.move_id.fiscal_position_id:
taxes = self.move_id.fiscal_position_id.map_tax(taxes, partner=self.partner_id)
@@ -4235,12 +4244,16 @@ class AccountMoveLine(models.Model):
if debit_line_currency == credit_line_currency:
# Reconcile on the same currency.
- # The debit line is now fully reconciled.
+ # The debit line is now fully reconciled because:
+ # - either amount_residual & amount_residual_currency are at 0.
+ # - either the credit_line is not an exchange difference one.
if not has_debit_residual_curr_left and (has_credit_residual_curr_left or not has_debit_residual_left):
debit_line = None
continue
- # The credit line is now fully reconciled.
+ # The credit line is now fully reconciled because:
+ # - either amount_residual & amount_residual_currency are at 0.
+ # - either the debit is not an exchange difference one.
if not has_credit_residual_curr_left and (has_debit_residual_curr_left or not has_credit_residual_left):
credit_line = None
continue
@@ -4252,13 +4265,13 @@ class AccountMoveLine(models.Model):
else:
# Reconcile on the company's currency.
- # The debit line is now fully reconciled.
- if not has_debit_residual_left and (has_credit_residual_left or not has_debit_residual_curr_left):
+ # The debit line is now fully reconciled since amount_residual is 0.
+ if not has_debit_residual_left:
debit_line = None
continue
- # The credit line is now fully reconciled.
- if not has_credit_residual_left and (has_debit_residual_left or not has_credit_residual_curr_left):
+ # The credit line is now fully reconciled since amount_residual is 0.
+ if not has_credit_residual_left:
credit_line = None
continue
@@ -4418,12 +4431,23 @@ class AccountMoveLine(models.Model):
if line.tax_repartition_line_id:
# Tax line.
grouping_key = self.env['account.partial.reconcile']._get_cash_basis_tax_line_grouping_key_from_record(line)
- account_vals_to_fix[grouping_key] = {
- **vals,
- 'account_id': line.account_id.id,
- 'tax_base_amount': line.tax_base_amount,
- 'tax_repartition_line_id': line.tax_repartition_line_id.id,
- }
+ if grouping_key in account_vals_to_fix:
+ debit = account_vals_to_fix[grouping_key]['debit'] + vals['debit']
+ credit = account_vals_to_fix[grouping_key]['credit'] + vals['credit']
+ balance = debit - credit
+
+ account_vals_to_fix[grouping_key].update({
+ 'debit': balance if balance > 0 else 0,
+ 'credit': -balance if balance < 0 else 0,
+ 'tax_base_amount': account_vals_to_fix[grouping_key]['tax_base_amount'] + line.tax_base_amount,
+ })
+ else:
+ account_vals_to_fix[grouping_key] = {
+ **vals,
+ 'account_id': line.account_id.id,
+ 'tax_base_amount': line.tax_base_amount,
+ 'tax_repartition_line_id': line.tax_repartition_line_id.id,
+ }
elif line.tax_ids:
# Base line.
account_to_fix = line.company_id.account_cash_basis_base_account_id
diff --git a/addons/account/models/account_partial_reconcile.py b/addons/account/models/account_partial_reconcile.py
index 087d3d7509..c5659cd081 100644
--- a/addons/account/models/account_partial_reconcile.py
+++ b/addons/account/models/account_partial_reconcile.py
@@ -522,33 +522,40 @@ class AccountPartialReconcile(models.Model):
if line.tax_repartition_line_id:
# Tax line.
- cb_tax_line_vals = self._prepare_cash_basis_tax_line_vals(line, balance, amount_currency)
- grouping_key = self._get_cash_basis_tax_line_grouping_key_from_vals(cb_tax_line_vals)
- partial_lines_to_create[grouping_key] = {
- 'tax_line': line,
- 'vals': cb_tax_line_vals,
- }
-
+ cb_line_vals = self._prepare_cash_basis_tax_line_vals(line, balance, amount_currency)
+ grouping_key = self._get_cash_basis_tax_line_grouping_key_from_vals(cb_line_vals)
elif line.tax_ids:
# Base line.
- cb_base_line_vals = self._prepare_cash_basis_base_line_vals(line, balance, amount_currency)
- grouping_key = self._get_cash_basis_base_line_grouping_key_from_vals(cb_base_line_vals)
+ cb_line_vals = self._prepare_cash_basis_base_line_vals(line, balance, amount_currency)
+ grouping_key = self._get_cash_basis_base_line_grouping_key_from_vals(cb_line_vals)
- if grouping_key in partial_lines_to_create:
- aggregated_vals = partial_lines_to_create[grouping_key]['vals']
- balance = aggregated_vals['debit'] - aggregated_vals['credit']
- balance += cb_base_line_vals['debit'] - cb_base_line_vals['credit']
+ if grouping_key in partial_lines_to_create:
+ aggregated_vals = partial_lines_to_create[grouping_key]['vals']
+ debit = aggregated_vals['debit'] + cb_line_vals['debit']
+ credit = aggregated_vals['credit'] + cb_line_vals['credit']
+ balance = debit - credit
+
+ aggregated_vals.update({
+ 'debit': balance if balance > 0 else 0,
+ 'credit': -balance if balance < 0 else 0,
+ 'amount_currency': aggregated_vals['amount_currency'] + cb_line_vals['amount_currency'],
+ })
+
+ if line.tax_repartition_line_id:
aggregated_vals.update({
- 'debit': balance if balance > 0.0 else 0.0,
- 'credit': -balance if balance < 0.0 else 0.0,
+ 'tax_base_amount': aggregated_vals['tax_base_amount'] + cb_line_vals['tax_base_amount'],
+ })
+ partial_lines_to_create[grouping_key]['tax_line'] += line
+ else:
+ partial_lines_to_create[grouping_key] = {
+ 'vals': cb_line_vals,
+ }
+ if line.tax_repartition_line_id:
+ partial_lines_to_create[grouping_key].update({
+ 'tax_line': line,
})
- aggregated_vals['amount_currency'] += cb_base_line_vals['amount_currency']
- else:
- partial_lines_to_create[grouping_key] = {
- 'vals': cb_base_line_vals,
- }
# ==========================================================================
# Create the counterpart journal items.
diff --git a/addons/account/models/account_reconcile_model.py b/addons/account/models/account_reconcile_model.py
index 6d5069e95c..dad9a631e2 100644
--- a/addons/account/models/account_reconcile_model.py
+++ b/addons/account/models/account_reconcile_model.py
@@ -884,7 +884,8 @@ class AccountReconcileModel(models.Model):
# Statement line amount is equal to the total residual.
if line_currency.is_zero(line_residual_after_reconciliation):
return True
- reconciled_percentage = 100 - abs(line_residual_after_reconciliation) / abs(line_residual - line_residual_after_reconciliation) * 100
+ residual_difference = line_residual - line_residual_after_reconciliation
+ reconciled_percentage = 100 - abs(line_residual_after_reconciliation) / abs(residual_difference) * 100 if (residual_difference != 0) else 0
return reconciled_percentage >= self.match_total_amount_param
def _filter_candidates(self, candidates, aml_ids_to_exclude, reconciled_amls_ids):
diff --git a/addons/account/models/account_tax_report.py b/addons/account/models/account_tax_report.py
index a8ffef1601..fb4f565c2b 100644
--- a/addons/account/models/account_tax_report.py
+++ b/addons/account/models/account_tax_report.py
@@ -31,12 +31,11 @@ class AccountTaxReport(models.Model):
new_tags = tags_cache[cache_key]
if new_tags:
- tags_to_unlink = line.tag_ids.filtered(lambda x: record == x.mapped('tax_report_line_ids.report_id'))
- # == instead of in, as we only want tags_to_unlink to contain the tags that are not linked to any other report than the one we're considering
+ line._remove_tags_used_only_by_self()
line.write({'tag_ids': [(6, 0, new_tags.ids)]})
- self.env['account.tax.report.line']._delete_tags_from_taxes(tags_to_unlink.ids)
elif line.mapped('tag_ids.tax_report_line_ids.report_id').filtered(lambda x: x not in self):
+ line._remove_tags_used_only_by_self()
line.write({'tag_ids': [(5, 0, 0)] + line._get_tags_create_vals(line.tag_name, vals['country_id'])})
tags_cache[cache_key] = line.tag_ids
@@ -203,7 +202,7 @@ class AccountTaxReportLine(models.Model):
# All the lines sharing their tags must always be synchronized,
tags_to_remove += records_to_link.mapped('tag_ids')
records_to_link = tags_to_remove.mapped('tax_report_line_ids')
- self._delete_tags_from_taxes(tags_to_remove.ids)
+ tags_to_remove.mapped('tax_report_line_ids')._remove_tags_used_only_by_self()
records_to_link.write({'tag_name': tag_name_postponed, 'tag_ids': [(2, tag.id) for tag in tags_to_remove] + [(6, 0, existing_tags.ids)]})
else:
@@ -220,17 +219,27 @@ class AccountTaxReportLine(models.Model):
return rslt
def unlink(self):
- self._delete_tags_from_taxes(self.mapped('tag_ids.id'))
+ self._remove_tags_used_only_by_self()
children = self.mapped('children_line_ids')
if children:
children.unlink()
return super(AccountTaxReportLine, self).unlink()
+ def _remove_tags_used_only_by_self(self):
+ """ Deletes and removes from taxes and move lines all the
+ tags from the provided tax report lines that are not linked
+ to any other tax report lines.
+ """
+ all_tags = self.mapped('tag_ids')
+ tags_to_unlink = all_tags.filtered(lambda x: not (x.tax_report_line_ids - self))
+ self.write({'tag_ids': [(3, tag.id, 0) for tag in tags_to_unlink]})
+ self._delete_tags_from_taxes(tags_to_unlink.ids)
+
@api.model
def _delete_tags_from_taxes(self, tag_ids_to_delete):
""" Based on a list of tag ids, removes them first from the
repartition lines they are linked to, then deletes them
- from the account move lines.
+ from the account move lines, and finally unlink them.
"""
if not tag_ids_to_delete:
# Nothing to do, then!
@@ -247,6 +256,8 @@ class AccountTaxReportLine(models.Model):
self.env['account.move.line'].invalidate_cache(fnames=['tax_tag_ids'])
self.env['account.tax.repartition.line'].invalidate_cache(fnames=['tag_ids'])
+ self.env['account.account.tag'].browse(tag_ids_to_delete).unlink()
+
@api.constrains('formula', 'tag_name')
def _validate_formula(self):
for record in self:
diff --git a/addons/account/report/account_invoice_report.py b/addons/account/report/account_invoice_report.py
index 0515d3d894..2ed4dade3b 100644
--- a/addons/account/report/account_invoice_report.py
+++ b/addons/account/report/account_invoice_report.py
@@ -98,7 +98,11 @@ class AccountInvoiceReport(models.Model):
line.quantity / NULLIF(COALESCE(uom_line.factor, 1) / COALESCE(uom_template.factor, 1), 0.0) * (CASE WHEN move.move_type IN ('in_invoice','out_refund','in_receipt') THEN -1 ELSE 1 END)
AS quantity,
-line.balance * currency_table.rate AS price_subtotal,
- -line.balance / NULLIF(COALESCE(uom_line.factor, 1) / COALESCE(uom_template.factor, 1), 0.0) * currency_table.rate
+ -COALESCE(line.balance
+ / NULLIF(line.quantity, 0.0)
+ / NULLIF(COALESCE(uom_line.factor, 1), 0.0)
+ / NULLIF(COALESCE(uom_template.factor, 1), 0.0),
+ 0.0) * currency_table.rate
AS price_average,
COALESCE(partner.country_id, commercial_partner.country_id) AS country_id
'''
diff --git a/addons/account/tests/common.py b/addons/account/tests/common.py
index 202173b65a..8e197996e6 100644
--- a/addons/account/tests/common.py
+++ b/addons/account/tests/common.py
@@ -364,6 +364,7 @@ class AccountTestInvoicingCommon(SavepointCase):
def init_invoice(cls, move_type, partner=None, invoice_date=None, post=False, products=[], amounts=[], taxes=None):
move_form = Form(cls.env['account.move'].with_context(default_move_type=move_type, account_predictive_bills_disable_prediction=True))
move_form.invoice_date = invoice_date or fields.Date.from_string('2019-01-01')
+ move_form.date = move_form.invoice_date
move_form.partner_id = partner or cls.partner_a
for product in products:
diff --git a/addons/account/tests/test_account_invoice_report.py b/addons/account/tests/test_account_invoice_report.py
index ba6e5a637b..f341db0208 100644
--- a/addons/account/tests/test_account_invoice_report.py
+++ b/addons/account/tests/test_account_invoice_report.py
@@ -21,7 +21,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
(0, None, {
'product_id': cls.product_a.id,
'quantity': 3,
- 'price_unit': 1000,
+ 'price_unit': 750,
}),
(0, None, {
'product_id': cls.product_a.id,
@@ -110,7 +110,7 @@ class TestAccountInvoiceReport(AccountTestInvoicingCommon):
#price_average price_subtotal quantity
[2000, 2000, 1],
[1000, 1000, 1],
- [1000, 1000, 3],
+ [250, 750, 3],
[6, 6, 1],
[-20, -20, -1],
[-20, -20, -1],
diff --git a/addons/account/tests/test_account_move_reconcile.py b/addons/account/tests/test_account_move_reconcile.py
index 5ad0c9a172..b2c82d01ee 100644
--- a/addons/account/tests/test_account_move_reconcile.py
+++ b/addons/account/tests/test_account_move_reconcile.py
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from flectra.addons.account.tests.common import AccountTestInvoicingCommon
from flectra.tests import tagged
+from flectra.tests.common import Form
+from flectra import fields
@tagged('post_install', '-at_install')
@@ -677,6 +679,232 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
self.assertFullReconcile(res['full_reconcile'], line_1 + line_2 + line_3 + line_4 + line_5)
+ def test_reconcile_asymetric_rate_change(self):
+ account_id = self.company_data['default_account_receivable'].id
+
+ # Rate is 3.0 in 2016, 2.0 in 2017.
+ currency1_id = self.currency_data['currency'].id
+ # Rate is 6.0 in 2016, 4.0 in 2017.
+ currency2_id = self.currency_data_2['currency'].id
+
+ # Create rate changes for 2018: currency1 rate increases while currency2 rate decreases.
+ self.env['res.currency.rate'].create({
+ 'name': '2018-01-01',
+ 'rate': 8.0,
+ 'currency_id': currency1_id,
+ 'company_id': self.company_data['company'].id,
+ })
+
+ self.env['res.currency.rate'].create({
+ 'name': '2018-01-01',
+ 'rate': 2.0,
+ 'currency_id': currency2_id,
+ 'company_id': self.company_data['company'].id,
+ })
+
+ moves = self.env['account.move'].create([
+ {
+ 'move_type': 'entry',
+ 'date': '2018-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 1200.0,
+ 'credit': 0.0,
+ 'amount_currency': 9600.0,
+ 'account_id': account_id,
+ 'currency_id': currency1_id,
+ }),
+ (0, 0, {
+ 'debit': 960.0,
+ 'credit': 0.0,
+ 'amount_currency': 1920.0,
+ 'account_id': account_id,
+ 'currency_id': currency2_id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 2160.0,
+ 'account_id': account_id,
+ }),
+ ]
+ },
+ {
+ 'move_type': 'entry',
+ 'date': '2017-01-01',
+ 'line_ids': [
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 1200.0,
+ 'amount_currency': -4800.0,
+ 'account_id': account_id,
+ 'currency_id': currency2_id,
+ }),
+ (0, 0, {
+ 'debit': 0.0,
+ 'credit': 960.0,
+ 'amount_currency': -1920.0,
+ 'account_id': account_id,
+ 'currency_id': currency1_id,
+ }),
+ (0, 0, {
+ 'debit': 2160.0,
+ 'credit': 0.0,
+ 'account_id': account_id,
+ }),
+ ]
+ }
+ ])
+
+ moves.action_post()
+
+ line_1 = moves.line_ids.filtered(lambda line: line.debit == 1200.0)
+ line_2 = moves.line_ids.filtered(lambda line: line.debit == 960.0)
+ line_3 = moves.line_ids.filtered(lambda line: line.credit == 1200.0)
+ line_4 = moves.line_ids.filtered(lambda line: line.credit == 960.0)
+
+ self.assertRecordValues(line_1 + line_2 + line_3 + line_4, [
+ {'amount_residual': 1200.0, 'amount_residual_currency': 9600.0, 'reconciled': False},
+ {'amount_residual': 960.0, 'amount_residual_currency': 1920.0, 'reconciled': False},
+ {'amount_residual': -1200.0, 'amount_residual_currency': -4800.0, 'reconciled': False},
+ {'amount_residual': -960.0, 'amount_residual_currency': -1920.0, 'reconciled': False},
+ ])
+
+ # Reconcile with debit_line currency rate increased and credit_line currency rate decreased between
+ # credit_line.date and debit_line.date.
+
+ res = (line_1 + line_3).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 2400.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -2400.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 7200.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -7200.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ }
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 2400.0,
+ 'credit_amount_currency': 2400.0,
+ 'debit_move_id': exchange_diff_lines[0].id,
+ 'credit_move_id': line_3.id,
+ },
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 7200.0,
+ 'credit_amount_currency': 7200.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': exchange_diff_lines[3].id,
+ },
+ {
+ 'amount': 1200.0,
+ 'debit_amount_currency': 2400.0,
+ 'credit_amount_currency': 2400.0,
+ 'debit_move_id': line_1.id,
+ 'credit_move_id': line_3.id,
+ },
+ ])
+
+ self.assertRecordValues(line_1 + line_3, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
+ # Reconcile with debit_line currency rate decreased and credit_line currency rate increased between
+ # credit_line.date and debit_line.date.
+
+ res = (line_2 + line_4).reconcile()
+
+ exchange_diff = res['full_reconcile'].exchange_move_id
+ exchange_diff_lines = exchange_diff.line_ids.sorted(lambda line: (line.currency_id, abs(line.amount_currency), -line.amount_currency))
+
+ self.assertRecordValues(exchange_diff_lines, [
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 5760.0,
+ 'currency_id': currency1_id,
+ 'account_id': exchange_diff.journal_id.company_id.expense_currency_exchange_account_id.id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -5760.0,
+ 'currency_id': currency1_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': 1920.0,
+ 'currency_id': currency2_id,
+ 'account_id': account_id,
+ },
+ {
+ 'debit': 0.0,
+ 'credit': 0.0,
+ 'amount_currency': -1920.0,
+ 'currency_id': currency2_id,
+ 'account_id': exchange_diff.journal_id.company_id.income_currency_exchange_account_id.id,
+ }
+ ])
+
+ self.assertPartialReconcile(res['partials'], [
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 1920.0,
+ 'credit_amount_currency': 1920.0,
+ 'debit_move_id': exchange_diff_lines[2].id,
+ 'credit_move_id': line_2.id,
+ },
+ {
+ 'amount': 0.0,
+ 'debit_amount_currency': 5760.0,
+ 'credit_amount_currency': 5760.0,
+ 'debit_move_id': line_4.id,
+ 'credit_move_id': exchange_diff_lines[1].id,
+ },
+ {
+ 'amount': 960.0,
+ 'debit_amount_currency': 3840.0,
+ 'credit_amount_currency': 7680.0,
+ 'debit_move_id': line_2.id,
+ 'credit_move_id': line_4.id,
+ },
+ ])
+
+ self.assertRecordValues(line_2 + line_4, [
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ {'amount_residual': 0.0, 'amount_residual_currency': 0.0, 'reconciled': True},
+ ])
+
def test_reverse_exchange_difference_same_foreign_currency(self):
move_2016 = self.env['account.move'].create({
'move_type': 'entry',
@@ -2076,6 +2304,82 @@ class TestAccountMoveReconcile(AccountTestInvoicingCommon):
# Check full reconciliation
self.assertTrue(all(line.full_reconcile_id for line in lines_to_reconcile), "All tax lines should be fully reconciled")
+ def test_caba_double_tax(self):
+ """ Test the CABA entries generated from an invoice with almost
+ equal lines, different only on analytic accounting
+ """
+ # Make the tax account reconcilable
+ self.tax_account_1.reconcile = True
+
+ # Create an invoice with a CABA tax using 'Include in analytic cost'
+ move_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice', account_predictive_bills_disable_prediction=True))
+ move_form.invoice_date = fields.Date.from_string('2019-01-01')
+ move_form.partner_id = self.partner_a
+ self.cash_basis_tax_a_third_amount.analytic = True
+ test_analytic_account = self.env['account.analytic.account'].create({'name': 'test_analytic_account'})
+
+ tax = self.cash_basis_tax_a_third_amount
+
+ # line with analytic account, will generate 2 lines in CABA move
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.name = "test line with analytic account"
+ line_form.product_id = self.product_a
+ line_form.tax_ids.clear()
+ line_form.tax_ids.add(tax)
+ line_form.analytic_account_id = test_analytic_account
+ line_form.price_unit = 100
+
+ # line with analytic account, will generate other 2 lines in CABA move
+ # even if the tax is the same
+ with move_form.invoice_line_ids.new() as line_form:
+ line_form.name = "test line"
+ line_form.product_id = self.product_a
+ line_form.tax_ids.clear()
+ line_form.tax_ids.add(tax)
+ line_form.price_unit = 100
+
+ rslt = move_form.save()
+ rslt.action_post()
+
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=rslt.ids).create({
+ 'amount': rslt.amount_total,
+ 'payment_date': rslt.date,
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ })
+ pmt_wizard._create_payments()
+
+ partial_rec = rslt.mapped('line_ids.matched_debit_ids')
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
+ self.assertEqual(len(caba_move.line_ids), 4, "All lines should be there")
+ self.assertEqual(caba_move.line_ids.filtered(lambda x: x.tax_line_id).balance, 66.66, "Tax amount should take into account both lines")
+
+ def test_caba_double_tax_negative_line(self):
+ """ Tests making a cash basis invoice with 2 lines using the same tax: a positive and a negative one.
+ """
+ invoice = self.init_invoice('in_invoice', amounts=[300, -60], post=True, taxes=self.cash_basis_tax_a_third_amount)
+
+ pmt_wizard = self.env['account.payment.register'].with_context(active_model='account.move', active_ids=invoice.ids).create({
+ 'amount': 320,
+ 'payment_date': invoice.date,
+ 'journal_id': self.company_data['default_journal_bank'].id,
+ 'payment_method_id': self.env.ref('account.account_payment_method_manual_in').id,
+ })
+
+ pmt_wizard._create_payments()
+
+ partial_rec = invoice.mapped('line_ids.matched_debit_ids')
+ caba_move = self.env['account.move'].search([('tax_cash_basis_rec_id', '=', partial_rec.id)])
+
+ self.assertRecordValues(caba_move.line_ids.sorted(lambda line: (-abs(line.balance), -line.debit, line.account_id)), [
+ # Base amount:
+ {'debit': 240.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[0].ids, 'account_id': self.cash_basis_base_account.id},
+ {'debit': 0.0, 'credit': 240.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_base_account.id},
+ # tax:
+ {'debit': 80.0, 'credit': 0.0, 'tax_tag_ids': self.tax_tags[1].ids, 'account_id': self.tax_account_1.id},
+ {'debit': 0.0, 'credit': 80.0, 'tax_tag_ids': [], 'account_id': self.cash_basis_transfer_account.id},
+ ])
+
def test_caba_dest_acc_reconciliation_partial_pmt(self):
""" Test the reconciliation of tax lines (when using a reconcilable tax account)
for partially paid invoices with cash basis taxes.
diff --git a/addons/account/tests/test_tax_report.py b/addons/account/tests/test_tax_report.py
index dd696aedf8..0fb9011ab9 100644
--- a/addons/account/tests/test_tax_report.py
+++ b/addons/account/tests/test_tax_report.py
@@ -68,6 +68,13 @@ class TaxReportTest(AccountTestInvoicingCommon):
'sequence': 7,
})
+ cls.tax_report_line_1_6 = cls.env['account.tax.report.line'].create({
+ 'name': "[100] Line 100",
+ 'tag_name': '100',
+ 'report_id': cls.tax_report_1.id,
+ 'sequence': 8,
+ })
+
cls.tax_report_2 = cls.env['account.tax.report'].create({
'name': "Tax report 2",
'country_id': cls.test_country_1.id,
@@ -93,6 +100,13 @@ class TaxReportTest(AccountTestInvoicingCommon):
'sequence': 3,
})
+ cls.tax_report_line_2_6 = cls.env['account.tax.report.line'].create({
+ 'name': "[100] Line 100",
+ 'tag_name': '100',
+ 'report_id': cls.tax_report_2.id,
+ 'sequence': 4,
+ })
+
def _get_tax_tags(self, tag_name=None):
domain = [('country_id', '=', self.test_country_1.id), ('applicability', '=', 'taxes')]
if tag_name:
@@ -255,3 +269,17 @@ class TaxReportTest(AccountTestInvoicingCommon):
self.assertEqual(line.tag_ids.ids, original_report_2_tags[line.id], "The tax report lines not sharing their tags with any other report should keep the same tags when the country of their report is changed")
elif line.tag_ids or original_report_2_tags[line.id]:
self.assertNotEqual(line.tag_ids.ids, original_report_2_tags[line.id], "The tax report lines sharing their tags with other report should receive new tags when the country of their report is changed")
+
+ def test_unlink_report_line_tags(self):
+ """ Under certain circumstances, unlinking a tax report line should also unlink
+ the tags that are linked to it. We test those cases here.
+ """
+ def check_tags_unlink(tag_name, report_lines, unlinked, error_message):
+ report_lines.unlink()
+ surviving_tags = self._get_tax_tags(tag_name)
+ required_len = 0 if unlinked else 2 # 2 for + and - tag
+ self.assertEqual(len(surviving_tags), required_len, error_message)
+
+ check_tags_unlink('42', self.tax_report_line_2_42, True, "Unlinking one line not sharing its tags should also unlink them")
+ check_tags_unlink('01', self.tax_report_line_1_1, False, "Unlinking one line sharing its tags with others should keep the tags")
+ check_tags_unlink('100', self.tax_report_line_1_6 + self.tax_report_line_2_6, True, "Unlinkink all the lines sharing the same tags should also unlink them")
diff --git a/addons/account/views/account_account_tag_views.xml b/addons/account/views/account_account_tag_views.xml
index d1bc04041a..a34f99e134 100644
--- a/addons/account/views/account_account_tag_views.xml
+++ b/addons/account/views/account_account_tag_views.xml
@@ -13,9 +13,9 @@
-
-
-
+
+
+
diff --git a/addons/account/views/account_move_views.xml b/addons/account/views/account_move_views.xml
index 7c55018a94..65a7176ee1 100644
--- a/addons/account/views/account_move_views.xml
+++ b/addons/account/views/account_move_views.xml
@@ -1186,12 +1186,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/account/views/account_payment_view.xml b/addons/account/views/account_payment_view.xml
index d5e688f8f3..3ae67ec727 100644
--- a/addons/account/views/account_payment_view.xml
+++ b/addons/account/views/account_payment_view.xml
@@ -118,12 +118,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/account/wizard/account_payment_register.py b/addons/account/wizard/account_payment_register.py
index dd1290b3aa..08f815d01a 100644
--- a/addons/account/wizard/account_payment_register.py
+++ b/addons/account/wizard/account_payment_register.py
@@ -128,7 +128,7 @@ class AccountPaymentRegister(models.TransientModel):
'partner_id': line.partner_id.id,
'account_id': line.account_id.id,
'currency_id': (line.currency_id or line.company_currency_id).id,
- 'partner_bank_id': line.move_id.partner_bank_id.id,
+ 'partner_bank_id': (line.move_id.partner_bank_id or line.partner_id.commercial_partner_id.bank_ids[:1]).id,
'partner_type': 'customer' if line.account_internal_type == 'receivable' else 'supplier',
'payment_type': 'inbound' if line.balance > 0.0 else 'outbound',
}
diff --git a/addons/account_edi/models/account_edi_format.py b/addons/account_edi/models/account_edi_format.py
index f03c17aecb..223aabb0fc 100644
--- a/addons/account_edi/models/account_edi_format.py
+++ b/addons/account_edi/models/account_edi_format.py
@@ -5,6 +5,7 @@ from flectra import models, fields, api
from flectra.exceptions import UserError
from flectra.tools.pdf import FlectraPdfFileReader, FlectraPdfFileWriter
from flectra.osv import expression
+from flectra.tools import html_escape
from lxml import etree
import base64
@@ -499,5 +500,5 @@ class AccountEdiFormat(models.Model):
@api.model
def _format_error_message(self, error_title, errors):
- bullet_list_msg = ''.join('
%s
' % msg for msg in errors)
+ bullet_list_msg = ''.join('
%s
' % html_escape(msg) for msg in errors)
return '%s
%s
' % (error_title, bullet_list_msg)
diff --git a/addons/account_edi_facturx/models/account_edi_format.py b/addons/account_edi_facturx/models/account_edi_format.py
index 4693a2dc02..741861bfea 100644
--- a/addons/account_edi_facturx/models/account_edi_format.py
+++ b/addons/account_edi_facturx/models/account_edi_format.py
@@ -192,17 +192,19 @@ class AccountEdiFormat(models.Model):
account_predictive_bills_disable_prediction=True)) as invoice_form:
# Partner (first step to avoid warning 'Warning! You must first select a partner.').
partner_type = invoice_form.journal_id.type == 'purchase' and 'SellerTradeParty' or 'BuyerTradeParty'
- elements = tree.xpath('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', namespaces=tree.nsmap)
- partner = elements and self.env['res.partner'].search([('vat', '=', elements[0].text)], limit=1)
- if not partner:
- elements = tree.xpath('//ram:' + partner_type + '/ram:Name', namespaces=tree.nsmap)
- partner_name = elements and elements[0].text
- partner = elements and self.env['res.partner'].search([('name', 'ilike', partner_name)], limit=1)
- if not partner:
- elements = tree.xpath('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', namespaces=tree.nsmap)
- partner = elements and self.env['res.partner'].search([('email', '=', elements[0].text)], limit=1)
- if partner:
- invoice_form.partner_id = partner
+ invoice_form.partner_id = self._retrieve_partner(
+ name=self._find_value('//ram:' + partner_type + '/ram:Name', tree, namespaces=tree.nsmap),
+ mail=self._find_value('//ram:' + partner_type + '//ram:URIID[@schemeID=\'SMTP\']', tree, namespaces=tree.nsmap),
+ vat=self._find_value('//ram:' + partner_type + '/ram:SpecifiedTaxRegistration/ram:ID', tree, namespaces=tree.nsmap),
+ )
+
+ # Delivery partner
+ if 'partner_shipping_id' in invoice._fields:
+ invoice_form.partner_shipping_id = self._retrieve_partner(
+ name=self._find_value('//ram:ShipToTradeParty/ram:Name', tree, namespaces=tree.nsmap),
+ mail=self._find_value('//ram:ShipToTradeParty//ram:URIID[@schemeID=\'SMTP\']', tree, namespaces=tree.nsmap),
+ vat=self._find_value('//ram:ShipToTradeParty/ram:SpecifiedTaxRegistration/ram:ID', tree, namespaces=tree.nsmap),
+ )
# Reference.
elements = tree.xpath('//rsm:ExchangedDocument/ram:ID', namespaces=tree.nsmap)
diff --git a/addons/base_vat/models/res_partner.py b/addons/base_vat/models/res_partner.py
index debca5773c..981cb01ca3 100644
--- a/addons/base_vat/models/res_partner.py
+++ b/addons/base_vat/models/res_partner.py
@@ -312,7 +312,11 @@ class ResPartner(models.Model):
vat = clean(vat, ' -.').upper().strip()
- if not (len(vat) == 14):
+ # Remove the prefix
+ if vat.startswith("NL"):
+ vat = vat[2:]
+
+ if not len(vat) == 12:
return False
# Check the format
@@ -325,9 +329,6 @@ class ResPartner(models.Model):
char_to_int['+'] = '36'
char_to_int['*'] = '37'
- # Remove the prefix
- vat = vat[2:]
-
# 2 possible checks:
# - For natural persons
# - For non-natural persons and combinations of natural persons (company)
diff --git a/addons/crm/models/crm_lead.py b/addons/crm/models/crm_lead.py
index 8cce315d31..86637a2c93 100644
--- a/addons/crm/models/crm_lead.py
+++ b/addons/crm/models/crm_lead.py
@@ -348,40 +348,24 @@ class Lead(models.Model):
@api.depends('partner_id.email')
def _compute_email_from(self):
for lead in self:
- if lead.partner_id.email and lead.partner_id.email != lead.email_from:
+ if lead.partner_id.email and lead._get_partner_email_update():
lead.email_from = lead.partner_id.email
def _inverse_email_from(self):
for lead in self:
- if lead.partner_id and lead.email_from != lead.partner_id.email:
- # force reset
- if not lead.email_from or not lead.partner_id.email:
- lead.partner_id.email = lead.email_from
- # compare formatted values as we may have formatting differences between equivalent email
- else:
- lead_email_normalized = tools.email_normalize(lead.email_from)
- partner_email_normalized = tools.email_normalize(lead.partner_id.email)
- if lead_email_normalized != partner_email_normalized:
- lead.partner_id.email = lead.email_from
+ if lead._get_partner_email_update():
+ lead.partner_id.email = lead.email_from
@api.depends('partner_id.phone')
def _compute_phone(self):
for lead in self:
- if lead.partner_id.phone and lead.phone != lead.partner_id.phone:
+ if lead.partner_id.phone and lead._get_partner_phone_update():
lead.phone = lead.partner_id.phone
def _inverse_phone(self):
for lead in self:
- if lead.partner_id and lead.phone != lead.partner_id.phone:
- # force reset
- if not lead.phone or not lead.partner_id.phone:
- lead.partner_id.phone = lead.phone
- # compare formatted values as we may have encoding differences between equivalent numbers
- else:
- lead_phone_formatted = lead.phone_format(lead.phone)
- partner_phone_formatted = lead.phone_format(lead.partner_id.phone)
- if lead_phone_formatted != partner_phone_formatted:
- lead.partner_id.phone = lead.phone
+ if lead._get_partner_phone_update():
+ lead.partner_id.phone = lead.phone
@api.depends('phone', 'country_id.code')
def _compute_phone_state(self):
@@ -454,21 +438,8 @@ class Lead(models.Model):
@api.depends('email_from', 'phone', 'partner_id')
def _compute_ribbon_message(self):
for lead in self:
- # beware: void user input gives '' which is different from False
- lead_email_normalized = tools.email_normalize(lead.email_from) or (lead.email_from if lead.email_from else False)
- partner_email_normalized = tools.email_normalize(lead.partner_id.email) or lead.partner_id.email
- will_write_email = lead_email_normalized != partner_email_normalized if lead.partner_id else False
- will_write_phone = False
- if lead.partner_id and lead.phone != lead.partner_id.phone:
- # if reset -> obviously new value will be propagated
- if not lead.phone or not lead.partner_id.phone:
- will_write_phone = True
- # otherwise compare formatted values as we may have encoding differences
- else:
- lead_phone_formatted = lead.phone_format(lead.phone)
- partner_phone_formatted = lead.phone_format(lead.partner_id.phone)
- if lead_phone_formatted != partner_phone_formatted:
- will_write_phone = True
+ will_write_email = lead._get_partner_email_update()
+ will_write_phone = lead._get_partner_phone_update()
if will_write_email and will_write_phone:
lead.ribbon_message = _('By saving this change, the customer email and phone number will also be updated.')
@@ -550,6 +521,36 @@ class Lead(models.Model):
partner_name = partner.name
return {'partner_name': partner_name or self.partner_name}
+ def _get_partner_email_update(self):
+ """Calculate if we should write the email on the related partner. When
+ the email of the lead / partner is an empty string, we force it to False
+ to not propagate a False on an empty string.
+
+ Done in a separate method so it can be used in both ribbon and inverse
+ and compute of email update methods.
+ """
+ self.ensure_one()
+ if self.partner_id and self.email_from != self.partner_id.email:
+ lead_email_normalized = tools.email_normalize(self.email_from) or self.email_from or False
+ partner_email_normalized = tools.email_normalize(self.partner_id.email) or self.partner_id.email or False
+ return lead_email_normalized != partner_email_normalized
+ return False
+
+ def _get_partner_phone_update(self):
+ """Calculate if we should write the phone on the related partner. When
+ the phone of the lead / partner is an empty string, we force it to False
+ to not propagate a False on an empty string.
+
+ Done in a separate method so it can be used in both ribbon and inverse
+ and compute of phone update methods.
+ """
+ self.ensure_one()
+ if self.partner_id and self.phone != self.partner_id.phone:
+ lead_phone_formatted = self.phone_format(self.phone) if self.phone else False or self.phone or False
+ partner_phone_formatted = self.phone_format(self.partner_id.phone) if self.partner_id.phone else False or self.partner_id.phone or False
+ return lead_phone_formatted != partner_phone_formatted
+ return False
+
# ------------------------------------------------------------
# ORM
# ------------------------------------------------------------
diff --git a/addons/crm/tests/test_crm_lead.py b/addons/crm/tests/test_crm_lead.py
index eb5a3de83f..1e8050a20f 100644
--- a/addons/crm/tests/test_crm_lead.py
+++ b/addons/crm/tests/test_crm_lead.py
@@ -302,6 +302,62 @@ class TestCRMLead(TestCrmCommon):
self.assertEqual(partner.phone_sanitized, partner_mobile_sanitized,
'Partner sanitized should be computed on mobile')
+ @users('user_sales_manager')
+ def test_crm_lead_partner_sync_email_phone_corner_cases(self):
+ """ Test corner cases of email and phone sync (False versus '', formatting
+ differences, wrong input, ...) """
+ test_email = 'amy.wong@test.example.com'
+ lead = self.lead_1.with_user(self.env.user)
+ contact = self.env['res.partner'].create({
+ 'name': 'NoContact Partner',
+ 'phone': '',
+ 'email': '',
+ 'mobile': '',
+ })
+
+ lead_form = Form(lead)
+ self.assertEqual(lead_form.email_from, test_email)
+ self.assertFalse(lead_form.ribbon_message)
+
+ # email: False versus empty string
+ lead_form.partner_id = contact
+ self.assertIn('the customer email', lead_form.ribbon_message)
+ lead_form.email_from = ''
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.email_from = False
+ self.assertFalse(lead_form.ribbon_message)
+
+ # phone: False versus empty string
+ lead_form.phone = '+1 202-555-0888'
+ self.assertIn('the customer phone', lead_form.ribbon_message)
+ lead_form.phone = ''
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.phone = False
+ self.assertFalse(lead_form.ribbon_message)
+
+ # email/phone: formatting should not trigger ribbon
+ lead.write({
+ 'email_from': '"My Name" <%s>' % test_email,
+ 'phone': '+1 202-555-0888',
+ })
+ contact.write({
+ 'email': '"My Name" <%s>' % test_email,
+ 'phone': '+1 202-555-0888',
+ })
+
+ lead_form = Form(lead)
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.partner_id = contact
+ self.assertFalse(lead_form.ribbon_message)
+ lead_form.email_from = '"Another Name" <%s>' % test_email # same email normalized
+ self.assertFalse(lead_form.ribbon_message, 'Formatting-only change should not trigger write')
+ lead_form.phone = '2025550888' # same number but another format
+ self.assertFalse(lead_form.ribbon_message, 'Formatting-only change should not trigger write')
+
+ # wrong value are also propagated
+ lead_form.phone = '666 789456789456789456'
+ self.assertIn('the customer phone', lead_form.ribbon_message)
+
@users('user_sales_manager')
def test_crm_lead_stages(self):
lead = self.lead_1.with_user(self.env.user)
diff --git a/addons/crm/views/crm_lead_views.xml b/addons/crm/views/crm_lead_views.xml
index c415ff4220..c17c37d765 100644
--- a/addons/crm/views/crm_lead_views.xml
+++ b/addons/crm/views/crm_lead_views.xml
@@ -734,16 +734,16 @@
to 0. but this approach is not working. the work around is temporary -->
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
@@ -705,12 +705,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/fleet/views/fleet_vehicle_cost_views.xml b/addons/fleet/views/fleet_vehicle_cost_views.xml
index 00698dd232..4bc78aa9b2 100644
--- a/addons/fleet/views/fleet_vehicle_cost_views.xml
+++ b/addons/fleet/views/fleet_vehicle_cost_views.xml
@@ -145,12 +145,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/fleet/views/fleet_vehicle_views.xml b/addons/fleet/views/fleet_vehicle_views.xml
index 2a1668fac1..08581d0ddf 100644
--- a/addons/fleet/views/fleet_vehicle_views.xml
+++ b/addons/fleet/views/fleet_vehicle_views.xml
@@ -176,12 +176,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/hr/models/hr_employee_base.py b/addons/hr/models/hr_employee_base.py
index e4a901807a..1bbadaaff6 100644
--- a/addons/hr/models/hr_employee_base.py
+++ b/addons/hr/models/hr_employee_base.py
@@ -158,7 +158,7 @@ class HrEmployeeBase(models.AbstractModel):
def _get_employee_working_now(self):
working_now = []
# We loop over all the employee tz and the resource calendar_id to detect working hours in batch.
- all_employee_tz = self.mapped('tz')
+ all_employee_tz = set(self.mapped('tz'))
for tz in all_employee_tz:
employee_ids = self.filtered(lambda e: e.tz == tz)
resource_calendar_ids = employee_ids.mapped('resource_calendar_id')
diff --git a/addons/hr/views/hr_employee_views.xml b/addons/hr/views/hr_employee_views.xml
index 1781ca807e..4df9e1359a 100644
--- a/addons/hr/views/hr_employee_views.xml
+++ b/addons/hr/views/hr_employee_views.xml
@@ -14,11 +14,11 @@
+ domain="[('my_activity_date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/hr_contract/views/hr_contract_views.xml b/addons/hr_contract/views/hr_contract_views.xml
index 8658e94436..343069752b 100644
--- a/addons/hr_contract/views/hr_contract_views.xml
+++ b/addons/hr_contract/views/hr_contract_views.xml
@@ -115,12 +115,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/hr_expense/models/hr_expense.py b/addons/hr_expense/models/hr_expense.py
index fac37df204..58d5fa8621 100644
--- a/addons/hr_expense/models/hr_expense.py
+++ b/addons/hr_expense/models/hr_expense.py
@@ -401,16 +401,10 @@ Or send your receipts at
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
@@ -842,12 +842,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/hr_expense/wizard/account_payment_register.py b/addons/hr_expense/wizard/account_payment_register.py
index b8e687e47a..15027041c8 100644
--- a/addons/hr_expense/wizard/account_payment_register.py
+++ b/addons/hr_expense/wizard/account_payment_register.py
@@ -9,6 +9,15 @@ class AccountPaymentRegister(models.TransientModel):
# -------------------------------------------------------------------------
# BUSINESS METHODS
# -------------------------------------------------------------------------
+
+ @api.model
+ def _get_line_batch_key(self, line):
+ # OVERRIDE to set the bank account defined on the employee
+ res = super()._get_line_batch_key(line)
+ expense_sheet = self.env['hr.expense.sheet'].search([('payment_mode', '=', 'own_account'), ('account_move_id', 'in', line.move_id.ids)])
+ if expense_sheet.employee_id.bank_account_id and not line.move_id.partner_bank_id:
+ res['partner_bank_id'] = expense_sheet.employee_id.bank_account_id.id
+ return res
def _create_payments(self):
# OVERRIDE to set the 'done' state on expense sheets.
diff --git a/addons/hr_holidays/views/hr_leave_allocation_views.xml b/addons/hr_holidays/views/hr_leave_allocation_views.xml
index 3643e36693..33e4ac0766 100644
--- a/addons/hr_holidays/views/hr_leave_allocation_views.xml
+++ b/addons/hr_holidays/views/hr_leave_allocation_views.xml
@@ -29,12 +29,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/hr_holidays/views/hr_leave_views.xml b/addons/hr_holidays/views/hr_leave_views.xml
index f62d9d898c..19d9bd7f16 100644
--- a/addons/hr_holidays/views/hr_leave_views.xml
+++ b/addons/hr_holidays/views/hr_leave_views.xml
@@ -70,12 +70,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/hr_recruitment/data/digest_data.xml b/addons/hr_recruitment/data/digest_data.xml
index 2eff95bf35..0a5a800b4d 100644
--- a/addons/hr_recruitment/data/digest_data.xml
+++ b/addons/hr_recruitment/data/digest_data.xml
@@ -18,7 +18,7 @@
By setting an alias to a job position, emails sent to this address create applications automatically. You can even use multiple trackers to get statistics according to the source of the application: LinkedIn, Monster, Indeed, etc.
% set record = object.env['hr.job'].search([('alias_name', '!=', False)], limit=1)
% if record and record.alias_domain
- Try sending an email
+ Try sending an email
% endif
diff --git a/addons/hr_recruitment/data/hr_recruitment_data.xml b/addons/hr_recruitment/data/hr_recruitment_data.xml
index 031ab5802f..42016fd10a 100644
--- a/addons/hr_recruitment/data/hr_recruitment_data.xml
+++ b/addons/hr_recruitment/data/hr_recruitment_data.xml
@@ -85,7 +85,7 @@
% if 'website_url' in object.job_id and object.job_id.website_url:
% endif
diff --git a/addons/hr_recruitment/views/hr_recruitment_views.xml b/addons/hr_recruitment/views/hr_recruitment_views.xml
index 2f5ceca089..aa2a7a6389 100644
--- a/addons/hr_recruitment/views/hr_recruitment_views.xml
+++ b/addons/hr_recruitment/views/hr_recruitment_views.xml
@@ -212,12 +212,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/hw_drivers/tools/helpers.py b/addons/hw_drivers/tools/helpers.py
index ccf6fbde6d..17012e0575 100644
--- a/addons/hw_drivers/tools/helpers.py
+++ b/addons/hw_drivers/tools/helpers.py
@@ -146,7 +146,7 @@ def get_mac_address():
return netifaces.ifaddresses('wlan0')[netifaces.AF_LINK][0]['addr']
def get_ssid():
- ap = subprocess.call(['systemctl', 'is-active', 'hostapd']) # if service is active return 0 else inactive
+ ap = subprocess.call(['systemctl', 'is-active', '--quiet', 'hostapd']) # if service is active return 0 else inactive
if not ap:
return subprocess.check_output(['grep', '-oP', '(?<=ssid=).*', '/etc/hostapd/hostapd.conf']).decode('utf-8').rstrip()
process_iwconfig = subprocess.Popen(['iwconfig'], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
@@ -154,7 +154,7 @@ def get_ssid():
return subprocess.check_output(['sed', 's/.*"\\(.*\\)"/\\1/'], stdin=process_grep.stdout).decode('utf-8').rstrip()
def get_flectra_server_url():
- ap = subprocess.call(['systemctl', 'is-active', 'hostapd']) # if service is active return 0 else inactive
+ ap = subprocess.call(['systemctl', 'is-active', '--quiet', 'hostapd']) # if service is active return 0 else inactive
if not ap:
return False
return read_file_first_line('flectra-remote-server.conf')
diff --git a/addons/l10n_it_edi/data/account_invoice_demo.xml b/addons/l10n_it_edi/data/account_invoice_demo.xml
index a17c95a7b6..d083923584 100644
--- a/addons/l10n_it_edi/data/account_invoice_demo.xml
+++ b/addons/l10n_it_edi/data/account_invoice_demo.xml
@@ -3,14 +3,14 @@
- 0123456789987654
+ 09814700101RF0112345Palazzo dell'Arte
- IT12345670124
+ IT00000010215Piazza Marconi 5Cremona
@@ -21,10 +21,12 @@
- IT12345670231
+ IT00079760328
+ XS00001
- IT12345670348
+ IT00140390501
+ XS00001
diff --git a/addons/l10n_it_edi/data/invoice_it_template.xml b/addons/l10n_it_edi/data/invoice_it_template.xml
index 05ade3cc70..c1867fafa4 100644
--- a/addons/l10n_it_edi/data/invoice_it_template.xml
+++ b/addons/l10n_it_edi/data/invoice_it_template.xml
@@ -52,7 +52,7 @@
-
+
@@ -72,7 +72,7 @@
-
+
diff --git a/addons/l10n_it_edi/models/account_edi_format.py b/addons/l10n_it_edi/models/account_edi_format.py
index fc6515bc79..64906e9c1c 100644
--- a/addons/l10n_it_edi/models/account_edi_format.py
+++ b/addons/l10n_it_edi/models/account_edi_format.py
@@ -20,6 +20,96 @@ DEFAULT_FACTUR_ITALIAN_DATE_FORMAT = '%Y-%m-%d'
class AccountEdiFormat(models.Model):
_inherit = 'account.edi.format'
+ # -------------------------------------------------------------------------
+ # Helpers
+ # -------------------------------------------------------------------------
+
+ @api.model
+ def _l10n_it_edi_generate_electronic_invoice_filename(self, invoice):
+ '''Returns a name conform to the Fattura pa Specifications:
+ See ES documentation 2.2
+ '''
+ a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ n = invoice.id
+ progressive_number = ""
+ while n:
+ (n, m) = divmod(n, len(a))
+ progressive_number = a[m] + progressive_number
+
+ return '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
+ 'country_code': invoice.company_id.country_id.code,
+ 'codice': invoice.company_id.l10n_it_codice_fiscale.replace(' ', ''),
+ 'progressive_number': progressive_number.zfill(5),
+ }
+
+ def _l10n_it_edi_check_invoice_configuration(self, invoice):
+ errors = []
+ seller = invoice.company_id
+ buyer = invoice.commercial_partner_id
+
+ # <1.1.1.1>
+ if not seller.country_id:
+ errors.append(_("%s must have a country", seller.display_name))
+
+ # <1.1.1.2>
+ if not seller.vat:
+ errors.append(_("%s must have a VAT number", seller.display_name))
+ elif len(seller.vat) > 30:
+ errors.append(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.", seller.display_name, seller.vat))
+
+ # <1.2.1.2>
+ if not seller.l10n_it_codice_fiscale:
+ errors.append(_("%s must have a codice fiscale number", seller.display_name))
+
+ # <1.2.1.8>
+ if not seller.l10n_it_tax_system:
+ errors.append(_("The seller's company must have a tax system."))
+
+ # <1.2.2>
+ if not seller.street and not seller.street2:
+ errors.append(_("%s must have a street.", seller.display_name))
+ if not seller.zip:
+ errors.append(_("%s must have a post code.", seller.display_name))
+ elif len(seller.zip) != 5 and seller.country_id.code == 'IT':
+ errors.append(_("%s must have a post code of length 5.", seller.display_name))
+ if not seller.city:
+ errors.append(_("%s must have a city.", seller.display_name))
+ if not seller.country_id:
+ errors.append(_("%s must have a country.", seller.display_name))
+
+ if seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
+ errors.append(_("Tax representative partner %s of %s must have a tax number.", seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
+
+ # <1.4.1>
+ if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
+ errors.append(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).", buyer.display_name))
+
+ # <1.4.2>
+ if not buyer.street and not buyer.street2:
+ errors.append(_("%s must have a street.", buyer.display_name))
+ if not buyer.zip:
+ errors.append(_("%s must have a post code.", buyer.display_name))
+ if len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
+ errors.append(_("%s must have a post code of length 5.", buyer.display_name))
+ if not buyer.city:
+ errors.append(_("%s must have a city.", buyer.display_name))
+ if not buyer.country_id:
+ errors.append(_("%s must have a country.", buyer.display_name))
+
+ # <2.2.1>
+ for invoice_line in invoice.invoice_line_ids:
+ if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
+ raise UserError(_("You must select one and only one tax by line."))
+
+ for tax_line in invoice.line_ids.filtered(lambda line: line.tax_line_id):
+ if not tax_line.tax_line_id.l10n_it_kind_exoneration and tax_line.tax_line_id.amount == 0:
+ errors.append(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
+
+ if not invoice.partner_bank_id:
+ errors.append(_("The seller must have a bank account."))
+
+ return errors
+
# -------------------------------------------------------------------------
# Export
# -------------------------------------------------------------------------
@@ -36,25 +126,30 @@ class AccountEdiFormat(models.Model):
return super()._is_compatible_with_journal(journal)
return journal.type == 'sale' and journal.country_code == 'IT'
+ def _l10n_it_edi_is_required_for_invoice(self, invoice):
+ """ Is the edi required for this invoice based on the method (here: PEC mail)
+ Deprecated: in future release PEC mail will be removed.
+ TO OVERRIDE
+ """
+ return invoice.is_sale_document() and invoice.l10n_it_send_state not in ('sent', 'delivered', 'delivered_accepted') and invoice.country_code == 'IT'
+
def _is_required_for_invoice(self, invoice):
# OVERRIDE
self.ensure_one()
if self.code != 'fattura_pa':
return super()._is_required_for_invoice(invoice)
- # Determine on which invoices the Mexican CFDI must be generated.
- return invoice.is_sale_document() and invoice.l10n_it_send_state not in ['sent', 'delivered', 'delivered_accepted'] and invoice.country_code == 'IT'
-
- def _post_invoice_edi(self, invoices, test_mode=False):
- # OVERRIDE
- self.ensure_one()
- edi_result = super()._post_invoice_edi(invoices, test_mode=test_mode)
- if self.code != 'fattura_pa':
- return edi_result
+ return self._l10n_it_edi_is_required_for_invoice(invoice)
+ def _post_fattura_pa(self, invoices):
+ # TO OVERRIDE
invoice = invoices # no batching ensure that we only have one invoice
invoice.l10n_it_send_state = 'other'
invoice._check_before_xml_exporting()
+ if invoice.l10n_it_einvoice_id and invoice.l10n_it_send_state not in ['invalid', 'to_send']:
+ return {'error': _("You can't regenerate an E-Invoice when the first one is sent and there are no errors")}
+ if invoice.l10n_it_einvoice_id:
+ invoice.l10n_it_einvoice_id.unlink()
res = invoice.invoice_generate_xml()
if len(invoice.commercial_partner_id.l10n_it_pa_index or '') == 6:
invoice.message_post(
@@ -64,6 +159,15 @@ class AccountEdiFormat(models.Model):
invoice.l10n_it_send_state = 'to_send'
return {invoice: res}
+ def _post_invoice_edi(self, invoices, test_mode=False):
+ # OVERRIDE
+ self.ensure_one()
+ edi_result = super()._post_invoice_edi(invoices)
+ if self.code != 'fattura_pa':
+ return edi_result
+
+ return self._post_fattura_pa(invoices)
+
# -------------------------------------------------------------------------
# Import
# -------------------------------------------------------------------------
@@ -109,6 +213,18 @@ class AccountEdiFormat(models.Model):
invoice = self.env['account.move']
first_run = False
+ # Type must be present in the context to get the right behavior of the _default_journal method (account.move).
+ # journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line).
+ elements = tree.xpath('//CessionarioCommittente//IdCodice')
+ company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
+ if not company:
+ elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
+ company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1)
+ if not company:
+ # Only invoices with a correct VAT or Codice Fiscale can be imported
+ _logger.warning('No company found with VAT or Codice Fiscale like %r.', elements[0].text)
+ continue
+
# Refund type.
# TD01 == invoice
# TD02 == advance/down payment on invoice
@@ -123,32 +239,14 @@ class AccountEdiFormat(models.Model):
move_type = 'in_refund'
elif elements and elements[0].text and elements[0].text != 'TD01':
_logger.info('Document type not managed: %s. Invoice type is set by default.', elements[0].text)
- invoice_ctx = invoice.with_context(default_move_type=move_type)
- # type must be present in the context to get the right behavior of the _default_journal method (account.move).
- # journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line).
-
- elements = tree.xpath('//CessionarioCommittente//IdCodice')
- company = elements and self.env['res.company'].search([('vat', 'ilike', elements[0].text)], limit=1)
- if not company:
- elements = tree.xpath('//CessionarioCommittente//CodiceFiscale')
- company = elements and self.env['res.company'].search([('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1)
-
- if company:
- invoice_ctx = invoice_ctx.with_context(company_id=company.id)
- else:
- company = self.env.company
- if elements:
- _logger.info('No company found with codice fiscale: %s. The user\'s company is set by default.', elements[0].text)
- else:
- _logger.info('Company not found. The user\'s company is set by default.')
-
- if not self.env.is_superuser():
- if self.env.company != company:
- raise UserError(_("You can only import invoice concern your current company: %s", self.env.company.display_name))
+ # Setup the context for the Invoice Form
+ invoice_ctx = invoice.with_company(company) \
+ .with_context(default_move_type=move_type,
+ account_predictive_bills_disable_prediction=True)
# move could be a single record (editing) or be empty (new).
- with Form(invoice_ctx.with_context(account_predictive_bills_disable_prediction=True)) as invoice_form:
+ with Form(invoice_ctx) as invoice_form:
message_to_log = []
# Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2>
@@ -190,7 +288,7 @@ class AccountEdiFormat(models.Model):
if elements:
date_str = elements[0].text
date_obj = datetime.strptime(date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
- invoice_form.invoice_date = date_obj.strftime(DEFAULT_FACTUR_ITALIAN_DATE_FORMAT)
+ invoice_form.invoice_date = date_obj
# Dati Bollo. <2.1.1.6>
elements = body_tree.xpath('.//DatiGeneraliDocumento/DatiBollo/ImportoBollo')
@@ -295,7 +393,7 @@ class AccountEdiFormat(models.Model):
if elements:
message_to_log.append("%s %s" % (
_("Bank account not found, useful informations from XML file:"),
- self._compose_info_message(body_tree, './/DatiPagamento')))
+ invoice._compose_info_message(body_tree, './/DatiPagamento')))
# Invoice lines. <2.2.1>
elements = body_tree.xpath('.//DettaglioLinee')
diff --git a/addons/l10n_it_edi/models/account_invoice.py b/addons/l10n_it_edi/models/account_invoice.py
index 1bcad97f11..df0449575f 100644
--- a/addons/l10n_it_edi/models/account_invoice.py
+++ b/addons/l10n_it_edi/models/account_invoice.py
@@ -56,87 +56,14 @@ class AccountMove(models.Model):
invoice.l10n_it_einvoice_name = einvoice.attachment_id.name
def _check_before_xml_exporting(self):
- self.ensure_one()
- seller = self.company_id
- buyer = self.commercial_partner_id
-
- # <1.1.1.1>
- if not seller.country_id:
- raise UserError(_("%s must have a country") % (seller.display_name))
-
- # <1.1.1.2>
- if not seller.vat:
- raise UserError(_("%s must have a VAT number") % (seller.display_name))
- elif len(seller.vat) > 30:
- raise UserError(_("The maximum length for VAT number is 30. %s have a VAT number too long: %s.") % (seller.display_name, seller.vat))
-
- # <1.2.1.2>
- if not seller.l10n_it_codice_fiscale:
- raise UserError(_("%s must have a codice fiscale number") % (seller.display_name))
-
- # <1.2.1.8>
- if not seller.l10n_it_tax_system:
- raise UserError(_("The seller's company must have a tax system."))
-
- # <1.2.2>
- if not seller.street and not seller.street2:
- raise UserError(_("%s must have a street.") % (seller.display_name))
- if not seller.zip:
- raise UserError(_("%s must have a post code.") % (seller.display_name))
- if len(seller.zip) != 5 and seller.country_id.code == 'IT':
- raise UserError(_("%s must have a post code of length 5.") % (seller.display_name))
- if not seller.city:
- raise UserError(_("%s must have a city.") % (seller.display_name))
- if not seller.country_id:
- raise UserError(_("%s must have a country.") % (seller.display_name))
-
- if seller.l10n_it_has_tax_representative and not seller.l10n_it_tax_representative_partner_id.vat:
- raise UserError(_("Tax representative partner %s of %s must have a tax number.") % (seller.l10n_it_tax_representative_partner_id.display_name, seller.display_name))
-
- # <1.4.1>
- if not buyer.vat and not buyer.l10n_it_codice_fiscale and buyer.country_id.code == 'IT':
- raise UserError(_("The buyer, %s, or his company must have either a VAT number either a tax code (Codice Fiscale).") % (buyer.display_name))
-
- # <1.4.2>
- if not buyer.street and not buyer.street2:
- raise UserError(_("%s must have a street.") % (buyer.display_name))
- if not buyer.zip:
- raise UserError(_("%s must have a post code.") % (buyer.display_name))
- if len(buyer.zip) != 5 and buyer.country_id.code == 'IT':
- raise UserError(_("%s must have a post code of length 5.") % (buyer.display_name))
- if not buyer.city:
- raise UserError(_("%s must have a city.") % (buyer.display_name))
- if not buyer.country_id:
- raise UserError(_("%s must have a country.") % (buyer.display_name))
-
- # <2.2.1>
- for invoice_line in self.invoice_line_ids:
- if not invoice_line.display_type and len(invoice_line.tax_ids) != 1:
- raise UserError(_("You must select one and only one tax by line."))
-
- for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id):
- if not tax_line.tax_line_id.l10n_it_has_exoneration and tax_line.tax_line_id.amount == 0:
- raise ValidationError(_("%s has an amount of 0.0, you must indicate the kind of exoneration.", tax_line.name))
+ # DEPRECATED use AccountEdiFormat._l10n_it_edi_check_invoice_configuration instead
+ errors = self.env['account.edi.format']._l10n_it_edi_check_invoice_configuration(self)
+ if errors:
+ raise UserError(self.env['account.edi.format']._format_error_message(_("Invalid configuration:"), errors))
def invoice_generate_xml(self):
self.ensure_one()
- if self.l10n_it_einvoice_id and self.l10n_it_send_state not in ['invalid', 'to_send']:
- return {'error': _("You can't regenerate an E-Invoice when the first one is sent and there are no errors")}
- if self.l10n_it_einvoice_id:
- self.l10n_it_einvoice_id.unlink()
-
- a = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
- n = self.id
- progressive_number = ""
- while n:
- (n,m) = divmod(n,len(a))
- progressive_number = a[m] + progressive_number
-
- report_name = '%(country_code)s%(codice)s_%(progressive_number)s.xml' % {
- 'country_code': self.company_id.country_id.code,
- 'codice': self.company_id.l10n_it_codice_fiscale,
- 'progressive_number': progressive_number.zfill(5),
- }
+ report_name = self.env['account.edi.format']._l10n_it_edi_generate_electronic_invoice_filename(self)
data = b"" + self._export_as_xml()
description = _('Italian invoice: %s', self.move_type)
@@ -155,9 +82,6 @@ class AccountMove(models.Model):
return {'attachment': attachment}
def _prepare_fatturapa_export_values(self):
- ''' Create the xml file content.
- :return: The XML content as str.
- '''
self.ensure_one()
def format_date(dt):
@@ -252,6 +176,10 @@ class AccountMove(models.Model):
return template_values
def _export_as_xml(self):
+ '''DEPRECATED : this will be moved to AccountEdiFormat in a future version.
+ Create the xml file content.
+ :return: The XML content as str.
+ '''
template_values = self._prepare_fatturapa_export_values()
content = self.env.ref('l10n_it_edi.account_invoice_it_FatturaPA_export')._render(template_values)
return content
diff --git a/addons/l10n_it_edi/models/ir_mail_server.py b/addons/l10n_it_edi/models/ir_mail_server.py
index 5044c702c4..16d71ac456 100644
--- a/addons/l10n_it_edi/models/ir_mail_server.py
+++ b/addons/l10n_it_edi/models/ir_mail_server.py
@@ -135,7 +135,8 @@ class FetchmailServer(models.Model):
# we have a receipt
self._message_receipt_invoice(split_underscore[1], attachment)
else:
- match = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", attachment.fname)
+ att_filename = attachment.fname
+ match = re.search("([A-Z]{2}[A-Za-z0-9]{2,28}_[A-Za-z0-9]{0,5}.(xml.p7m|xml))", att_filename)
# If match, we have an invoice.
if match:
# If it's signed, the content has a bytes type and we just remove the signature's envelope
@@ -143,12 +144,13 @@ class FetchmailServer(models.Model):
att_content_data = remove_signature(attachment.content)
# If the envelope cannot be removed, the remove_signature returns None, so we skip
if not att_content_data:
- _logger.warning("E-invoice couldn't be read: %s", attachment.fname)
+ _logger.warning("E-invoice couldn't be read: %s", att_filename)
continue
- # Otherwise, it should be an utf-8 encoded XML string
+ att_filename = att_filename.replace('.xml.p7m', '.xml')
else:
+ # Otherwise, it should be an utf-8 encoded XML string
att_content_data = attachment.content.encode()
- self._create_invoice_from_mail(att_content_data, attachment.fname, from_address)
+ self._create_invoice_from_mail(att_content_data, att_filename, from_address)
else:
if split_underscore[1] == 'AT':
# Attestazione di avvenuta trasmissione della fattura con impossibilità di recapito
@@ -174,7 +176,7 @@ class FetchmailServer(models.Model):
return invoices
# Create the new attachment for the file
- self.env['ir.attachment'].create({
+ attachment = self.env['ir.attachment'].create({
'name': att_name,
'raw': att_content_data,
'res_model': 'account.move',
@@ -187,14 +189,16 @@ class FetchmailServer(models.Model):
_logger.info('The xml file is badly formatted: %s', att_name)
return invoices
- # Create the invoice
invoices = self.env.ref('l10n_it_edi.edi_fatturaPA')._create_invoice_from_xml_tree(att_name, tree)
if not invoices:
_logger.info('E-invoice not found in file: %s', att_name)
return invoices
-
- invoices.l10n_it_send_state = 'new'
+ invoices.l10n_it_send_state = "new"
invoices.invoice_source_email = from_address
+ for invoice in invoices:
+ invoice.with_context(no_new_invoice=True, default_res_id=invoice.id) \
+ .message_post(body=(_("Original E-invoice XML file")), attachment_ids=[attachment.id])
+
self._cr.commit()
_logger.info('New E-invoices (%s), ids: %s', att_name, [x.id for x in invoices])
@@ -403,8 +407,10 @@ class FetchmailServer(models.Model):
class IrMailServer(models.Model):
_name = "ir.mail_server"
_inherit = "ir.mail_server"
+
def _get_test_email_addresses(self):
self.ensure_one()
+
company = self.env["res.company"].search([("l10n_it_mail_pec_server_id", "=", self.id)], limit=1)
if not company:
# it's not a PEC server
diff --git a/addons/l10n_it_edi/tests/test_ir_mail_server.py b/addons/l10n_it_edi/tests/test_ir_mail_server.py
index f93f14f654..10323bce76 100644
--- a/addons/l10n_it_edi/tests/test_ir_mail_server.py
+++ b/addons/l10n_it_edi/tests/test_ir_mail_server.py
@@ -5,6 +5,7 @@ import datetime
import logging
from collections import namedtuple
from unittest.mock import patch
+import freezegun
from flectra import tools
from flectra.addons.account_edi.tests.common import AccountEdiTestCommon
@@ -44,8 +45,10 @@ class PecMailServerTests(AccountEdiTestCommon):
super().setUpClass(chart_template_ref='l10n_it.l10n_it_chart_template_generic',
edi_format_ref='l10n_it_edi.edi_fatturaPA')
+ # Use the company_data_2 to test that the e-invoice is imported for the right company
+ cls.company = cls.company_data_2['company']
+
# Initialize the company's codice fiscale
- cls.company = cls.company_data['company']
cls.company.l10n_it_codice_fiscale = 'IT01234560157'
# Build test data.
@@ -106,12 +109,14 @@ class PecMailServerTests(AccountEdiTestCommon):
def test_receive_signed_vendor_bill(self):
""" Test a signed (P7M) sample e-invoice file from https://www.fatturapa.gov.it/export/documenti/fatturapa/v1.2/IT01234567890_FPR01.xml """
- invoices = self._create_invoice(self.signed_invoice_content, self.signed_invoice_filename)
- self.assertRecordValues(invoices, [{
- 'name': 'BILL/2014/12/0001',
- 'date': datetime.date(2014, 12, 18),
- 'ref': '01234567890',
- }])
+ with freezegun.freeze_time('2020-04-06'):
+ invoices = self._create_invoice(self.signed_invoice_content, self.signed_invoice_filename)
+ self.assertRecordValues(invoices, [{
+ 'company_id': self.company.id,
+ 'name': 'BILL/2020/04/0001',
+ 'invoice_date': datetime.date(2014, 12, 18),
+ 'ref': '01234567890',
+ }])
def test_receive_same_vendor_bill_twice(self):
""" Test that the second time we are receiving a PEC mail with the same attachment, the second is discarded """
diff --git a/addons/link_tracker/models/link_tracker.py b/addons/link_tracker/models/link_tracker.py
index 1808e6a705..d8353c447f 100644
--- a/addons/link_tracker/models/link_tracker.py
+++ b/addons/link_tracker/models/link_tracker.py
@@ -85,8 +85,11 @@ class LinkTracker(models.Model):
for tracker in self:
parsed = urls.url_parse(tracker.url)
utms = {}
- for key, field, cook in self.env['utm.mixin'].tracking_fields():
- attr = getattr(tracker, field).name
+ for key, field_name, cook in self.env['utm.mixin'].tracking_fields():
+ field = self._fields[field_name]
+ attr = getattr(tracker, field_name)
+ if field.type == 'many2one':
+ attr = attr.name
if attr:
utms[key] = attr
utms.update(parsed.decode_query())
diff --git a/addons/mail/models/mail_activity.py b/addons/mail/models/mail_activity.py
index 9426144df1..b064c60622 100644
--- a/addons/mail/models/mail_activity.py
+++ b/addons/mail/models/mail_activity.py
@@ -688,6 +688,10 @@ class MailActivityMixin(models.AbstractModel):
compute='_compute_activity_date_deadline', search='_search_activity_date_deadline',
compute_sudo=False, readonly=True, store=False,
groups="base.group_user")
+ my_activity_date_deadline = fields.Date(
+ 'My Activity Deadline',
+ compute='_compute_my_activity_date_deadline', search='_search_my_activity_date_deadline',
+ compute_sudo=False, readonly=True, groups="base.group_user")
activity_summary = fields.Char(
'Next Activity Summary',
related='activity_ids.summary', readonly=False,
@@ -757,6 +761,24 @@ class MailActivityMixin(models.AbstractModel):
def _search_activity_summary(self, operator, operand):
return [('activity_ids.summary', operator, operand)]
+ @api.depends('activity_ids.date_deadline', 'activity_ids.user_id')
+ @api.depends_context('uid')
+ def _compute_my_activity_date_deadline(self):
+ for record in self:
+ record.my_activity_date_deadline = next((
+ activity.date_deadline
+ for activity in record.activity_ids
+ if activity.user_id.id == record.env.uid
+ ), False)
+
+ def _search_my_activity_date_deadline(self, operator, operand):
+ activity_ids = self.env['mail.activity']._search([
+ ('date_deadline', operator, operand),
+ ('res_model', '=', self._name),
+ ('user_id', '=', self.env.user.id)
+ ])
+ return [('activity_ids', 'in', activity_ids)]
+
def write(self, vals):
# Delete activities of archived record.
if 'active' in vals and vals['active'] is False:
diff --git a/addons/mail/static/src/components/message/message_tests.js b/addons/mail/static/src/components/message/message_tests.js
index d1c9c96635..6b02b052f0 100644
--- a/addons/mail/static/src/components/message/message_tests.js
+++ b/addons/mail/static/src/components/message/message_tests.js
@@ -1502,6 +1502,77 @@ QUnit.test('rendering of tracked field of type many2one: from no related record
);
});
+QUnit.test('message should not be considered as "clicked" after clicking on its author name', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const message = this.env.models['mail.message'].create({
+ author: [['insert', { id: 7, display_name: "Demo User" }]],
+ body: "
Test
",
+ id: 100,
+ });
+ await this.createMessageComponent(message);
+ document.querySelector(`.o_Message_authorName`).click();
+ await nextAnimationFrame();
+ assert.doesNotHaveClass(
+ document.querySelector(`.o_Message`),
+ 'o-clicked',
+ "message should not be considered as 'clicked' after clicking on its author name"
+ );
+});
+
+QUnit.test('message should not be considered as "clicked" after clicking on its author avatar', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const message = this.env.models['mail.message'].create({
+ author: [['insert', { id: 7, display_name: "Demo User" }]],
+ body: "
Test
",
+ id: 100,
+ });
+ await this.createMessageComponent(message);
+ document.querySelector(`.o_Message_authorAvatar`).click();
+ await nextAnimationFrame();
+ assert.doesNotHaveClass(
+ document.querySelector(`.o_Message`),
+ 'o-clicked',
+ "message should not be considered as 'clicked' after clicking on its author avatar"
+ );
+});
+
+QUnit.test('message should not be considered as "clicked" after clicking on notification failure icon', async function (assert) {
+ assert.expect(1);
+
+ await this.start();
+ const threadViewer = this.env.models['mail.thread_viewer'].create({
+ hasThreadView: true,
+ thread: [['create', {
+ id: 11,
+ model: 'mail.channel',
+ }]],
+ });
+ const message = this.env.models['mail.message'].create({
+ id: 10,
+ message_type: 'email',
+ notifications: [['insert', {
+ id: 11,
+ notification_status: 'exception',
+ notification_type: 'email',
+ }]],
+ originThread: [['link', threadViewer.thread]],
+ });
+ await this.createMessageComponent(message, {
+ threadViewLocalId: threadViewer.threadView.localId
+ });
+ document.querySelector('.o_Message_notificationIconClickable.o-error').click();
+ await nextAnimationFrame();
+ assert.doesNotHaveClass(
+ document.querySelector(`.o_Message`),
+ 'o-clicked',
+ "message should not be considered as 'clicked' after clicking on notification failure icon"
+ );
+});
+
});
});
});
diff --git a/addons/mail/views/res_partner_views.xml b/addons/mail/views/res_partner_views.xml
index 3eb92fe74c..8f06ea6eb6 100644
--- a/addons/mail/views/res_partner_views.xml
+++ b/addons/mail/views/res_partner_views.xml
@@ -49,12 +49,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/maintenance/views/maintenance_views.xml b/addons/maintenance/views/maintenance_views.xml
index ed0ccb1b42..32ffa58608 100644
--- a/addons/maintenance/views/maintenance_views.xml
+++ b/addons/maintenance/views/maintenance_views.xml
@@ -32,12 +32,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
@@ -518,12 +518,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/mass_mailing/data/mailing_data_templates.xml b/addons/mass_mailing/data/mailing_data_templates.xml
index 83c45bc501..47c22b55fb 100644
--- a/addons/mass_mailing/data/mailing_data_templates.xml
+++ b/addons/mass_mailing/data/mailing_data_templates.xml
@@ -22,7 +22,7 @@
-
+
Label
%Click (Total)
diff --git a/addons/mrp/models/mrp_production.py b/addons/mrp/models/mrp_production.py
index 5be04e4c85..50be63d963 100644
--- a/addons/mrp/models/mrp_production.py
+++ b/addons/mrp/models/mrp_production.py
@@ -1715,7 +1715,9 @@ class MrpProduction(models.Model):
])
if duplicates:
# Maybe some move lines have been compensated by unbuild
- duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
+ duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [
+ ('move_id.unbuild_id', '!=', False)
+ ])
if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
raise UserError(message)
# Check presence of same sn in current production
@@ -1749,7 +1751,9 @@ class MrpProduction(models.Model):
])
if duplicates:
# Maybe some move lines have been compensated by unbuild
- duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild)
+ duplicates_unbuild = self.env['stock.move.line'].search_count(domain_unbuild + [
+ ('move_id.unbuild_id', '!=', False)
+ ])
if not (duplicates_unbuild and duplicates - duplicates_unbuild == 0):
raise UserError(message)
# Check presence of same sn in current production
diff --git a/addons/mrp/models/stock_move.py b/addons/mrp/models/stock_move.py
index 1514b5af91..a1e3297b8d 100644
--- a/addons/mrp/models/stock_move.py
+++ b/addons/mrp/models/stock_move.py
@@ -133,7 +133,7 @@ class StockMove(models.Model):
operation_domain = [
('id', 'in', move.raw_material_production_id.bom_id.operation_ids.ids),
'|',
- ('company_id', '=', self.company_id.id),
+ ('company_id', '=', move.company_id.id),
('company_id', '=', False)
]
move.allowed_operation_ids = self.env['mrp.routing.workcenter'].search(operation_domain)
@@ -359,12 +359,14 @@ class StockMove(models.Model):
def _prepare_merge_moves_distinct_fields(self):
distinct_fields = super()._prepare_merge_moves_distinct_fields()
distinct_fields.append('created_production_id')
+ distinct_fields.append('bom_line_id')
return distinct_fields
@api.model
def _prepare_merge_move_sort_method(self, move):
keys_sorted = super()._prepare_merge_move_sort_method(move)
keys_sorted.append(move.created_production_id.id)
+ keys_sorted.append(move.bom_line_id.id)
return keys_sorted
def _compute_kit_quantities(self, product_id, kit_qty, kit_bom, filters):
diff --git a/addons/mrp/views/mrp_bom_views.xml b/addons/mrp/views/mrp_bom_views.xml
index 6d92097b3b..839a268a23 100644
--- a/addons/mrp/views/mrp_bom_views.xml
+++ b/addons/mrp/views/mrp_bom_views.xml
@@ -123,11 +123,11 @@
-
-
+
+
-
+
diff --git a/addons/mrp/views/mrp_production_views.xml b/addons/mrp/views/mrp_production_views.xml
index 0bef59618b..b6acbd3233 100644
--- a/addons/mrp/views/mrp_production_views.xml
+++ b/addons/mrp/views/mrp_production_views.xml
@@ -483,12 +483,12 @@
domain="['|', ('delay_alert_date', '!=', False), '&', ('date_deadline', '<', current_date), ('state', '=', 'confirmed')]"/>
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
+ domain="[('my_activity_date_deadline', '>', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/mrp_account/models/product.py b/addons/mrp_account/models/product.py
index 95f682fd96..dcc4d8b77a 100644
--- a/addons/mrp_account/models/product.py
+++ b/addons/mrp_account/models/product.py
@@ -54,7 +54,7 @@ class ProductProduct(models.Model):
bom_line = move.bom_line_id
bom_line_data = bom_lines[bom_line]
bom_line_qty = bom_line_data['qty']
- value += move.product_id._compute_average_price(qty_invoiced * bom_line_qty, qty_to_invoice * bom_line_qty, move)
+ value += bom_line_qty * move.product_id._compute_average_price(qty_invoiced * bom_line_qty, qty_to_invoice * bom_line_qty, move)
return value
def _compute_bom_price(self, bom, boms_to_recompute=False):
diff --git a/addons/mrp_account/models/stock_move.py b/addons/mrp_account/models/stock_move.py
index 54ce9ac0ca..e6487eb9f4 100644
--- a/addons/mrp_account/models/stock_move.py
+++ b/addons/mrp_account/models/stock_move.py
@@ -22,3 +22,8 @@ class StockMove(models.Model):
return super()._get_dest_account(accounts_data)
else:
return self.location_id.valuation_in_account_id.id or accounts_data['stock_output'].id
+
+ def _filter_anglo_saxon_moves(self, product):
+ res = super(StockMove, self)._filter_anglo_saxon_moves(product)
+ res += self.filtered(lambda m: m.bom_line_id.bom_id.product_tmpl_id.id == product.product_tmpl_id.id)
+ return res
diff --git a/addons/note/views/note_views.xml b/addons/note/views/note_views.xml
index 3e31080518..646e4800a0 100644
--- a/addons/note/views/note_views.xml
+++ b/addons/note/views/note_views.xml
@@ -179,12 +179,12 @@
+ domain="[('my_activity_date_deadline', '=', context_today().strftime('%Y-%m-%d'))]"/>
diff --git a/addons/payment_ingenico/controllers/main.py b/addons/payment_ingenico/controllers/main.py
index 9cc7df583f..86b55b23e5 100644
--- a/addons/payment_ingenico/controllers/main.py
+++ b/addons/payment_ingenico/controllers/main.py
@@ -23,7 +23,7 @@ class OgoneController(http.Controller):
'/payment/ogone/decline', '/payment/ogone/test/decline',
'/payment/ogone/exception', '/payment/ogone/test/exception',
'/payment/ogone/cancel', '/payment/ogone/test/cancel',
- ], type='http', auth='public', csrf=False, method=['GET', 'POST'])
+ ], type='http', auth='public', csrf=False)
def ogone_form_feedback(self, **post):
""" Handle both redirection from Ingenico (GET) and s2s notification (POST/GET) """
_logger.info('Ogone: entering form_feedback with post data %s', pprint.pformat(post)) # debug
diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py
index 029f74e5cc..35dc9d66b4 100644
--- a/addons/point_of_sale/controllers/main.py
+++ b/addons/point_of_sale/controllers/main.py
@@ -57,7 +57,9 @@ class PosController(http.Controller):
'session_info': session_info,
'login_number': pos_session.login(),
}
- return request.render('point_of_sale.index', qcontext=context)
+ response = request.render('point_of_sale.index', context)
+ response.headers['Cache-Control'] = 'no-store'
+ return response
@http.route('/pos/ui/tests', type='http', auth="user")
def test_suite(self, mod=None, **kwargs):
diff --git a/addons/point_of_sale/models/pos_order.py b/addons/point_of_sale/models/pos_order.py
index 07c934ad35..8d0da03ff6 100644
--- a/addons/point_of_sale/models/pos_order.py
+++ b/addons/point_of_sale/models/pos_order.py
@@ -198,7 +198,7 @@ class PosOrder(models.Model):
def _get_pos_anglo_saxon_price_unit(self, product, partner_id, quantity):
moves = self.filtered(lambda o: o.partner_id.id == partner_id)\
.mapped('picking_ids.move_lines')\
- .filtered(lambda m: m.product_id.id == product.id)\
+ ._filter_anglo_saxon_moves(product)\
.sorted(lambda x: x.date)
price_unit = product._compute_average_price(0, quantity, moves)
return price_unit
diff --git a/addons/point_of_sale/models/pos_session.py b/addons/point_of_sale/models/pos_session.py
index 5ce946efdf..d408eccff5 100644
--- a/addons/point_of_sale/models/pos_session.py
+++ b/addons/point_of_sale/models/pos_session.py
@@ -259,6 +259,8 @@ class PosSession(models.Model):
def action_pos_session_closing_control(self):
self._check_pos_session_balance()
for session in self:
+ if any(order.state == 'draft' for order in session.order_ids):
+ raise UserError(_("You cannot close the POS when orders are still in draft"))
if session.state == 'closed':
raise UserError(_('This session is already closed.'))
session.write({'state': 'closing_control', 'stop_at': fields.Datetime.now()})
@@ -296,6 +298,7 @@ class PosSession(models.Model):
def _validate_session(self):
self.ensure_one()
+ sudo = self.user_has_groups('point_of_sale.group_pos_user')
if self.order_ids or self.statement_ids.line_ids:
self.cash_real_transaction = self.cash_register_total_entry_encoding
self.cash_real_expected = self.cash_register_balance_end
@@ -310,7 +313,7 @@ class PosSession(models.Model):
try:
self.with_company(self.company_id)._create_account_move()
except AccessError as e:
- if self.user_has_groups('point_of_sale.group_pos_user'):
+ if sudo:
self.sudo().with_company(self.company_id)._create_account_move()
else:
raise e
@@ -319,6 +322,10 @@ class PosSession(models.Model):
self.env['pos.order'].search([('session_id', '=', self.id), ('state', '=', 'paid')]).write({'state': 'done'})
else:
self.move_id.unlink()
+ elif not self.cash_register_id.difference:
+ cash_register = self.cash_register_id.sudo() if sudo else self.cash_register_id
+ cash_register.pos_session_id = False
+ cash_register.unlink()
self.write({'state': 'closed'})
return {
'type': 'ir.actions.client',
@@ -657,12 +664,13 @@ class PosSession(models.Model):
invoice_receivable_vals[commercial_partner].append(self._get_invoice_receivable_vals(account_id, amounts['amount'], amounts['amount_converted'], partner=commercial_partner))
for commercial_partner, vals in invoice_receivable_vals.items():
account_id = commercial_partner.property_account_receivable_id.id
- receivable_line = MoveLine.create(vals)
- if (not receivable_line.reconciled):
- if account_id not in invoice_receivable_lines:
- invoice_receivable_lines[account_id] = receivable_line
- else:
- invoice_receivable_lines[account_id] |= receivable_line
+ receivable_lines = MoveLine.create(vals)
+ for receivable_line in receivable_lines:
+ if (not receivable_line.reconciled):
+ if account_id not in invoice_receivable_lines:
+ invoice_receivable_lines[account_id] = receivable_line
+ else:
+ invoice_receivable_lines[account_id] |= receivable_line
data.update({'invoice_receivable_lines': invoice_receivable_lines})
return data
diff --git a/addons/point_of_sale/models/stock_picking.py b/addons/point_of_sale/models/stock_picking.py
index 04653bff89..63da33cd89 100644
--- a/addons/point_of_sale/models/stock_picking.py
+++ b/addons/point_of_sale/models/stock_picking.py
@@ -117,7 +117,7 @@ class StockPicking(models.Model):
'product_id': line.product_id.id,
'name': lot.lot_name,
})
- quant = existing_lot.quant_ids.filtered(lambda q: q.quantity > 0.0 or q.location_id.parent_path.startswith(move.location_id.parent_path))[-1:]
+ quant = existing_lot.quant_ids.filtered(lambda q: q.quantity > 0.0 and q.location_id.parent_path.startswith(move.location_id.parent_path))[-1:]
ml_vals.update({
'lot_id': existing_lot.id,
'location_id': quant.location_id.id or move.location_id.id
diff --git a/addons/point_of_sale/static/src/css/pos.css b/addons/point_of_sale/static/src/css/pos.css
index ef441395f8..118443c045 100644
--- a/addons/point_of_sale/static/src/css/pos.css
+++ b/addons/point_of_sale/static/src/css/pos.css
@@ -1696,6 +1696,9 @@ td {
}
@media print {
+ body {
+ background: white;
+ }
body * {
visibility: hidden;
}
@@ -1717,7 +1720,7 @@ td {
margin-left: auto !important;
margin-right: auto !important;
border: none !important;
- font-size: 13px !important;
+ font-size: 14px !important;
width: 266px !important;
}
}
@@ -2973,10 +2976,6 @@ td {
color: white;
}
-.order-receipt-hidden {
- display: none;
-}
-
.order-receipt {
color: white;
font-size: medium;
diff --git a/addons/point_of_sale/static/src/css/pos_receipts.css b/addons/point_of_sale/static/src/css/pos_receipts.css
index 53731cc06e..b8ed9af277 100644
--- a/addons/point_of_sale/static/src/css/pos_receipts.css
+++ b/addons/point_of_sale/static/src/css/pos_receipts.css
@@ -7,7 +7,7 @@
top: 0;
text-align: left;
direction: ltr;
- font-size: 28px;
+ font-size: 27px;
color: #000000;
}
diff --git a/addons/point_of_sale/static/src/js/Chrome.js b/addons/point_of_sale/static/src/js/Chrome.js
index 57c5bcb928..b4c3746bfc 100644
--- a/addons/point_of_sale/static/src/js/Chrome.js
+++ b/addons/point_of_sale/static/src/js/Chrome.js
@@ -222,7 +222,7 @@ flectra.define('point_of_sale.Chrome', function(require) {
// 3. Save the screen to the order.
// - This screen is shown when the order is selected.
- if (!(component.prototype instanceof IndependentToOrderScreen)) {
+ if (!(component.prototype instanceof IndependentToOrderScreen) && name !== "ReprintReceiptScreen") {
this._setScreenData(name, props);
}
}
diff --git a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js
index a408f2c395..d0d43dda98 100644
--- a/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js
+++ b/addons/point_of_sale/static/src/js/Misc/AbstractReceiptScreen.js
@@ -39,13 +39,9 @@ flectra.define('point_of_sale.AbstractReceiptScreen', function (require) {
return await this._printWeb();
}
}
- /**
- * https://stackoverflow.com/questions/21285902/printing-a-part-of-webpage-with-javascript
- */
async _printWeb() {
try {
- const isPrinted = document.execCommand('print', false, null);
- if (!isPrinted) window.print();
+ window.print();
return true;
} catch (err) {
await this.showPopup('ErrorPopup', {
diff --git a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js
index 411f88e605..6e1bc62c23 100644
--- a/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js
+++ b/addons/point_of_sale/static/src/js/Screens/ClientListScreen/ClientDetailsEdit.js
@@ -10,7 +10,11 @@ flectra.define('point_of_sale.ClientDetailsEdit', function(require) {
constructor() {
super(...arguments);
this.intFields = ['country_id', 'state_id', 'property_product_pricelist'];
- this.changes = {};
+ const partner = this.props.partner;
+ this.changes = {
+ 'country_id': partner.country_id && partner.country_id[0],
+ 'state_id': partner.state_id && partner.state_id[0],
+ };
}
mounted() {
this.env.bus.on('save-customer', this, this.saveChanges);
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js
index 1ae083dfdc..fc82bc5668 100644
--- a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ControlButtons/ReprintReceiptButton.js
@@ -18,15 +18,7 @@ flectra.define('point_of_sale.ReprintReceiptButton', function (require) {
const order = this.orderManagementContext.selectedOrder;
if (!order) return;
- if (this.env.pos.proxy.printer) {
- const receiptHtml = this.props.hiddenOrderReceipt.el.outerHTML;
- const printResult = await this.env.pos.proxy.printer.print_receipt(receiptHtml);
- if (!printResult.successful) {
- this.showTempScreen('ReprintReceiptScreen', { order: order });
- }
- } else {
- this.showTempScreen('ReprintReceiptScreen', { order: order });
- }
+ this.showScreen('ReprintReceiptScreen', { order: order });
}
}
ReprintReceiptButton.template = 'ReprintReceiptButton';
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
index 72d2b6fd18..c524352ec8 100644
--- a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/OrderManagementScreen.js
@@ -1,7 +1,7 @@
flectra.define('point_of_sale.OrderManagementScreen', function (require) {
'use strict';
- const { useContext, useRef } = owl.hooks;
+ const { useContext } = owl.hooks;
const { useListener } = require('web.custom_hooks');
const ControlButtonsMixin = require('point_of_sale.ControlButtonsMixin');
const NumberBuffer = require('point_of_sale.NumberBuffer');
@@ -27,7 +27,6 @@ flectra.define('point_of_sale.OrderManagementScreen', function (require) {
OrderFetcher.setComponent(this);
OrderFetcher.setConfigId(this.env.pos.config_id);
this.orderManagementContext = useContext(contexts.orderManagement);
- this.receiptRef = useRef('order-receipt');
}
mounted() {
OrderFetcher.on('update', this, this.render);
@@ -77,9 +76,9 @@ flectra.define('point_of_sale.OrderManagementScreen', function (require) {
OrderFetcher.fetch();
}
_onClickOrder({ detail: clickedOrder }) {
- if (!clickedOrder || clickedOrder.locked) {
+ let currentPOSOrder = this.env.pos.get_order();
+ if (currentPOSOrder && (!clickedOrder || clickedOrder.locked)) {
this.orderManagementContext.selectedOrder = clickedOrder;
- let currentPOSOrder = this.env.pos.get_order();
if (clickedOrder.attributes.client){
currentPOSOrder.set_client(clickedOrder.attributes.client);
}
diff --git a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js
index deb1183995..f12bb3634a 100644
--- a/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/OrderManagementScreen/ReprintReceiptScreen.js
@@ -6,12 +6,21 @@ flectra.define('point_of_sale.ReprintReceiptScreen', function (require) {
const ReprintReceiptScreen = (AbstractReceiptScreen) => {
class ReprintReceiptScreen extends AbstractReceiptScreen {
- confirm() {
- this.props.resolve({ confirmed: true, payload: null });
- this.trigger('close-temp-screen');
+ mounted() {
+ this.printReceipt();
}
- tryReprint() {
- this._printReceipt();
+ confirm() {
+ this.showScreen('OrderManagementScreen');
+ }
+ async printReceipt() {
+ if(this.env.pos.proxy.printer && this.env.pos.config.iface_print_skip_screen) {
+ let result = await this._printReceipt();
+ if(result)
+ this.showScreen('OrderManagementScreen');
+ }
+ }
+ async tryReprint() {
+ await this._printReceipt();
}
}
ReprintReceiptScreen.template = 'ReprintReceiptScreen';
diff --git a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
index eaaeb90a34..894c06265e 100644
--- a/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/PaymentScreen/PaymentScreen.js
@@ -31,6 +31,7 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
onChangeOrder(this._onPrevOrder, this._onNewOrder);
useErrorHandlers();
this.payment_interface = null;
+ this.error = false;
this.payment_methods_from_config = this.env.pos.payment_methods.filter(method => this.env.pos.config.payment_method_ids.includes(method.id));
}
get currentOrder() {
@@ -75,7 +76,7 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
}
_updateSelectedPaymentline() {
if (this.paymentLines.every((line) => line.paid)) {
- this.currentOrder.add_paymentline(this.env.pos.payment_methods[0]);
+ this.currentOrder.add_paymentline(this.payment_methods_from_config[0]);
}
if (!this.selectedPaymentLine) return; // do nothing if no selected payment line
// disable changing amount on paymentlines with running or done payments on a payment terminal
@@ -177,6 +178,8 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
syncedOrderBackendIds = await this.env.pos.push_single_order(this.currentOrder);
}
} catch (error) {
+ if (error.code == 700)
+ this.error = true;
if (error instanceof Error) {
throw error;
} else {
@@ -217,7 +220,7 @@ flectra.define('point_of_sale.PaymentScreen', function (require) {
}
}
get nextScreen() {
- return 'ReceiptScreen';
+ return !this.error? 'ReceiptScreen' : 'ProductScreen';
}
async _isOrderValid(isForceValidate) {
if (this.currentOrder.get_orderlines().length === 0) {
diff --git a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js
index 9b52155986..206a978359 100644
--- a/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js
+++ b/addons/point_of_sale/static/src/js/Screens/ReceiptScreen/ReceiptScreen.js
@@ -63,6 +63,9 @@ flectra.define('point_of_sale.ReceiptScreen', function (require) {
get nextScreen() {
return { name: 'ProductScreen' };
}
+ whenClosing() {
+ this.orderDone();
+ }
/**
* This function is called outside the rendering call stack. This way,
* we don't block the displaying of ReceiptScreen when it is mounted; additionally,
@@ -72,7 +75,7 @@ flectra.define('point_of_sale.ReceiptScreen', function (require) {
if (this._shouldAutoPrint()) {
await this.printReceipt();
if (this.currentOrder._printed && this._shouldCloseImmediately()) {
- this.orderDone();
+ this.whenClosing();
}
}
}
diff --git a/addons/point_of_sale/static/src/js/custom_hooks.js b/addons/point_of_sale/static/src/js/custom_hooks.js
index 79bffe3da3..1aef387520 100644
--- a/addons/point_of_sale/static/src/js/custom_hooks.js
+++ b/addons/point_of_sale/static/src/js/custom_hooks.js
@@ -38,6 +38,14 @@ flectra.define('point_of_sale.custom_hooks', function (require) {
error.data.debug ||
this.env._t('The server encountered an error while receiving your order.'),
});
+ } else if (error.code === 700) {
+ // Fiscal module errors
+ await this.showPopup('ErrorPopup', {
+ title: this.env._t('Fiscal data module error'),
+ body:
+ error.data.error.status ||
+ this.env._t('The fiscal data module encountered an error while receiving your order.'),
+ });
} else {
// ???
await this.showPopup('ErrorPopup', {
diff --git a/addons/point_of_sale/static/src/js/models.js b/addons/point_of_sale/static/src/js/models.js
index 84f675da52..b47a09d07b 100644
--- a/addons/point_of_sale/static/src/js/models.js
+++ b/addons/point_of_sale/static/src/js/models.js
@@ -2735,7 +2735,7 @@ exports.Order = Backbone.Model.extend({
amount_paid: this.get_total_paid() - this.get_change(),
amount_total: this.get_total_with_tax(),
amount_tax: this.get_total_tax(),
- amount_return: this.amount_return ? this.amount_return : this.get_change(),
+ amount_return: this.get_change(),
lines: orderLines,
statement_ids: paymentLines,
pos_session_id: this.pos_session_id,
diff --git a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml
index 94d7358e7f..8992e2c80e 100644
--- a/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml
+++ b/addons/point_of_sale/static/src/xml/Screens/OrderManagementScreen/OrderManagementScreen.xml
@@ -9,7 +9,7 @@