[IMP] account, l10n_it*: ActionableErrors in Send&Print
All the pre-sending checks that were done on the invoices before sending are now split by model, so that there's more separation.
ActionableErrors is used to show all the errors and give the user a quick action to fix the problems.
"These partners have an incomplete address, please verify xyz"
-> View partners ----> <partners view>
- Tooltip for IT XML export when readonly
We relied on the Italian xml export field's help tooltip before.
Now like other export options we have a dedicated <i> tag only shown when the field is readonly.
- Cannot change to test/official if you don't register
If you have Demo edi_mode, and you change to Test/Prod but don't register,
the EDI proxy user won't be deleted, and the edi_mode won't be saved.
So you think you changed to Test, but you haven't.
For compatibility in 17.0
- `l10n_it_edi_warning_message` must be maintained for the old view to work and make sense.
- Modified `l10n_it_edi_warning_message` becomes then `l10n_it_edi_actionable_errors`
Part-of: odoo/odoo#142596
This commit is contained in:
@@ -82,8 +82,8 @@ registry.category("web_tour.tours").add('account_tour', {
|
||||
content: _t("Send the invoice to the customer and check what he'll receive."),
|
||||
position: "bottom",
|
||||
}, {
|
||||
trigger: "button[name=action_open_partners_without_email]",
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice], [name=move_type][raw-value=out_invoice]",
|
||||
trigger: "div[name=partner_missing_email] a",
|
||||
extra_trigger: "[name=move_type] [raw-value=out_invoice]",
|
||||
content: _t("Complete the partner data with email"),
|
||||
}, {
|
||||
trigger: ".o_field_widget[name=email] input, input[name=email]",
|
||||
|
||||
@@ -41,7 +41,7 @@ class AccountMoveSend(models.TransientModel):
|
||||
readonly=False,
|
||||
)
|
||||
display_mail_composer = fields.Boolean(compute='_compute_send_mail_extra_fields')
|
||||
send_mail_warning_message = fields.Boolean(compute='_compute_send_mail_extra_fields')
|
||||
send_mail_warning_message = fields.Json(compute='_compute_send_mail_extra_fields')
|
||||
send_mail_readonly = fields.Boolean(compute='_compute_send_mail_extra_fields')
|
||||
mail_template_id = fields.Many2one(
|
||||
comodel_name='mail.template',
|
||||
@@ -254,7 +254,17 @@ class AccountMoveSend(models.TransientModel):
|
||||
wizard.display_mail_composer = wizard.mode == 'invoice_single'
|
||||
invoices_without_mail_data = wizard.move_ids.filtered(lambda x: not x.partner_id.email)
|
||||
wizard.send_mail_readonly = invoices_without_mail_data == wizard.move_ids
|
||||
wizard.send_mail_warning_message = bool(invoices_without_mail_data) and (wizard.checkbox_send_mail or wizard.send_mail_readonly)
|
||||
if not (invoices_without_mail_data and wizard.checkbox_send_mail or wizard.send_mail_readonly):
|
||||
wizard.send_mail_warning_message = False
|
||||
else:
|
||||
partners = invoices_without_mail_data.partner_id
|
||||
wizard.send_mail_warning_message = {
|
||||
**(wizard.send_mail_warning_message or {}),
|
||||
'partner_missing_email': {
|
||||
'message': _("Partner(s) should have an email address."),
|
||||
'action_text': _("View Partner(s)"),
|
||||
'action': partners._get_records_action(name=_("Check Partner(s)"))
|
||||
}}
|
||||
|
||||
@api.depends('mail_template_id')
|
||||
def _compute_mail_lang(self):
|
||||
@@ -326,25 +336,8 @@ class AccountMoveSend(models.TransientModel):
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def action_open_partners_without_email(self, res_ids=None):
|
||||
partners = self.move_ids.mapped("partner_id").filtered(lambda x: not x.email)
|
||||
if len(partners) == 1:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
'res_id': partners.id,
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'res.partner',
|
||||
'view_mode': 'tree,form',
|
||||
'target': 'current',
|
||||
'name': _('Partners without email'),
|
||||
'context': {'create': False, 'delete': False},
|
||||
'domain': [('id', 'in', partners.ids)],
|
||||
}
|
||||
# TODO: remove this method in master
|
||||
return self.move_ids.mapped("partner_id").filtered(lambda x: not x.email)._get_records_action(name=_("Partners without email"))
|
||||
|
||||
@api.model
|
||||
def _need_invoice_document(self, invoice):
|
||||
|
||||
@@ -18,16 +18,8 @@
|
||||
<field name="display_mail_composer" invisible="1"/>
|
||||
<field name="mail_lang" invisible="1"/>
|
||||
|
||||
<!-- Warnings -->
|
||||
<div name="warnings">
|
||||
<div class="alert alert-warning"
|
||||
role="alert"
|
||||
invisible="not send_mail_warning_message">
|
||||
Please specify an email address on the
|
||||
<button name="action_open_partners_without_email"
|
||||
type="object"
|
||||
class="btn btn-link p-0 align-baseline fst-italic">following partners.</button>
|
||||
</div>
|
||||
<div class="m-0" name="warnings">
|
||||
<field name="send_mail_warning_message" class="o_field_html" widget="actionable_errors"/>
|
||||
</div>
|
||||
|
||||
<!-- Options -->
|
||||
|
||||
@@ -51,6 +51,11 @@ class AccountTax(models.Model):
|
||||
if tax.l10n_it_exempt_reason == 'N6' and tax._l10n_it_is_split_payment():
|
||||
raise UserError(_("Split Payment is not compatible with exoneration of kind 'N6'"))
|
||||
|
||||
def _l10n_it_get_tax_kind(self):
|
||||
if self.amount_type == 'percent' and self.amount >= 0:
|
||||
return 'vat'
|
||||
return None
|
||||
|
||||
def _l10n_it_filter_kind(self, kind):
|
||||
""" This can be overridden by l10n_it_edi_withholding for different kind of taxes (withholding, pension_fund)."""
|
||||
return self if kind == 'vat' else self.env['account.tax']
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
'name': 'Italy - E-invoicing',
|
||||
'countries': ['it'],
|
||||
'version': '0.3',
|
||||
'version': '0.4',
|
||||
'depends': [
|
||||
'l10n_it',
|
||||
'account_edi_proxy_client',
|
||||
|
||||
@@ -115,8 +115,8 @@ class AccountMove(models.Model):
|
||||
@api.depends('commercial_partner_id.l10n_it_pa_index', 'company_id')
|
||||
def _compute_l10n_it_partner_pa(self):
|
||||
for move in self:
|
||||
move.l10n_it_partner_pa = (move.country_code == 'IT' and move.commercial_partner_id.l10n_it_pa_index and
|
||||
len(move.commercial_partner_id.l10n_it_pa_index) == 6)
|
||||
partner = move.commercial_partner_id
|
||||
move.l10n_it_partner_pa = partner and partner._l10n_it_edi_is_public_administration()
|
||||
|
||||
@api.depends('move_type', 'line_ids.tax_tag_ids')
|
||||
def _compute_l10n_it_edi_is_self_invoice(self):
|
||||
@@ -184,10 +184,8 @@ class AccountMove(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
|
||||
if (errors := self._l10n_it_edi_export_data_check()):
|
||||
message = _("Errors occured while creating the e-invoice file.")
|
||||
message += "\n- " + "\n- ".join(errors)
|
||||
raise UserError(message)
|
||||
if self._l10n_it_edi_export_data_check():
|
||||
raise UserError(_("The invoices you're trying to send have incomplete or incorrect data, please verify before sending."))
|
||||
|
||||
attachment_vals = self._l10n_it_edi_get_attachment_values(pdf_values=None)
|
||||
self.env['ir.attachment'].create(attachment_vals)
|
||||
@@ -460,10 +458,11 @@ class AccountMove(models.Model):
|
||||
self.ensure_one()
|
||||
template_reference = self.env.ref('l10n_it_edi.account_invoice_it_simplified_FatturaPA_export', raise_if_not_found=False)
|
||||
buyer = self.commercial_partner_id
|
||||
checks = ['partner_address_missing', 'partner_vat_codice_fiscale_missing']
|
||||
return bool(
|
||||
template_reference
|
||||
and not self.l10n_it_edi_is_self_invoice
|
||||
and self._l10n_it_edi_export_buyer_data_check()
|
||||
and list(buyer._l10n_it_edi_export_check(checks).keys()) == ['partner_address_missing']
|
||||
and (not buyer.country_id or buyer.country_id.code == 'IT')
|
||||
and (buyer.l10n_it_codice_fiscale or (buyer.vat and (buyer.vat[:2].upper() == 'IT' or buyer.vat[:2].isdecimal())))
|
||||
and self.amount_total <= 400
|
||||
@@ -1031,117 +1030,67 @@ class AccountMove(models.Model):
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def _l10n_it_edi_export_data_check(self):
|
||||
errors = self._l10n_it_edi_base_export_data_check()
|
||||
if not self._l10n_it_edi_is_simplified():
|
||||
errors += self._l10n_it_edi_export_buyer_data_check()
|
||||
""" This function checks the Settings, Company, Partners, Moves involved in the
|
||||
sending activity and returns an errors dictionary ready for the
|
||||
actionable_errors widget to display. """
|
||||
|
||||
companies = self.mapped("company_id")
|
||||
companies_partners = companies.mapped("partner_id")
|
||||
moves_full = self.filtered(lambda m: not m._l10n_it_edi_is_simplified())
|
||||
moves_simplified = self.filtered(lambda m: m._l10n_it_edi_is_simplified())
|
||||
|
||||
full = moves_full.mapped("commercial_partner_id").filtered(lambda p: p not in companies_partners)
|
||||
simplified = moves_simplified.mapped("commercial_partner_id").filtered(lambda p: p not in companies_partners | full)
|
||||
representatives = companies.mapped("l10n_it_tax_representative_partner_id").filtered(lambda p: p not in companies_partners | simplified | full)
|
||||
|
||||
return {
|
||||
**companies._l10n_it_edi_export_check(),
|
||||
**full._l10n_it_edi_export_check(['partner_address_missing']),
|
||||
**simplified._l10n_it_edi_export_check(['partner_country_missing']),
|
||||
**(simplified | full)._l10n_it_edi_export_check(['partner_vat_codice_fiscale_missing']),
|
||||
**representatives._l10n_it_edi_export_check(['partner_vat_missing']),
|
||||
**self._l10n_it_edi_base_export_check(),
|
||||
**self._l10n_it_edi_export_taxes_check(),
|
||||
}
|
||||
|
||||
def _l10n_it_edi_base_export_check(self):
|
||||
def build_error(message, records):
|
||||
return {
|
||||
'message': message,
|
||||
**({
|
||||
'action_text': _("View invoice(s)"),
|
||||
'action': records._get_records_action(name=_("Invoice(s) to check")),
|
||||
} if len(self) > 1 else {})
|
||||
}
|
||||
|
||||
errors = {}
|
||||
if moves := self.filtered(lambda move: move.l10n_it_edi_is_self_invoice and move._l10n_it_edi_services_or_goods() == 'both'):
|
||||
errors['move_reverse_charge_with_mixed_services_and_goods'] = build_error(
|
||||
message=_("Cannot apply Reverse Charge to bills which contains both services and goods."),
|
||||
records=moves)
|
||||
if pa_moves := self.filtered(lambda move: move.company_id.partner_id._l10n_it_edi_is_public_administration()):
|
||||
if moves := pa_moves.filtered(lambda move: move.l10n_it_origin_document_type):
|
||||
message = _("Your company belongs to the Public Administration, please fill out Origin Document Type field in the Electronic Invoicing tab.")
|
||||
errors['move_missing_origin_document'] = build_error(message=message, records=moves)
|
||||
if moves := pa_moves.filtered(lambda move: move.l10n_it_origin_document_date and move.l10n_it_origin_document_date > fields.Date.today()):
|
||||
message = _("The Origin Document Date cannot be in the future.")
|
||||
errors['move_future_origin_document_date'] = build_error(message=message, records=moves)
|
||||
return errors
|
||||
|
||||
def _l10n_it_edi_format_export_data_errors(self):
|
||||
messages = (
|
||||
self._l10n_it_edi_format_errors(move.name + ":" if len(self) > 1 else False, move_warnings)
|
||||
for move in self if (move_warnings := move._l10n_it_edi_export_data_check())
|
||||
)
|
||||
return Markup("<br>").join(messages) or False
|
||||
|
||||
def _l10n_it_edi_base_export_data_check(self):
|
||||
errors = []
|
||||
seller = self.company_id
|
||||
buyer = self.commercial_partner_id
|
||||
is_self_invoice = self.l10n_it_edi_is_self_invoice
|
||||
if is_self_invoice:
|
||||
seller, buyer = buyer, seller
|
||||
|
||||
# <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 self.company_id.vat:
|
||||
errors.append(_("%s must have a VAT number", seller.display_name))
|
||||
if seller.vat and 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 is_self_invoice and not seller.l10n_it_codice_fiscale:
|
||||
errors.append(_("%s must have a codice fiscale number", seller.display_name))
|
||||
|
||||
# <1.2.1.8>
|
||||
if not is_self_invoice and 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 not is_self_invoice and 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 a VAT number and/or a tax code (Codice Fiscale).", buyer.display_name))
|
||||
|
||||
if is_self_invoice and self._l10n_it_edi_services_or_goods() == 'both':
|
||||
errors.append(_("Cannot apply Reverse Charge to a bill which contains both services and goods."))
|
||||
|
||||
if is_self_invoice and not buyer.partner_id.l10n_it_pa_index:
|
||||
errors.append(_("Vendor bills sent as self-invoices to the SdI require a valid PA Index (Codice Destinatario) on the company's contact."))
|
||||
|
||||
for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id):
|
||||
if not tax_line.tax_line_id.l10n_it_exempt_reason 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 self.l10n_it_partner_pa:
|
||||
if not self.l10n_it_origin_document_type:
|
||||
errors.append(_("This invoice targets the Public Administration, please fill out"
|
||||
" Origin Document Type field in the Electronic Invoicing tab."))
|
||||
if self.l10n_it_origin_document_date and self.l10n_it_origin_document_date > fields.Date.today():
|
||||
errors.append(_("The Origin Document Date cannot be in the future."))
|
||||
|
||||
errors += self._l10n_it_edi_export_taxes_data_check()
|
||||
|
||||
return errors
|
||||
|
||||
def _l10n_it_edi_export_taxes_data_check(self):
|
||||
"""
|
||||
Can be overridden by submodules like l10n_it_edi_withholding, which also allows for withholding and pension_fund taxes.
|
||||
"""
|
||||
errors = []
|
||||
for invoice_line in self.invoice_line_ids.filtered(lambda x: x.display_type == 'product'):
|
||||
all_taxes = invoice_line.tax_ids.flatten_taxes_hierarchy()
|
||||
vat_taxes = all_taxes.filtered(lambda t: t.amount_type == 'percent' and t.amount >= 0)
|
||||
if len(vat_taxes) != 1:
|
||||
errors.append(_("In line %s, you must select one and only one VAT tax.", invoice_line.name))
|
||||
return errors
|
||||
|
||||
def _l10n_it_edi_export_buyer_data_check(self):
|
||||
errors = []
|
||||
buyer = self.commercial_partner_id
|
||||
|
||||
# <1.4.2>
|
||||
if not buyer.street and not buyer.street2:
|
||||
errors.append(_("%s must have a street.", buyer.display_name))
|
||||
if not buyer.country_id:
|
||||
errors.append(_("%s must have a country.", buyer.display_name))
|
||||
if not buyer.zip:
|
||||
errors.append(_("%s must have a post code.", buyer.display_name))
|
||||
elif 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))
|
||||
|
||||
for tax_line in self.line_ids.filtered(lambda line: line.tax_line_id):
|
||||
if not tax_line.tax_line_id.l10n_it_exempt_reason 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))
|
||||
|
||||
return errors
|
||||
def _l10n_it_edi_export_taxes_check(self):
|
||||
if move_lines := self.mapped("invoice_line_ids").filtered(lambda line:
|
||||
line.display_type == 'product'
|
||||
and len(line.tax_ids.flatten_taxes_hierarchy()._l10n_it_filter_kind('vat')) != 1
|
||||
):
|
||||
return {
|
||||
'move_only_one_vat_tax_per_line': {
|
||||
'message': _("Invoices must have exactly one VAT tax set per line."),
|
||||
**({
|
||||
'action_text': _("View invoice(s)"),
|
||||
'action': move_lines.mapped("move_id")._get_records_action(name=_("Check taxes on invoice lines")),
|
||||
} if len(self) > 1 else {})
|
||||
}}
|
||||
return {}
|
||||
|
||||
def _l10n_it_edi_get_formatters(self):
|
||||
def format_alphanumeric(text, maxlen=None):
|
||||
|
||||
@@ -115,3 +115,41 @@ class ResCompany(models.Model):
|
||||
def _compute_l10n_it_edi_proxy_user_id(self):
|
||||
for company in self:
|
||||
company.l10n_it_edi_proxy_user_id = company.account_edi_proxy_client_ids.filtered(lambda x: x.proxy_type == 'l10n_it_edi')
|
||||
|
||||
def _l10n_it_edi_export_check(self):
|
||||
checks = {
|
||||
'company_vat_codice_fiscale_missing': {
|
||||
'fields': [('vat', 'l10n_it_codice_fiscale')],
|
||||
'message': _("Company/ies should have a VAT number or Codice Fiscale."),
|
||||
},
|
||||
'company_address_missing': {
|
||||
'fields': [('street', 'street2'), ('zip',), ('city',), ('country_id',)],
|
||||
'message': _("Company/ies should have a complete address, verify their Street, City, Zipcode and Country."),
|
||||
},
|
||||
'company_l10n_it_tax_system_missing': {
|
||||
'fields': [('l10n_it_tax_system',)],
|
||||
'message': _("Company/ies should have a Tax System"),
|
||||
},
|
||||
}
|
||||
errors = {}
|
||||
for key, check in checks.items():
|
||||
for fields_tuple in check.pop('fields'):
|
||||
if invalid_records := self.filtered(lambda record: not any(record[field] for field in fields_tuple)):
|
||||
errors[key] = {
|
||||
'message': check['message'],
|
||||
'action_text': _("View Company/ies"),
|
||||
'action': invalid_records._get_records_action(name=_("Check Company Data")),
|
||||
}
|
||||
if self.filtered(lambda x: not x.l10n_it_edi_proxy_user_id):
|
||||
new_context = {
|
||||
**self.env.context,
|
||||
'module': 'account',
|
||||
'default_search_setting': _("Italian Electronic Invoicing"),
|
||||
'bin_size': False,
|
||||
}
|
||||
errors['settings_l10n_it_edi_proxy_user_id'] = {
|
||||
'message': _("You must accept the terms and conditions in the Settings to use the IT EDI."),
|
||||
'action_text': _("View Settings"),
|
||||
'action': self.env['res.config.settings']._get_records_action(name=_("Settings"), context=new_context),
|
||||
}
|
||||
return errors
|
||||
|
||||
@@ -36,8 +36,7 @@ class ResPartner(models.Model):
|
||||
def _l10n_it_edi_is_public_administration(self):
|
||||
""" Returns True if the destination of the FatturaPA belongs to the Public Administration. """
|
||||
self.ensure_one()
|
||||
return len(self.l10n_it_pa_index or '') == 6
|
||||
|
||||
return self.country_id.code == 'IT' and len(self.l10n_it_pa_index or '') == 6
|
||||
|
||||
def _l10n_it_edi_get_values(self):
|
||||
""" Generates all partner values needed by l10n_it_edi XML export.
|
||||
@@ -148,3 +147,39 @@ class ResPartner(models.Model):
|
||||
for record in self:
|
||||
if record.l10n_it_codice_fiscale and (not codicefiscale.is_valid(record.l10n_it_codice_fiscale) and not iva.is_valid(record.l10n_it_codice_fiscale)):
|
||||
raise UserError(_("Invalid Codice Fiscale '%s': should be like 'MRTMTT91D08F205J' for physical person and '12345670546' for businesses.", record.l10n_it_codice_fiscale))
|
||||
|
||||
def _l10n_it_edi_export_check(self, checks=None):
|
||||
checks = checks or ['partner_vat_codice_fiscale_missing', 'partner_address_missing']
|
||||
fields_to_check = {
|
||||
'partner_vat_missing': {
|
||||
'fields': [('vat',)],
|
||||
'message': _("Partner(s) should have a VAT number."),
|
||||
},
|
||||
'partner_vat_codice_fiscale_missing': {
|
||||
'fields': [('vat', 'l10n_it_codice_fiscale')],
|
||||
'message': _("Partner(s) should have a VAT number or Codice Fiscale."),
|
||||
},
|
||||
'partner_country_missing': {
|
||||
'fields': [('country_id',)],
|
||||
'message': _("Partner(s) should have a Country when used for simplified invoices."),
|
||||
},
|
||||
'partner_address_missing': {
|
||||
'fields': [('street', 'street2'), ('zip',), ('city',), ('country_id',)],
|
||||
'message': _("Partner(s) should have a complete address, verify their Street, City, Zipcode and Country."),
|
||||
},
|
||||
}
|
||||
selected_checks = {k: v for k, v in fields_to_check.items() if k in checks}
|
||||
single_views = [(False, 'form')]
|
||||
list_view = (self.env.ref('l10n_it_edi.res_partner_tree_l10n_it', raise_if_not_found=False))
|
||||
multi_views = [(list_view.id if list_view else False, 'list'), (False, 'form')]
|
||||
errors = {}
|
||||
for key, check in selected_checks.items():
|
||||
for fields_tuple in check['fields']:
|
||||
if invalid_records := self.filtered(lambda record: not any(record[field] for field in fields_tuple)):
|
||||
views = single_views if len(invalid_records) == 1 else multi_views
|
||||
errors[key] = {
|
||||
'message': check['message'],
|
||||
'action_text': _("View Partner(s)"),
|
||||
'action': invalid_records._get_records_action(name=_("Check Partner(s)"), views=views),
|
||||
}
|
||||
return errors
|
||||
|
||||
@@ -230,10 +230,7 @@ class TestItEdiExport(TestItEdi):
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
expected = ['Alessi must have a street.', 'Alessi must have a country.', 'Alessi must have a post code.', 'Alessi must have a city.']
|
||||
actual = invoice._l10n_it_edi_export_data_check()
|
||||
self.assertEqual(expected, actual)
|
||||
self.assertEqual(['partner_address_missing'], list(invoice._l10n_it_edi_export_data_check().keys()))
|
||||
|
||||
def test_invoice_non_domestic_simplified(self):
|
||||
invoice = self.env['account.move'].with_company(self.company).create({
|
||||
@@ -249,9 +246,7 @@ class TestItEdiExport(TestItEdi):
|
||||
}),
|
||||
],
|
||||
})
|
||||
expected = ['Alessi must have a street.', 'Alessi must have a post code.', 'Alessi must have a city.']
|
||||
actual = invoice._l10n_it_edi_export_data_check()
|
||||
self.assertEqual(expected, actual)
|
||||
self.assertEqual(['partner_address_missing'], list(invoice._l10n_it_edi_export_data_check().keys()))
|
||||
|
||||
def test_invoice_zero_percent_taxes(self):
|
||||
tax_zero_percent_hundred_percent_repartition = self.env['account.tax'].with_company(self.company).create({
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_partner_tree_l10n_it" model="ir.ui.view">
|
||||
<field name="name">res.partner.tree.l10n.it</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="inherit_id" ref="base.view_partner_tree"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='vat']" position="after">
|
||||
<field name="l10n_it_codice_fiscale"/>
|
||||
<field name="l10n_it_pa_index"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_partner_form_l10n_it" model="ir.ui.view">
|
||||
<field name="name">res.partner.form.l10n.it</field>
|
||||
<field name="model">res.partner</field>
|
||||
@@ -109,7 +122,7 @@
|
||||
<div class="alert alert-warning" role="alert"
|
||||
invisible="not l10n_it_edi_header
|
||||
or state == 'draft'
|
||||
or l10n_it_edi_state in (False, 'forwarded', 'accepted_by_pa_partner', 'accepted_by_pa_partner_after_expiry', 'forward_failed')">
|
||||
or l10n_it_edi_state in ('forwarded', 'accepted_by_pa_partner', 'accepted_by_pa_partner_after_expiry', 'forward_failed')">
|
||||
<div class="p-0 m-0"><i class='fa fa-warning' role="img" title="EDI (Italy)"/><span class="mx-1">E-invoicing (Italy)</span></div>
|
||||
<field name="l10n_it_edi_header"/>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//block[@id='account_vendor_bills']" position="after">
|
||||
<block title="Electronic Document Invoicing" invisible="country_code != 'IT'" id='account_edi'>
|
||||
<block title="Italian Electronic Invoicing" invisible="country_code != 'IT'" id='account_edi'>
|
||||
<setting>
|
||||
<div class="group-content">
|
||||
<field name="l10n_it_edi_proxy_current_state" invisible="1"/>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from markupsafe import Markup, escape
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.addons.base.models.ir_qweb_fields import nl2br
|
||||
|
||||
|
||||
class AccountMoveSend(models.TransientModel):
|
||||
_inherit = 'account.move.send'
|
||||
|
||||
l10n_it_edi_warning_message = fields.Html(compute='_compute_l10n_it_edi_xml_export')
|
||||
|
||||
l10n_it_edi_warning_message = fields.Html(compute='_compute_l10n_it_edi_warning_message')
|
||||
l10n_it_edi_actionable_errors = fields.Json(compute='_compute_l10n_it_edi_xml_export')
|
||||
l10n_it_edi_enable_xml_export = fields.Boolean(compute='_compute_l10n_it_edi_xml_export')
|
||||
l10n_it_edi_readonly_xml_export = fields.Boolean(compute='_compute_l10n_it_edi_xml_export')
|
||||
l10n_it_edi_checkbox_xml_export = fields.Boolean('E-invoice XML',
|
||||
compute='_compute_l10n_it_edi_checkbox_xml_export',
|
||||
store=True,
|
||||
readonly=False,
|
||||
help="Create the e-invoice XML ready to be sent to the Italian Tax Agency.\n"
|
||||
"It is set as readonly if a report has already been created, to avoid inconsistencies.\n"
|
||||
"To re-enable it, delete the PDF attachment.")
|
||||
readonly=False)
|
||||
|
||||
l10n_it_edi_enable_send = fields.Boolean(compute='_compute_l10n_it_edi_enable_readonly_send')
|
||||
l10n_it_edi_readonly_send = fields.Boolean(compute='_compute_l10n_it_edi_enable_readonly_send')
|
||||
@@ -37,30 +37,54 @@ class AccountMoveSend(models.TransientModel):
|
||||
# COMPUTE/CONSTRAINS METHODS
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@api.depends('l10n_it_edi_actionable_errors')
|
||||
def _compute_l10n_it_edi_warning_message(self):
|
||||
# To be removed -- Proxy feature to be replaced with actionable_errors as soon as the user updates the module
|
||||
for wizard in self:
|
||||
messages = []
|
||||
if wizard.l10n_it_edi_actionable_errors:
|
||||
messages.append(_("Please upgrade the Italian EDI module to update this widget."))
|
||||
messages.append(_("Go to Applications page and update the 'Italia - Fatturazione Elettronica' module."))
|
||||
messages.append("")
|
||||
for error_key, error_data in wizard.l10n_it_edi_actionable_errors.items():
|
||||
message = error_data['message']
|
||||
split = error_key.split("_")
|
||||
if len(split) > 1 and (model_id := {
|
||||
'partner': 'res.partner',
|
||||
'move': 'account.move',
|
||||
'company': 'res.company'
|
||||
}.get(split[0], None)):
|
||||
action = error_data['action']
|
||||
if 'res_id' in action:
|
||||
record_ids = [action['res_id']]
|
||||
else:
|
||||
record_ids = action['domain'][0][2]
|
||||
records = self.env[model_id].browse(record_ids)
|
||||
message = f"{message} - {', '.join(records.mapped('display_name'))}"
|
||||
messages.append(nl2br(escape(message)))
|
||||
wizard.l10n_it_edi_warning_message = Markup("<br/>").join(messages)
|
||||
|
||||
@api.depends('move_ids')
|
||||
def _compute_l10n_it_edi_xml_export(self):
|
||||
for wizard in self:
|
||||
if wizard.company_id.account_fiscal_country_id.code == 'IT':
|
||||
if not wizard.company_id.l10n_it_edi_proxy_user_id:
|
||||
wizard.l10n_it_edi_warning_message = _("You must accept the terms and conditions in the Settings to use the IT EDI.")
|
||||
else:
|
||||
wizard.l10n_it_edi_warning_message = wizard.move_ids._l10n_it_edi_format_export_data_errors()
|
||||
has_pdf_but_no_xml = any(move.invoice_pdf_report_id and not move.l10n_it_edi_attachment_id for move in wizard.move_ids)
|
||||
all_have_xml = all(move.l10n_it_edi_attachment_id for move in wizard.move_ids)
|
||||
wizard.l10n_it_edi_actionable_errors = self.move_ids._l10n_it_edi_export_data_check()
|
||||
wizard.l10n_it_edi_enable_xml_export = any(m._l10n_it_edi_ready_for_xml_export() for m in wizard.move_ids)
|
||||
wizard.l10n_it_edi_readonly_xml_export = bool(wizard.l10n_it_edi_warning_message) or has_pdf_but_no_xml or all_have_xml
|
||||
wizard.l10n_it_edi_readonly_xml_export = wizard.l10n_it_edi_actionable_errors or has_pdf_but_no_xml or all_have_xml
|
||||
else:
|
||||
wizard.l10n_it_edi_warning_message = False
|
||||
wizard.l10n_it_edi_actionable_errors = False
|
||||
wizard.l10n_it_edi_enable_xml_export = False
|
||||
wizard.l10n_it_edi_readonly_xml_export = False
|
||||
|
||||
@api.depends('move_ids', 'l10n_it_edi_checkbox_xml_export', 'l10n_it_edi_warning_message')
|
||||
@api.depends('move_ids', 'l10n_it_edi_checkbox_xml_export', 'l10n_it_edi_actionable_errors')
|
||||
def _compute_l10n_it_edi_enable_readonly_send(self):
|
||||
for wizard in self:
|
||||
if wizard.company_id.account_fiscal_country_id.code == 'IT':
|
||||
xml_already_sent = all(m.l10n_it_edi_state not in (False, 'rejected') for m in wizard.move_ids)
|
||||
wizard.l10n_it_edi_enable_send = wizard.l10n_it_edi_checkbox_xml_export
|
||||
wizard.l10n_it_edi_readonly_send = bool(wizard.l10n_it_edi_warning_message) or xml_already_sent
|
||||
wizard.l10n_it_edi_readonly_send = bool(wizard.l10n_it_edi_actionable_errors or xml_already_sent)
|
||||
else:
|
||||
wizard.l10n_it_edi_enable_send = False
|
||||
wizard.l10n_it_edi_readonly_send = False
|
||||
|
||||
@@ -10,15 +10,18 @@
|
||||
<field name="l10n_it_edi_enable_xml_export" invisible="1"/>
|
||||
<field name="l10n_it_edi_readonly_send" invisible="1"/>
|
||||
<field name="l10n_it_edi_enable_send" invisible="1"/>
|
||||
<div class="alert alert-warning mb-8" role="alert" invisible="not l10n_it_edi_warning_message">
|
||||
<field name="l10n_it_edi_warning_message"/>
|
||||
</div>
|
||||
<field name="l10n_it_edi_actionable_errors" class="o_field_html" widget="actionable_errors"/>
|
||||
</xpath>
|
||||
<xpath expr="//div[@name='option_send_mail']" position='after'>
|
||||
<div name="option_l10n_it_edi">
|
||||
<div name="option_l10n_it_edi_xml_export" invisible="not l10n_it_edi_enable_xml_export">
|
||||
<field name="l10n_it_edi_checkbox_xml_export" readonly="l10n_it_edi_readonly_xml_export"/>
|
||||
<b><label for="l10n_it_edi_checkbox_xml_export"/></b>
|
||||
<i class="fa fa-question-circle ml4"
|
||||
role="img"
|
||||
aria-label="Warning"
|
||||
invisible="not l10n_it_edi_readonly_xml_export"
|
||||
title="Create the e-invoice XML ready to be sent to the Italian Tax Agency. It is set as readonly if a report has already been created, to avoid inconsistencies. To re-enable it, delete the PDF attachment."/>
|
||||
</div>
|
||||
<div name="option_l10n_it_edi_send" invisible="not l10n_it_edi_enable_send">
|
||||
<field name="l10n_it_edi_checkbox_send" readonly="l10n_it_edi_readonly_send"/>
|
||||
|
||||
@@ -19,10 +19,11 @@ class AccountMove(models.Model):
|
||||
@api.depends('amount_total_signed')
|
||||
def _compute_amount_extended(self):
|
||||
for move in self:
|
||||
totals = dict(vat=0.0, withholding=0.0, pension_fund=0.0)
|
||||
totals = {None: 0.0, 'vat':0.0, 'withholding': 0.0, 'pension_fund': 0.0}
|
||||
if move.is_invoice(True):
|
||||
for line in [line for line in move.line_ids if line.tax_line_id]:
|
||||
totals[line.tax_line_id._l10n_it_get_tax_kind()] -= line.balance
|
||||
kind = line.tax_line_id._l10n_it_get_tax_kind()
|
||||
totals[kind] -= line.balance
|
||||
move.l10n_it_amount_vat_signed = totals['vat']
|
||||
move.l10n_it_amount_withholding_signed = totals['withholding']
|
||||
move.l10n_it_amount_pension_fund_signed = totals['pension_fund']
|
||||
|
||||
@@ -89,7 +89,7 @@ class AccountTax(models.Model):
|
||||
def _l10n_it_get_tax_kind(self):
|
||||
return ((self.l10n_it_withholding_type and 'withholding')
|
||||
or (self.l10n_it_pension_fund_type and 'pension_fund')
|
||||
or 'vat')
|
||||
or super()._l10n_it_get_tax_kind())
|
||||
|
||||
def _l10n_it_filter_kind(self, kind):
|
||||
""" Filters taxes depending on _l10n_it_get_tax_kind. """
|
||||
|
||||
Reference in New Issue
Block a user