788 lines
37 KiB
Python
788 lines
37 KiB
Python
from lxml import etree
|
|
from urllib.parse import urlencode
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools import cleanup_xml_node
|
|
from odoo.tools.sql import column_exists, create_column
|
|
|
|
from odoo.addons.l10n_gr_edi.models.l10n_gr_edi_document import _make_mydata_request
|
|
from odoo.addons.l10n_gr_edi.models.preferred_classification import (
|
|
CLASSIFICATION_CATEGORY_EXPENSE,
|
|
COMBINATIONS_WITH_POSSIBLE_EMPTY_TYPE,
|
|
INVOICE_TYPES_HAVE_EXPENSE,
|
|
INVOICE_TYPES_HAVE_INCOME,
|
|
INVOICE_TYPES_SELECTION,
|
|
PAYMENT_METHOD_SELECTION,
|
|
TYPES_WITH_CORRELATE_INVOICE,
|
|
TYPES_WITH_FORBIDDEN_CLASSIFICATION,
|
|
TYPES_WITH_FORBIDDEN_COUNTERPART,
|
|
TYPES_WITH_FORBIDDEN_QUANTITY,
|
|
TYPES_WITH_MANDATORY_COUNTERPART,
|
|
TYPES_WITH_MANDATORY_PAYMENT,
|
|
TYPES_WITH_VAT_CATEGORY_8,
|
|
TYPES_WITH_VAT_EXEMPT,
|
|
VALID_TAX_AMOUNTS,
|
|
VALID_TAX_CATEGORY_MAP,
|
|
)
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = 'account.move'
|
|
|
|
l10n_gr_edi_mark = fields.Char(
|
|
string='Mark',
|
|
compute='_compute_from_l10n_gr_edi_document_ids',
|
|
store=True,
|
|
)
|
|
l10n_gr_edi_cls_mark = fields.Char(
|
|
string='Classification Mark',
|
|
compute='_compute_from_l10n_gr_edi_document_ids',
|
|
store=True,
|
|
)
|
|
l10n_gr_edi_document_ids = fields.One2many(
|
|
comodel_name='l10n_gr_edi.document',
|
|
inverse_name='move_id',
|
|
copy=False,
|
|
readonly=True,
|
|
)
|
|
l10n_gr_edi_state = fields.Selection(
|
|
selection=[
|
|
('invoice_sent', 'Invoice sent'),
|
|
('bill_fetched', "Expense classification ready to send"),
|
|
('bill_sent', "Expense classification sent"),
|
|
],
|
|
string='myDATA Status',
|
|
compute='_compute_from_l10n_gr_edi_document_ids',
|
|
store=True,
|
|
tracking=True,
|
|
)
|
|
l10n_gr_edi_available_inv_type = fields.Char(compute='_compute_l10n_gr_edi_available_inv_type')
|
|
l10n_gr_edi_correlation_id = fields.Many2one(
|
|
comodel_name='account.move',
|
|
string='Correlated Invoice',
|
|
)
|
|
l10n_gr_edi_inv_type = fields.Selection(
|
|
selection=INVOICE_TYPES_SELECTION,
|
|
string='myDATA Invoice Type',
|
|
compute='_compute_l10n_gr_edi_inv_type',
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
l10n_gr_edi_payment_method = fields.Selection(
|
|
selection=PAYMENT_METHOD_SELECTION,
|
|
string='Payment Method',
|
|
compute='_compute_l10n_gr_edi_payment_method',
|
|
store=True,
|
|
)
|
|
l10n_gr_edi_alerts = fields.Json(compute='_compute_l10n_gr_edi_alerts')
|
|
l10n_gr_edi_need_correlated = fields.Boolean(compute='_compute_l10n_gr_edi_need_fields')
|
|
l10n_gr_edi_need_payment_method = fields.Boolean(compute='_compute_l10n_gr_edi_need_fields')
|
|
l10n_gr_edi_enable_view_mydata = fields.Boolean(compute='_compute_l10n_gr_edi_enable_fields')
|
|
l10n_gr_edi_enable_send_invoices = fields.Boolean(compute='_compute_l10n_gr_edi_enable_fields')
|
|
l10n_gr_edi_enable_send_expense_classification = fields.Boolean(compute='_compute_l10n_gr_edi_enable_fields')
|
|
l10n_gr_edi_attachment_id = fields.Many2one(
|
|
comodel_name='ir.attachment',
|
|
compute='_compute_from_l10n_gr_edi_document_ids',
|
|
store=True,
|
|
)
|
|
|
|
def _auto_init(self):
|
|
"""
|
|
Create all compute-stored fields here to avoid MemoryError when initializing on large databases.
|
|
"""
|
|
for column_name, column_type in (
|
|
('l10n_gr_edi_mark', 'varchar'),
|
|
('l10n_gr_edi_cls_mark', 'varchar'),
|
|
('l10n_gr_edi_state', 'varchar'),
|
|
('l10n_gr_edi_inv_type', 'varchar'),
|
|
('l10n_gr_edi_payment_method', 'varchar'),
|
|
('l10n_gr_edi_attachment_id', 'int4'),
|
|
):
|
|
if not column_exists(self.env.cr, 'account_move', column_name):
|
|
create_column(self.env.cr, 'account_move', column_name, column_type)
|
|
|
|
return super()._auto_init()
|
|
|
|
################################################################################
|
|
# Standard Field Computes
|
|
################################################################################
|
|
|
|
@api.depends('l10n_gr_edi_document_ids')
|
|
def _compute_from_l10n_gr_edi_document_ids(self):
|
|
self.l10n_gr_edi_state = False
|
|
self.l10n_gr_edi_mark = False
|
|
self.l10n_gr_edi_cls_mark = False
|
|
self.l10n_gr_edi_attachment_id = False
|
|
|
|
for move in self:
|
|
for document in move.l10n_gr_edi_document_ids.sorted():
|
|
if document.state in ('invoice_sent', 'bill_fetched', 'bill_sent'):
|
|
move.l10n_gr_edi_state = document.state
|
|
move.l10n_gr_edi_mark = document.mydata_mark
|
|
move.l10n_gr_edi_cls_mark = document.mydata_cls_mark
|
|
move.l10n_gr_edi_attachment_id = document.attachment_id
|
|
break
|
|
|
|
@api.depends('l10n_gr_edi_state')
|
|
def _compute_show_reset_to_draft_button(self):
|
|
# EXTENDS 'account'
|
|
""" Prevent user from resetting the move to draft if it's already sent to myDATA """
|
|
super()._compute_show_reset_to_draft_button()
|
|
for move in self:
|
|
if move.l10n_gr_edi_state in ('invoice_sent', 'bill_sent'):
|
|
move.show_reset_to_draft_button = False
|
|
|
|
@api.depends('country_code', 'state')
|
|
def _compute_l10n_gr_edi_alerts(self):
|
|
for move in self:
|
|
# Warnings are only calculated when the move state is posted.
|
|
# We use `._origin` to make sure the validated move have all the needed data for validation.
|
|
if move._l10n_gr_edi_eligible_for_mydata():
|
|
move.l10n_gr_edi_alerts = move._origin._l10n_gr_edi_get_pre_error_dict()
|
|
else:
|
|
move.l10n_gr_edi_alerts = False
|
|
|
|
@api.depends('state', 'l10n_gr_edi_state')
|
|
def _compute_l10n_gr_edi_enable_fields(self):
|
|
for move in self:
|
|
common_send_requirements = all((
|
|
move._l10n_gr_edi_eligible_for_mydata(),
|
|
move.l10n_gr_edi_state in (False, 'bill_fetched'),
|
|
))
|
|
move.l10n_gr_edi_enable_view_mydata = all((
|
|
move.country_code == 'GR',
|
|
move.is_invoice(include_receipts=True),
|
|
))
|
|
move.l10n_gr_edi_enable_send_invoices = all((
|
|
common_send_requirements,
|
|
move.is_sale_document(include_receipts=True),
|
|
))
|
|
move.l10n_gr_edi_enable_send_expense_classification = all((
|
|
common_send_requirements,
|
|
move.is_purchase_document(include_receipts=True),
|
|
move.l10n_gr_edi_mark,
|
|
))
|
|
|
|
@api.depends('country_code')
|
|
def _compute_l10n_gr_edi_payment_method(self):
|
|
for move in self:
|
|
if move.country_code == 'GR':
|
|
move.l10n_gr_edi_payment_method = move.l10n_gr_edi_payment_method or '1'
|
|
else:
|
|
move.l10n_gr_edi_payment_method = False
|
|
|
|
################################################################################
|
|
# Dynamic Selection Field Computes
|
|
################################################################################
|
|
|
|
@api.depends('move_type')
|
|
def _compute_l10n_gr_edi_available_inv_type(self):
|
|
for move in self:
|
|
if move.is_sale_document(include_receipts=True):
|
|
move.l10n_gr_edi_available_inv_type = ','.join(INVOICE_TYPES_HAVE_INCOME)
|
|
elif move.is_purchase_document(include_receipts=True):
|
|
move.l10n_gr_edi_available_inv_type = ','.join(INVOICE_TYPES_HAVE_EXPENSE)
|
|
else: # move.move_type == 'entry'
|
|
move.l10n_gr_edi_available_inv_type = False
|
|
|
|
@api.depends('fiscal_position_id', 'l10n_gr_edi_available_inv_type')
|
|
def _compute_l10n_gr_edi_inv_type(self):
|
|
for move in self:
|
|
if move.country_code == 'GR':
|
|
if move.l10n_gr_edi_inv_type or move.move_type == 'entry':
|
|
# If we have previously calculated the inv_type, reuse it here.
|
|
# For entry moves, we want the inv_type to be False. (we don't send anything to myDATA on entry moves)
|
|
move.l10n_gr_edi_inv_type = move.l10n_gr_edi_inv_type
|
|
elif move.move_type in ('out_refund', 'in_refund'):
|
|
# inv_type specific for credit notes
|
|
if move.l10n_gr_edi_correlation_id:
|
|
# when possible, we must add the associate invoice/bill mark (id)
|
|
move.l10n_gr_edi_inv_type = '5.1'
|
|
else:
|
|
move.l10n_gr_edi_inv_type = '5.2'
|
|
else: # move.move_type in ('out_invoice', 'in_invoice', 'out_receipt', 'in_receipt')
|
|
inv_type = '1.1' if move.move_type == 'out_invoice' else '13.1'
|
|
preferred_clss = move.fiscal_position_id.l10n_gr_edi_preferred_classification_ids.filtered(
|
|
lambda p: p.l10n_gr_edi_inv_type in (move.l10n_gr_edi_available_inv_type or "").split(','))
|
|
if preferred_clss:
|
|
inv_type = preferred_clss[0].l10n_gr_edi_inv_type
|
|
move.l10n_gr_edi_inv_type = inv_type
|
|
else:
|
|
move.l10n_gr_edi_inv_type = False
|
|
|
|
@api.depends('l10n_gr_edi_inv_type')
|
|
def _compute_l10n_gr_edi_need_fields(self):
|
|
for move in self:
|
|
move.l10n_gr_edi_need_correlated = all((
|
|
move.l10n_gr_edi_inv_type in TYPES_WITH_CORRELATE_INVOICE,
|
|
move.is_sale_document(include_receipts=True),
|
|
))
|
|
move.l10n_gr_edi_need_payment_method = move.l10n_gr_edi_inv_type in TYPES_WITH_MANDATORY_PAYMENT
|
|
|
|
################################################################################
|
|
# Greece Document Helpers
|
|
################################################################################
|
|
|
|
def _l10n_gr_edi_create_error_document(self, values: dict):
|
|
"""
|
|
Creates ``l10n_gr_edi.document`` of state ``invoice_error`` or ``bill_error``.
|
|
:param values: dictionary in the format of: {'error': <str>, 'xml_content': <optional/str>}
|
|
"""
|
|
self.ensure_one()
|
|
document = self.env['l10n_gr_edi.document'].create({
|
|
'move_id': self.id,
|
|
'state': 'invoice_error' if self.is_sale_document(include_receipts=True) else 'bill_error',
|
|
'message': values['error'],
|
|
})
|
|
if xml_content := values.get('xml_content'):
|
|
document.attachment_id = self.env['ir.attachment'].sudo().create({
|
|
'name': f"mydata_{self.name.replace('/', '_')}.xml",
|
|
'res_model': document._name,
|
|
'res_id': document.id,
|
|
'raw': xml_content,
|
|
'type': 'binary',
|
|
'mimetype': 'application/xml',
|
|
})
|
|
return document
|
|
|
|
def _l10n_gr_edi_create_sent_document(self, values: dict):
|
|
"""
|
|
Creates ``l10n_gr_edi.document`` of state ``invoice_sent`` or ``bill_sent``.
|
|
:param values: dictionary in the format of:
|
|
{
|
|
'mydata_mark': <str>,
|
|
'mydata_cls_mark': <optional/str>,
|
|
'mydata_url': <str>,
|
|
'xml_content': <str>,
|
|
}
|
|
"""
|
|
self.ensure_one()
|
|
document = self.env['l10n_gr_edi.document'].create({
|
|
'move_id': self.id,
|
|
'state': 'invoice_sent' if self.is_sale_document(include_receipts=True) else 'bill_sent',
|
|
'mydata_mark': values['mydata_mark'],
|
|
'mydata_cls_mark': values.get('mydata_cls_mark'),
|
|
'mydata_url': values['mydata_url'],
|
|
})
|
|
document.attachment_id = self.env['ir.attachment'].sudo().create({
|
|
'name': f"mydata_{self.name.replace('/', '_')}.xml",
|
|
'res_model': self._name,
|
|
'res_id': self.id,
|
|
'raw': values['xml_content'],
|
|
'type': 'binary',
|
|
'mimetype': 'application/xml',
|
|
})
|
|
return document
|
|
|
|
################################################################################
|
|
# Helpers
|
|
################################################################################
|
|
|
|
@api.model
|
|
def _l10n_gr_edi_generate_xml_content(self, xml_template, xml_vals):
|
|
xml_content = self.env['ir.qweb']._render(xml_template, xml_vals)
|
|
return etree.tostring(element_or_tree=cleanup_xml_node(xml_content), encoding='ISO-8859-7', standalone='yes')
|
|
|
|
def _l10n_gr_edi_eligible_for_mydata(self):
|
|
"""Shorthand for getting the eligibility of the current move to send to myDATA."""
|
|
self.ensure_one()
|
|
return all((
|
|
self.country_code == 'GR',
|
|
self.state == 'posted',
|
|
))
|
|
|
|
def _get_name_invoice_report(self):
|
|
# EXTENDS account
|
|
self.ensure_one()
|
|
if self.l10n_gr_edi_state == 'invoice_sent':
|
|
return 'l10n_gr_edi.report_invoice_document'
|
|
return super()._get_name_invoice_report()
|
|
|
|
def _l10n_gr_edi_get_extra_invoice_report_values(self):
|
|
"""Get the values used to render the invoice PDF."""
|
|
self.ensure_one()
|
|
document = self.l10n_gr_edi_document_ids.sorted()[:1]
|
|
|
|
if document.state in ('invoice_sent', 'bill_sent'):
|
|
barcode_params = urlencode({
|
|
'barcode_type': 'QR',
|
|
'value': document.mydata_url,
|
|
'width': 180,
|
|
'height': 180,
|
|
})
|
|
return {
|
|
'barcode_src': f'/report/barcode/?{barcode_params}',
|
|
'mydata_mark': document.mydata_mark,
|
|
'mydata_cls_mark': document.mydata_cls_mark,
|
|
}
|
|
else:
|
|
return {}
|
|
|
|
################################################################################
|
|
# Prepare XML Values
|
|
################################################################################
|
|
|
|
def _l10n_gr_edi_add_address_vals(self, values):
|
|
"""
|
|
Adds all the address values needed for the ``invoice_vals`` dictionary.
|
|
The only guaranteed keys in to add in the dictionary is the issuer's VAT, country code, and branch number.
|
|
Everything else is only displayed on some specific case/configuration.
|
|
The appended dictionary will have the following additional keys:
|
|
{
|
|
'issuer_vat_number': <str>,
|
|
'issuer_country': <str>,
|
|
'issuer_branch': <int>,
|
|
'issuer_name': <str | None>,
|
|
'issuer_postal_code': <str | None>,
|
|
'issuer_city': <str | None>,
|
|
'counterpart_vat': <str | None>,
|
|
'counterpart_country': <str | None>,
|
|
'counterpart_branch': <int | None>,
|
|
'counterpart_name': <str | None>,
|
|
'counterpart_postal_code': <str | None>,
|
|
'counterpart_city': <str | None>,
|
|
'counterpart_postal_code': <str | None>,
|
|
'counterpart_city': <str | None>,
|
|
}
|
|
:param dict values: dictionary where the address values will be added
|
|
:rtype: dict[str, str|int]
|
|
"""
|
|
self.ensure_one()
|
|
issuer_not_from_greece = self.company_id.country_code != 'GR'
|
|
inv_type_allows_counterpart = self.l10n_gr_edi_inv_type not in TYPES_WITH_FORBIDDEN_COUNTERPART
|
|
partner_not_from_greece = self.partner_id.country_code != 'GR'
|
|
inv_type_require_counterpart = self.l10n_gr_edi_inv_type in TYPES_WITH_MANDATORY_COUNTERPART
|
|
|
|
conditional_address_keys = ('issuer_name', 'issuer_postal_code', 'issuer_city', 'counterpart_vat', 'counterpart_country',
|
|
'counterpart_branch', 'counterpart_name', 'counterpart_postal_code', 'counterpart_city')
|
|
values.update({
|
|
'issuer_vat_number': self.company_id.vat.replace('EL', '').replace('GR', ''),
|
|
'issuer_country': self.company_id.country_code,
|
|
'issuer_branch': self.company_id.l10n_gr_edi_branch_number or 0,
|
|
**dict.fromkeys(conditional_address_keys),
|
|
})
|
|
|
|
if issuer_not_from_greece:
|
|
values.update({
|
|
'issuer_name': self.company_id.name.encode('ISO-8859-7'),
|
|
'issuer_postal_code': self.company_id.zip,
|
|
'issuer_city': (self.company_id.city or "").encode('ISO-8859-7') or None,
|
|
})
|
|
|
|
if inv_type_allows_counterpart:
|
|
values.update({
|
|
'counterpart_vat': self.commercial_partner_id.vat.replace('EL', '').replace('GR', ''),
|
|
'counterpart_country': self.commercial_partner_id.country_code,
|
|
'counterpart_branch': (self.commercial_partner_id.l10n_gr_edi_branch_number or 0),
|
|
})
|
|
if partner_not_from_greece:
|
|
values['counterpart_name'] = self.commercial_partner_id.name.encode('ISO-8859-7')
|
|
|
|
if inv_type_require_counterpart or (inv_type_allows_counterpart and partner_not_from_greece):
|
|
values.update({
|
|
'counterpart_postal_code': self.commercial_partner_id.zip,
|
|
'counterpart_city': (self.commercial_partner_id.city or "").encode('ISO-8859-7') or None,
|
|
})
|
|
|
|
def _l10n_gr_edi_add_payment_method_vals(self, values):
|
|
"""
|
|
Adds payment values needed for the ``invoice_vals`` dictionary.
|
|
The appended dictionary will have the following additional key:
|
|
{ 'payment_details': [ { 'type': <str>, 'amount': <float> }, ... ] }
|
|
:param dict values:
|
|
:rtype: dict[str, list[dict]]
|
|
"""
|
|
self.ensure_one()
|
|
values.update({'payment_details': []})
|
|
payment_terms = self.line_ids.filtered(lambda line: line.display_type == 'payment_term')
|
|
|
|
for match_field, amount_field in (('debit', 'credit'), ('credit', 'debit')):
|
|
for apr in payment_terms[f'matched_{match_field}_ids']:
|
|
values['payment_details'].append({
|
|
'type': self.l10n_gr_edi_payment_method or '1',
|
|
'amount': apr[f'{amount_field}_amount_currency'],
|
|
})
|
|
|
|
if not values['payment_details'] and self.l10n_gr_edi_inv_type in TYPES_WITH_MANDATORY_PAYMENT:
|
|
# paymentMethods element is required, even if its amount is zero (no payment have been made yet)
|
|
values['payment_details'].append({
|
|
'type': self.l10n_gr_edi_payment_method or '1',
|
|
'amount': 0,
|
|
})
|
|
|
|
@api.model
|
|
def _l10n_gr_edi_common_base_line_details_values(self, base_line):
|
|
"""
|
|
Returns additional income/expense classification items ("icls"/"ecls") if needed for the detail values.
|
|
The returned format is: {'ecls': [ {'category': <str>, 'type': <str>, 'amount': <float>}, ... ], 'icls': <same_as_ecls> }
|
|
:param dict base_line: dictionary obtained from the tax computation helper methods; such as `_get_rounded_base_and_tax_lines`.
|
|
:rtype: dict[str, list[dict]]
|
|
"""
|
|
line = base_line['record']
|
|
net_amount = base_line['tax_details']['raw_total_excluded']
|
|
cls_vals = {'ecls': [], 'icls': []}
|
|
|
|
if line.l10n_gr_edi_cls_category:
|
|
cls_vals_list = cls_vals['ecls'] if line.l10n_gr_edi_cls_category in CLASSIFICATION_CATEGORY_EXPENSE else cls_vals['icls']
|
|
cls_type = line.l10n_gr_edi_cls_type or ''
|
|
if len(cls_type) > 0 and cls_type[0] == 'X': # handle duplicate E3 type on inv type 17.5
|
|
cls_type = cls_type[1:]
|
|
|
|
cls_vals_list.append({
|
|
'category': line.l10n_gr_edi_cls_category,
|
|
'type': cls_type,
|
|
'amount': net_amount,
|
|
})
|
|
if line.l10n_gr_edi_cls_vat:
|
|
cls_vals_list.append({
|
|
'category': '',
|
|
'type': line.l10n_gr_edi_cls_vat,
|
|
'amount': net_amount,
|
|
})
|
|
|
|
return cls_vals
|
|
|
|
@api.model
|
|
def _l10n_gr_edi_add_sum_classification_vals(self, values):
|
|
"""
|
|
Aggregates all amounts from the common categories and types of the list vals from the ``details`` key,
|
|
and then add them to the `values` dictionary parameter.
|
|
[!WARNING!] The `values` parameter **must** have the `details` key.
|
|
let ``XCLSList`` be a list with format of:
|
|
[
|
|
{'category': <str>, 'type': <str>, 'amount': <float>},
|
|
...,
|
|
]
|
|
All in all, a subset of the `values` parameter should follow the following type formats:
|
|
{
|
|
'details': {
|
|
'icls': XCLSList,
|
|
'ecls': XCLSList,
|
|
}
|
|
}
|
|
The `values` dictionary will then be appended with the following keys:
|
|
{
|
|
'summary_icls': XCLSList,
|
|
'summary_ecls': XCLSList,
|
|
}
|
|
:param dict values: the dictionary where the sum classification values will be added.
|
|
:rtype: dict[str, list[dict]]
|
|
"""
|
|
cls_vals_type = dict[tuple[str, str], float]
|
|
icls_vals: cls_vals_type = {}
|
|
ecls_vals: cls_vals_type = {}
|
|
|
|
for detail in values['details']:
|
|
icls_list, ecls_list = detail['icls'], detail['ecls']
|
|
for icls in icls_list:
|
|
category_type = (icls['category'], icls['type'])
|
|
icls_vals.setdefault(category_type, 0)
|
|
icls_vals[category_type] += icls['amount']
|
|
for ecls in ecls_list:
|
|
category_type = (ecls['category'], ecls['type'])
|
|
ecls_vals.setdefault(category_type, 0)
|
|
ecls_vals[category_type] += ecls['amount']
|
|
|
|
for summary_key, cls_vals in (
|
|
('summary_icls', icls_vals),
|
|
('summary_ecls', ecls_vals),
|
|
):
|
|
values[summary_key] = [
|
|
{
|
|
'category': category,
|
|
'type': cls_type,
|
|
'amount': total_amount,
|
|
}
|
|
for (category, cls_type), total_amount in cls_vals.items()
|
|
]
|
|
|
|
def _l10n_gr_edi_get_invoices_xml_vals(self):
|
|
"""
|
|
Generates a dictionary containing the values needed for rendering ``l10n_gr_edi.mydata_invoice`` XML.
|
|
:return: dict
|
|
"""
|
|
xml_vals = {'invoice_values_list': []}
|
|
|
|
for move in self.sorted(key='id'):
|
|
details = []
|
|
base_lines, _tax_lines = move._get_rounded_base_and_tax_lines()
|
|
|
|
for line_no, base_line in enumerate(base_lines, start=1):
|
|
line = base_line['record']
|
|
vat_category = 8
|
|
vat_exemption_category = ''
|
|
if line.tax_ids and move.l10n_gr_edi_inv_type not in TYPES_WITH_VAT_EXEMPT:
|
|
tax = base_line['tax_details']['taxes_data'][0]['tax'] # here, `tax` is guaranteed to be a single `account.tax` record
|
|
vat_category = VALID_TAX_CATEGORY_MAP[int(tax.amount)]
|
|
if vat_category == 7 and move.l10n_gr_edi_inv_type in TYPES_WITH_VAT_CATEGORY_8:
|
|
vat_category = 8
|
|
if vat_category == 7: # Need vat exemption category
|
|
vat_exemption_category = line.l10n_gr_edi_tax_exemption_category
|
|
|
|
details.append({
|
|
'line_number': line_no,
|
|
'quantity': line.quantity if move.l10n_gr_edi_inv_type not in TYPES_WITH_FORBIDDEN_QUANTITY else '',
|
|
'detail_type': line.l10n_gr_edi_detail_type or '',
|
|
'net_value': base_line['tax_details']['raw_total_excluded'],
|
|
'vat_amount': sum(tax_data['tax_amount'] for tax_data in base_line['tax_details']['taxes_data']),
|
|
'vat_category': vat_category,
|
|
'vat_exemption_category': vat_exemption_category,
|
|
**self._l10n_gr_edi_common_base_line_details_values(base_line),
|
|
})
|
|
|
|
invoice_values = {
|
|
'__move__': move, # will not be rendered; for creating {move_id -> move_xml} mapping
|
|
'header_series': '_'.join(move.name.split('/')[:-1]),
|
|
'header_aa': move.name.split('/')[-1],
|
|
'header_issue_date': move.date.isoformat(),
|
|
'header_invoice_type': move.l10n_gr_edi_inv_type,
|
|
'header_currency': move.currency_id.name,
|
|
'header_correlate': move.l10n_gr_edi_correlation_id.l10n_gr_edi_mark or '',
|
|
'details': details,
|
|
'summary_total_net_value': move.amount_untaxed,
|
|
'summary_total_vat_amount': move.amount_tax,
|
|
'summary_total_withheld_amount': 0,
|
|
'summary_total_fees_amount': 0,
|
|
'summary_total_stamp_duty_amount': 0,
|
|
'summary_total_other_taxes_amount': 0,
|
|
'summary_total_deductions_amount': 0,
|
|
'summary_total_gross_value': move.amount_total,
|
|
}
|
|
move._l10n_gr_edi_add_address_vals(invoice_values)
|
|
move._l10n_gr_edi_add_payment_method_vals(invoice_values)
|
|
self._l10n_gr_edi_add_sum_classification_vals(invoice_values)
|
|
xml_vals['invoice_values_list'].append(invoice_values)
|
|
|
|
return xml_vals
|
|
|
|
def _l10n_gr_edi_get_expense_classification_xml_vals(self):
|
|
"""
|
|
Generates a dictionary containing the values needed for rendering ``l10n_gr_edi.mydata_expense_classification`` XML.
|
|
:return: dict
|
|
"""
|
|
xml_vals = {'invoice_values_list': []}
|
|
|
|
for move in self:
|
|
details = []
|
|
base_lines, _tax_lines = move._get_rounded_base_and_tax_lines()
|
|
|
|
for line_no, base_line in enumerate(base_lines, start=1):
|
|
details.append({
|
|
'line_number': line_no,
|
|
**self._l10n_gr_edi_common_base_line_details_values(base_line),
|
|
})
|
|
|
|
xml_vals['invoice_values_list'].append({
|
|
'__move__': move,
|
|
'mark': move.l10n_gr_edi_mark,
|
|
'transaction_mode': '', # Later, add a way to 'reject' received invoices
|
|
'details': details,
|
|
})
|
|
|
|
return xml_vals
|
|
|
|
################################################################################
|
|
# Send Logics
|
|
################################################################################
|
|
|
|
def _l10n_gr_edi_get_pre_error_dict(self):
|
|
"""
|
|
Try to catch all possible errors before sending to myDATA.
|
|
Returns an error dictionary in the format of Actionable Error JSON.
|
|
"""
|
|
self.ensure_one()
|
|
errors = {}
|
|
error_action_company = {'action_text': _("View Company"), 'action': self.company_id._get_records_action(name=_("Company"))}
|
|
error_action_partner = {'action_text': _("View Partner"), 'action': self.commercial_partner_id._get_records_action(name=_("Partner"))}
|
|
error_action_gr_settings = {
|
|
'action_text': _("View Settings"),
|
|
'action': {
|
|
'name': _("Settings"),
|
|
'type': 'ir.actions.act_url',
|
|
'target': 'self',
|
|
'url': '/odoo/settings#l10n_gr_edi_aade_settings',
|
|
},
|
|
}
|
|
|
|
if self.state != 'posted':
|
|
errors['l10n_gr_edi_move_not_posted'] = {
|
|
'message': _("You can only send to myDATA from a posted invoice."),
|
|
}
|
|
if not self.company_id.l10n_gr_edi_aade_id or not self.company_id.l10n_gr_edi_aade_key:
|
|
errors['l10n_gr_edi_company_no_cred'] = {
|
|
'message': _("You need to set AADE ID and Key in the company settings."),
|
|
**error_action_gr_settings,
|
|
}
|
|
if self.company_id.country_code != 'GR' and (not self.company_id.city or not self.company_id.zip):
|
|
errors['l10n_gr_edi_company_no_zip_street'] = {
|
|
'message': _("Missing city and/or ZIP code on company %s.", self.company_id.name),
|
|
**error_action_company,
|
|
}
|
|
if not self.company_id.vat:
|
|
errors['l10n_gr_edi_company_no_vat'] = {
|
|
'message': _("Missing VAT on company %s.", self.company_id.name),
|
|
**error_action_company,
|
|
}
|
|
if not self.l10n_gr_edi_inv_type:
|
|
errors['l10n_gr_edi_no_inv_type'] = {
|
|
'message': _("Missing myDATA Invoice Type."),
|
|
}
|
|
if not self.commercial_partner_id:
|
|
errors['l10n_gr_edi_no_partner'] = {
|
|
'message': _("Partner must be filled to be able to send to myDATA."),
|
|
}
|
|
if self.commercial_partner_id:
|
|
if not self.commercial_partner_id.vat:
|
|
errors['l10n_gr_edi_partner_no_vat'] = {
|
|
'message': _("Missing VAT on partner %s.", self.commercial_partner_id.name),
|
|
**error_action_partner,
|
|
}
|
|
if ((self.commercial_partner_id.country_code != 'GR' or self.l10n_gr_edi_inv_type in TYPES_WITH_MANDATORY_COUNTERPART) and
|
|
(not self.commercial_partner_id.zip or not self.commercial_partner_id.city)):
|
|
errors['l10n_gr_edi_partner_no_zip_street'] = {
|
|
'message': _("Missing city and/or ZIP code on partner %s.", self.commercial_partner_id.name),
|
|
**error_action_partner,
|
|
}
|
|
|
|
move_disallow_classification = self.is_purchase_document(include_receipts=True) and self.l10n_gr_edi_inv_type in TYPES_WITH_FORBIDDEN_CLASSIFICATION
|
|
|
|
for line_no, line in enumerate(self.invoice_line_ids, start=1):
|
|
if line.display_type in ('line_section', 'line_note'):
|
|
continue
|
|
if move_disallow_classification and line.l10n_gr_edi_cls_category:
|
|
errors[f'l10n_gr_edi_{line_no}_forbidden_classification'] = {
|
|
'message': _('myDATA classification is not allowed on line %s.', line_no),
|
|
}
|
|
if not line.l10n_gr_edi_cls_category and line.l10n_gr_edi_available_cls_category and not move_disallow_classification:
|
|
errors[f'l10n_gr_edi_line_{line_no}_missing_cls_category'] = {
|
|
'message': _('Missing myDATA classification category on line %s.', line_no),
|
|
}
|
|
if not line.l10n_gr_edi_cls_type \
|
|
and line.l10n_gr_edi_available_cls_type \
|
|
and (line.move_id.l10n_gr_edi_inv_type, line.l10n_gr_edi_cls_category) \
|
|
not in COMBINATIONS_WITH_POSSIBLE_EMPTY_TYPE:
|
|
errors[f'l10n_gr_edi_line_{line_no}_missing_cls_type'] = {
|
|
'message': _('Missing myDATA classification type on line %s.', line_no),
|
|
}
|
|
taxes = line.tax_ids.flatten_taxes_hierarchy()
|
|
if len(taxes) > 1:
|
|
errors[f'l10n_gr_edi_line_{line_no}_multi_tax'] = {
|
|
'message': _('myDATA does not support multiple taxes on line %s.', line_no),
|
|
}
|
|
if not taxes and self.l10n_gr_edi_inv_type not in TYPES_WITH_VAT_CATEGORY_8:
|
|
errors[f'l10n_gr_edi_line_{line_no}_missing_tax'] = {
|
|
'message': _('Missing tax on line %s.', line_no),
|
|
}
|
|
if len(taxes) == 1 and taxes.amount == 0 and not line.l10n_gr_edi_tax_exemption_category:
|
|
errors[f'l10n_gr_edi_line_{line_no}_missing_tax_exempt'] = {
|
|
'message': _('Missing myDATA Tax Exemption Category for line %s.', line_no),
|
|
}
|
|
if len(taxes) == 1 and taxes.amount not in VALID_TAX_AMOUNTS:
|
|
errors[f'l10n_gr_edi_line_{line_no}_invalid_tax_amount'] = {
|
|
'message': _('Invalid tax amount for line %(line_no)s. The valid values are %(valid_values)s.',
|
|
line_no=line_no,
|
|
valid_values=', '.join(str(tax) for tax in VALID_TAX_AMOUNTS)),
|
|
}
|
|
return errors
|
|
|
|
def _l10n_gr_edi_get_pre_error_string(self):
|
|
self.ensure_one()
|
|
pre_error = self._l10n_gr_edi_get_pre_error_dict()
|
|
error_messages = (error_val['message'] for error_val in pre_error.values())
|
|
return '\n'.join(error_messages)
|
|
|
|
@api.model
|
|
def _l10n_gr_edi_handle_send_result(self, result, xml_vals):
|
|
"""
|
|
Handle the result object received from sending xml to myDATA.
|
|
Create the related error/sent document with the necessary values.
|
|
"""
|
|
move_xml_map = {} # Dictionary mapping of ``move_id`` -> ``xml_content``.
|
|
for invoice_vals in xml_vals['invoice_values_list']:
|
|
single_xml_vals = {'invoice_values_list': [invoice_vals]}
|
|
move = invoice_vals['__move__']
|
|
xml_template = 'l10n_gr_edi.mydata_invoice' if move.is_sale_document(include_receipts=True) else 'l10n_gr_edi.mydata_expense_classification'
|
|
xml_content = self._l10n_gr_edi_generate_xml_content(xml_template, single_xml_vals)
|
|
move_xml_map[move] = xml_content
|
|
|
|
move_ids = list(move_xml_map.keys())
|
|
|
|
if 'error' in result:
|
|
# If the request failed at this stage, it is probably caused by connection/credentials issues.
|
|
# In such case, we don't need to attach the xml here as it won't be helpful for the user.
|
|
for move in move_ids:
|
|
move._l10n_gr_edi_create_error_document(result)
|
|
else:
|
|
for result_id, result_dict in result.items():
|
|
move = move_ids[result_id]
|
|
xml_content = move_xml_map[move]
|
|
document_values = {**result_dict, 'xml_content': xml_content}
|
|
# Delete previous error documents
|
|
move.l10n_gr_edi_document_ids.filtered(lambda d: d.state in ('invoice_error', 'bill_error')).unlink()
|
|
if 'error' in result_dict:
|
|
# In this stage, the sending process has succeeded, and any error we receive is generated from the myDATA API.
|
|
# Previous error(s) without attachments (generated from pre-compute) are now useless and can be unlinked.
|
|
move._l10n_gr_edi_create_error_document(document_values)
|
|
else:
|
|
move._l10n_gr_edi_create_sent_document(document_values)
|
|
|
|
if self._can_commit():
|
|
self._cr.commit()
|
|
|
|
def _l10n_gr_edi_send_invoices(self):
|
|
""" Send batches of invoice SendInvoice XML to myDATA. """
|
|
for company, invoices in self.grouped('company_id').items():
|
|
xml_vals = invoices._l10n_gr_edi_get_invoices_xml_vals()
|
|
xml_content = invoices._l10n_gr_edi_generate_xml_content('l10n_gr_edi.mydata_invoice', xml_vals)
|
|
result = _make_mydata_request(company=company, endpoint='SendInvoices', xml_content=xml_content)
|
|
self._l10n_gr_edi_handle_send_result(result, xml_vals)
|
|
|
|
def _l10n_gr_edi_send_expense_classification(self):
|
|
""" Send batches of bill SendExpensesClassification XML to myDATA. """
|
|
for company, bills in self.grouped('company_id').items():
|
|
xml_vals = bills._l10n_gr_edi_get_expense_classification_xml_vals()
|
|
xml_content = bills._l10n_gr_edi_generate_xml_content('l10n_gr_edi.mydata_expense_classification', xml_vals)
|
|
result = _make_mydata_request(company=company, endpoint='SendExpensesClassification', xml_content=xml_content)
|
|
self._l10n_gr_edi_handle_send_result(result, xml_vals)
|
|
|
|
def l10n_gr_edi_try_send_invoices(self):
|
|
moves_to_send = self.env['account.move']
|
|
for move in self:
|
|
if error := move._l10n_gr_edi_get_pre_error_string():
|
|
move._l10n_gr_edi_create_error_document({'error': error})
|
|
else:
|
|
moves_to_send |= move
|
|
|
|
if moves_to_send:
|
|
self.env['res.company']._with_locked_records(moves_to_send)
|
|
moves_to_send._l10n_gr_edi_send_invoices()
|
|
|
|
def l10n_gr_edi_try_send_expense_classification(self):
|
|
moves_to_send = self.env['account.move']
|
|
for move in self:
|
|
if error_message := move._l10n_gr_edi_get_pre_error_string():
|
|
move._l10n_gr_edi_create_error_document({'error': error_message})
|
|
|
|
# Simulate the error handling behavior on invoice's send and print wizard.
|
|
# If we're only sending one bill, raise the warning error immediately.
|
|
if len(self) == 1 and self._can_commit():
|
|
self._cr.commit()
|
|
raise UserError(error_message)
|
|
else:
|
|
moves_to_send |= move
|
|
|
|
if moves_to_send:
|
|
moves_to_send._l10n_gr_edi_send_expense_classification()
|
|
if len(self) == 1 and (error_message := self.l10n_gr_edi_document_ids.sorted()[0].message):
|
|
raise UserError(error_message)
|
|
|
|
def _l10n_gr_edi_try_send_batch(self):
|
|
""" Only available for Vendor Bills. In case of invoices, user should use Send & Print instead. """
|
|
if any(move.is_sale_document(include_receipts=True) for move in self):
|
|
raise UserError(_("You should use Send & Print wizard for sending customer invoices to myDATA."))
|
|
if any(not move.l10n_gr_edi_enable_send_expense_classification for move in self):
|
|
raise UserError(_("Some of the selected moves does not meet the requirements to be sent to myDATA."))
|
|
|
|
self.l10n_gr_edi_try_send_expense_classification()
|