696 lines
30 KiB
Python
696 lines
30 KiB
Python
from base64 import b64encode, b64decode
|
|
from datetime import datetime
|
|
from lxml import etree
|
|
from lxml.etree import CDATA
|
|
from markupsafe import Markup
|
|
|
|
from odoo import api, fields, models, modules
|
|
from odoo.tools import html_escape, cleanup_xml_node, date_utils
|
|
from odoo.addons.l10n_co_dian import xml_utils
|
|
from odoo.exceptions import UserError
|
|
|
|
EVENT_FILE_SEQUENCE_CODE = "l10n_co_dian_file_sequence_code"
|
|
|
|
COMMERCIAL_STATE_SELECTION = [
|
|
('pending', "Pending"),
|
|
('received', "030 - Received"),
|
|
('goods_received', "032 - Goods Received"),
|
|
('claimed', "031 - Claimed"),
|
|
('accepted', "033 - Accepted by Customer"),
|
|
('accepted_by_issuer', "034 - Accepted by Issuer"),
|
|
]
|
|
|
|
|
|
class L10n_Co_DianDocument(models.Model):
|
|
_name = 'l10n_co_dian.document'
|
|
_description = "Colombian documents used for each interaction with the DIAN"
|
|
_order = 'datetime DESC, id DESC'
|
|
|
|
# Relational fields
|
|
attachment_id = fields.Many2one(comodel_name='ir.attachment')
|
|
move_id = fields.Many2one(comodel_name='account.move')
|
|
|
|
# Business fields
|
|
identifier = fields.Char(string="CUFE/CUDE/CUDS")
|
|
zip_key = fields.Char(
|
|
help="ID returned by the DIAN when sending a document with the certification process activated."
|
|
) # ID returned when calling SendTestSetAsync
|
|
state = fields.Selection(selection=[
|
|
('invoice_sending_failed', "Sending Failed"), # webservice is not reachable
|
|
('invoice_pending', "Pending"), # document was sent and the response is not yet known
|
|
('invoice_rejected', "Rejected"),
|
|
('invoice_accepted', "Accepted"),
|
|
])
|
|
message_json = fields.Json()
|
|
message = fields.Html(compute="_compute_message")
|
|
datetime = fields.Datetime()
|
|
test_environment = fields.Boolean(help="Indicates whether the test endpoint was used to send this document")
|
|
certification_process = fields.Boolean(
|
|
help="Indicates whether we were in the certification process when sending this document",
|
|
)
|
|
commercial_state = fields.Selection(
|
|
selection=COMMERCIAL_STATE_SELECTION,
|
|
string="Commercial Status"
|
|
)
|
|
|
|
# Buttons
|
|
show_button_get_status = fields.Boolean(compute="_compute_show_button_get_status")
|
|
show_button_fetch_attached_document = fields.Boolean(compute='_compute_show_button_fetch_attached_document')
|
|
|
|
@api.depends('zip_key', 'state', 'test_environment', 'certification_process')
|
|
def _compute_show_button_get_status(self):
|
|
for doc in self:
|
|
doc.show_button_get_status = (
|
|
doc.zip_key
|
|
and doc.state not in ('invoice_accepted', 'invoice_rejected')
|
|
and doc.test_environment
|
|
and doc.certification_process
|
|
)
|
|
|
|
@api.depends('attachment_id', 'move_id.move_type')
|
|
def _compute_show_button_fetch_attached_document(self):
|
|
for doc in self:
|
|
doc.show_button_fetch_attached_document = doc.attachment_id and doc.move_id.move_type not in ('in_invoice', 'in_refund')
|
|
|
|
@api.depends('message_json')
|
|
def _compute_message(self):
|
|
for doc in self:
|
|
msg = html_escape(doc.message_json.get('status', ""))
|
|
if doc.message_json.get('errors'):
|
|
msg += Markup("<ul>{errors}</ul>").format(
|
|
errors=Markup().join(
|
|
Markup("<li>%s</li>") % error for error in doc.message_json['errors']
|
|
),
|
|
)
|
|
doc.message = msg
|
|
|
|
def unlink(self):
|
|
if self.attachment_id:
|
|
self.attachment_id.unlink()
|
|
return super().unlink()
|
|
|
|
@api.model
|
|
def _parse_errors(self, root):
|
|
""" Returns a list containing the errors/warnings from a DIAN response """
|
|
return [node.text for node in root.findall(".//{*}ErrorMessage/{*}string")]
|
|
|
|
@api.model
|
|
def _build_message(self, root):
|
|
msg = {'status': False, 'errors': []}
|
|
fault = root.find('.//{*}Fault/{*}Reason/{*}Text')
|
|
if fault is not None and fault.text:
|
|
msg['status'] = fault.text + " (This might be caused by using incorrect certificates)"
|
|
status = root.find('.//{*}StatusDescription')
|
|
if status is not None and status.text:
|
|
msg['status'] = status.text
|
|
msg['errors'] = self._parse_errors(root)
|
|
return msg
|
|
|
|
@api.model
|
|
def _create_document(self, xml, move, state, **kwargs):
|
|
def resolve_doc_datetime(demo_mode, root, kwargs_dict):
|
|
doc_datetime = fields.Datetime.context_timestamp(self.with_context(tz='America/Bogota'), fields.Datetime.now()).replace(tzinfo=None)
|
|
if demo_mode:
|
|
return doc_datetime
|
|
if 'datetime' in kwargs_dict:
|
|
return kwargs_dict.pop('datetime')
|
|
if root is None:
|
|
return doc_datetime
|
|
return date_utils.to_timezone(None)(datetime.fromisoformat(root.find('.//{*}SigningTime').text))
|
|
|
|
def resolve_identifier(demo_mode, root, kwargs_dict):
|
|
if demo_mode:
|
|
return 'DEMO'
|
|
if 'identifier' in kwargs_dict:
|
|
return kwargs_dict.pop('identifier')
|
|
if root is None:
|
|
return ''
|
|
return root.find('.//{*}UUID').text
|
|
|
|
move.ensure_one()
|
|
|
|
root = etree.fromstring(xml) if xml else None
|
|
demo_mode = move.company_id.l10n_co_dian_demo_mode
|
|
|
|
# pop attachment_name here so it is not passed to the document create function
|
|
attachment_name = kwargs.pop('attachment_name', None)
|
|
|
|
# create document
|
|
doc = self.create([{
|
|
'move_id': move.id,
|
|
'identifier': resolve_identifier(demo_mode, root, kwargs),
|
|
'state': state,
|
|
'datetime': resolve_doc_datetime(demo_mode, root, kwargs),
|
|
'test_environment': move.company_id.l10n_co_dian_test_environment,
|
|
'certification_process': move.company_id.l10n_co_dian_certification_process,
|
|
**kwargs,
|
|
}])
|
|
|
|
if state == 'invoice_accepted' and not doc.commercial_state:
|
|
doc.commercial_state = 'pending'
|
|
|
|
# create attachment
|
|
if root is not None:
|
|
doc.attachment_id = self.env['ir.attachment'].create([{
|
|
'raw': xml,
|
|
'name': attachment_name or self.env['account.edi.xml.ubl_dian']._export_invoice_filename(move),
|
|
'res_id': doc.id if state != 'invoice_accepted' else move.id,
|
|
'res_model': doc._name if state != 'invoice_accepted' else move._name,
|
|
}])
|
|
|
|
return doc
|
|
|
|
@api.model
|
|
def _document_already_processed(self, response_element):
|
|
"""
|
|
This function checks if a response contains a specific error that DIAN
|
|
returns when a document has already been sent and processed.
|
|
:param response_element: etree.Element
|
|
"""
|
|
is_valid = response_element.findtext('.//{*}IsValid') == 'true'
|
|
response_status_code = response_element.findtext('.//{*}StatusCode')
|
|
|
|
if not is_valid and response_status_code == '99':
|
|
errors = response_element.findall(".//{*}ErrorMessage/{*}string")
|
|
return len(errors) == 1 and errors[0].text == 'Regla: 90, Rechazo: Documento procesado anteriormente.'
|
|
|
|
return False
|
|
|
|
@api.model
|
|
def _send_test_set_async(self, zipped_content, move):
|
|
""" Send the document to the 'SendTestSetAsync' (asynchronous) webservice.
|
|
NB: later, need to fetch the result by calling the 'GetStatusZip' webservice.
|
|
"""
|
|
operation_mode = self.env['account.edi.xml.ubl_dian']._dian_get_operation_mode(move)
|
|
response = xml_utils._build_and_send_request(
|
|
self,
|
|
payload={
|
|
'file_name': "invoice.zip",
|
|
'content_file': b64encode(zipped_content).decode(),
|
|
'test_set_id': operation_mode.dian_testing_id,
|
|
'soap_body_template': "l10n_co_dian.send_test_set_async",
|
|
},
|
|
service="SendTestSetAsync",
|
|
company=move.company_id,
|
|
)
|
|
if not response['response']:
|
|
return {
|
|
'state': 'invoice_sending_failed',
|
|
'message_json': {'status': self.env._("The DIAN server did not respond.")},
|
|
}
|
|
root = etree.fromstring(response['response'])
|
|
if response['status_code'] != 200:
|
|
return {
|
|
'state': 'invoice_sending_failed',
|
|
'message_json': self._build_message(root),
|
|
}
|
|
zip_key = root.findtext('.//{*}ZipKey')
|
|
if zip_key:
|
|
return {
|
|
'state': 'invoice_pending',
|
|
'message_json': {'status': self.env._("Invoice is being processed by the DIAN.")},
|
|
'zip_key': zip_key,
|
|
}
|
|
return {
|
|
'state': 'invoice_rejected',
|
|
'message_json': {'errors': [node.text for node in root.findall('.//{*}ProcessedMessage')]},
|
|
}
|
|
|
|
@api.model
|
|
def _send_bill_sync(self, zipped_content, move):
|
|
""" Send the document to the 'SendBillSync' (synchronous) webservice. """
|
|
|
|
if move.company_id.l10n_co_dian_demo_mode:
|
|
return {
|
|
'state': 'invoice_accepted',
|
|
'message_json': {'status': self.env._("Demo mode response")},
|
|
}
|
|
|
|
response = xml_utils._build_and_send_request(
|
|
self,
|
|
payload={
|
|
'file_name': "invoice.zip",
|
|
'content_file': b64encode(zipped_content).decode(),
|
|
'soap_body_template': "l10n_co_dian.send_bill_sync",
|
|
},
|
|
service="SendBillSync",
|
|
company=move.company_id,
|
|
)
|
|
if not response['response']:
|
|
return {
|
|
'state': 'invoice_sending_failed',
|
|
'message_json': {'status': self.env._("The DIAN server did not respond.")},
|
|
}
|
|
root = etree.fromstring(response['response'])
|
|
if response['status_code'] != 200:
|
|
return {
|
|
'state': 'invoice_sending_failed',
|
|
'message_json': self._build_message(root),
|
|
}
|
|
|
|
is_valid = root.findtext('.//{*}IsValid') == 'true'
|
|
document_vals = {
|
|
'state': 'invoice_accepted' if is_valid else 'invoice_rejected',
|
|
'message_json': self._build_message(root),
|
|
}
|
|
|
|
if self._document_already_processed(root) and (identifier := root.findtext('.//{*}XmlDocumentKey')):
|
|
# We have to make sure that the identifier is the one associated to this document by fetching the XML from DIAN
|
|
if xml := self._get_xml_by_document_key(identifier, move):
|
|
xml_element = etree.fromstring(xml)
|
|
xml_customer_name = xml_element.findtext('.//{*}AccountingCustomerParty/{*}Party/{*}PartyName/{*}Name')
|
|
xml_issue_date = xml_element.findtext('./{*}IssueDate')
|
|
xml_issue_time = xml_element.findtext('./{*}IssueTime')
|
|
|
|
customer_name = move.partner_id.name
|
|
issue_date = move.l10n_co_dian_post_time.date().isoformat()
|
|
issue_time = move.l10n_co_dian_post_time.strftime("%H:%M:%S-05:00")
|
|
# check that the customer name, the issue date and time from the XML on DIAN are the same than those of the move
|
|
if xml_customer_name == customer_name and xml_issue_date == issue_date and xml_issue_time == issue_time:
|
|
# Document has already been processed by DIAN -> correctly set the identifier and state so GetStatus is called correctly
|
|
document_vals |= {
|
|
'state': 'invoice_accepted',
|
|
'identifier': identifier,
|
|
}
|
|
|
|
return document_vals
|
|
|
|
@api.model
|
|
def _send_event_update_status(self, zipped_content, move, next_commercial_state):
|
|
if move.company_id.l10n_co_dian_demo_mode:
|
|
return {
|
|
'state': 'invoice_accepted',
|
|
'commercial_state': next_commercial_state,
|
|
'message_json': {'status': self.env._("Demo mode response")},
|
|
}, dict()
|
|
|
|
response = xml_utils._build_and_send_request(
|
|
self,
|
|
payload={
|
|
'content_file': b64encode(zipped_content).decode(),
|
|
'soap_body_template': "l10n_co_dian.send_event_update_status",
|
|
},
|
|
service="SendEventUpdateStatus",
|
|
company=move.company_id,
|
|
)
|
|
|
|
if not response['response']:
|
|
return {
|
|
'state': 'invoice_sending_failed',
|
|
'message_json': {'status': self.env._("The DIAN server did not respond.")},
|
|
}, None
|
|
|
|
root = etree.fromstring(response['response'])
|
|
state = 'invoice_rejected'
|
|
|
|
if response['status_code'] != 200:
|
|
state = 'invoice_sending_failed'
|
|
elif root.findtext('.//{*}IsValid') == 'true':
|
|
state = 'invoice_accepted'
|
|
|
|
document_vals = {
|
|
'state': state,
|
|
'commercial_state': next_commercial_state,
|
|
'message_json': self._build_message(root),
|
|
}
|
|
|
|
if self._document_already_processed(root):
|
|
# DIAN rejected this call because it already accepted a call with the next commercial state
|
|
# so we can safely force the state to accepted so the user can continue the commercial event flow
|
|
document_vals['state'] = 'invoice_accepted'
|
|
|
|
return document_vals, response
|
|
|
|
@api.model
|
|
def _get_status_event(self, company_id, track_id):
|
|
response = xml_utils._build_and_send_request(
|
|
self,
|
|
payload={
|
|
'track_id': track_id,
|
|
'soap_body_template': "l10n_co_dian.get_status_event",
|
|
},
|
|
service="GetStatusEvent",
|
|
company=company_id,
|
|
)
|
|
|
|
if not response['response']:
|
|
return {
|
|
'state': 'invoice_sending_failed',
|
|
'message_json': {'status': self.env._("The DIAN server did not respond.")},
|
|
}, None
|
|
|
|
root = etree.fromstring(response['response'])
|
|
state = 'invoice_rejected'
|
|
|
|
if response['status_code'] != 200:
|
|
state = 'invoice_sending_failed'
|
|
elif root.findtext('.//{*}IsValid') == 'true':
|
|
state = 'invoice_accepted'
|
|
|
|
return {
|
|
'state': state,
|
|
'message_json': self._build_message(root),
|
|
}, response
|
|
|
|
def _get_status_zip(self):
|
|
""" Fetch the status of a document sent to 'SendTestSetAsync' using the 'GetStatusZip' webservice. """
|
|
self.ensure_one()
|
|
response = xml_utils._build_and_send_request(
|
|
self,
|
|
payload={
|
|
'track_id': self.zip_key,
|
|
'soap_body_template': "l10n_co_dian.get_status_zip",
|
|
},
|
|
service="GetStatusZip",
|
|
company=self._get_company(),
|
|
)
|
|
if response['status_code'] == 200:
|
|
root = etree.fromstring(response['response'])
|
|
self.message_json = self._build_message(root)
|
|
if root.findtext('.//{*}IsValid') == 'true':
|
|
self.state = 'invoice_accepted'
|
|
elif not root.findtext('.//{*}StatusCode'):
|
|
self.state = 'invoice_pending'
|
|
else:
|
|
self.state = 'invoice_rejected'
|
|
elif response['status_code']:
|
|
raise UserError(self.env._("The DIAN server returned an error (code %s)", response['status_code']))
|
|
else:
|
|
raise UserError(self.env._("The DIAN server did not respond."))
|
|
|
|
def _get_status(self):
|
|
return xml_utils._build_and_send_request(
|
|
self,
|
|
payload={
|
|
'track_id': self.identifier,
|
|
'soap_body_template': "l10n_co_dian.get_status",
|
|
},
|
|
service="GetStatus",
|
|
company=self._get_company(),
|
|
)
|
|
|
|
@api.model
|
|
def _get_xml_by_document_key(self, identifier, move):
|
|
""" Fetch the XML linked to the CUFE using the 'GetXmlByDocumentKey' webservice. """
|
|
# This check is required because the template is added in stable and it's possible
|
|
# that it doesn't exist if the module has not been upgraded
|
|
if self.env.ref('l10n_co_dian.get_xml_by_document_key', raise_if_not_found=False):
|
|
response = xml_utils._build_and_send_request(
|
|
self,
|
|
payload={
|
|
'track_id': identifier,
|
|
'soap_body_template': "l10n_co_dian.get_xml_by_document_key",
|
|
},
|
|
service="GetXmlByDocumentKey",
|
|
company=move.company_id,
|
|
)
|
|
if response['status_code'] == 200:
|
|
root = etree.fromstring(response['response'])
|
|
response_code = root.findtext('.//{*}Code')
|
|
# Code 100 means that the XML has been retrieved correctly
|
|
if response_code == '100':
|
|
return b64decode(root.findtext('.//{*}XmlBytesBase64'))
|
|
return False
|
|
|
|
def _get_attached_document_values(self, original_xml_etree, response_history):
|
|
values = {
|
|
'profile_execution_id': original_xml_etree.findtext('./{*}ProfileExecutionID'),
|
|
'id': original_xml_etree.findtext('./{*}ID'),
|
|
'uuid': self[-1].identifier,
|
|
'uuid_attrs': {
|
|
'schemeName': self[-1]._get_identifier_type().upper() + "-SHA384",
|
|
},
|
|
'issue_date': original_xml_etree.findtext('./{*}IssueDate'),
|
|
'issue_time': original_xml_etree.findtext('./{*}IssueTime'),
|
|
'document_type': "Contenedor de Factura Electrónica",
|
|
'parent_document_id': original_xml_etree.findtext('./{*}ID'),
|
|
'parent_documents': [],
|
|
}
|
|
|
|
for idx, event_xml in enumerate(response_history, start=1):
|
|
event_tree = etree.fromstring(event_xml)
|
|
values['parent_documents'].append({
|
|
'id': idx,
|
|
'uuid': self[-idx].identifier,
|
|
'uuid_attrs': {
|
|
'schemeName': self[-idx]._get_identifier_type().upper() + "-SHA384",
|
|
},
|
|
'document_id': event_tree.findtext('./{*}DocumentResponse/{*}DocumentReference/{*}ID'),
|
|
'issue_date': event_tree.findtext('./{*}IssueDate'),
|
|
'issue_time': event_tree.findtext('./{*}IssueTime'),
|
|
'response_code': event_tree.findtext('.//{*}Response/{*}ResponseCode'),
|
|
'validation_date': event_tree.findtext('./{*}IssueDate'),
|
|
'validation_time': event_tree.findtext('./{*}IssueTime'),
|
|
})
|
|
|
|
return values
|
|
|
|
def _demo_get_attached_document_values(self, original_xml_etree):
|
|
# Demo mode version: use all values that do not require a DIAN response
|
|
identifier_type = self[-1]._get_identifier_type().upper()
|
|
return {
|
|
'profile_execution_id': original_xml_etree.findtext('./{*}ProfileExecutionID'),
|
|
'id': original_xml_etree.findtext('./{*}ID'),
|
|
'uuid': self[-1].identifier,
|
|
'uuid_attrs': {
|
|
'schemeName': f"{identifier_type}-SHA384",
|
|
},
|
|
'issue_date': original_xml_etree.findtext('./{*}IssueDate'),
|
|
'issue_time': original_xml_etree.findtext('./{*}IssueTime'),
|
|
'document_type': "Contenedor de Factura Electrónica",
|
|
'parent_document_id': original_xml_etree.findtext('./{*}ID'),
|
|
'parent_document': {
|
|
'id': original_xml_etree.findtext('./{*}ID'),
|
|
'uuid': self[-1].identifier,
|
|
'uuid_attrs': {
|
|
'schemeName': f"{identifier_type}-SHA384",
|
|
},
|
|
'document_id': original_xml_etree.findtext('./{*}ID'),
|
|
'issue_date': 'Demo',
|
|
'issue_time': 'Demo',
|
|
'response_code': 'Demo',
|
|
'validation_date': 'Demo',
|
|
'validation_time': 'Demo',
|
|
},
|
|
}
|
|
|
|
def _get_response_history(self, current_response=None):
|
|
"""
|
|
Return a tuple (history, error_msg) where:
|
|
- history: is the responses of all the documents in 'self'
|
|
- error_msg: is set when we get a non 200 status code
|
|
"""
|
|
if self.move_id.company_id.l10n_co_dian_demo_mode:
|
|
return [etree.fromstring('<ApplicationResponse></ApplicationResponse>')], ""
|
|
|
|
if not current_response:
|
|
# Should not enter this if statement when handling Commercial Events
|
|
# call to GetStatus to get the ApplicationResponse
|
|
current_response = self._get_status()
|
|
if current_response['status_code'] != 200:
|
|
return [], self.env._(
|
|
"Error %(code)s when calling the DIAN server: %(response)s",
|
|
code=current_response['status_code'],
|
|
response=current_response['response'],
|
|
)
|
|
|
|
current_response_etree = etree.fromstring(current_response['response'])
|
|
current_response_raw = b64decode(current_response_etree.findtext(".//{*}XmlBase64Bytes"))
|
|
history = [current_response_raw]
|
|
|
|
# exclude the first and last documents:
|
|
# last: its xml has already been added to the history in the above section
|
|
# first: is a dummy document created by Odoo which represents the 'pending' state before
|
|
# the commercial event flow has started, its xml should therefore not be added to the history
|
|
for document in self.sorted()[1:-1]:
|
|
# Unzip attachment -> return the event xml from the AttachedDocument (in the last ParentDocumentLineReference)
|
|
attached_document = etree.fromstring(xml_utils._unzip(document.attachment_id.raw))
|
|
document_line_ref = attached_document.findall('./{*}ParentDocumentLineReference')[-1]
|
|
document_event_xml = document_line_ref.findtext('.//{*}Description').encode()
|
|
history.append(document_event_xml)
|
|
|
|
return history, ""
|
|
|
|
def _get_attached_document(self, status_response=None):
|
|
""" Return a tuple: (the attached document xml, an error message) """
|
|
if self.move_id.l10n_co_dian_commercial_state == 'pending':
|
|
# ensure_one for moves != ('in_invoice', 'in_refund') or when no event has been sent yet
|
|
self.ensure_one()
|
|
|
|
# all event xml's for every document in self in order
|
|
response_history, error_msg = self._get_response_history(current_response=status_response)
|
|
if error_msg:
|
|
return "", error_msg
|
|
|
|
current_attachment_raw = self.sorted()[:1].attachment_id.raw
|
|
original_xml_etree = etree.fromstring(current_attachment_raw)
|
|
|
|
if self[0]._get_company().l10n_co_dian_demo_mode:
|
|
vals = self._demo_get_attached_document_values(original_xml_etree=original_xml_etree)
|
|
else:
|
|
vals = self._get_attached_document_values(
|
|
original_xml_etree=original_xml_etree,
|
|
response_history=response_history,
|
|
)
|
|
|
|
attached_document = self.env['ir.qweb']._render('l10n_co_dian.attached_document', vals)
|
|
attached_doc_etree = etree.fromstring(attached_document)
|
|
|
|
# copy the Sender and Receiver from the original xml
|
|
supplier_node = original_xml_etree.find('./{*}AccountingSupplierParty//{*}PartyTaxScheme')
|
|
if supplier_node is None:
|
|
supplier_node = original_xml_etree.find('./{*}SenderParty//{*}PartyTaxScheme')
|
|
|
|
customer_node = original_xml_etree.find('./{*}AccountingCustomerParty//{*}PartyTaxScheme')
|
|
if customer_node is None:
|
|
customer_node = original_xml_etree.find('./{*}ReceiverParty//{*}PartyTaxScheme')
|
|
|
|
attached_doc_etree.find('./{*}SenderParty').append(supplier_node)
|
|
attached_doc_etree.find('./{*}ReceiverParty').append(customer_node)
|
|
|
|
# Add the xmls (enclosed in CDATA)
|
|
attached_doc_etree.find('./{*}Attachment/{*}ExternalReference/{*}Description').text = CDATA(current_attachment_raw.decode())
|
|
for idx, event_xml in enumerate(response_history, start=1):
|
|
document_element = attached_doc_etree.find(f'./{{*}}ParentDocumentLineReference/{{*}}LineID[.="{idx}"]/..')
|
|
|
|
if document_element is not None:
|
|
# Roundtrip it through etree to remove the XML declaration (<?xml version="1.0" encoding="utf-8"...)
|
|
event_xml = etree.tostring(etree.fromstring(event_xml), encoding='unicode')
|
|
document_element.find('.//{*}Description').text = CDATA(event_xml)
|
|
|
|
return etree.tostring(cleanup_xml_node(attached_doc_etree), encoding="UTF-8", xml_declaration=True), ""
|
|
|
|
def action_get_attached_document(self):
|
|
self.ensure_one()
|
|
attached_document, error = self._get_attached_document()
|
|
if error:
|
|
raise UserError(error)
|
|
attachment = self._create_attached_document(raw=attached_document)
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': f'/web/content/{attachment.id}?download=true',
|
|
}
|
|
|
|
def _create_attached_document(self, raw):
|
|
self.ensure_one()
|
|
return self.env['ir.attachment'].create([{
|
|
'raw': raw,
|
|
'name': self.move_id._l10n_co_dian_get_attached_document_filename() + '_manual.xml',
|
|
'res_model': 'account.move',
|
|
'res_id': self.move_id.id,
|
|
}])
|
|
|
|
@api.model
|
|
def _send_to_dian(self, xml, move):
|
|
""" Send an xml to DIAN.
|
|
If the Certification Process is activated, use the dedicated 'SendTestSetAsync' (asynchronous) webservice,
|
|
otherwise, use the 'SendBillSync' (synchronous) webservice.
|
|
|
|
:return: a l10n_co_dian.document
|
|
"""
|
|
# Zip the xml
|
|
zipped_content = xml_utils._zip_xml('invoice', xml)
|
|
|
|
if move.company_id.l10n_co_dian_test_environment and move.company_id.l10n_co_dian_certification_process:
|
|
document_vals = self._send_test_set_async(zipped_content, move)
|
|
else:
|
|
document_vals = self._send_bill_sync(zipped_content, move)
|
|
return self._create_document(xml, move, **document_vals)
|
|
|
|
@api.model
|
|
def _send_commercial_event(self, move, commercial_state_next):
|
|
locked_move = move.try_lock_for_update()
|
|
if not locked_move:
|
|
return self.env['l10n_co_dian.document']
|
|
|
|
xml, errors = self.env['account.edi.xml.ubl_dian']._export_co_send_event_update_status_invoice(locked_move, commercial_state_next)
|
|
if errors:
|
|
raise UserError(self.env._("Error(s) while generating the UBL file:\n- %s", '\n- '.join(errors)))
|
|
|
|
locked_move.l10n_co_dian_document_ids.filtered(lambda doc: doc.state == 'invoice_rejected').unlink()
|
|
|
|
filename = locked_move._l10n_co_dian_get_commercial_event_document_filename('zip')
|
|
zipped_content = xml_utils._zip_xml(filename, xml)
|
|
|
|
document_vals, response = self._send_event_update_status(zipped_content, locked_move, commercial_state_next)
|
|
document = self._create_document(xml, move, **document_vals)
|
|
|
|
if document.state != 'invoice_accepted':
|
|
# reset the filename sequence
|
|
sequence = self.env['ir.sequence'].search([('code', '=', EVENT_FILE_SEQUENCE_CODE), ('company_id', '=', self.move_id.company_id.id)])
|
|
sequence.number_next = sequence.number_next - 1
|
|
|
|
return document
|
|
|
|
attached_document_xml, error = move.l10n_co_dian_document_ids._get_attached_document(response)
|
|
if error:
|
|
raise UserError(error)
|
|
|
|
attached_document = self.env['ir.attachment'].create([{
|
|
'raw': attached_document_xml,
|
|
'name': f"{filename}.xml",
|
|
'res_model': 'l10n_co_dian.document',
|
|
'res_id': document.id,
|
|
}])
|
|
|
|
attached_document_zip = self.env['ir.attachment'].create([{
|
|
'name': f"{filename}.zip",
|
|
'raw': attached_document._build_zip_from_attachments(),
|
|
'res_model': 'l10n_co_dian.document',
|
|
'res_id': document.id,
|
|
}])
|
|
|
|
attached_document.unlink() # was only necessary to build the zip file
|
|
document.attachment_id.unlink() # _create_document sets the attachment_id to the sent xml file
|
|
|
|
document.attachment_id = attached_document_zip
|
|
if not modules.module.current_test:
|
|
self.env.cr.commit()
|
|
return document
|
|
|
|
@api.model
|
|
def _send_get_status_event(self, move, track_id):
|
|
if move.company_id.l10n_co_dian_demo_mode:
|
|
return
|
|
|
|
document_vals, response = self._get_status_event(move.company_id, track_id)
|
|
if not response:
|
|
raise UserError('\n- '.join(document_vals['message_json']['errors']))
|
|
|
|
date_time = datetime.now()
|
|
document = self._create_document(response['response'], move, identifier=track_id, datetime=date_time, **document_vals)
|
|
if document.state != 'invoice_accepted':
|
|
return
|
|
|
|
document_xml = b64decode(etree.fromstring(response['response']).findtext('.//{*}XmlBase64Bytes'))
|
|
|
|
# Get the commercial state
|
|
root = etree.fromstring(document_xml)
|
|
last_response = root.findall('.//{*}DocumentResponse')[-1]
|
|
commercial_status_code = last_response.findtext('./{*}Response/{*}ResponseCode')
|
|
|
|
document.attachment_id.raw = document_xml
|
|
|
|
commercial_states = self.env['account.move']._fields['l10n_co_dian_commercial_state']._description_selection(self.env)
|
|
commercial_state = next(k for k, v in commercial_states if v.split(' - ', 1)[0] == commercial_status_code)
|
|
document.commercial_state = commercial_state
|
|
|
|
def action_get_status(self):
|
|
for doc in self:
|
|
doc._get_status_zip()
|
|
|
|
def action_download_file(self):
|
|
self.ensure_one()
|
|
return {
|
|
'type': 'ir.actions.act_url',
|
|
'url': f'/web/content/{self.attachment_id.id}?download=true',
|
|
}
|
|
|
|
def _get_company(self):
|
|
self.ensure_one()
|
|
return self.move_id.company_id
|
|
|
|
def _get_identifier_type(self):
|
|
self.ensure_one()
|
|
return self.move_id.l10n_co_dian_identifier_type
|