219 lines
11 KiB
Python
219 lines
11 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
import base64
|
|
import logging
|
|
from contextlib import suppress
|
|
from lxml import etree, html
|
|
from markupsafe import Markup
|
|
from odoo import api, fields, models
|
|
from odoo.exceptions import UserError
|
|
from odoo.tools.translate import _
|
|
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class AccountMove(models.Model):
|
|
_inherit = 'account.move'
|
|
|
|
l10n_cl_aec_attachment_file = fields.Binary(copy=False, attachment=True)
|
|
l10n_cl_aec_attachment_id = fields.Many2one(
|
|
comodel_name='ir.attachment',
|
|
compute=lambda self: self._compute_linked_attachment_id(
|
|
'l10n_cl_aec_attachment_id', 'l10n_cl_aec_attachment_file'), string="AEC Attachment")
|
|
l10n_cl_aec_entry_ids = fields.One2many('account.move', 'l10n_cl_yielded_invoice_id', string='AEC Entries', copy=False)
|
|
l10n_cl_yielded_invoice_id = fields.Many2one('account.move', string='Yielded Invoice')
|
|
l10n_cl_aec_yielded = fields.Selection([('to_yield', 'To Yield'), ('yielded', 'Yielded')],
|
|
string='Yield Status', compute='_compute_l10n_cl_yielded_status')
|
|
|
|
def l10n_cl_button_yield_entry(self):
|
|
self.ensure_one()
|
|
return self.l10n_cl_aec_entry_ids._get_records_action(
|
|
name=_('Yield Entries')
|
|
)
|
|
|
|
def l10n_cl_yield_invoice(self):
|
|
self.ensure_one()
|
|
return self.l10n_cl_yielded_invoice_id._get_records_action(
|
|
name=_('Yielded Invoice')
|
|
)
|
|
|
|
@api.depends('l10n_cl_aec_entry_ids')
|
|
def _compute_l10n_cl_yielded_status(self):
|
|
for move in self:
|
|
if (len(move.l10n_cl_aec_entry_ids.filtered(lambda x: x.state == 'posted')) == 1 and
|
|
move.payment_state == 'paid'):
|
|
move.l10n_cl_aec_yielded = 'to_yield' \
|
|
if move.l10n_cl_aec_entry_ids.filtered(lambda x: x.l10n_cl_dte_status != 'accepted') else 'yielded'
|
|
else:
|
|
move.l10n_cl_aec_yielded = False
|
|
|
|
def _post(self, soft=True):
|
|
if self.filtered(lambda x: x._l10n_cl_is_aec_move() and x.posted_before):
|
|
raise UserError(_('You cannot post an AEC posted before. You should cancel and create'
|
|
'a new one from the related invoice.'))
|
|
res = super()._post(soft=soft)
|
|
return res
|
|
|
|
def _parse_response(self, response):
|
|
# This method is to ensure to find the response in the correct encoding
|
|
try:
|
|
return {'result': etree.fromstring(response), 'type': 'xml'}
|
|
except etree.XMLSyntaxError as e:
|
|
_logger.error(f'XMLSyntaxError: {e}')
|
|
_logger.error(f'Response: {response}')
|
|
if isinstance(response, bytes):
|
|
for encoding in ['utf-8', 'iso-8859-1', 'windows-1252']:
|
|
with suppress(UnicodeDecodeError, etree.XMLSyntaxError):
|
|
return {'result': etree.fromstring(response.decode(encoding)), 'type': 'xml'}
|
|
# If we arrive here, we don't have an XML response, and probably there is a
|
|
# message with a warning or similar, which we should put on chatter
|
|
try:
|
|
return {'result': html.fromstring(response), 'type': 'html'}
|
|
except Exception as e:
|
|
_logger.error(f"Parse error as HTML: {e}")
|
|
return {'result': None, 'type': None}
|
|
|
|
def _get_fields_to_detach(self):
|
|
# EXTENDS account
|
|
fields_list = super()._get_fields_to_detach()
|
|
fields_list.append('l10n_cl_aec_attachment_file')
|
|
return fields_list
|
|
|
|
def _l10n_cl_is_aec_move(self):
|
|
return self.move_type == 'entry' and self.l10n_cl_aec_attachment_id
|
|
|
|
def l10n_cl_send_dte_to_sii(self, retry_send=True):
|
|
if not self._l10n_cl_is_aec_move():
|
|
return super().l10n_cl_send_dte_to_sii(retry_send)
|
|
digital_signature_sudo = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id)
|
|
if self.company_id.l10n_cl_dte_service_provider == 'SIIDEMO':
|
|
self.message_post(body=_("AEC is simulated as 'accepted' in DEMO mode."))
|
|
self.l10n_cl_dte_status = 'accepted'
|
|
return None
|
|
params = {
|
|
'emailNotif': self.company_id.l10n_cl_dte_email,
|
|
'rutCompany': self._l10n_cl_format_vat(self.company_id.vat)[:-2],
|
|
'dvCompany': self._l10n_cl_format_vat(self.company_id.vat)[-1],
|
|
'archivo': (
|
|
self.l10n_cl_aec_attachment_id.name,
|
|
base64.b64decode(self.l10n_cl_aec_attachment_file),
|
|
'application/xml'),
|
|
}
|
|
response = self._send_xml_to_sii(
|
|
self.company_id.l10n_cl_dte_service_provider,
|
|
self.company_id.website,
|
|
params,
|
|
digital_signature_sudo,
|
|
post='/cgi_rtc/RTC/RTCAnotEnvio.cgi'
|
|
)
|
|
if not response:
|
|
digital_signature_sudo.last_token = False
|
|
return None
|
|
analyze_response = self._parse_response(response)
|
|
if analyze_response['type'] == 'html':
|
|
self.message_post(body=_('WARNING: Message from SII was: %s', analyze_response['result']))
|
|
digital_signature_sudo.last_token = False
|
|
return None
|
|
elif analyze_response['type'] is None:
|
|
self.message_post(body=_('The SII did not answered properly. Please try again'))
|
|
digital_signature_sudo.last_token = False
|
|
return None
|
|
response_parsed = analyze_response['result']
|
|
self.l10n_cl_sii_send_ident = response_parsed.findtext('TRACKID')
|
|
sii_response_status = response_parsed.findtext('STATUS')
|
|
if sii_response_status == '5':
|
|
digital_signature_sudo.last_token = False
|
|
_logger.warning('The response status is %s. Clearing the token.',
|
|
self._l10n_cl_get_sii_reception_status_message(sii_response_status))
|
|
if retry_send:
|
|
_logger.info('Retrying send DTE to SII')
|
|
self.l10n_cl_send_dte_to_sii(retry_send=False)
|
|
# cleans the token and keeps the l10n_cl_dte_status until new attempt to connect
|
|
# would like to resend from here, because we cannot wait till tomorrow to attempt
|
|
# a new send
|
|
elif sii_response_status == '0':
|
|
self.l10n_cl_dte_status = 'ask_for_status'
|
|
else:
|
|
self.l10n_cl_dte_status = 'rejected'
|
|
self.message_post(body=_('DTE has been sent to SII with response: %s.',
|
|
self._l10n_cl_get_sii_reception_status_message(sii_response_status)))
|
|
|
|
def l10n_cl_verify_dte_status(self, send_dte_to_partner=True):
|
|
if not self._l10n_cl_is_aec_move():
|
|
return super().l10n_cl_verify_dte_status(send_dte_to_partner)
|
|
|
|
digital_signature = self.company_id.sudo()._get_digital_signature(user_id=self.env.user.id)
|
|
response = self._get_aec_send_status(
|
|
self.company_id.l10n_cl_dte_service_provider,
|
|
self.l10n_cl_sii_send_ident,
|
|
digital_signature)
|
|
if not response:
|
|
self.l10n_cl_dte_status = 'ask_for_status'
|
|
digital_signature.last_token = False
|
|
return None
|
|
response_parsed = etree.fromstring(response.encode('utf-8'))
|
|
if response_parsed.findtext(
|
|
'{http://www.sii.cl/XMLSchema}RESP_HDR/{http://www.sii.cl/XMLSchema}ESTADO') in ['3', '4']:
|
|
digital_signature.last_token = False
|
|
_logger.error('Token is invalid.')
|
|
return None
|
|
self.l10n_cl_dte_status = self._analyze_aec_sii_result(response_parsed)
|
|
self.message_post(
|
|
body=_('Asking for DTE status with response:') +
|
|
Markup('<br /><li><b>%s</b>: %s</li><li><b>%s</b>: %s</li>') % (
|
|
_('Sending status'),
|
|
response_parsed.findtext('{http://www.sii.cl/XMLSchema}RESP_BODY/ESTADO_ENVIO'),
|
|
_('Description'),
|
|
response_parsed.findtext('{http://www.sii.cl/XMLSchema}RESP_BODY/DESC_ESTADO')))
|
|
|
|
def _l10n_cl_render_and_sign_xml(
|
|
self, template, values, doc_id, doc_type, digital_signature, without_xml_declaration=True):
|
|
rendered_xml = self.env['ir.qweb']._render(template, values)
|
|
return self._sign_full_xml(
|
|
rendered_xml, digital_signature, doc_id, doc_type, without_xml_declaration=without_xml_declaration)
|
|
|
|
def _validate_aec_creation_conditions(self):
|
|
aec_codes = ['33', '34', '46', '43']
|
|
if not self.company_id.l10n_cl_factoring_journal_id:
|
|
raise UserError(_('There is no journal configured for factoring. '
|
|
'Please go to the company settings and set a journal'))
|
|
if not self.company_id.l10n_cl_factoring_counterpart_account_id:
|
|
raise UserError(_('There is no counterpart account configured for factoring. '
|
|
'Please go to the company settings and set a counterpart account'))
|
|
if not self.partner_id.email:
|
|
raise UserError(_('The Factoring company %s does not have an email', self.partner_id.name))
|
|
if not self.partner_id.l10n_cl_dte_email:
|
|
raise UserError(
|
|
_('The partner has no DTE email defined. This is mandatory for electronic invoicing.'))
|
|
if not self.partner_id.vat:
|
|
raise UserError(_('The Factoring company %s does not have a RUT number', self.partner_id.name))
|
|
if self.partner_id.country_id.code != 'CL':
|
|
raise UserError(_('The Factoring company %s is not from Chile. You cannot use this factoring method '
|
|
'for a foreign factoring company', self.partner_id.name))
|
|
if not self.company_id.partner_id.city:
|
|
raise UserError(_('There is no city configured in your partner company. This is mandatory for AEC. '
|
|
'Please go to your partner company and set the city.', self.partner_id.name))
|
|
if not self.company_id.partner_id.street:
|
|
raise UserError(_('There is no address configured in your partner company. '
|
|
'This is mandatory for AEC. Please go to the partner company and set the address'))
|
|
non_factoring_invoices = self.filtered(lambda x: x.l10n_latam_document_type_id.code not in aec_codes)
|
|
if non_factoring_invoices:
|
|
raise UserError(_('These invoices cannot be factored: %s', ', '.join(
|
|
non_factoring_invoices.mapped('name'))))
|
|
non_accepted_invoices = self.filtered(lambda x: x.l10n_cl_dte_status not in ['accepted', 'objected'])
|
|
if non_accepted_invoices:
|
|
raise UserError(_('These invoices cannot be factored until they are accepted by SII: %s', ', '.join(
|
|
non_accepted_invoices.mapped('name'))))
|
|
already_yielded = self.filtered(lambda x: x.l10n_cl_aec_yielded in ['yielded', 'to_yield'])
|
|
if already_yielded:
|
|
raise UserError(_('Selected documents are already yielded or in process: %s.', ', '.join(
|
|
already_yielded.mapped('name'))))
|
|
in_payment_or_paid = self.filtered(lambda x: x.payment_state in ['in_payment', 'paid'])
|
|
if in_payment_or_paid:
|
|
raise UserError(_('Selected documents are in payment state or already paid: %s.', ', '.join(
|
|
in_payment_or_paid.mapped('name'))))
|
|
|
|
def action_l10n_cl_create_aec(self):
|
|
self._validate_aec_creation_conditions() # Configuration and factoring company validations
|
|
return self.env['ir.actions.actions']._for_xml_id('l10n_cl_edi_factoring.action_create_aec_wizard')
|