1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/documents_spreadsheet/models/documents_document.py
2024-12-10 09:04:09 +07:00

355 lines
13 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
import io
import json
import zipfile
import datetime
from lxml import etree
from odoo import _, fields, models, api
from odoo.exceptions import UserError, AccessError, ValidationError
from odoo.osv import expression
from odoo.tools import image_process
SUPPORTED_PATHS = (
"[Content_Types].xml",
"xl/sharedStrings.xml",
"xl/styles.xml",
"xl/workbook.xml",
"_rels/",
"xl/_rels",
"xl/charts/",
"xl/drawings/",
"xl/externalLinks/",
"xl/pivotTables/",
"xl/tables/",
"xl/theme/",
"xl/worksheets/",
"xl/media",
)
XLSX_MIME_TYPES = [
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/wps-office.xlsx",
]
class Document(models.Model):
_name = "documents.document"
_inherit = ["documents.document", "spreadsheet.mixin"]
spreadsheet_binary_data = fields.Binary(compute='_compute_spreadsheet_binary_data', inverse='_inverse_spreadsheet_binary_data', default=None)
handler = fields.Selection(
[("spreadsheet", "Spreadsheet")], ondelete={"spreadsheet": "cascade"}
)
@api.model_create_multi
def create(self, vals_list):
vals_list = self._assign_spreadsheet_default_folder(vals_list)
vals_list = self._resize_spreadsheet_thumbnails(vals_list)
documents = super().create(vals_list)
documents._update_spreadsheet_contributors()
return documents
def write(self, vals):
if 'handler' not in vals and 'mimetype' in vals and vals['mimetype'] != 'application/o-spreadsheet':
vals['handler'] = False
if 'spreadsheet_data' in vals:
self._update_spreadsheet_contributors()
if all(document.handler == 'spreadsheet' for document in self):
vals = self._resize_thumbnail_value(vals)
return super().write(vals)
def join_spreadsheet_session(self, share_id=None, access_token=None):
if self.sudo().handler != "spreadsheet":
raise ValidationError(_("The spreadsheet you are trying to access does not exist."))
data = super().join_spreadsheet_session(share_id, access_token)
self._update_spreadsheet_contributors()
return dict(data, is_favorited=self.sudo().is_favorited, folder_id=self.sudo().folder_id.id)
def _check_spreadsheet_share(self, operation, share_id, access_token):
share = self.env['documents.share'].browse(share_id).sudo()
available_documents = share._get_documents_and_check_access(access_token, operation=operation)
if not available_documents or self not in available_documents:
raise AccessError(_("You don't have access to this document"))
def _compute_file_extension(self):
""" Spreadsheet documents do not have file extension. """
spreadsheet_docs = self.filtered(lambda rec: rec.handler == "spreadsheet")
spreadsheet_docs.file_extension = False
super(Document, self - spreadsheet_docs)._compute_file_extension()
@api.depends("attachment_id", "handler")
def _compute_spreadsheet_data(self):
for document in self.with_context(bin_size=False):
if document.handler == "spreadsheet":
document.spreadsheet_data = document.attachment_id.raw
else:
document.spreadsheet_data = False
@api.depends("datas", "handler")
def _compute_spreadsheet_binary_data(self):
for document in self:
if document.handler == "spreadsheet":
document.spreadsheet_binary_data = document.datas
else:
document.spreadsheet_binary_data = False
def _inverse_spreadsheet_binary_data(self):
for document in self:
if document.handler == "spreadsheet":
document.write({
"datas": document.spreadsheet_binary_data,
"mimetype": "application/o-spreadsheet"
})
@api.depends("checksum", "handler")
def _compute_thumbnail(self):
# Spreadsheet thumbnails cannot be computed from their binary data.
# They should be saved independently.
spreadsheets = self.filtered(lambda d: d.handler == "spreadsheet")
super(Document, self - spreadsheets)._compute_thumbnail()
def _copy_spreadsheet_image_attachments(self):
if self.handler != "spreadsheet":
return
super()._copy_spreadsheet_image_attachments()
def _resize_thumbnail_value(self, vals):
if 'thumbnail' in vals:
return dict(
vals,
thumbnail=base64.b64encode(image_process(base64.b64decode(vals['thumbnail'] or ''), size=(750, 750), crop='center')),
)
return vals
def _resize_spreadsheet_thumbnails(self, vals_list):
return [
(
self._resize_thumbnail_value(vals)
if vals.get('handler') == 'spreadsheet'
else vals
)
for vals in vals_list
]
def _assign_spreadsheet_default_folder(self, vals_list):
"""Make sure spreadsheet values have a `folder_id`. Assign the
default spreadsheet folder if there is none.
"""
# Use the current company's spreadsheet workspace, since `company_id` on `documents.document` is a related field
# on `folder_id` we do not need to check vals_list for different companies.
default_folder = self.env.company.documents_spreadsheet_folder_id
if not default_folder:
default_folder = self.env['documents.folder'].search([], limit=1, order="sequence asc")
return [
(
dict(vals, folder_id=vals.get('folder_id', default_folder.id))
if vals.get('handler') == 'spreadsheet'
else vals
)
for vals in vals_list
]
def _update_spreadsheet_contributors(self):
"""Add the current user to the spreadsheet contributors.
"""
for document in self:
if document.handler == 'spreadsheet':
self.env['spreadsheet.contributor']._update(self.env.user, document)
@api.model
def action_open_new_spreadsheet(self, vals=None):
if vals is None:
vals = {}
spreadsheet = self.create({
"name": _("Untitled spreadsheet"),
"mimetype": "application/o-spreadsheet",
"datas": self._empty_spreadsheet_data_base64(),
"handler": "spreadsheet",
**vals,
})
return {
"type": "ir.actions.client",
"tag": "action_open_spreadsheet",
"params": {
"spreadsheet_id": spreadsheet.id,
"is_new_spreadsheet": True,
},
}
@api.model
def get_spreadsheets_to_display(self, domain, offset=0, limit=None):
"""
Get all the spreadsheets, with the spreadsheet that the user has recently
opened at first.
"""
Contrib = self.env["spreadsheet.contributor"]
visible_docs = self.search(expression.AND([domain, [("handler", "=", "spreadsheet")]]))
contribs = Contrib.search(
[
("document_id", "in", visible_docs.ids),
("user_id", "=", self.env.user.id),
],
order="last_update_date desc",
)
user_docs = contribs.document_id
# Intersection is used to apply the `domain` to `user_doc`, the union is
# here to keep only the visible docs, but with the order of contribs.
docs = ((user_docs & visible_docs) | visible_docs)
if (limit):
docs = docs[offset:offset + limit]
else:
docs = docs[offset:]
return docs.read(["name", "thumbnail"])
def clone_xlsx_into_spreadsheet(self, archive_source=False):
"""Clone an XLSX document into a new document with its content unzipped, and return the new document id"""
self.ensure_one()
unzipped, attachments = self._unzip_xlsx()
doc = self.copy({
"handler": "spreadsheet",
"mimetype": "application/o-spreadsheet",
"name": self.name.rstrip(".xlsx"),
"spreadsheet_data": json.dumps(unzipped)
})
for attachment in attachments:
attachment.write({'res_id': doc.id})
if archive_source:
self.action_archive()
return doc.id
def _get_is_multipage(self):
"""Override for spreadsheets and xlsx."""
is_multipage = super()._get_is_multipage()
if is_multipage is not None:
return is_multipage
if self.mimetype in XLSX_MIME_TYPES and self.attachment_id.raw:
file = io.BytesIO(self.attachment_id.raw)
if not zipfile.is_zipfile(file):
return None
with zipfile.ZipFile(file) as archive:
if '[Content_Types].xml' not in archive.namelist():
# the xlsx file is invalid
return None
with archive.open("[Content_Types].xml") as myfile:
content = myfile.read()
tree = etree.fromstring(content)
nodes = tree.xpath(
"//ns:Override[@ContentType='application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml']",
namespaces={"ns": "http://schemas.openxmlformats.org/package/2006/content-types"}
)
return len(nodes) > 1
if self.handler == "spreadsheet":
spreadsheet_data = json.loads(self.spreadsheet_data)
if spreadsheet_data.get("sheets"):
return len(spreadsheet_data["sheets"]) > 1
return self._is_xlsx_data_multipage(spreadsheet_data)
@api.model
def _is_xlsx_data_multipage(self, spreadsheet_data):
for filename, content in spreadsheet_data.items():
if filename.endswith("workbook.xml.rels"):
tree = etree.fromstring(content.encode())
nodes = tree.findall(
'.//rels:Relationship',
{'rels': 'http://schemas.openxmlformats.org/package/2006/relationships'}
)
found_first_sheet = False
for node in nodes:
if node.attrib["Type"].endswith('/relationships/worksheet'):
if found_first_sheet:
return True
found_first_sheet = True
break
return False
def _unzip_xlsx(self):
file = io.BytesIO(self.attachment_id.raw)
if not zipfile.is_zipfile(file) or self.mimetype not in XLSX_MIME_TYPES:
raise XSLXReadUserError(_("The file is not a xlsx file"))
unzipped_size = 0
with zipfile.ZipFile(file) as input_zip:
if len(input_zip.infolist()) > 1000:
raise XSLXReadUserError(_("The xlsx file is too big"))
if "[Content_Types].xml" not in input_zip.namelist() or \
not any(name.startswith("xl/") for name in input_zip.namelist()):
raise XSLXReadUserError(_("The xlsx file is corrupted"))
unzipped = {}
attachments = []
for info in input_zip.infolist():
if not (info.filename.endswith((".xml", ".xml.rels")) or "media/image" in info.filename) or\
not info.filename.startswith(SUPPORTED_PATHS):
# Don't extract files others than xmls or unsupported xmls
continue
unzipped_size += info.file_size
if unzipped_size > 50 * 1000 * 1000: # 50MB
raise XSLXReadUserError(_("The xlsx file is too big"))
if info.filename.endswith((".xml", ".xml.rels")):
unzipped[info.filename] = input_zip.read(info.filename).decode()
elif "media/image" in info.filename:
image_file = input_zip.read(info.filename)
attachment = self._upload_image_file(image_file, info.filename)
attachments.append(attachment)
unzipped[info.filename] = {
"imageSrc": "/web/image/" + str(attachment.id),
}
return unzipped, attachments
def _upload_image_file(self, image_file, filename):
attachment_model = self.env['ir.attachment']
attachment = attachment_model.create({
'name': filename,
'datas': base64.encodebytes(image_file),
'res_model': "documents.document",
})
attachment._post_add_create()
return attachment
@api.autovacuum
def _gc_spreadsheet(self):
yesterday = datetime.datetime.utcnow() - datetime.timedelta(days=1)
domain = [
('handler', '=', 'spreadsheet'),
('create_date', '<', yesterday),
('spreadsheet_revision_ids', '=', False),
('spreadsheet_snapshot', '=', False),
('previous_attachment_ids', '=', False)
]
self.search(domain).action_archive()
def action_edit(self):
self.ensure_one()
return {
'type': 'ir.actions.client',
'tag': 'action_open_spreadsheet',
'params': {
'spreadsheet_id': self.id,
}
}
def _creation_msg(self):
return _("New spreadsheet created in Documents")
class XSLXReadUserError(UserError):
pass