add storage
|
@ -0,0 +1,96 @@
|
|||
================================
|
||||
DB attachments saved by checksum
|
||||
================================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:c73ebaaf529b35687e5e74d04c15be4c57e6fd38a47931733ced9bc6304b0b54
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/storage/tree/14.0/attachment_db_by_checksum
|
||||
:alt: OCA/storage
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-attachment_db_by_checksum
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Allow to identify database attachments through their hash, avoiding duplicates.
|
||||
|
||||
This is typically useful when you want to save attachments to database but you want to save space avoiding to write the same content in several attachments (think of email attachments, for example, or any file uploaded more than once).
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
Set system parameter ``ir_attachment.location`` to ``hashed_db`` to activate saving by checksum.
|
||||
|
||||
Run ``force_storage``, method of ``ir.attachment``, to move existing attachments.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20attachment_db_by_checksum%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* TAKOBI
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* `TAKOBI <https://takobi.online>`_:
|
||||
|
||||
* Lorenzo Battistini
|
||||
* Simone Rubino <sir@takobi.online>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
.. |maintainer-eLBati| image:: https://github.com/eLBati.png?size=40px
|
||||
:target: https://github.com/eLBati
|
||||
:alt: eLBati
|
||||
|
||||
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|
||||
|
||||
|maintainer-eLBati|
|
||||
|
||||
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/14.0/attachment_db_by_checksum>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
|
@ -0,0 +1,3 @@
|
|||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import models
|
|
@ -0,0 +1,21 @@
|
|||
# Copyright 2021 Lorenzo Battistini @ TAKOBI
|
||||
# Copyright 2023 Simone Rubino - TAKOBI
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
{
|
||||
"name": "DB attachments saved by checksum",
|
||||
"summary": "Allow to identify database attachments through their hash, avoiding duplicates",
|
||||
"version": "14.0.1.0.0",
|
||||
"category": "Storage",
|
||||
"website": "https://github.com/OCA/storage",
|
||||
"author": "TAKOBI, Odoo Community Association (OCA)",
|
||||
"maintainers": [
|
||||
"eLBati",
|
||||
],
|
||||
"license": "LGPL-3",
|
||||
"depends": [
|
||||
"base",
|
||||
],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
],
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * attachment_db_by_checksum
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model,name:attachment_db_by_checksum.model_ir_attachment
|
||||
msgid "Attachment"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model,name:attachment_db_by_checksum.model_ir_attachment_content
|
||||
msgid "Attachment content by hash"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,help:attachment_db_by_checksum.field_ir_attachment_content__checksum
|
||||
msgid "Checksum in the shape 2a/2a...\n"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__checksum
|
||||
msgid "Checksum/SHA1"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__db_datas
|
||||
msgid "Database Data"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment__display_name
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment__id
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment____last_update
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.fields,field_description:attachment_db_by_checksum.field_ir_attachment_content__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: code:addons/attachment_db_by_checksum/models/ir_attachment.py:0
|
||||
#, python-format
|
||||
msgid "Only administrators can execute this action."
|
||||
msgstr ""
|
||||
|
||||
#. module: attachment_db_by_checksum
|
||||
#: model:ir.model.constraint,message:attachment_db_by_checksum.constraint_ir_attachment_content_checksum_uniq
|
||||
msgid "The checksum of the file must be unique!"
|
||||
msgstr ""
|
|
@ -0,0 +1,4 @@
|
|||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import ir_attachment_content
|
||||
from . import ir_attachment
|
|
@ -0,0 +1,136 @@
|
|||
# Copyright 2023 Simone Rubino - TAKOBI
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import AccessError
|
||||
from odoo.osv import expression
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
HASHED_STORAGE_PARAMETER = "hashed_db"
|
||||
|
||||
|
||||
class Attachment(models.Model):
|
||||
_inherit = "ir.attachment"
|
||||
|
||||
@api.model
|
||||
def _file_write_by_checksum(self, bin_value, checksum):
|
||||
"""Store attachment content in `Attachment content by hash`."""
|
||||
fname, full_path = self._get_path(bin_value, checksum)
|
||||
attachment_content = self.env["ir.attachment.content"].search_by_checksum(fname)
|
||||
if not attachment_content:
|
||||
self.env["ir.attachment.content"].create(
|
||||
{
|
||||
"checksum": fname,
|
||||
"db_datas": bin_value,
|
||||
}
|
||||
)
|
||||
return fname
|
||||
|
||||
@api.model
|
||||
def _file_write(self, bin_value, checksum):
|
||||
location = self._storage()
|
||||
if location == HASHED_STORAGE_PARAMETER:
|
||||
return self._file_write_by_checksum(bin_value, checksum)
|
||||
return super()._file_write(bin_value, checksum)
|
||||
|
||||
@api.model
|
||||
def _file_read_by_checksum(self, fname):
|
||||
"""Read attachment content from `Attachment content by hash`."""
|
||||
attachment_content = self.env["ir.attachment.content"].search_by_checksum(fname)
|
||||
if attachment_content:
|
||||
bin_value = attachment_content.db_datas
|
||||
else:
|
||||
# Fallback on standard behavior
|
||||
_logger.debug("File %s not found" % fname)
|
||||
bin_value = super()._file_read(fname)
|
||||
return bin_value
|
||||
|
||||
@api.model
|
||||
def _file_read(self, fname):
|
||||
location = self._storage()
|
||||
if location == HASHED_STORAGE_PARAMETER:
|
||||
return self._file_read_by_checksum(fname)
|
||||
return super()._file_read(fname)
|
||||
|
||||
@api.model
|
||||
def _get_all_attachments_by_checksum_domain(self, fname=None):
|
||||
"""Get domain for finding all the attachments.
|
||||
|
||||
If `checksum` is provided,
|
||||
get domain for finding all the attachments having checksum `checksum`.
|
||||
"""
|
||||
# trick to get every attachment, see _search method of ir.attachment
|
||||
domain = [
|
||||
("id", "!=", 0),
|
||||
]
|
||||
if fname is not None:
|
||||
checksum_domain = [
|
||||
("store_fname", "=", fname),
|
||||
]
|
||||
domain = expression.AND(
|
||||
[
|
||||
domain,
|
||||
checksum_domain,
|
||||
]
|
||||
)
|
||||
return domain
|
||||
|
||||
@api.model
|
||||
def _get_all_attachments_by_checksum(self, fname=None):
|
||||
"""Get all attachments.
|
||||
|
||||
If `checksum` is provided,
|
||||
get all the attachments having checksum `checksum`.
|
||||
"""
|
||||
domain = self._get_all_attachments_by_checksum_domain(fname)
|
||||
invisible_menu_context = {
|
||||
"ir.ui.menu.full_list": True,
|
||||
}
|
||||
attachments = self.with_context(**invisible_menu_context).search(domain)
|
||||
return attachments
|
||||
|
||||
@api.model
|
||||
def _file_delete_by_checksum(self, fname):
|
||||
"""Delete attachment content in `Attachment content by hash`."""
|
||||
attachments = self._get_all_attachments_by_checksum(fname=fname)
|
||||
if not attachments:
|
||||
attachment_content = self.env["ir.attachment.content"].search_by_checksum(
|
||||
fname
|
||||
)
|
||||
attachment_content.unlink()
|
||||
|
||||
@api.model
|
||||
def _file_delete(self, fname):
|
||||
location = self._storage()
|
||||
if location == HASHED_STORAGE_PARAMETER:
|
||||
self._file_delete_by_checksum(fname)
|
||||
return super()._file_delete(fname)
|
||||
|
||||
@api.model
|
||||
def force_storage_by_checksum(self):
|
||||
"""Copy all the attachments to `Attachment content by hash`."""
|
||||
if not self.env.is_admin():
|
||||
raise AccessError(_("Only administrators can execute this action."))
|
||||
|
||||
# we don't know if previous storage was file system or DB:
|
||||
# we run for every attachment
|
||||
all_attachments = self._get_all_attachments_by_checksum()
|
||||
for attach in all_attachments:
|
||||
attach.write(
|
||||
{
|
||||
"datas": attach.datas,
|
||||
# do not try to guess mimetype overwriting existing value
|
||||
"mimetype": attach.mimetype,
|
||||
}
|
||||
)
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def force_storage(self):
|
||||
location = self._storage()
|
||||
if location == HASHED_STORAGE_PARAMETER:
|
||||
return self.force_storage_by_checksum()
|
||||
return super().force_storage()
|
|
@ -0,0 +1,43 @@
|
|||
from odoo import fields, models
|
||||
|
||||
|
||||
class AttachmentContent(models.Model):
|
||||
_name = "ir.attachment.content"
|
||||
_rec_name = "checksum"
|
||||
_description = "Attachment content by hash"
|
||||
|
||||
checksum = fields.Char(
|
||||
string="Checksum/SHA1",
|
||||
help="Checksum in the shape 2a/2a...\n",
|
||||
index=True,
|
||||
readonly=True,
|
||||
required=True,
|
||||
)
|
||||
db_datas = fields.Binary(
|
||||
string="Database Data",
|
||||
attachment=False,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"checksum_uniq",
|
||||
"unique(checksum)",
|
||||
"The checksum of the file must be unique!",
|
||||
),
|
||||
]
|
||||
|
||||
def search_by_checksum(self, fname):
|
||||
"""Get Attachment content, searching by `fname`.
|
||||
|
||||
Note that `fname` is the relative path of the attachment
|
||||
as it would be saved by the core, for example 2a/2a...,
|
||||
this is the same value that we store
|
||||
in field `ir.attachment.content.checksum`.
|
||||
"""
|
||||
attachment_content = self.env["ir.attachment.content"].search(
|
||||
[
|
||||
("checksum", "=", fname),
|
||||
],
|
||||
limit=1,
|
||||
)
|
||||
return attachment_content
|
|
@ -0,0 +1,3 @@
|
|||
Set system parameter ``ir_attachment.location`` to ``hashed_db`` to activate saving by checksum.
|
||||
|
||||
Run ``force_storage``, method of ``ir.attachment``, to move existing attachments.
|
|
@ -0,0 +1,4 @@
|
|||
* `TAKOBI <https://takobi.online>`_:
|
||||
|
||||
* Lorenzo Battistini
|
||||
* Simone Rubino <sir@takobi.online>
|
|
@ -0,0 +1,3 @@
|
|||
Allow to identify database attachments through their hash, avoiding duplicates.
|
||||
|
||||
This is typically useful when you want to save attachments to database but you want to save space avoiding to write the same content in several attachments (think of email attachments, for example, or any file uploaded more than once).
|
|
@ -0,0 +1,4 @@
|
|||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
"access_ir_attachment_all","Everyone can read Attachment Contents","model_ir_attachment_content",,1,0,0,0
|
||||
"access_ir_attachment_group_user","Internal Users can manage Attachment Contents","model_ir_attachment_content","base.group_user",1,1,1,1
|
||||
"access_ir_attachment_portal","Portal Users can read and create Attachment Contents","model_ir_attachment_content","base.group_portal",1,0,1,0
|
|
After Width: | Height: | Size: 9.2 KiB |
|
@ -0,0 +1,434 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>DB attachments saved by checksum</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="db-attachments-saved-by-checksum">
|
||||
<h1 class="title">DB attachments saved by checksum</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:c73ebaaf529b35687e5e74d04c15be4c57e6fd38a47931733ced9bc6304b0b54
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/14.0/attachment_db_by_checksum"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-attachment_db_by_checksum"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>Allow to identify database attachments through their hash, avoiding duplicates.</p>
|
||||
<p>This is typically useful when you want to save attachments to database but you want to save space avoiding to write the same content in several attachments (think of email attachments, for example, or any file uploaded more than once).</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-1">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Configuration</a></h1>
|
||||
<p>Set system parameter <tt class="docutils literal">ir_attachment.location</tt> to <tt class="docutils literal">hashed_db</tt> to activate saving by checksum.</p>
|
||||
<p>Run <tt class="docutils literal">force_storage</tt>, method of <tt class="docutils literal">ir.attachment</tt>, to move existing attachments.</p>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/storage/issues/new?body=module:%20attachment_db_by_checksum%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>TAKOBI</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li><a class="reference external" href="https://takobi.online">TAKOBI</a>:<ul>
|
||||
<li>Lorenzo Battistini</li>
|
||||
<li>Simone Rubino <<a class="reference external" href="mailto:sir@takobi.online">sir@takobi.online</a>></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
|
||||
<p><a class="reference external image-reference" href="https://github.com/eLBati"><img alt="eLBati" src="https://github.com/eLBati.png?size=40px" /></a></p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/14.0/attachment_db_by_checksum">OCA/storage</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,3 @@
|
|||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from . import test_attachment_by_checksum
|
|
@ -0,0 +1,118 @@
|
|||
# Copyright 2023 Simone Rubino - TAKOBI
|
||||
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
|
||||
from odoo.tests import SavepointCase
|
||||
|
||||
from odoo.addons.attachment_db_by_checksum.models.ir_attachment import (
|
||||
HASHED_STORAGE_PARAMETER,
|
||||
)
|
||||
|
||||
|
||||
class TestAttachmentByChecksum(SavepointCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
|
||||
cls.data = b"Test attachment data"
|
||||
cls.attachment = cls.env["ir.attachment"].create(
|
||||
{
|
||||
"name": "Test attachment",
|
||||
"datas": base64.b64encode(cls.data),
|
||||
}
|
||||
)
|
||||
# Save the fname (a2/a2...) of the attachment
|
||||
# so that we can use it in tests where the attachment is deleted
|
||||
cls.fname = cls.attachment.store_fname
|
||||
|
||||
@classmethod
|
||||
def _set_hashed_db_storage(cls):
|
||||
"""Set `hashed_db` Attachment Storage."""
|
||||
cls.env["ir.config_parameter"].set_param(
|
||||
"ir_attachment.location",
|
||||
HASHED_STORAGE_PARAMETER,
|
||||
)
|
||||
|
||||
def test_force_storage(self):
|
||||
"""Move storage from default to `hashed_db`:
|
||||
attachments are copied in `Attachment content by hash` records.
|
||||
"""
|
||||
# Arrange: Create an attachment
|
||||
data = self.data
|
||||
fname = self.fname
|
||||
attachment = self.attachment
|
||||
# pre-condition: The storage is not `hashed_db`
|
||||
self.assertNotEqual(
|
||||
self.env["ir.attachment"]._storage(), HASHED_STORAGE_PARAMETER
|
||||
)
|
||||
self.assertEqual(attachment.raw, data)
|
||||
|
||||
# Act: Move the storage
|
||||
self._set_hashed_db_storage()
|
||||
self.env["ir.attachment"].force_storage()
|
||||
|
||||
# Assert: The attachment value is both in the attachment
|
||||
# and in the Attachment content by hash
|
||||
self.assertEqual(self.env["ir.attachment"]._storage(), HASHED_STORAGE_PARAMETER)
|
||||
self.assertEqual(attachment.raw, data)
|
||||
attachment_content = self.env["ir.attachment.content"].search_by_checksum(fname)
|
||||
self.assertEqual(attachment_content.db_datas, data)
|
||||
|
||||
def test_new_hashed_attachment(self):
|
||||
"""Storage is `hashed_db`:
|
||||
new attachments are only stored in `Attachment content by hash` records.
|
||||
"""
|
||||
# Arrange: Set the storage to `hashed_db`
|
||||
data = self.data
|
||||
fname = self.fname
|
||||
self.attachment.unlink()
|
||||
self._set_hashed_db_storage()
|
||||
# pre-condition
|
||||
self.assertEqual(self.env["ir.attachment"]._storage(), HASHED_STORAGE_PARAMETER)
|
||||
|
||||
# Act: Create an attachment
|
||||
self.env["ir.attachment"].create(
|
||||
{
|
||||
"name": "Test attachment",
|
||||
"datas": base64.b64encode(data),
|
||||
}
|
||||
)
|
||||
|
||||
# Assert: The new attachment value is in the Attachment content by hash
|
||||
self.assertEqual(self.env["ir.attachment"]._storage(), HASHED_STORAGE_PARAMETER)
|
||||
attachment_content = self.env["ir.attachment.content"].search_by_checksum(fname)
|
||||
self.assertEqual(attachment_content.db_datas, data)
|
||||
|
||||
def test_force_storage_invisible_menu(self):
|
||||
"""Move storage from default to `hashed_db`:
|
||||
attachments linked to invisible menus
|
||||
are copied in `Attachment content by hash` records.
|
||||
"""
|
||||
# Arrange: Create a menu invisible for current user
|
||||
fname = self.fname
|
||||
self.attachment.unlink()
|
||||
menu_model = self.env["ir.ui.menu"]
|
||||
invisible_menu = menu_model.create(
|
||||
{
|
||||
"name": "Test invisible menu",
|
||||
"web_icon_data": base64.b64encode(self.data),
|
||||
"groups_id": [(6, 0, self.env.ref("base.group_no_one").ids)],
|
||||
}
|
||||
)
|
||||
# pre-condition: The menu is invisible and storage is not `hashed_db`
|
||||
self.assertNotEqual(
|
||||
self.env["ir.attachment"]._storage(), HASHED_STORAGE_PARAMETER
|
||||
)
|
||||
self.assertNotIn(invisible_menu, menu_model.search([]))
|
||||
|
||||
# Act: Move the storage to `hashed_db`
|
||||
self._set_hashed_db_storage()
|
||||
self.env["ir.attachment"].with_user(
|
||||
self.env.ref("base.user_admin")
|
||||
).force_storage()
|
||||
|
||||
# Assert: The menu's attachment value is in the Attachment content by hash
|
||||
self.assertEqual(self.env["ir.attachment"]._storage(), HASHED_STORAGE_PARAMETER)
|
||||
attachment_content = self.env["ir.attachment.content"].search_by_checksum(fname)
|
||||
self.assertTrue(attachment_content)
|
|
@ -0,0 +1,41 @@
|
|||
|
||||
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
|
||||
============
|
||||
Storage File
|
||||
============
|
||||
|
||||
|
||||
External file management depending on Storage Backend module.
|
||||
|
||||
It include these features:
|
||||
* link to any Odoo model/record
|
||||
* store metadata like: checksum, mimetype
|
||||
|
||||
Use cases (with help of additionnal modules):
|
||||
- store pdf (like invoices) on a file server with high SLA
|
||||
- access attachments with read/write on prod environment and only read only on dev / testing
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* Update README with the last model of README when migration to v11 in OCA
|
||||
* No file deletion / unlink
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Sebastien Beau <sebastien.beau@akretion.com>
|
||||
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
* Akretion
|
|
@ -0,0 +1,3 @@
|
|||
from . import controllers
|
||||
from . import models
|
||||
from . import wizards
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
{
|
||||
"name": "Storage File",
|
||||
"summary": "Storage file in storage backend",
|
||||
"version": "14.0.2.4.0",
|
||||
"category": "Storage",
|
||||
"website": "https://github.com/OCA/storage",
|
||||
"author": " Akretion, Odoo Community Association (OCA)",
|
||||
"license": "LGPL-3",
|
||||
"development_status": "Production/Stable",
|
||||
"application": False,
|
||||
"installable": True,
|
||||
"external_dependencies": {"python": ["python_slugify"]},
|
||||
"depends": ["storage_backend"],
|
||||
"data": [
|
||||
"views/storage_file_view.xml",
|
||||
"views/storage_backend_view.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"security/storage_file.xml",
|
||||
"data/ir_cron.xml",
|
||||
"data/storage_backend.xml",
|
||||
],
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
from . import main
|
|
@ -0,0 +1,39 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import base64
|
||||
|
||||
import werkzeug.utils
|
||||
import werkzeug.wrappers
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
|
||||
|
||||
class StorageFileController(http.Controller):
|
||||
@http.route(
|
||||
["/storage.file/<string:slug_name_with_id>"], type="http", auth="public"
|
||||
)
|
||||
def content_common(self, slug_name_with_id, token=None, download=None, **kw):
|
||||
storage_file = request.env["storage.file"].get_from_slug_name_with_id(
|
||||
slug_name_with_id
|
||||
)
|
||||
status, headers, content = request.env["ir.http"].binary_content(
|
||||
model=storage_file._name,
|
||||
id=storage_file.id,
|
||||
field="data",
|
||||
filename_field="name",
|
||||
download=download,
|
||||
)
|
||||
if status == 304:
|
||||
response = werkzeug.wrappers.Response(status=status, headers=headers)
|
||||
elif status == 301:
|
||||
return werkzeug.utils.redirect(content, code=301)
|
||||
elif status != 200:
|
||||
response = request.not_found()
|
||||
else:
|
||||
content_base64 = base64.b64decode(content)
|
||||
headers.append(("Content-Length", len(content_base64)))
|
||||
response = request.make_response(content_base64, headers)
|
||||
if token:
|
||||
response.set_cookie("fileToken", token)
|
||||
return response
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record forcecreate="True" id="ir_cron_clean_storage_file" model="ir.cron">
|
||||
<field name="name">Clean Storage File</field>
|
||||
<field eval="True" name="active" />
|
||||
<field name="user_id" ref="base.user_root" />
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">days</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field eval="False" name="doall" />
|
||||
<field name="model_id" ref="model_storage_file" />
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._clean_storage_file()</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="storage_backend.default_storage_backend" model="storage.backend">
|
||||
<field name="filename_strategy">name_with_id</field>
|
||||
<field name="served_by">odoo</field>
|
||||
<field name="is_public" eval="False" />
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,361 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * storage_file
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_file__url_path
|
||||
msgid "Accessible path, no base URL"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__active
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__backend_view_use_internal_url
|
||||
msgid "Backend View Use Internal Url"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form
|
||||
msgid "Base URL used for files"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url
|
||||
msgid "Base Url"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__base_url_for_files
|
||||
msgid "Base Url For Files"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__checksum
|
||||
msgid "Checksum/SHA1"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.actions.server,name:storage_file.ir_cron_clean_storage_file_ir_actions_server
|
||||
#: model:ir.cron,cron_name:storage_file.ir_cron_clean_storage_file
|
||||
#: model:ir.cron,name:storage_file.ir_cron_clean_storage_file
|
||||
msgid "Clean Storage File"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__company_id
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_uid
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__create_date
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__data
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__data
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_file__data
|
||||
msgid "Data"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_backend__backend_view_use_internal_url
|
||||
msgid ""
|
||||
"Decide if Odoo backend views should use the external URL (usually a CDN) or "
|
||||
"the internal url with direct access to the storage. This could save you some"
|
||||
" money if you pay by CDN traffic."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_backend__is_public
|
||||
msgid ""
|
||||
"Define if every files stored into this backend are public or not. Examples:\n"
|
||||
"Private: your file/image can not be displayed is the user is not logged (not available on other website);\n"
|
||||
"Public: your file/image can be displayed if nobody is logged (useful to display files on external websites)"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_ir_actions_report__display_name
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__display_name
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__display_name
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__extension
|
||||
msgid "Extension"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__external
|
||||
msgid "External"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.actions.act_window,name:storage_file.act_open_storage_file_view
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__file_id
|
||||
#: model:ir.ui.menu,name:storage_file.menu_storage_file
|
||||
#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:storage_file.storage_file_view_search
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__file_name
|
||||
msgid "File Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_size
|
||||
msgid "File Size"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__file_type
|
||||
msgid "File Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: code:addons/storage_file/models/storage_file.py:0
|
||||
#, python-format
|
||||
msgid "File can not be updated,remove it and create a new one"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__filename_strategy
|
||||
msgid "Filename Strategy"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__filename
|
||||
msgid "Filename without extension"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_file__internal_url
|
||||
msgid "HTTP URL to load the file directly from storage."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_file__url
|
||||
msgid "HTTP accessible path to the file"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__human_file_size
|
||||
msgid "Human File Size"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_ir_actions_report__id
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__id
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__id
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form
|
||||
msgid ""
|
||||
"If you have changed parameters via server env settings the URL might look "
|
||||
"outdated."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__internal_url
|
||||
msgid "Internal Url"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__is_public
|
||||
msgid "Is Public"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_ir_actions_report____last_update
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend____last_update
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file____last_update
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_uid
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__write_date
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file_replace__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__mimetype
|
||||
msgid "Mime Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__name
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__name_with_id
|
||||
msgid "Name and ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_backend__url_include_directory_path
|
||||
msgid ""
|
||||
"Normally the directory_path it's for internal usage. If this flag is enabled"
|
||||
" the path will be used to compute the public URL."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__served_by__odoo
|
||||
msgid "Odoo"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form
|
||||
msgid "Recompute base URL for files"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__relative_path
|
||||
msgid "Relative Path"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_file__relative_path
|
||||
msgid "Relative location for backend"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model,name:storage_file.model_ir_actions_report
|
||||
msgid "Report Action"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields.selection,name:storage_file.selection__storage_backend__filename_strategy__hash
|
||||
msgid "SHA hash"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__served_by
|
||||
msgid "Served By"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form
|
||||
msgid ""
|
||||
"Served by Odoo option will use `web.base.url` as the base URL.\n"
|
||||
" <br/>Make sure this parameter is properly configured and accessible\n"
|
||||
" from everwhere you want to access the service."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__slug
|
||||
msgid "Slug"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_file__slug
|
||||
msgid "Slug-ified name with ID for URL"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__backend_id
|
||||
msgid "Storage"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model,name:storage_file.model_storage_backend
|
||||
msgid "Storage Backend"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model,name:storage_file.model_storage_file
|
||||
msgid "Storage File"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,help:storage_file.field_storage_backend__filename_strategy
|
||||
msgid ""
|
||||
"Strategy to build the name of the file to be stored.\n"
|
||||
"Name and ID: will store the file with its name + its id.\n"
|
||||
"SHA Hash: will use the hash of the file as filename (same method as the native attachment storage)"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: code:addons/storage_file/models/storage_file.py:0
|
||||
#, python-format
|
||||
msgid ""
|
||||
"The filename strategy is empty for the backend %s.\n"
|
||||
"Please configure it"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.constraint,message:storage_file.constraint_storage_file_path_uniq
|
||||
msgid "The private path must be uniq per backend"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__to_delete
|
||||
msgid "To Delete"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__url
|
||||
msgid "Url"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_backend__url_include_directory_path
|
||||
msgid "Url Include Directory Path"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model.fields,field_description:storage_file.field_storage_file__url_path
|
||||
msgid "Url Path"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model_terms:ir.ui.view,arch_db:storage_file.storage_backend_view_form
|
||||
msgid ""
|
||||
"When served by external service you might have special environment configuration\n"
|
||||
" for building final files URLs.\n"
|
||||
" <br/>For performance reasons, the base URL is computed and stored.\n"
|
||||
" If you change some parameters (eg: in local dev environment or special instances)\n"
|
||||
" and you still want to see the images you might need to refresh this URL\n"
|
||||
" to make sure images and/or files are loaded correctly."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_file
|
||||
#: model:ir.model,name:storage_file.model_storage_file_replace
|
||||
msgid "Wizard template allowing to replace a storage.file"
|
||||
msgstr ""
|
|
@ -0,0 +1,3 @@
|
|||
from . import storage_file
|
||||
from . import storage_backend
|
||||
from . import ir_actions_report
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2021 Camptocamp SA (http://www.camptocamp.com).
|
||||
# @author Simone Orsi <simahawk@gmail.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class IrActionsReport(models.Model):
|
||||
_inherit = "ir.actions.report"
|
||||
|
||||
def render_qweb_pdf(self, res_ids=None, data=None):
|
||||
return super(
|
||||
IrActionsReport, self.with_context(print_report_pdf=True)
|
||||
).render_qweb_pdf(res_ids=res_ids, data=data)
|
|
@ -0,0 +1,170 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# Copyright 2019 Camptocamp SA (http://www.camptocamp.com).
|
||||
# @author Simone Orsi <simone.orsi@camptocamp.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StorageBackend(models.Model):
|
||||
_inherit = "storage.backend"
|
||||
|
||||
filename_strategy = fields.Selection(
|
||||
selection=[("name_with_id", "Name and ID"), ("hash", "SHA hash")],
|
||||
default="name_with_id",
|
||||
help=(
|
||||
"Strategy to build the name of the file to be stored.\n"
|
||||
"Name and ID: will store the file with its name + its id.\n"
|
||||
"SHA Hash: will use the hash of the file as filename "
|
||||
"(same method as the native attachment storage)"
|
||||
),
|
||||
)
|
||||
served_by = fields.Selection(
|
||||
selection=[("odoo", "Odoo"), ("external", "External")],
|
||||
required=True,
|
||||
default="odoo",
|
||||
)
|
||||
base_url = fields.Char(default="")
|
||||
is_public = fields.Boolean(
|
||||
default=False,
|
||||
help="Define if every files stored into this backend are "
|
||||
"public or not. Examples:\n"
|
||||
"Private: your file/image can not be displayed is the user is "
|
||||
"not logged (not available on other website);\n"
|
||||
"Public: your file/image can be displayed if nobody is "
|
||||
"logged (useful to display files on external websites)",
|
||||
)
|
||||
url_include_directory_path = fields.Boolean(
|
||||
default=False,
|
||||
help="Normally the directory_path it's for internal usage. "
|
||||
"If this flag is enabled "
|
||||
"the path will be used to compute the public URL.",
|
||||
)
|
||||
base_url_for_files = fields.Char(compute="_compute_base_url_for_files", store=True)
|
||||
backend_view_use_internal_url = fields.Boolean(
|
||||
help="Decide if Odoo backend views should use the external URL (usually a CDN) "
|
||||
"or the internal url with direct access to the storage. "
|
||||
"This could save you some money if you pay by CDN traffic."
|
||||
)
|
||||
|
||||
def write(self, vals):
|
||||
# Ensure storage file URLs are up to date
|
||||
clear_url_cache = False
|
||||
url_related_fields = (
|
||||
"served_by",
|
||||
"base_url",
|
||||
"directory_path",
|
||||
"url_include_directory_path",
|
||||
)
|
||||
for fname in url_related_fields:
|
||||
if fname in vals:
|
||||
clear_url_cache = True
|
||||
break
|
||||
res = super().write(vals)
|
||||
if clear_url_cache:
|
||||
self.action_recompute_base_url_for_files()
|
||||
return res
|
||||
|
||||
@property
|
||||
def _server_env_fields(self):
|
||||
env_fields = super()._server_env_fields
|
||||
env_fields.update(
|
||||
{
|
||||
"filename_strategy": {},
|
||||
"served_by": {},
|
||||
"base_url": {},
|
||||
"url_include_directory_path": {},
|
||||
}
|
||||
)
|
||||
return env_fields
|
||||
|
||||
_default_backend_xid = "storage_backend.default_storage_backend"
|
||||
|
||||
@classmethod
|
||||
def _get_backend_id_from_param(cls, env, param_name, default_fallback=True):
|
||||
backend_id = None
|
||||
param = env["ir.config_parameter"].sudo().get_param(param_name)
|
||||
if param:
|
||||
if param.isdigit():
|
||||
backend_id = int(param)
|
||||
elif "." in param:
|
||||
backend = env.ref(param, raise_if_not_found=False)
|
||||
if backend:
|
||||
backend_id = backend.id
|
||||
if not backend_id and default_fallback:
|
||||
backend = env.ref(cls._default_backend_xid, raise_if_not_found=False)
|
||||
if backend:
|
||||
backend_id = backend.id
|
||||
else:
|
||||
_logger.warn("No backend found, no default fallback found.")
|
||||
return backend_id
|
||||
|
||||
@api.depends(
|
||||
"served_by",
|
||||
"base_url",
|
||||
"directory_path",
|
||||
"url_include_directory_path",
|
||||
)
|
||||
def _compute_base_url_for_files(self):
|
||||
for record in self:
|
||||
record.base_url_for_files = record._get_base_url_for_files()
|
||||
|
||||
def _get_base_url_for_files(self):
|
||||
"""Retrieve base URL for files."""
|
||||
backend = self.sudo()
|
||||
parts = []
|
||||
if backend.served_by == "external":
|
||||
parts = [backend.base_url or ""]
|
||||
if backend.url_include_directory_path and backend.directory_path:
|
||||
parts.append(backend.directory_path)
|
||||
return "/".join(parts)
|
||||
|
||||
def action_recompute_base_url_for_files(self):
|
||||
"""Refresh base URL for files.
|
||||
|
||||
Rationale: all the params for computing this URL might come from server env.
|
||||
When this is the case, the URL - being stored - might be out of date.
|
||||
This is because changes to server env fields are not detected at startup.
|
||||
Hence, let's offer an easy way to promptly force this manually when needed.
|
||||
"""
|
||||
self._compute_base_url_for_files()
|
||||
self.env["storage.file"].invalidate_cache(["url"])
|
||||
|
||||
def _get_base_url_from_param(self):
|
||||
base_url_param = (
|
||||
"report.url" if self.env.context.get("print_report_pdf") else "web.base.url"
|
||||
)
|
||||
return self.env["ir.config_parameter"].sudo().get_param(base_url_param)
|
||||
|
||||
def _get_url_for_file(self, storage_file, exclude_base_url=False):
|
||||
"""Return final full URL for given file."""
|
||||
backend = self.sudo()
|
||||
if backend.served_by == "odoo":
|
||||
parts = [
|
||||
self._get_base_url_from_param() if not exclude_base_url else "/",
|
||||
"storage.file",
|
||||
storage_file.slug,
|
||||
]
|
||||
else:
|
||||
base_url = backend.base_url_for_files
|
||||
if exclude_base_url:
|
||||
base_url = base_url.replace(backend.base_url, "") or "/"
|
||||
parts = [base_url, storage_file.relative_path or ""]
|
||||
return "/".join([x.rstrip("/") for x in parts if x])
|
||||
|
||||
def _register_hook(self):
|
||||
super()._register_hook()
|
||||
backends = self.search([]).filtered(
|
||||
lambda x: x._get_base_url_for_files() != x.base_url_for_files
|
||||
)
|
||||
if not backends:
|
||||
return
|
||||
sql = f"SELECT id FROM {self._table} WHERE ID IN %s FOR UPDATE"
|
||||
self.env.cr.execute(sql, (tuple(backends.ids),), log_exceptions=False)
|
||||
backends.action_recompute_base_url_for_files()
|
||||
_logger.info("storage.backend base URL for files refreshed")
|
|
@ -0,0 +1,238 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools import human_size
|
||||
from odoo.tools.translate import _
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from slugify import slugify
|
||||
except ImportError: # pragma: no cover
|
||||
_logger.debug("Cannot `import slugify`.")
|
||||
|
||||
|
||||
REGEX_SLUGIFY = r"[^-a-z0-9_]+"
|
||||
|
||||
|
||||
class StorageFile(models.Model):
|
||||
_name = "storage.file"
|
||||
_description = "Storage File"
|
||||
|
||||
name = fields.Char(required=True, index=True)
|
||||
backend_id = fields.Many2one(
|
||||
"storage.backend", "Storage", index=True, required=True
|
||||
)
|
||||
url = fields.Char(compute="_compute_url", help="HTTP accessible path to the file")
|
||||
url_path = fields.Char(
|
||||
compute="_compute_url_path", help="Accessible path, no base URL"
|
||||
)
|
||||
internal_url = fields.Char(
|
||||
compute="_compute_internal_url",
|
||||
help="HTTP URL to load the file directly from storage.",
|
||||
)
|
||||
slug = fields.Char(
|
||||
compute="_compute_slug", help="Slug-ified name with ID for URL", store=True
|
||||
)
|
||||
relative_path = fields.Char(
|
||||
readonly=True, help="Relative location for backend", copy=False
|
||||
)
|
||||
file_size = fields.Integer("File Size")
|
||||
human_file_size = fields.Char(
|
||||
"Human File Size", compute="_compute_human_file_size", store=True
|
||||
)
|
||||
checksum = fields.Char("Checksum/SHA1", size=40, index=True, readonly=True)
|
||||
filename = fields.Char(
|
||||
"Filename without extension", compute="_compute_extract_filename", store=True
|
||||
)
|
||||
extension = fields.Char(
|
||||
"Extension", compute="_compute_extract_filename", store=True
|
||||
)
|
||||
mimetype = fields.Char("Mime Type", compute="_compute_extract_filename", store=True)
|
||||
data = fields.Binary(
|
||||
help="Data",
|
||||
inverse="_inverse_data",
|
||||
compute="_compute_data",
|
||||
store=False,
|
||||
copy=True,
|
||||
)
|
||||
to_delete = fields.Boolean()
|
||||
active = fields.Boolean(default=True)
|
||||
company_id = fields.Many2one(
|
||||
"res.company", "Company", default=lambda self: self.env.user.company_id.id
|
||||
)
|
||||
file_type = fields.Selection([])
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"path_uniq",
|
||||
"unique(relative_path, backend_id)",
|
||||
"The private path must be uniq per backend",
|
||||
)
|
||||
]
|
||||
|
||||
def write(self, vals):
|
||||
if "data" in vals:
|
||||
for record in self:
|
||||
if record.data:
|
||||
raise UserError(
|
||||
_("File can not be updated," "remove it and create a new one")
|
||||
)
|
||||
return super(StorageFile, self).write(vals)
|
||||
|
||||
@api.depends("file_size")
|
||||
def _compute_human_file_size(self):
|
||||
for record in self:
|
||||
record.human_file_size = human_size(record.file_size)
|
||||
|
||||
@api.depends("filename", "extension")
|
||||
def _compute_slug(self):
|
||||
for record in self:
|
||||
record.slug = record._slugify_name_with_id()
|
||||
|
||||
def _slugify_name_with_id(self):
|
||||
return "{}{}".format(
|
||||
slugify(
|
||||
"{}-{}".format(self.filename, self.id), regex_pattern=REGEX_SLUGIFY
|
||||
),
|
||||
self.extension,
|
||||
)
|
||||
|
||||
def _build_relative_path(self, checksum):
|
||||
self.ensure_one()
|
||||
strategy = self.sudo().backend_id.filename_strategy
|
||||
if not strategy:
|
||||
raise UserError(
|
||||
_(
|
||||
"The filename strategy is empty for the backend %s.\n"
|
||||
"Please configure it"
|
||||
)
|
||||
% self.backend_id.name
|
||||
)
|
||||
if strategy == "hash":
|
||||
return checksum[:2] + "/" + checksum
|
||||
elif strategy == "name_with_id":
|
||||
return self.slug
|
||||
|
||||
def _prepare_meta_for_file(self):
|
||||
bin_data = base64.b64decode(self.data)
|
||||
checksum = hashlib.sha1(bin_data).hexdigest()
|
||||
relative_path = self._build_relative_path(checksum)
|
||||
return {
|
||||
"checksum": checksum,
|
||||
"file_size": len(bin_data),
|
||||
"relative_path": relative_path,
|
||||
}
|
||||
|
||||
def _inverse_data(self):
|
||||
for record in self:
|
||||
record.write(record._prepare_meta_for_file())
|
||||
record.backend_id.sudo().add(
|
||||
record.relative_path,
|
||||
record.data,
|
||||
mimetype=record.mimetype,
|
||||
binary=False,
|
||||
)
|
||||
|
||||
def _compute_data(self):
|
||||
for rec in self:
|
||||
if self._context.get("bin_size"):
|
||||
rec.data = rec.file_size
|
||||
elif rec.relative_path:
|
||||
rec.data = rec.backend_id.sudo().get(rec.relative_path, binary=False)
|
||||
else:
|
||||
rec.data = None
|
||||
|
||||
@api.depends("relative_path", "backend_id")
|
||||
def _compute_url(self):
|
||||
for record in self:
|
||||
record.url = record._get_url()
|
||||
|
||||
@api.depends("relative_path", "backend_id")
|
||||
def _compute_url_path(self):
|
||||
# Keep this separated from `url` to avoid multiple compute:
|
||||
# you'll need either one or the other.
|
||||
for record in self:
|
||||
record.url_path = record._get_url(exclude_base_url=True)
|
||||
|
||||
def _get_url(self, exclude_base_url=False):
|
||||
"""Retrieve file URL based on backend params.
|
||||
|
||||
:param exclude_base_url: skip base_url
|
||||
"""
|
||||
return self.backend_id._get_url_for_file(
|
||||
self, exclude_base_url=exclude_base_url
|
||||
)
|
||||
|
||||
@api.depends("slug")
|
||||
def _compute_internal_url(self):
|
||||
for record in self:
|
||||
record.internal_url = record._get_internal_url()
|
||||
|
||||
def _get_internal_url(self):
|
||||
"""Retrieve file URL to load file directly from the storage.
|
||||
|
||||
It is recommended to use this for Odoo backend internal usage
|
||||
to not generate traffic on external services.
|
||||
"""
|
||||
return f"/storage.file/{self.slug}"
|
||||
|
||||
@api.depends("name")
|
||||
def _compute_extract_filename(self):
|
||||
for rec in self:
|
||||
if rec.name:
|
||||
rec.filename, rec.extension = os.path.splitext(rec.name)
|
||||
mime, __ = mimetypes.guess_type(rec.name)
|
||||
else:
|
||||
rec.filename = rec.extension = mime = False
|
||||
rec.mimetype = mime
|
||||
|
||||
def unlink(self):
|
||||
if self._context.get("cleanning_storage_file"):
|
||||
super(StorageFile, self).unlink()
|
||||
else:
|
||||
self.write({"to_delete": True, "active": False})
|
||||
return True
|
||||
|
||||
@api.model
|
||||
def _clean_storage_file(self):
|
||||
# we must be sure that all the changes are into the DB since
|
||||
# we by pass the ORM
|
||||
self.flush()
|
||||
self._cr.execute(
|
||||
"""SELECT id
|
||||
FROM storage_file
|
||||
WHERE to_delete=True FOR UPDATE"""
|
||||
)
|
||||
ids = [x[0] for x in self._cr.fetchall()]
|
||||
for st_file in self.browse(ids):
|
||||
st_file.backend_id.sudo().delete(st_file.relative_path)
|
||||
st_file.with_context(cleanning_storage_file=True).unlink()
|
||||
# commit is required since the backend could be an external system
|
||||
# therefore, if the record is deleted on the external system
|
||||
# we must be sure that the record is also deleted into Odoo
|
||||
st_file._cr.commit()
|
||||
|
||||
@api.model
|
||||
def get_from_slug_name_with_id(self, slug_name_with_id):
|
||||
"""
|
||||
Return a browse record from a string generated by the method
|
||||
_slugify_name_with_id
|
||||
:param slug_name_with_id:
|
||||
:return: a BrowseRecord (could be empty...)
|
||||
"""
|
||||
# id is the last group of digit after '-'
|
||||
_id = re.findall(r"-([0-9]+)", slug_name_with_id)[-1:]
|
||||
if _id:
|
||||
_id = int(_id[0])
|
||||
return self.browse(_id)
|
|
@ -0,0 +1,3 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_storage_file_edit,storage_file edit,model_storage_file,base.group_system,1,1,1,1
|
||||
access_storage_file_read_public,storage_file public read,model_storage_file,,1,0,0,0
|
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2018 ACSONE SA/NV
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
|
||||
<odoo>
|
||||
<!--Security rule to allow public access on storage.file (depending on the storage.backend)-->
|
||||
<record id="ir_rule_storage_file_public" model="ir.rule">
|
||||
<field name="name">Storage file public</field>
|
||||
<field name="model_id" ref="model_storage_file" />
|
||||
<field name="groups" eval="[(4, ref('base.group_public'))]" />
|
||||
<field name="domain_force">[('backend_id.is_public', '=', True)]</field>
|
||||
<field name="perm_read" eval="True" />
|
||||
<field name="perm_write" eval="False" />
|
||||
<field name="perm_create" eval="False" />
|
||||
<field name="perm_unlink" eval="False" />
|
||||
</record>
|
||||
</odoo>
|
After Width: | Height: | Size: 9.2 KiB |
|
@ -0,0 +1 @@
|
|||
from . import test_storage_file
|
|
@ -0,0 +1,311 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
import base64
|
||||
from urllib import parse
|
||||
|
||||
import mock
|
||||
|
||||
from odoo.exceptions import AccessError, UserError
|
||||
|
||||
from odoo.addons.component.tests.common import TransactionComponentCase
|
||||
|
||||
|
||||
class StorageFileCase(TransactionComponentCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.backend = self.env.ref("storage_backend.default_storage_backend")
|
||||
data = b"This is a simple file"
|
||||
self.filesize = len(data)
|
||||
self.filedata = base64.b64encode(data)
|
||||
self.filename = "test of my_file.txt"
|
||||
|
||||
def _create_storage_file(self):
|
||||
return self.env["storage.file"].create(
|
||||
{
|
||||
"name": self.filename,
|
||||
"backend_id": self.backend.id,
|
||||
"data": self.filedata,
|
||||
}
|
||||
)
|
||||
|
||||
def test_create_and_read_served_by_odoo(self):
|
||||
stfile = self._create_storage_file()
|
||||
self.assertEqual(stfile.data, self.filedata)
|
||||
self.assertEqual(stfile.mimetype, "text/plain")
|
||||
self.assertEqual(stfile.extension, ".txt")
|
||||
self.assertEqual(stfile.filename, "test of my_file")
|
||||
self.assertEqual(stfile.relative_path, "test-of-my_file-%s.txt" % stfile.id)
|
||||
url = parse.urlparse(stfile.url)
|
||||
self.assertEqual(url.path, "/storage.file/test-of-my_file-%s.txt" % stfile.id)
|
||||
self.assertEqual(stfile.file_size, self.filesize)
|
||||
|
||||
def test_get_from_slug_name_with_id(self):
|
||||
stfile = self._create_storage_file()
|
||||
stfile2 = self.env["storage.file"].get_from_slug_name_with_id(
|
||||
"test-of-my_file-%s.txt" % stfile.id
|
||||
)
|
||||
self.assertEqual(stfile, stfile2)
|
||||
# the method parse the given string to find the id. The id is the
|
||||
# last sequence of digit starting with '-'
|
||||
stfile2 = self.env["storage.file"].get_from_slug_name_with_id(
|
||||
"test-999-%s.txt2" % stfile.id
|
||||
)
|
||||
self.assertEqual(stfile, stfile2)
|
||||
stfile2 = self.env["storage.file"].get_from_slug_name_with_id(
|
||||
"test-999-%s" % stfile.id
|
||||
)
|
||||
self.assertEqual(stfile, stfile2)
|
||||
|
||||
def test_slug(self):
|
||||
stfile = self._create_storage_file()
|
||||
self.assertEqual(
|
||||
stfile.slug,
|
||||
"test-of-my_file-{}.txt".format(stfile.id),
|
||||
)
|
||||
stfile.name = "Name has changed.png"
|
||||
self.assertEqual(
|
||||
stfile.slug,
|
||||
"name-has-changed-{}.png".format(stfile.id),
|
||||
)
|
||||
|
||||
def test_internal_url(self):
|
||||
stfile = self._create_storage_file()
|
||||
self.assertEqual(
|
||||
stfile.internal_url,
|
||||
"/storage.file/test-of-my_file-{}.txt".format(stfile.id),
|
||||
)
|
||||
stfile.name = "Name has changed.png"
|
||||
self.assertEqual(
|
||||
stfile.slug,
|
||||
"name-has-changed-{}.png".format(stfile.id),
|
||||
)
|
||||
self.assertEqual(
|
||||
stfile.internal_url,
|
||||
"/storage.file/name-has-changed-{}.png".format(stfile.id),
|
||||
)
|
||||
|
||||
def test_url(self):
|
||||
stfile = self._create_storage_file()
|
||||
params = self.env["ir.config_parameter"].sudo()
|
||||
base_url = params.get_param("web.base.url")
|
||||
# served by odoo
|
||||
self.assertEqual(
|
||||
stfile.url,
|
||||
"{}/storage.file/test-of-my_file-{}.txt".format(base_url, stfile.id),
|
||||
)
|
||||
# served by external
|
||||
stfile.backend_id.update(
|
||||
{
|
||||
"served_by": "external",
|
||||
"base_url": "https://foo.com",
|
||||
"directory_path": "baz",
|
||||
}
|
||||
)
|
||||
# path not included
|
||||
self.assertEqual(
|
||||
stfile.url, "https://foo.com/test-of-my_file-{}.txt".format(stfile.id)
|
||||
)
|
||||
# path included
|
||||
stfile.backend_id.url_include_directory_path = True
|
||||
self.assertEqual(
|
||||
stfile.url, "https://foo.com/baz/test-of-my_file-{}.txt".format(stfile.id)
|
||||
)
|
||||
|
||||
def test_url_without_base_url(self):
|
||||
stfile = self._create_storage_file()
|
||||
# served by odoo
|
||||
self.assertEqual(
|
||||
stfile.url_path,
|
||||
"/storage.file/test-of-my_file-{}.txt".format(stfile.id),
|
||||
)
|
||||
# served by external
|
||||
stfile.backend_id.update(
|
||||
{
|
||||
"served_by": "external",
|
||||
"base_url": "https://foo.com",
|
||||
"directory_path": "baz",
|
||||
}
|
||||
)
|
||||
stfile.invalidate_cache()
|
||||
# path not included
|
||||
self.assertEqual(
|
||||
stfile.with_context(foo=1).url_path,
|
||||
"/test-of-my_file-{}.txt".format(stfile.id),
|
||||
)
|
||||
# path included
|
||||
stfile.backend_id.url_include_directory_path = True
|
||||
stfile.invalidate_cache()
|
||||
self.assertEqual(
|
||||
stfile.url_path,
|
||||
"/baz/test-of-my_file-{}.txt".format(stfile.id),
|
||||
)
|
||||
|
||||
def test_url_for_report(self):
|
||||
stfile = self._create_storage_file()
|
||||
params = self.env["ir.config_parameter"].sudo()
|
||||
params.set_param("report.url", "http://report.url")
|
||||
# served by odoo
|
||||
self.assertEqual(
|
||||
stfile.with_context(print_report_pdf=True).url,
|
||||
"http://report.url/storage.file/test-of-my_file-{}.txt".format(stfile.id),
|
||||
)
|
||||
|
||||
def test_create_store_with_hash(self):
|
||||
self.backend.filename_strategy = "hash"
|
||||
stfile = self._create_storage_file()
|
||||
self.assertEqual(stfile.data, self.filedata)
|
||||
self.assertEqual(stfile.mimetype, "text/plain")
|
||||
self.assertEqual(stfile.extension, ".txt")
|
||||
self.assertEqual(stfile.filename, "test of my_file")
|
||||
self.assertEqual(
|
||||
stfile.relative_path, "13/1322d9ccb3d257095185b205eadc9307aae5dc84"
|
||||
)
|
||||
|
||||
def test_missing_name_strategy(self):
|
||||
self.backend.filename_strategy = None
|
||||
with self.assertRaises(UserError):
|
||||
self._create_storage_file()
|
||||
|
||||
def test_create_and_read_served_by_external(self):
|
||||
self.backend.write(
|
||||
{"served_by": "external", "base_url": "https://cdn.example.com"}
|
||||
)
|
||||
stfile = self._create_storage_file()
|
||||
self.assertEqual(stfile.data, self.filedata)
|
||||
self.assertEqual(
|
||||
stfile.url, "https://cdn.example.com/test-of-my_file-%s.txt" % stfile.id
|
||||
)
|
||||
self.assertEqual(stfile.file_size, self.filesize)
|
||||
|
||||
def test_read_bin_size(self):
|
||||
stfile = self._create_storage_file()
|
||||
self.assertEqual(stfile.with_context(bin_size=True).data, b"21.00 bytes")
|
||||
|
||||
def test_cannot_update_data(self):
|
||||
stfile = self._create_storage_file()
|
||||
data = base64.b64encode(b"This is different data")
|
||||
with self.assertRaises(UserError):
|
||||
stfile.write({"data": data})
|
||||
|
||||
# check that the file have been not modified
|
||||
self.assertEqual(stfile.read()[0]["data"], self.filedata)
|
||||
|
||||
def test_unlink(self):
|
||||
# Do not commit during the test
|
||||
self.cr.commit = lambda: True
|
||||
stfile = self._create_storage_file()
|
||||
|
||||
backend = stfile.backend_id
|
||||
relative_path = stfile.relative_path
|
||||
stfile.unlink()
|
||||
|
||||
# Check the the storage file is set to delete
|
||||
# and the file still exist on the storage
|
||||
self.assertEqual(stfile.to_delete, True)
|
||||
self.assertIn(relative_path, backend.list_files())
|
||||
|
||||
# Run the method to clean the storage.file
|
||||
self.env["storage.file"]._clean_storage_file()
|
||||
|
||||
# Check that the file is deleted
|
||||
files = (
|
||||
self.env["storage.file"]
|
||||
.with_context(active_test=False)
|
||||
.search([("id", "=", stfile.id)])
|
||||
)
|
||||
self.assertEqual(len(files), 0)
|
||||
self.assertNotIn(relative_path, backend.list_files())
|
||||
|
||||
def test_public_access1(self):
|
||||
"""
|
||||
Test the public access (when is_public on the backend).
|
||||
When checked, the public user should have access to every content
|
||||
(storage.file).
|
||||
For this case, we use this public user and try to read a field on
|
||||
no-public storage.file.
|
||||
An exception should be raised because the backend is not public
|
||||
:return: bool
|
||||
"""
|
||||
storage_file = self._create_storage_file()
|
||||
# Ensure it's False (we shouldn't specify a is_public = False on the
|
||||
# storage.backend creation because False must be the default value)
|
||||
self.assertFalse(storage_file.backend_id.is_public)
|
||||
# Public user used on the controller when authentication is 'public'
|
||||
public_user = self.env.ref("base.public_user")
|
||||
with self.assertRaises(AccessError):
|
||||
# BUG OR NOT with_user doesn't invalidate the cache...
|
||||
# force cache invalidation
|
||||
self.env.cache.invalidate()
|
||||
self.env[storage_file._name].with_user(public_user).browse(
|
||||
storage_file.ids
|
||||
).name
|
||||
return True
|
||||
|
||||
def test_public_access2(self):
|
||||
"""
|
||||
Test the public access (when is_public on the backend).
|
||||
When checked, the public user should have access to every content
|
||||
(storage.file).
|
||||
For this case, we use this public user and try to read a field on
|
||||
no-public storage.file.
|
||||
This public user should have access because the backend is public
|
||||
:return: bool
|
||||
"""
|
||||
storage_file = self._create_storage_file()
|
||||
storage_file.backend_id.write({"is_public": True})
|
||||
self.assertTrue(storage_file.backend_id.is_public)
|
||||
# Public user used on the controller when authentication is 'public'
|
||||
public_user = self.env.ref("base.public_user")
|
||||
env = self.env(user=public_user)
|
||||
storage_file_public = env[storage_file._name].browse(storage_file.ids)
|
||||
self.assertTrue(storage_file_public.name)
|
||||
return True
|
||||
|
||||
def test_public_access3(self):
|
||||
"""
|
||||
Test the public access (when is_public on the backend).
|
||||
When checked, the public user should have access to every content
|
||||
(storage.file).
|
||||
For this case, we use the demo user and try to read a field on
|
||||
no-public storage.file (no exception should be raised)
|
||||
:return: bool
|
||||
"""
|
||||
storage_file = self._create_storage_file()
|
||||
# Ensure it's False (we shouldn't specify a is_public = False on the
|
||||
# storage.backend creation because False must be the default value)
|
||||
self.assertFalse(storage_file.backend_id.is_public)
|
||||
demo_user = self.env.ref("base.user_demo")
|
||||
env = self.env(user=demo_user)
|
||||
storage_file_public = env[storage_file._name].browse(storage_file.ids)
|
||||
self.assertTrue(storage_file_public.name)
|
||||
return True
|
||||
|
||||
def test_get_backend_from_param(self):
|
||||
storage_file = self._create_storage_file()
|
||||
with mock.patch.object(
|
||||
type(self.env["ir.config_parameter"]), "get_param"
|
||||
) as mocked:
|
||||
mocked.return_value = str(storage_file.backend_id.id)
|
||||
self.assertEqual(
|
||||
self.env["storage.backend"]._get_backend_id_from_param(
|
||||
self.env, "foo.baz"
|
||||
),
|
||||
storage_file.backend_id.id,
|
||||
)
|
||||
with mock.patch.object(
|
||||
type(self.env["ir.config_parameter"]), "get_param"
|
||||
) as mocked:
|
||||
mocked.return_value = "storage_backend.default_storage_backend"
|
||||
self.assertEqual(
|
||||
self.env["storage.backend"]._get_backend_id_from_param(
|
||||
self.env, "foo.baz"
|
||||
),
|
||||
storage_file.backend_id.id,
|
||||
)
|
||||
|
||||
def test_empty(self):
|
||||
# get_url is called on new records
|
||||
empty = self.env["storage.file"].new({})._get_url()
|
||||
self.assertEqual(empty, "")
|
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="storage_backend_view_form" model="ir.ui.view">
|
||||
<field name="model">storage.backend</field>
|
||||
<field name="inherit_id" ref="storage_backend.storage_backend_view_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="backend_type" position="after">
|
||||
<field name="served_by" />
|
||||
<div
|
||||
class="alert alert-info"
|
||||
role="alert"
|
||||
attrs="{'invisible': [('served_by', '!=', 'odoo')]}"
|
||||
>
|
||||
Served by Odoo option will use `web.base.url` as the base URL.
|
||||
<br />Make sure this parameter is properly configured and accessible
|
||||
from everwhere you want to access the service.
|
||||
</div>
|
||||
<field
|
||||
name="is_public"
|
||||
attrs="{'invisible': [('served_by', '!=', 'odoo')]}"
|
||||
/>
|
||||
<field
|
||||
name="base_url"
|
||||
attrs="{'invisible':[('served_by', '!=', 'external')]}"
|
||||
/>
|
||||
<field name="filename_strategy" />
|
||||
<field name="backend_view_use_internal_url" />
|
||||
</field>
|
||||
<field name="directory_path" position="after">
|
||||
<field
|
||||
name="url_include_directory_path"
|
||||
attrs="{'invisible': [('directory_path', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="base_url_for_files"
|
||||
string="Base URL used for files"
|
||||
attrs="{'invisible':[('served_by', '!=', 'external')]}"
|
||||
/>
|
||||
<div
|
||||
class="alert alert-info"
|
||||
role="alert"
|
||||
attrs="{'invisible': [('served_by', '!=', 'external')]}"
|
||||
>
|
||||
When served by external service you might have special environment configuration
|
||||
for building final files URLs.
|
||||
<br />For performance reasons, the base URL is computed and stored.
|
||||
If you change some parameters (eg: in local dev environment or special instances)
|
||||
and you still want to see the images you might need to refresh this URL
|
||||
to make sure images and/or files are loaded correctly.
|
||||
</div>
|
||||
<button
|
||||
type="object"
|
||||
name="action_recompute_base_url_for_files"
|
||||
string="Recompute base URL for files"
|
||||
help="If you have changed parameters via server env settings the URL might look outdated."
|
||||
/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="storage_file_view_tree" model="ir.ui.view">
|
||||
<field name="model">storage.file</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="name" />
|
||||
<field name="backend_id" />
|
||||
<field name="file_size" />
|
||||
<field name="file_type" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="storage_file_view_form" model="ir.ui.view">
|
||||
<field name="model">storage.file</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="File">
|
||||
<div class="oe_title">
|
||||
<label for="name" class="oe_edit_only" />
|
||||
<h1>
|
||||
<field name="name" />
|
||||
</h1>
|
||||
<group>
|
||||
<field name="id" invisible="True" />
|
||||
<field
|
||||
name="backend_id"
|
||||
attrs="{'readonly': [('id', '!=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="data"
|
||||
attrs="{'readonly': [('id', '!=', False)]}"
|
||||
/>
|
||||
<field name="url" widget="url" />
|
||||
<field name="human_file_size" />
|
||||
<field name="checksum" />
|
||||
<field name="relative_path" />
|
||||
<field name="file_type" />
|
||||
<field name="company_id" groups="base.group_multi_company" />
|
||||
</group>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="storage_file_view_search" model="ir.ui.view">
|
||||
<field name="model">storage.file</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="File">
|
||||
<field name="name" />
|
||||
<field name="backend_id" />
|
||||
<field name="url" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="act_open_storage_file_view">
|
||||
<field name="name">File</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">storage.file</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="storage_file_view_search" />
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window.view" id="act_open_storage_file_view_form">
|
||||
<field name="act_window_id" ref="act_open_storage_file_view" />
|
||||
<field name="sequence" eval="20" />
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="storage_file_view_form" />
|
||||
</record>
|
||||
<record model="ir.actions.act_window.view" id="act_open_storage_file_view_tree">
|
||||
<field name="act_window_id" ref="act_open_storage_file_view" />
|
||||
<field name="sequence" eval="10" />
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="storage_file_view_tree" />
|
||||
</record>
|
||||
<menuitem
|
||||
id="menu_storage_file"
|
||||
parent="storage_backend.menu_storage"
|
||||
sequence="20"
|
||||
action="act_open_storage_file_view"
|
||||
/>
|
||||
</odoo>
|
|
@ -0,0 +1 @@
|
|||
from . import replace_file
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2023 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StorageFileReplace(models.TransientModel):
|
||||
_name = "storage.file.replace"
|
||||
_description = "Wizard template allowing to replace a storage.file"
|
||||
|
||||
file_id = fields.Many2one("storage.file")
|
||||
data = fields.Binary()
|
||||
file_name = fields.Char()
|
||||
|
||||
def _get_file_from_data(self):
|
||||
file_model = self.env["storage.file"].sudo()
|
||||
return file_model.create(
|
||||
{
|
||||
"backend_id": self.file_id.backend_id.id,
|
||||
"data": self.data,
|
||||
"name": self.file_name,
|
||||
}
|
||||
)
|
||||
|
||||
def confirm(self):
|
||||
return
|
|
@ -0,0 +1,94 @@
|
|||
=============
|
||||
Storage Image
|
||||
=============
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:f55df80c5d4bfbf0658732ea818faa7d84ddb5bca05f988b1b1e52a432f5c132
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Production%2FStable-green.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Production/Stable
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/storage/tree/14.0/storage_image
|
||||
:alt: OCA/storage
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-storage_image
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
External image management depending on Storage File module.
|
||||
|
||||
It include these features:
|
||||
|
||||
* image resizing: thumbnail, etc.
|
||||
* store metadata like: checksum, mimetype
|
||||
|
||||
Use cases:
|
||||
|
||||
- product images on CDN for e-commerce website
|
||||
- raw products images on cheap storage (ie AWS Glacier, AWS S3)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20storage_image%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Akretion
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Sebastien Beau <sebastien.beau@akretion.com>
|
||||
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||
* Pedro M. Baeza <pedro.baeza@serviciosbaeza.com>
|
||||
* Antiun Ingeniería S.L. - Jairo Llopis
|
||||
* Denis Roussel <denis.roussel@acsone.eu>
|
||||
* Quentin Groulard <quentin.groulard@acsone.eu>
|
||||
* `Camptocamp <https://www.camptocamp.com>`_
|
||||
|
||||
* Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/14.0/storage_image>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
|
@ -0,0 +1,2 @@
|
|||
from . import models
|
||||
from . import wizards
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
{
|
||||
"name": "Storage Image",
|
||||
"summary": "Store image and resized image in a storage backend",
|
||||
"version": "14.0.2.3.0",
|
||||
"category": "Storage",
|
||||
"website": "https://github.com/OCA/storage",
|
||||
"author": " Akretion, Odoo Community Association (OCA)",
|
||||
"license": "LGPL-3",
|
||||
"development_status": "Production/Stable",
|
||||
"depends": ["storage_thumbnail"],
|
||||
"data": [
|
||||
"security/res_group.xml",
|
||||
"security/ir_rule.xml",
|
||||
"security/ir.model.access.csv",
|
||||
"wizards/replace_file.xml",
|
||||
"views/assets.xml",
|
||||
"views/storage_image.xml",
|
||||
"views/storage_image_relation_abstract.xml",
|
||||
"data/ir_config_parameter.xml",
|
||||
],
|
||||
"qweb": ["static/src/xml/custom_xml.xml"],
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record id="storage_image_backend" model="ir.config_parameter">
|
||||
<field name="key">storage.image.backend_id</field>
|
||||
<field name="value" ref="storage_backend.default_storage_backend" />
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,285 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * storage_image
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: storage_image
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.view_storage_image_abstract_kanban
|
||||
msgid "<i class=\"fa fa-times\" title=\"Unlink\"/>"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,help:storage_image.field_storage_image__url_path
|
||||
msgid "Accessible path, no base URL"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__active
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract__image_alt_name
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__alt_name
|
||||
msgid "Alt Image name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_file_replace_view_form
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__checksum
|
||||
msgid "Checksum/SHA1"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__company_id
|
||||
msgid "Company"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_file_replace_view_form
|
||||
msgid "Confirm"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__data
|
||||
#: model:ir.model.fields,help:storage_image.field_storage_image__data
|
||||
msgid "Data"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract__display_name
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file__display_name
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file_replace__display_name
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__extension
|
||||
msgid "Extension"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__file_id
|
||||
msgid "File"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_image_view_form
|
||||
msgid "File Informations"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__file_size
|
||||
msgid "File Size"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file__file_type
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__file_type
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_thumbnail__file_type
|
||||
msgid "File Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__filename
|
||||
msgid "Filename without extension"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,help:storage_image.field_storage_image__internal_url
|
||||
msgid "HTTP URL to load the file directly from storage."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,help:storage_image.field_storage_image__url
|
||||
msgid "HTTP accessible path to the file"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__human_file_size
|
||||
msgid "Human File Size"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract__id
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file__id
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file_replace__id
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.actions.act_window,name:storage_image.act_open_storage_image_view
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract__image_id
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file_replace__image_id
|
||||
#: model:ir.model.fields.selection,name:storage_image.selection__storage_file__file_type__image
|
||||
#: model:ir.ui.menu,name:storage_image.menu_storage_image
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_file_replace_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_image_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_image_view_search
|
||||
msgid "Image"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:res.groups,name:storage_image.group_image_manager
|
||||
msgid "Image Manager"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model,name:storage_image.model_image_relation_abstract
|
||||
msgid "Image Relation Abstract"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__internal_url
|
||||
msgid "Internal Url"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract____last_update
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file____last_update
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_file_replace____last_update
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract__image_url
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__image_medium_url
|
||||
msgid "Medium thumb URL"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__mimetype
|
||||
msgid "Mime Type"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract__image_name
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__name
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_file_replace_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_image_view_form
|
||||
msgid "Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__relative_path
|
||||
msgid "Relative Path"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,help:storage_image.field_storage_image__relative_path
|
||||
msgid "Relative location for backend"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.actions.act_window,name:storage_image.storage_replace_file_action
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_file_replace_view_form
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image.storage_image_view_form
|
||||
msgid "Replace File"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_image_relation_abstract__sequence
|
||||
msgid "Sequence"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__slug
|
||||
msgid "Slug"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,help:storage_image.field_storage_image__slug
|
||||
msgid "Slug-ified name with ID for URL"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__image_small_url
|
||||
msgid "Small thumb URL"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__backend_id
|
||||
msgid "Storage"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model,name:storage_image.model_storage_file
|
||||
msgid "Storage File"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model,name:storage_image.model_storage_image
|
||||
msgid "Storage Image"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__thumb_medium_id
|
||||
msgid "Thumb Medium"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__thumb_small_id
|
||||
msgid "Thumb Small"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__thumbnail_ids
|
||||
msgid "Thumbnails"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__to_delete
|
||||
msgid "To Delete"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__url
|
||||
msgid "Url"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model.fields,field_description:storage_image.field_storage_image__url_path
|
||||
msgid "Url Path"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image
|
||||
#: model:ir.model,name:storage_image.model_storage_file_replace
|
||||
msgid "Wizard template allowing to replace a storage.file"
|
||||
msgstr ""
|
|
@ -0,0 +1,3 @@
|
|||
from . import storage_file
|
||||
from . import storage_image
|
||||
from . import storage_image_relation_abstract
|
|
@ -0,0 +1,13 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StorageFile(models.Model):
|
||||
_inherit = "storage.file"
|
||||
|
||||
file_type = fields.Selection(
|
||||
selection_add=[("image", "Image")], ondelete={"image": "set null"}
|
||||
)
|
|
@ -0,0 +1,60 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# Copyright 2021 Camptocamp (http://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
from odoo.addons.storage_file.models.storage_file import REGEX_SLUGIFY
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from slugify import slugify
|
||||
except ImportError: # pragma: no cover
|
||||
_logger.debug("Cannot `import slugify`.")
|
||||
|
||||
|
||||
class StorageImage(models.Model):
|
||||
_name = "storage.image"
|
||||
_description = "Storage Image"
|
||||
_inherit = "thumbnail.mixin"
|
||||
_inherits = {"storage.file": "file_id"}
|
||||
_default_file_type = "image"
|
||||
|
||||
alt_name = fields.Char(string="Alt Image name")
|
||||
file_id = fields.Many2one("storage.file", "File", required=True, ondelete="cascade")
|
||||
|
||||
@api.onchange("name")
|
||||
def onchange_name(self):
|
||||
for record in self:
|
||||
if record.name:
|
||||
filename, extension = os.path.splitext(record.name)
|
||||
record.name = "{}{}".format(
|
||||
slugify(filename, regex_pattern=REGEX_SLUGIFY), extension
|
||||
)
|
||||
record.alt_name = filename
|
||||
for char in ["-", "_"]:
|
||||
record.alt_name = record.alt_name.replace(char, " ")
|
||||
|
||||
@api.model
|
||||
def create(self, vals):
|
||||
vals["file_type"] = self._default_file_type
|
||||
if "backend_id" not in vals:
|
||||
vals["backend_id"] = self._get_default_backend_id()
|
||||
return super().create(vals)
|
||||
|
||||
def _get_default_backend_id(self):
|
||||
return self.env["storage.backend"]._get_backend_id_from_param(
|
||||
self.env, "storage.image.backend_id"
|
||||
)
|
||||
|
||||
def unlink(self):
|
||||
files = self.mapped("file_id")
|
||||
thumbnails = self.mapped("thumbnail_ids")
|
||||
return super().unlink() and thumbnails.unlink() and files.unlink()
|
|
@ -0,0 +1,25 @@
|
|||
# Copyright 2020 ACSONE SA/NV
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ImageRelationAbstract(models.AbstractModel):
|
||||
"""Image Relation Abstract
|
||||
|
||||
Use this abstract if you want to add a relation between a model and storage.image
|
||||
|
||||
This module comes with a JS widget `image_handle`. Use this widget on your field
|
||||
in kanaban mode if you want to enable adding and reordering images by drag&drop.
|
||||
"""
|
||||
|
||||
_name = "image.relation.abstract"
|
||||
_description = "Image Relation Abstract"
|
||||
_order = "sequence, image_id"
|
||||
|
||||
sequence = fields.Integer()
|
||||
image_id = fields.Many2one("storage.image", required=True, ondelete="cascade")
|
||||
# for kanban view
|
||||
image_name = fields.Char(related="image_id.name")
|
||||
image_alt_name = fields.Char(related="image_id.alt_name")
|
||||
image_url = fields.Char(related="image_id.image_medium_url")
|
|
@ -0,0 +1,9 @@
|
|||
* Sebastien Beau <sebastien.beau@akretion.com>
|
||||
* Raphaël Reverdy <raphael.reverdy@akretion.com>
|
||||
* Pedro M. Baeza <pedro.baeza@serviciosbaeza.com>
|
||||
* Antiun Ingeniería S.L. - Jairo Llopis
|
||||
* Denis Roussel <denis.roussel@acsone.eu>
|
||||
* Quentin Groulard <quentin.groulard@acsone.eu>
|
||||
* `Camptocamp <https://www.camptocamp.com>`_
|
||||
|
||||
* Iván Todorovich <ivan.todorovich@gmail.com>
|
|
@ -0,0 +1,11 @@
|
|||
External image management depending on Storage File module.
|
||||
|
||||
It include these features:
|
||||
|
||||
* image resizing: thumbnail, etc.
|
||||
* store metadata like: checksum, mimetype
|
||||
|
||||
Use cases:
|
||||
|
||||
- product images on CDN for e-commerce website
|
||||
- raw products images on cheap storage (ie AWS Glacier, AWS S3)
|
|
@ -0,0 +1,6 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_storage_image_edit,storage_image edit,model_storage_image,group_image_manager,1,1,1,1
|
||||
access_storage_file_image_edit,storage_file image edit,model_storage_file,group_image_manager,1,1,1,1
|
||||
access_storage_thumbnail_edit,storage_thumbnail edit,storage_thumbnail.model_storage_thumbnail,group_image_manager,1,1,1,1
|
||||
access_storage_image_read,storage_image read,model_storage_image,base.group_user,1,0,0,0
|
||||
access_storage_file_replace_edit,storage_file_replace edit,model_storage_file_replace,group_image_manager,1,1,1,1
|
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo noupdate="1">
|
||||
<record model="ir.rule" id="storage_file_image_rule">
|
||||
<field name="name">Storage File for Image</field>
|
||||
<field name="model_id" ref="model_storage_file" />
|
||||
<field name="domain_force">[('file_type','in', ('image', 'thumbnail'))]</field>
|
||||
<field name="perm_read" eval="0" />
|
||||
<field name="perm_create" eval="1" />
|
||||
<field name="perm_write" eval="1" />
|
||||
<field name="perm_unlink" eval="1" />
|
||||
<field name="groups" eval="[(4, ref('storage_image.group_image_manager'))]" />
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record model="res.groups" id="group_image_manager">
|
||||
<field name="name">Image Manager</field>
|
||||
<field
|
||||
name="users"
|
||||
eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"
|
||||
/>
|
||||
</record>
|
||||
</odoo>
|
After Width: | Height: | Size: 9.2 KiB |
|
@ -0,0 +1,442 @@
|
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Storage Image</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: gray; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic, pre.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="storage-image">
|
||||
<h1 class="title">Storage Image</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:f55df80c5d4bfbf0658732ea818faa7d84ddb5bca05f988b1b1e52a432f5c132
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Production/Stable" src="https://img.shields.io/badge/maturity-Production%2FStable-green.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/lgpl-3.0-standalone.html"><img alt="License: LGPL-3" src="https://img.shields.io/badge/licence-LGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/14.0/storage_image"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-storage_image"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>External image management depending on Storage File module.</p>
|
||||
<p>It include these features:</p>
|
||||
<ul class="simple">
|
||||
<li>image resizing: thumbnail, etc.</li>
|
||||
<li>store metadata like: checksum, mimetype</li>
|
||||
</ul>
|
||||
<p>Use cases:</p>
|
||||
<ul class="simple">
|
||||
<li>product images on CDN for e-commerce website</li>
|
||||
<li>raw products images on cheap storage (ie AWS Glacier, AWS S3)</li>
|
||||
</ul>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-1">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-2">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-3">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-4">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-5">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/storage/issues/new?body=module:%20storage_image%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-3">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Akretion</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Sebastien Beau <<a class="reference external" href="mailto:sebastien.beau@akretion.com">sebastien.beau@akretion.com</a>></li>
|
||||
<li>Raphaël Reverdy <<a class="reference external" href="mailto:raphael.reverdy@akretion.com">raphael.reverdy@akretion.com</a>></li>
|
||||
<li>Pedro M. Baeza <<a class="reference external" href="mailto:pedro.baeza@serviciosbaeza.com">pedro.baeza@serviciosbaeza.com</a>></li>
|
||||
<li>Antiun Ingeniería S.L. - Jairo Llopis</li>
|
||||
<li>Denis Roussel <<a class="reference external" href="mailto:denis.roussel@acsone.eu">denis.roussel@acsone.eu</a>></li>
|
||||
<li>Quentin Groulard <<a class="reference external" href="mailto:quentin.groulard@acsone.eu">quentin.groulard@acsone.eu</a>></li>
|
||||
<li><a class="reference external" href="https://www.camptocamp.com">Camptocamp</a><ul>
|
||||
<li>Iván Todorovich <<a class="reference external" href="mailto:ivan.todorovich@gmail.com">ivan.todorovich@gmail.com</a>></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org">
|
||||
<img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" />
|
||||
</a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/14.0/storage_image">OCA/storage</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Copyright 2021 Camptocamp (http://www.camptocamp.com).
|
||||
@author Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
*/
|
||||
.o_field_storage_image_handle {
|
||||
&.drop-zone {
|
||||
outline: 2px dashed $o-enterprise-primary-color;
|
||||
outline-offset: +2px;
|
||||
}
|
||||
|
||||
.o_kanban_view.o_kanban_ungrouped {
|
||||
width: auto;
|
||||
|
||||
.o_kanban_record {
|
||||
flex: 0 1 50%;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
flex: 0 0 percentage(1/3);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
flex: 0 0 percentage(1/5);
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
flex: 0 0 percentage(1/6);
|
||||
}
|
||||
|
||||
// make the image square and in the center
|
||||
.o_squared_image {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-bottom: 100%;
|
||||
> img {
|
||||
position: absolute;
|
||||
margin: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
odoo.define("storage_image.FieldStorageImageHandle", function (require) {
|
||||
"use strict";
|
||||
|
||||
const registry = require("web.field_registry");
|
||||
const relational_fields = require("web.relational_fields");
|
||||
const utils = require("web.utils");
|
||||
|
||||
const FieldStorageImageHandle = relational_fields.FieldOne2Many.extend({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
_render: function () {
|
||||
this.$el.addClass("o_field_storage_image_handle");
|
||||
if (!this.isReadonly && this.activeActions.create) {
|
||||
this.$el.on("dragover dragenter", (e) => {
|
||||
this.$el.addClass("drop-zone");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
this.$el.on("dragleave dragend drop", (e) => {
|
||||
this.$el.removeClass("drop-zone");
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
this.$el.on("drop", (e) => {
|
||||
// StopImmediatePropagation to avoid event bubbling
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
this._uploadImages(e.originalEvent.dataTransfer.files);
|
||||
});
|
||||
}
|
||||
return this._super.apply(this, arguments);
|
||||
},
|
||||
|
||||
_uploadImages: async function (files) {
|
||||
// Prepare storage.image values
|
||||
const storageImageValues = [];
|
||||
for (const file of files) {
|
||||
if (!file.type.includes("image")) {
|
||||
continue;
|
||||
}
|
||||
const data = await utils.getDataURLFromFile(file);
|
||||
const content = data.split(",")[1];
|
||||
storageImageValues.push({
|
||||
name: file.name,
|
||||
data: content,
|
||||
});
|
||||
}
|
||||
// Create images
|
||||
await this._rpc({
|
||||
model: "storage.image",
|
||||
method: "create",
|
||||
args: [storageImageValues],
|
||||
}).then((record_ids) => {
|
||||
const context = record_ids.map(
|
||||
(rec_id) =>
|
||||
new Object({
|
||||
default_image_id: rec_id,
|
||||
})
|
||||
);
|
||||
this.trigger_up("add_record", {
|
||||
forceEditable: context.length > 1 ? "bottom" : false,
|
||||
allowWarning: true,
|
||||
context: context,
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
registry.add("storage_image_handle", FieldStorageImageHandle);
|
||||
return FieldStorageImageHandle;
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
from . import common
|
||||
from . import test_storage_image
|
||||
from . import test_storage_replace_file
|
|
@ -0,0 +1,52 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# Copyright 2021 Camptocamp (http://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
import base64
|
||||
import os
|
||||
|
||||
from odoo.addons.component.tests.common import SavepointComponentCase
|
||||
|
||||
|
||||
class StorageImageCommonCase(SavepointComponentCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
|
||||
# Run tests with demo user
|
||||
cls.user = cls.env.ref("base.user_demo")
|
||||
cls.user.write(
|
||||
{"groups_id": [(4, cls.env.ref("storage_image.group_image_manager").id)]}
|
||||
)
|
||||
cls.env = cls.env(user=cls.user)
|
||||
# Storage backend
|
||||
cls.backend = cls.env.ref("storage_backend.default_storage_backend")
|
||||
|
||||
@classmethod
|
||||
def _get_file_content(cls, name, base_path=None):
|
||||
path = base_path or cls.base_path
|
||||
with open(os.path.join(path, name), "rb") as f:
|
||||
data = f.read()
|
||||
return data
|
||||
|
||||
@classmethod
|
||||
def _create_storage_image(cls, filename, data):
|
||||
return cls.env["storage.image"].create({"name": filename, "data": data})
|
||||
|
||||
@classmethod
|
||||
def _create_storage_image_from_file(cls, filename, base_path=None):
|
||||
data = cls._get_file_content(filename, base_path=base_path)
|
||||
data = base64.b64encode(data)
|
||||
return cls._create_storage_image(os.path.basename(filename), data)
|
||||
|
||||
def _check_thumbnail(self, image):
|
||||
self.assertEqual(len(image.thumbnail_ids), 2)
|
||||
medium = image._get_thumb("medium")
|
||||
small = image._get_thumb("small")
|
||||
self.assertEqual(medium.size_x, 128)
|
||||
self.assertEqual(medium.size_y, 128)
|
||||
self.assertEqual(small.size_x, 64)
|
||||
self.assertEqual(small.size_y, 64)
|
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,117 @@
|
|||
# Copyright 2017 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# Copyright 2021 Camptocamp (http://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
|
||||
import base64
|
||||
from urllib import parse
|
||||
|
||||
import requests_mock
|
||||
|
||||
from odoo.exceptions import AccessError
|
||||
|
||||
from .common import StorageImageCommonCase
|
||||
|
||||
|
||||
class StorageImageCase(StorageImageCommonCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
# Demo file
|
||||
raw_data = cls._get_file_content("static/akretion-logo.png")
|
||||
cls.filesize = len(raw_data)
|
||||
cls.filedata = base64.b64encode(raw_data)
|
||||
cls.filename = "akretion-logo.png"
|
||||
|
||||
def test_create_and_read_image(self):
|
||||
image = self._create_storage_image(self.filename, self.filedata)
|
||||
self.assertEqual(image.data, self.filedata)
|
||||
self.assertEqual(image.mimetype, "image/png")
|
||||
self.assertEqual(image.extension, ".png")
|
||||
self.assertEqual(image.filename, "akretion-logo")
|
||||
url = parse.urlparse(image.url)
|
||||
self.assertEqual(
|
||||
url.path, "/storage.file/akretion-logo-%d.png" % image.file_id.id
|
||||
)
|
||||
self.assertEqual(image.file_size, self.filesize)
|
||||
self.assertEqual(self.backend.id, image.backend_id.id)
|
||||
|
||||
def test_create_thumbnail(self):
|
||||
image = self._create_storage_image(self.filename, self.filedata)
|
||||
self.assertIsNotNone(image.image_medium_url)
|
||||
self.assertIsNotNone(image.image_small_url)
|
||||
self._check_thumbnail(image)
|
||||
|
||||
def test_create_specific_thumbnail(self):
|
||||
image = self._create_storage_image(self.filename, self.filedata)
|
||||
thumbnail = image.get_or_create_thumbnail(100, 100, "my-image-thumbnail")
|
||||
self.assertEqual(thumbnail.url_key, "my-image-thumbnail")
|
||||
self.assertEqual(thumbnail.relative_path[0:26], "my-image-thumbnail_100_100")
|
||||
|
||||
# Check that method will return the same thumbnail
|
||||
# Check also that url_key have been slugified
|
||||
new_thumbnail = image.get_or_create_thumbnail(100, 100, "My Image Thumbnail")
|
||||
self.assertEqual(new_thumbnail.id, thumbnail.id)
|
||||
|
||||
# Check that method will return a new thumbnail
|
||||
new_thumbnail = image.get_or_create_thumbnail(
|
||||
100, 100, "My New Image Thumbnail"
|
||||
)
|
||||
self.assertNotEqual(new_thumbnail.id, thumbnail.id)
|
||||
|
||||
def test_name_onchange(self):
|
||||
image = self.env["storage.image"].new({"name": "Test-of image_name.png"})
|
||||
image.onchange_name()
|
||||
self.assertEqual(image.name, "test-of-image_name.png")
|
||||
self.assertEqual(image.alt_name, "Test of image name")
|
||||
|
||||
def test_unlink(self):
|
||||
image = self._create_storage_image(self.filename, self.filedata)
|
||||
stfile = image.file_id
|
||||
thumbnail_files = image.thumbnail_ids.mapped("file_id")
|
||||
image.unlink()
|
||||
self.assertEqual(stfile.to_delete, True)
|
||||
self.assertEqual(stfile.active, False)
|
||||
for thumbnail_file in thumbnail_files:
|
||||
self.assertEqual(thumbnail_file.to_delete, True)
|
||||
self.assertEqual(thumbnail_file.active, False)
|
||||
|
||||
def test_no_manager_user_can_not_write(self):
|
||||
# Remove access rigth to demo user
|
||||
group_manager = self.env.ref("storage_image.group_image_manager")
|
||||
self.user = self.env.ref("base.user_demo")
|
||||
self.user.sudo().write({"groups_id": [(3, group_manager.id)]})
|
||||
with self.assertRaises(AccessError):
|
||||
self._create_storage_image(self.filename, self.filedata)
|
||||
|
||||
def test_create_thumbnail_pilbox(self):
|
||||
self.env["ir.config_parameter"].sudo().create(
|
||||
{
|
||||
"key": "storage.image.resize.server",
|
||||
"value": "http://pilbox:8888?url={url}&w={width}&h={height}"
|
||||
"&mode=fill&fmt={fmt}",
|
||||
}
|
||||
)
|
||||
self.env["ir.config_parameter"].sudo().create(
|
||||
{"key": "storage.image.resize.format", "value": "webp"}
|
||||
)
|
||||
backend = self.env["storage.backend"].sudo().browse([self.backend.id])
|
||||
backend.served_by = "external"
|
||||
backend.base_url = "test"
|
||||
with requests_mock.mock() as m:
|
||||
m.get("http://pilbox:8888?", text="data")
|
||||
image = self._create_storage_image(self.filename, self.filedata)
|
||||
self.assertEqual(len(m.request_history), 2)
|
||||
urls = [x.url for x in m.request_history]
|
||||
self.assertIn(
|
||||
"http://pilbox:8888/?url=test/akretion-logo-%s.png"
|
||||
"&w=128&h=128&mode=fill&fmt=webp" % image.file_id.id,
|
||||
urls,
|
||||
)
|
||||
self.assertIn(
|
||||
"http://pilbox:8888/?url=test/akretion-logo-%s.png"
|
||||
"&w=64&h=64&mode=fill&fmt=webp" % image.file_id.id,
|
||||
urls,
|
||||
)
|
|
@ -0,0 +1,59 @@
|
|||
# Copyright 2023 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import base64
|
||||
|
||||
from odoo.tests.common import Form
|
||||
|
||||
from .common import StorageImageCommonCase
|
||||
|
||||
|
||||
class TestStorageReplaceFile(StorageImageCommonCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
raw_data_1 = cls._get_file_content("static/akretion-logo.png")
|
||||
cls.filesize_1 = len(raw_data_1)
|
||||
cls.filedata_1 = base64.b64encode(raw_data_1)
|
||||
cls.filename_1 = "akretion-logo.png"
|
||||
raw_data_2 = cls._get_file_content("static/oca.png")
|
||||
cls.filesize_2 = len(raw_data_2)
|
||||
cls.filedata_2 = base64.b64encode(raw_data_2)
|
||||
cls.filename_2 = "oca.png"
|
||||
|
||||
def test_wizard_change_file(self):
|
||||
image = self._create_storage_image(self.filename_1, self.filedata_1)
|
||||
wiz_form = Form(
|
||||
self.env["storage.file.replace"].with_context(
|
||||
{"active_model": "storage.image", "active_id": image.id}
|
||||
),
|
||||
view="storage_image.storage_file_replace_view_form",
|
||||
)
|
||||
# Check default_get
|
||||
self.assertEqual(wiz_form.image_id, image)
|
||||
self.assertEqual(wiz_form.image_id.file_id, image.file_id)
|
||||
# Write file name first, as when the file widget is used,
|
||||
# a default file name is retrieved from the image that has been picked.
|
||||
# We don't have that here.
|
||||
wiz_form.file_name = self.filename_2
|
||||
wiz_form.data = self.filedata_2
|
||||
# Now, confirm the wiz
|
||||
wiz = wiz_form.save()
|
||||
wiz.confirm()
|
||||
# When confirmed, the storage_image points to the new file
|
||||
self.assertEqual(image.file_id.name, self.filename_2)
|
||||
self.assertEqual(image.file_id.data, self.filedata_2)
|
||||
|
||||
# Now, do revert the change
|
||||
wiz_form = Form(
|
||||
self.env["storage.file.replace"].with_context(
|
||||
{"active_model": "storage.image", "active_id": image.id}
|
||||
)
|
||||
)
|
||||
wiz_form.file_name = self.filename_1
|
||||
wiz_form.data = self.filedata_1
|
||||
wiz = wiz_form.save()
|
||||
wiz.confirm()
|
||||
# And ensure that the new file
|
||||
self.assertEqual(image.file_id.name, self.filename_1)
|
||||
self.assertEqual(image.file_id.data, self.filedata_1)
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<template id="assets_backend" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/scss"
|
||||
href="/storage_image/static/src/css/FieldStorageImageHandle.scss"
|
||||
/>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/storage_image/static/src/js/FieldStorageImageHandle.js"
|
||||
/>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
|
@ -0,0 +1,118 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="storage_image_view_tree" model="ir.ui.view">
|
||||
<field name="model">storage.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree>
|
||||
<field name="write_date" />
|
||||
<field name="name" />
|
||||
<field name="alt_name" />
|
||||
<field name="human_file_size" />
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<record id="storage_image_view_form" model="ir.ui.view">
|
||||
<field name="model">storage.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Image">
|
||||
<header>
|
||||
<button
|
||||
id="button_storage_replace_file"
|
||||
string="Replace File"
|
||||
name="%(storage_image.storage_replace_file_action)d"
|
||||
type="action"
|
||||
/>
|
||||
</header>
|
||||
<group string="Image" name="image">
|
||||
<field
|
||||
name="data"
|
||||
widget="image"
|
||||
filename="name"
|
||||
nolabel="1"
|
||||
options="{'size':(128,128)}"
|
||||
/>
|
||||
</group>
|
||||
<group string="Name" name="name">
|
||||
<field name="name" />
|
||||
<field name="alt_name" />
|
||||
</group>
|
||||
<notebook>
|
||||
<page name="info" string="File Informations">
|
||||
<group name="info">
|
||||
<field name="file_id" readonly="True" required="False" />
|
||||
<field name="thumb_medium_id" />
|
||||
<field name="thumb_small_id" />
|
||||
<field name="thumbnail_ids" readonly="True" />
|
||||
</group>
|
||||
</page>
|
||||
</notebook>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="storage_image_view_kanban" model="ir.ui.view">
|
||||
<field name="model">storage.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="name" />
|
||||
<field name="alt_name" />
|
||||
<field name="image_small_url" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="oe_kanban_vignette oe_semantic_html_override">
|
||||
<a type="open">
|
||||
<img
|
||||
t-att-src="record.image_small_url.value"
|
||||
class="oe_kanban_image"
|
||||
t-att-alt="record.alt_name"
|
||||
/>
|
||||
</a>
|
||||
<div class="oe_kanban_details">
|
||||
<h4>
|
||||
<a type="open">
|
||||
<field name="name" />
|
||||
(<field name="alt_name" />)
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
<record id="storage_image_view_search" model="ir.ui.view">
|
||||
<field name="model">storage.image</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Image">
|
||||
<field name="name" />
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window" id="act_open_storage_image_view">
|
||||
<field name="name">Image</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">storage.image</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="search_view_id" ref="storage_image_view_search" />
|
||||
<field name="domain">[]</field>
|
||||
<field name="context">{}</field>
|
||||
</record>
|
||||
<record model="ir.actions.act_window.view" id="act_open_storage_image_view_form">
|
||||
<field name="act_window_id" ref="act_open_storage_image_view" />
|
||||
<field name="sequence" eval="20" />
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="storage_image_view_form" />
|
||||
</record>
|
||||
<record model="ir.actions.act_window.view" id="act_open_storage_image_view_tree">
|
||||
<field name="act_window_id" ref="act_open_storage_image_view" />
|
||||
<field name="sequence" eval="10" />
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="storage_image_view_tree" />
|
||||
</record>
|
||||
<menuitem
|
||||
id="menu_storage_image"
|
||||
parent="storage_backend.menu_storage"
|
||||
sequence="30"
|
||||
action="act_open_storage_image_view"
|
||||
/>
|
||||
</odoo>
|
|
@ -0,0 +1,102 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2021 Camptocamp (http://www.camptocamp.com).
|
||||
@author Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
<odoo>
|
||||
|
||||
<!--
|
||||
These views are not meant to be used directly, as we're dealing with
|
||||
an abstract class.
|
||||
|
||||
They are, however, meant to be used as a base for other primary views.
|
||||
|
||||
Example:
|
||||
|
||||
<record id="view_custom_image_relation_form" model="ir.ui.view">
|
||||
<field name="model">custom.image.relation</field>
|
||||
<field name="inherit_id" ref="storage_image.view_storage_image_abstract_form" />
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<group name="extra" position="inside">
|
||||
<field name="custom_field" />
|
||||
</group>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_custom_image_relation_kanban" model="ir.ui.view">
|
||||
<field name="model">custom.image.relation</field>
|
||||
<field name="inherit_id" ref="storage_image.view_storage_image_abstract_kanban" />
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban position="attributes">
|
||||
<attribute name="string">Images</attribute>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
NOTE: Views can't be empty, even if they're inheriting from others as primary views,
|
||||
for this reason the kanban string attribute is added as a dummy view content.
|
||||
-->
|
||||
|
||||
<record id="view_storage_image_abstract_form" model="ir.ui.view">
|
||||
<field name="model">image.relation.abstract</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<div class="row o_storage_image_relation">
|
||||
<div class="col-md-6 col-xs-5">
|
||||
<group name="image">
|
||||
<field name="image_id" />
|
||||
</group>
|
||||
<group name="extra">
|
||||
<!-- Add here custom relation fields -->
|
||||
</group>
|
||||
</div>
|
||||
<div class="col-md-6 col-xs-7 text-center">
|
||||
<field name="image_url" widget="image_url" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_storage_image_abstract_kanban" model="ir.ui.view">
|
||||
<field name="model">image.relation.abstract</field>
|
||||
<field name="arch" type="xml">
|
||||
<kanban>
|
||||
<field name="image_id" invisible="1" />
|
||||
<field name="image_url" />
|
||||
<field name="image_name" />
|
||||
<field name="image_alt_name" />
|
||||
<field name="sequence" widget="handle" />
|
||||
<templates>
|
||||
<t t-name="kanban-box">
|
||||
<div class="card oe_kanban_global_click p-0">
|
||||
<a
|
||||
t-if="! read_only_mode"
|
||||
type="delete"
|
||||
style="position: absolute; right: 0; padding: 4px; diplay: inline-block"
|
||||
>
|
||||
<i class="fa fa-times" title="Unlink" />
|
||||
</a>
|
||||
<div class="o_squared_image">
|
||||
<img
|
||||
class="card-img-top"
|
||||
t-att-src="record.image_url.value"
|
||||
t-att-alt="record.image_alt_name.value"
|
||||
/>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<h4 class="card-title p-2 m-0 bg-200">
|
||||
<small><field name="image_name" /></small>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -0,0 +1 @@
|
|||
from . import replace_file
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2023 Camptocamp SA
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class StorageFileReplace(models.TransientModel):
|
||||
_inherit = "storage.file.replace"
|
||||
|
||||
image_id = fields.Many2one("storage.image")
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
"""'default_get' method overloaded."""
|
||||
res = super().default_get(fields_list)
|
||||
active_model = self.env.context.get("active_model")
|
||||
if active_model == "storage.image":
|
||||
active_id = self.env.context.get("active_id")
|
||||
image = self.env["storage.image"].browse(active_id)
|
||||
res.update(
|
||||
{
|
||||
"image_id": image.id,
|
||||
"file_id": image.file_id.id,
|
||||
}
|
||||
)
|
||||
return res
|
||||
|
||||
def confirm(self):
|
||||
res = super().confirm()
|
||||
if self.image_id and self.data:
|
||||
self.image_id.file_id = self._get_file_from_data()
|
||||
return res
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!-- Copyright 2023 Camptocamp SA
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
|
||||
<odoo>
|
||||
|
||||
<record id="storage_file_replace_view_form" model="ir.ui.view">
|
||||
<field name="name">storage.file.replace.form</field>
|
||||
<field name="model">storage.file.replace</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Replace File" version="7.0">
|
||||
<sheet>
|
||||
<field name="file_id" invisible="1" />
|
||||
<field name="image_id" invisible="1" />
|
||||
<group string="Image" name="image">
|
||||
<field
|
||||
name="data"
|
||||
widget="image"
|
||||
filename="file_name"
|
||||
nolabel="1"
|
||||
options="{'size':(128,128)}"
|
||||
/>
|
||||
</group>
|
||||
<group string="Name" name="name">
|
||||
<field name="file_name" />
|
||||
</group>
|
||||
<footer>
|
||||
<button name="confirm" string="Confirm" type="object" />
|
||||
<button special="cancel" string="Cancel" class="btn-default" />
|
||||
</footer>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="storage_replace_file_action" model="ir.actions.act_window">
|
||||
<field name="name">Replace File</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">storage.file.replace</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="view_id" ref="storage_file_replace_view_form" />
|
||||
<field name="context">{"withControlPanel": False, "no_breadcrumbs": True}</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
|
@ -0,0 +1,96 @@
|
|||
===============================
|
||||
Storage Image Backend Migration
|
||||
===============================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:cbe7fbda64b174cd3adb1876170489db06f73b9afa9c40eaaebdd50d89d4fc31
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Alpha
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/storage/tree/14.0/storage_image_backend_migration
|
||||
:alt: OCA/storage
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-storage_image_backend_migration
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Action wizard that allow migrate from a source backend to destination backend.
|
||||
|
||||
.. IMPORTANT::
|
||||
This is an alpha version, the data model and design can change at any time without warning.
|
||||
Only for development or testing purpose, do not use in production.
|
||||
`More details on development status <https://odoo-community.org/page/development-status>`_
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
|
||||
- Go to Settings > Technical Settings > Storage Backend
|
||||
- Select source backend, run Migrate Backend Action
|
||||
- Select destination backend
|
||||
- Select chuck size and **run**
|
||||
|
||||
Queue jobs will be created automatically.
|
||||
|
||||
**NOTE:**
|
||||
|
||||
Default Backend Storage system parameter needs to be changed manually to change backend system.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20storage_image_backend_migration%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* ForgeFlow
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Héctor Villarreal Ortega <hector.villarreal@forgeflow.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/14.0/storage_image_backend_migration>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
|
@ -0,0 +1 @@
|
|||
from . import wizards
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "Storage Image Backend Migration",
|
||||
"version": "14.0.1.0.0",
|
||||
"summary": "Migrate src backend to destination backend",
|
||||
"author": "ForgeFlow, Odoo Community Association (OCA)",
|
||||
"company": "ForgeFlow",
|
||||
"development_status": "Alpha",
|
||||
"maintainer": "HviorForgeFlow",
|
||||
"website": "https://github.com/OCA/storage",
|
||||
"category": "Product",
|
||||
"depends": ["storage_file", "storage_image", "queue_job"],
|
||||
"external_dependencies": {
|
||||
"python": ["python-magic", "validators"],
|
||||
"deb": ["libmagic1"],
|
||||
},
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"data/queue_job_channel_data.xml",
|
||||
"data/queue_job_function_data.xml",
|
||||
"views/storage_image_backend_migration_wizard_views.xml",
|
||||
],
|
||||
"license": "AGPL-3",
|
||||
"installable": True,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<odoo noupdate="1">
|
||||
<record model="queue.job.channel" id="channel_storage_image_backend_migration">
|
||||
<field name="name">migrate_images</field>
|
||||
<field name="parent_id" ref="queue_job.channel_root" />
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,10 @@
|
|||
<odoo noupdate="1">
|
||||
<record
|
||||
id="job_function_storage_image_backend_migration_image_do_migrate"
|
||||
model="queue.job.function"
|
||||
>
|
||||
<field name="model_id" ref="model_storage_image_backend_migration_wizard" />
|
||||
<field name="method">do_migrate</field>
|
||||
<field name="channel_id" ref="channel_storage_image_backend_migration" />
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1,96 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * storage_image_backend_migration
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image_backend_migration.view_storage_image_backend_migration_wizard
|
||||
msgid "Cancel"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__chunk_size
|
||||
msgid "Chunk Size"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__create_uid
|
||||
msgid "Created by"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__create_date
|
||||
msgid "Created on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,help:storage_image_backend_migration.field_storage_image_backend_migration_wizard__chunk_size
|
||||
msgid "How many lines will be handled in each job."
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__write_uid
|
||||
msgid "Last Updated by"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__write_date
|
||||
msgid "Last Updated on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image_backend_migration.view_storage_image_backend_migration_wizard
|
||||
msgid "Migrate"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.actions.act_window,name:storage_image_backend_migration.act_storage_image_backend_migration
|
||||
#: model_terms:ir.ui.view,arch_db:storage_image_backend_migration.view_storage_image_backend_migration_wizard
|
||||
msgid "Migrate Backend"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: code:addons/storage_image_backend_migration/wizards/storage_image_backend_migration_wizard.py:0
|
||||
#, python-format
|
||||
msgid "No storage backend provided!"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__storage_backend_id
|
||||
msgid "Storage Backend"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model.fields,field_description:storage_image_backend_migration.field_storage_image_backend_migration_wizard__source_storage_backend_id
|
||||
msgid "Storage Backend with images"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_backend_migration
|
||||
#: model:ir.model,name:storage_image_backend_migration.model_storage_image_backend_migration_wizard
|
||||
msgid "Storage Image Backend Migration Wizard"
|
||||
msgstr ""
|
|
@ -0,0 +1 @@
|
|||
* Héctor Villarreal Ortega <hector.villarreal@forgeflow.com>
|
|
@ -0,0 +1 @@
|
|||
Action wizard that allow migrate from a source backend to destination backend.
|
|
@ -0,0 +1,11 @@
|
|||
|
||||
- Go to Settings > Technical Settings > Storage Backend
|
||||
- Select source backend, run Migrate Backend Action
|
||||
- Select destination backend
|
||||
- Select chuck size and **run**
|
||||
|
||||
Queue jobs will be created automatically.
|
||||
|
||||
**NOTE:**
|
||||
|
||||
Default Backend Storage system parameter needs to be changed manually to change backend system.
|
|
@ -0,0 +1,2 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_storage_image_backend_migration_wizard,access_storage_image_backend_migration_wizard,model_storage_image_backend_migration_wizard,,1,1,1,1
|
|
After Width: | Height: | Size: 9.2 KiB |
|
@ -0,0 +1,440 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Storage Image Backend Migration</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="storage-image-backend-migration">
|
||||
<h1 class="title">Storage Image Backend Migration</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:cbe7fbda64b174cd3adb1876170489db06f73b9afa9c40eaaebdd50d89d4fc31
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Alpha" src="https://img.shields.io/badge/maturity-Alpha-red.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/14.0/storage_image_backend_migration"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-storage_image_backend_migration"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>Action wizard that allow migrate from a source backend to destination backend.</p>
|
||||
<div class="admonition important">
|
||||
<p class="first admonition-title">Important</p>
|
||||
<p class="last">This is an alpha version, the data model and design can change at any time without warning.
|
||||
Only for development or testing purpose, do not use in production.
|
||||
<a class="reference external" href="https://odoo-community.org/page/development-status">More details on development status</a></p>
|
||||
</div>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
|
||||
<ul class="simple">
|
||||
<li>Go to Settings > Technical Settings > Storage Backend</li>
|
||||
<li>Select source backend, run Migrate Backend Action</li>
|
||||
<li>Select destination backend</li>
|
||||
<li>Select chuck size and <strong>run</strong></li>
|
||||
</ul>
|
||||
<p>Queue jobs will be created automatically.</p>
|
||||
<p><strong>NOTE:</strong></p>
|
||||
<p>Default Backend Storage system parameter needs to be changed manually to change backend system.</p>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/storage/issues/new?body=module:%20storage_image_backend_migration%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>ForgeFlow</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Héctor Villarreal Ortega <<a class="reference external" href="mailto:hector.villarreal@forgeflow.com">hector.villarreal@forgeflow.com</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/14.0/storage_image_backend_migration">OCA/storage</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<!-- Make Procurement with security access right -->
|
||||
<record id="view_storage_image_backend_migration_wizard" model="ir.ui.view">
|
||||
<field name="name">Migrate Backend</field>
|
||||
<field name="model">storage.image.backend.migration.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Migrate Backend">
|
||||
<group>
|
||||
<field name="source_storage_backend_id" />
|
||||
<field name="storage_backend_id" />
|
||||
<field name="chunk_size" />
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
string="Migrate"
|
||||
name="action_migrate"
|
||||
type="object"
|
||||
class="btn-primary"
|
||||
/>
|
||||
<button string="Cancel" class="btn-default" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="act_storage_image_backend_migration" model="ir.actions.act_window">
|
||||
<field name="name">Migrate Backend</field>
|
||||
<field name="res_model">storage.image.backend.migration.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="storage_file.model_storage_backend" />
|
||||
<field name="context">{'default_source_storage_backend_id': active_id}</field>
|
||||
<field
|
||||
name="view_id"
|
||||
ref="storage_image_backend_migration.view_storage_image_backend_migration_wizard"
|
||||
/>
|
||||
</record>
|
||||
</odoo>
|
|
@ -0,0 +1 @@
|
|||
from . import storage_image_backend_migration_wizard
|
|
@ -0,0 +1,131 @@
|
|||
import base64
|
||||
import logging
|
||||
import os
|
||||
|
||||
from odoo import _, api, exceptions, fields, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import magic
|
||||
except (ImportError, IOError) as err:
|
||||
_logger.debug(err)
|
||||
|
||||
|
||||
def gen_chunks(iterable, chunksize=10):
|
||||
"""Chunk generator.
|
||||
|
||||
Take an iterable and yield `chunksize` sized slices.
|
||||
"Borrowed" from connector_importer.
|
||||
"""
|
||||
chunk = []
|
||||
last_chunk = False
|
||||
for i, line in enumerate(iterable):
|
||||
if i % chunksize == 0 and i > 0:
|
||||
yield chunk, last_chunk
|
||||
del chunk[:]
|
||||
chunk.append(line)
|
||||
last_chunk = True
|
||||
yield chunk, last_chunk
|
||||
|
||||
|
||||
class StorageImageBackendMigrationWizard(models.TransientModel):
|
||||
_name = "storage.image.backend.migration.wizard"
|
||||
_description = "Storage Image Backend Migration Wizard"
|
||||
|
||||
source_storage_backend_id = fields.Many2one(
|
||||
"storage.backend", "Storage Backend with images", required=True
|
||||
)
|
||||
|
||||
storage_backend_id = fields.Many2one(
|
||||
"storage.backend", "Storage Backend", required=True
|
||||
)
|
||||
|
||||
chunk_size = fields.Integer(
|
||||
default=10,
|
||||
help="How many lines will be handled in each job.",
|
||||
)
|
||||
|
||||
def _get_storage_files(self):
|
||||
files = self.env["storage.file"].search(
|
||||
[
|
||||
("backend_id", "=", self.source_storage_backend_id.id),
|
||||
("file_type", "in", ["image", "thumbnail"]),
|
||||
]
|
||||
)
|
||||
return files
|
||||
|
||||
def action_migrate(self):
|
||||
# Generate N chunks to split in several jobs.
|
||||
chunks = gen_chunks(self._get_storage_files(), chunksize=self.chunk_size)
|
||||
for i, (chunk, is_last_chunk) in enumerate(chunks, 1):
|
||||
self.with_delay().do_migrate(lines=chunk)
|
||||
_logger.info(
|
||||
"Generated job for chunk nr %d. Is last: %s.",
|
||||
i,
|
||||
"yes" if is_last_chunk else "no",
|
||||
)
|
||||
|
||||
def do_migrate(self, lines=None):
|
||||
lines = lines or self._get_storage_files()
|
||||
self._do_migrate(lines)
|
||||
return True
|
||||
|
||||
def _update_file_from_image(self, old_file, new_file):
|
||||
image_obj = self.env["storage.image"]
|
||||
image = image_obj.search([("file_id", "=", old_file.id)])
|
||||
if image:
|
||||
image.update({"file_id": new_file.id})
|
||||
|
||||
def _update_file_from_thumbnail(self, old_file, new_file):
|
||||
thumbnail_obj = self.env["storage.thumbnail"]
|
||||
thumbnail = thumbnail_obj.search([("file_id", "=", old_file.id)])
|
||||
if thumbnail:
|
||||
thumbnail.update({"file_id": new_file.id})
|
||||
|
||||
def _do_migrate(self, lines):
|
||||
file_obj = self.env["storage.file"]
|
||||
for old_file in lines:
|
||||
file_path = old_file.relative_path
|
||||
file_vals = self._prepare_file_values(
|
||||
file_path, filetype=old_file.file_type
|
||||
)
|
||||
new_file = file_obj.create(file_vals)
|
||||
if old_file.file_type == "image":
|
||||
self._update_file_from_image(old_file, new_file)
|
||||
elif old_file.file_type == "thumbnail":
|
||||
self._update_file_from_thumbnail(old_file, new_file)
|
||||
else:
|
||||
pass
|
||||
# Unlink old file, cron cleanup storage needed
|
||||
old_file.unlink()
|
||||
return True
|
||||
|
||||
def _read_from_external_storage(self, file_path):
|
||||
if not self.source_storage_backend_id:
|
||||
raise exceptions.UserError(_("No storage backend provided!"))
|
||||
return self.source_storage_backend_id.get(file_path)
|
||||
|
||||
@api.model
|
||||
def _get_base64(self, file_path):
|
||||
res = {}
|
||||
mimetype = None
|
||||
binary = self._read_from_external_storage(file_path)
|
||||
if binary:
|
||||
mimetype = magic.from_buffer(binary, mime=True)
|
||||
res = {"mimetype": mimetype, "b64": base64.encodebytes(binary)}
|
||||
return res
|
||||
|
||||
def _prepare_file_values(self, file_path, filetype="image"):
|
||||
name = os.path.basename(file_path)
|
||||
file_data = self._get_base64(file_path)
|
||||
if not file_data:
|
||||
return {}
|
||||
vals = {
|
||||
"data": file_data["b64"],
|
||||
"name": name,
|
||||
"file_type": filetype,
|
||||
"mimetype": file_data["mimetype"],
|
||||
"backend_id": self.storage_backend_id.id,
|
||||
}
|
||||
return vals
|
|
@ -0,0 +1,30 @@
|
|||
|
||||
.. image:: https://img.shields.io/badge/licence-LGPL--3-blue.svg
|
||||
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
|
||||
:alt: License: LGPL-3
|
||||
|
||||
===========================
|
||||
Storage Image Category POS
|
||||
===========================
|
||||
|
||||
|
||||
Simple module installing the remove Category POS module and removing the image
|
||||
field in the form view
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Pierrick Brun <pierrick.brun@akretion.com>
|
||||
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
* Akretion
|
|
@ -0,0 +1,16 @@
|
|||
# Copyright 2018 Akretion (http://www.akretion.com).
|
||||
# @author Pierrick Brun <https://github.com/pierrickbrun>
|
||||
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
|
||||
|
||||
{
|
||||
"name": "Storage Image Category POS",
|
||||
"summary": "Add image handling to product category and use it for POS",
|
||||
"version": "10.0.1.0.0",
|
||||
"category": "Storage",
|
||||
"website": "https://github.com/OCA/storage",
|
||||
"author": " Akretion, Odoo Community Association (OCA)",
|
||||
"license": "LGPL-3",
|
||||
"installable": False,
|
||||
"depends": ["storage_image_product_pos", "pos_remove_pos_category"],
|
||||
"data": ["views/product_category.xml"],
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
|
||||
<record id="product_category_form_view" model="ir.ui.view">
|
||||
<field
|
||||
name="inherit_id"
|
||||
ref="pos_remove_pos_category.product_category_form_view"
|
||||
/>
|
||||
<field name="model">product.category</field>
|
||||
<field name="arch" type="xml">
|
||||
<field name="image_medium" position="attributes">
|
||||
<attribute name="invisible">True</attribute>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
|
@ -0,0 +1,82 @@
|
|||
====================
|
||||
Storage Image Import
|
||||
====================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:f68662397b0281f5612be18799e059c067aaee708405d05a4c799f97a751164f
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/storage/tree/14.0/storage_image_import
|
||||
:alt: OCA/storage
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-storage_image_import
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
This module allows to import images based on url (from csv file)
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
In a csv file, for each image per line only the field "imported_from_url" is mandatory. Since, the name is computed from the url, it could be usefull to add the "alt_name" field as a column.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/storage/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/storage/issues/new?body=module:%20storage_image_import%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
~~~~~~~
|
||||
|
||||
* Akretion
|
||||
|
||||
Contributors
|
||||
~~~~~~~~~~~~
|
||||
|
||||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
* Kévin Roche <kevin.roche@akretion.com>
|
||||
|
||||
Maintainers
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
This module is part of the `OCA/storage <https://github.com/OCA/storage/tree/14.0/storage_image_import>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
|
@ -0,0 +1,20 @@
|
|||
# Copyright 2021 Akretion (https://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
{
|
||||
"name": "Storage Image Import",
|
||||
"summary": "Add the possibility to import image for csv base on url",
|
||||
"version": "14.0.1.2.0",
|
||||
"category": "Storage",
|
||||
"website": "https://github.com/OCA/storage",
|
||||
"author": " Akretion, Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"application": False,
|
||||
"installable": True,
|
||||
"external_dependencies": {
|
||||
"python": ["unicodecsv"],
|
||||
},
|
||||
"depends": ["storage_image"],
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
# Translation of Odoo Server.
|
||||
# This file contains the translation of the following modules:
|
||||
# * storage_image_import
|
||||
#
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Odoo Server 14.0\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"Last-Translator: \n"
|
||||
"Language-Team: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: \n"
|
||||
"Plural-Forms: \n"
|
||||
|
||||
#. module: storage_image_import
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_image_relation_abstract__display_name
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_storage_image__display_name
|
||||
msgid "Display Name"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_import
|
||||
#: code:addons/storage_image_import/models/image_relation_abstract.py:0
|
||||
#, python-format
|
||||
msgid "Fail to import image {} check if the url is valid"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_import
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_image_relation_abstract__id
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_storage_image__id
|
||||
msgid "ID"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_import
|
||||
#: model:ir.model,name:storage_image_import.model_image_relation_abstract
|
||||
msgid "Image Relation Abstract"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_import
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_image_relation_abstract__import_from_url
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_storage_image__imported_from_url
|
||||
msgid "Imported From Url"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_import
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_image_relation_abstract____last_update
|
||||
#: model:ir.model.fields,field_description:storage_image_import.field_storage_image____last_update
|
||||
msgid "Last Modified on"
|
||||
msgstr ""
|
||||
|
||||
#. module: storage_image_import
|
||||
#: model:ir.model,name:storage_image_import.model_storage_image
|
||||
msgid "Storage Image"
|
||||
msgstr ""
|
|
@ -0,0 +1,2 @@
|
|||
from . import image_relation_abstract
|
||||
from . import storage_image
|
|
@ -0,0 +1,79 @@
|
|||
# Copyright 2020 Akretion (https://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import urllib
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageRelationAbstract(models.AbstractModel):
|
||||
_inherit = "image.relation.abstract"
|
||||
|
||||
import_from_url = fields.Char(related="image_id.imported_from_url")
|
||||
|
||||
def _create_image_from_url(self, url):
|
||||
try:
|
||||
data = urllib.request.urlopen(url).read()
|
||||
return self.env["storage.image"].create(
|
||||
{
|
||||
"name": os.path.basename(urlparse(url).path),
|
||||
"data": base64.b64encode(data),
|
||||
"imported_from_url": url,
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error(e)
|
||||
raise ValidationError(
|
||||
_("Fail to import image {} check if the url is valid").format(url)
|
||||
)
|
||||
|
||||
def _get_existing_image_from_url(self, url):
|
||||
return self.env["storage.image"].search([("imported_from_url", "=", url)])
|
||||
|
||||
def _process_import_from_url(self, vals):
|
||||
if vals.get("import_from_url"):
|
||||
url = vals.pop("import_from_url")
|
||||
image = self._get_existing_image_from_url(url)
|
||||
if not image:
|
||||
image = self._create_image_from_url(url)
|
||||
vals["image_id"] = image.id
|
||||
|
||||
def _get_domain_for_existing_relation(self, vals):
|
||||
return []
|
||||
|
||||
def _get_existing_relation(self, vals):
|
||||
domain = self._get_domain_for_existing_relation(vals)
|
||||
if domain:
|
||||
return self.search(domain)
|
||||
else:
|
||||
return self
|
||||
|
||||
@api.model_create_multi
|
||||
def create(self, vals_list):
|
||||
vals_to_create = []
|
||||
records = self
|
||||
for vals in vals_list:
|
||||
record = None
|
||||
if "import_from_url" in vals:
|
||||
record = self._get_existing_relation(vals)
|
||||
if record:
|
||||
vals.pop("import_from_url")
|
||||
record.write(vals)
|
||||
records |= record
|
||||
else:
|
||||
vals_to_create.append(vals)
|
||||
for vals in vals_to_create:
|
||||
self._process_import_from_url(vals)
|
||||
return records | super().create(vals_to_create)
|
||||
|
||||
def write(self, vals):
|
||||
self._process_import_from_url(vals)
|
||||
return super().write(vals)
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright 2020 Akretion (https://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class StorageImage(models.Model):
|
||||
_inherit = "storage.image"
|
||||
|
||||
imported_from_url = fields.Char()
|
|
@ -0,0 +1,2 @@
|
|||
* Sébastien Beau <sebastien.beau@akretion.com>
|
||||
* Kévin Roche <kevin.roche@akretion.com>
|
|
@ -0,0 +1 @@
|
|||
This module allows to import images based on url (from csv file)
|
|
@ -0,0 +1 @@
|
|||
In a csv file, for each image per line only the field "imported_from_url" is mandatory. Since, the name is computed from the url, it could be usefull to add the "alt_name" field as a column.
|
After Width: | Height: | Size: 9.2 KiB |
|
@ -0,0 +1,427 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="generator" content="Docutils: https://docutils.sourceforge.io/" />
|
||||
<title>Storage Image Import</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 8954 2022-01-20 10:10:25Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
|
||||
See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to
|
||||
customize this style sheet.
|
||||
*/
|
||||
|
||||
/* used to remove borders from tables and images */
|
||||
.borderless, table.borderless td, table.borderless th {
|
||||
border: 0 }
|
||||
|
||||
table.borderless td, table.borderless th {
|
||||
/* Override padding for "table.docutils td" with "! important".
|
||||
The right padding separates the table cells. */
|
||||
padding: 0 0.5em 0 0 ! important }
|
||||
|
||||
.first {
|
||||
/* Override more specific margin styles with "! important". */
|
||||
margin-top: 0 ! important }
|
||||
|
||||
.last, .with-subtitle {
|
||||
margin-bottom: 0 ! important }
|
||||
|
||||
.hidden {
|
||||
display: none }
|
||||
|
||||
.subscript {
|
||||
vertical-align: sub;
|
||||
font-size: smaller }
|
||||
|
||||
.superscript {
|
||||
vertical-align: super;
|
||||
font-size: smaller }
|
||||
|
||||
a.toc-backref {
|
||||
text-decoration: none ;
|
||||
color: black }
|
||||
|
||||
blockquote.epigraph {
|
||||
margin: 2em 5em ; }
|
||||
|
||||
dl.docutils dd {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||
dl.docutils dt {
|
||||
font-weight: bold }
|
||||
*/
|
||||
|
||||
div.abstract {
|
||||
margin: 2em 5em }
|
||||
|
||||
div.abstract p.topic-title {
|
||||
font-weight: bold ;
|
||||
text-align: center }
|
||||
|
||||
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||
div.hint, div.important, div.note, div.tip, div.warning {
|
||||
margin: 2em ;
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||
div.important p.admonition-title, div.note p.admonition-title,
|
||||
div.tip p.admonition-title {
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||
div.danger p.admonition-title, div.error p.admonition-title,
|
||||
div.warning p.admonition-title, .code .error {
|
||||
color: red ;
|
||||
font-weight: bold ;
|
||||
font-family: sans-serif }
|
||||
|
||||
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||
compound paragraphs.
|
||||
div.compound .compound-first, div.compound .compound-middle {
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
div.compound .compound-last, div.compound .compound-middle {
|
||||
margin-top: 0.5em }
|
||||
*/
|
||||
|
||||
div.dedication {
|
||||
margin: 2em 5em ;
|
||||
text-align: center ;
|
||||
font-style: italic }
|
||||
|
||||
div.dedication p.topic-title {
|
||||
font-weight: bold ;
|
||||
font-style: normal }
|
||||
|
||||
div.figure {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
div.footer, div.header {
|
||||
clear: both;
|
||||
font-size: smaller }
|
||||
|
||||
div.line-block {
|
||||
display: block ;
|
||||
margin-top: 1em ;
|
||||
margin-bottom: 1em }
|
||||
|
||||
div.line-block div.line-block {
|
||||
margin-top: 0 ;
|
||||
margin-bottom: 0 ;
|
||||
margin-left: 1.5em }
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em ;
|
||||
border: medium outset ;
|
||||
padding: 1em ;
|
||||
background-color: #ffffee ;
|
||||
width: 40% ;
|
||||
float: right ;
|
||||
clear: right }
|
||||
|
||||
div.sidebar p.rubric {
|
||||
font-family: sans-serif ;
|
||||
font-size: medium }
|
||||
|
||||
div.system-messages {
|
||||
margin: 5em }
|
||||
|
||||
div.system-messages h1 {
|
||||
color: red }
|
||||
|
||||
div.system-message {
|
||||
border: medium outset ;
|
||||
padding: 1em }
|
||||
|
||||
div.system-message p.system-message-title {
|
||||
color: red ;
|
||||
font-weight: bold }
|
||||
|
||||
div.topic {
|
||||
margin: 2em }
|
||||
|
||||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||
margin-top: 0.4em }
|
||||
|
||||
h1.title {
|
||||
text-align: center }
|
||||
|
||||
h2.subtitle {
|
||||
text-align: center }
|
||||
|
||||
hr.docutils {
|
||||
width: 75% }
|
||||
|
||||
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||
clear: left ;
|
||||
float: left ;
|
||||
margin-right: 1em }
|
||||
|
||||
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||
clear: right ;
|
||||
float: right ;
|
||||
margin-left: 1em }
|
||||
|
||||
img.align-center, .figure.align-center, object.align-center {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
table.align-center {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.align-left {
|
||||
text-align: left }
|
||||
|
||||
.align-center {
|
||||
clear: both ;
|
||||
text-align: center }
|
||||
|
||||
.align-right {
|
||||
text-align: right }
|
||||
|
||||
/* reset inner alignment in figures */
|
||||
div.align-right {
|
||||
text-align: inherit }
|
||||
|
||||
/* div.align-center * { */
|
||||
/* text-align: left } */
|
||||
|
||||
.align-top {
|
||||
vertical-align: top }
|
||||
|
||||
.align-middle {
|
||||
vertical-align: middle }
|
||||
|
||||
.align-bottom {
|
||||
vertical-align: bottom }
|
||||
|
||||
ol.simple, ul.simple {
|
||||
margin-bottom: 1em }
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal }
|
||||
|
||||
ol.loweralpha {
|
||||
list-style: lower-alpha }
|
||||
|
||||
ol.upperalpha {
|
||||
list-style: upper-alpha }
|
||||
|
||||
ol.lowerroman {
|
||||
list-style: lower-roman }
|
||||
|
||||
ol.upperroman {
|
||||
list-style: upper-roman }
|
||||
|
||||
p.attribution {
|
||||
text-align: right ;
|
||||
margin-left: 50% }
|
||||
|
||||
p.caption {
|
||||
font-style: italic }
|
||||
|
||||
p.credits {
|
||||
font-style: italic ;
|
||||
font-size: smaller }
|
||||
|
||||
p.label {
|
||||
white-space: nowrap }
|
||||
|
||||
p.rubric {
|
||||
font-weight: bold ;
|
||||
font-size: larger ;
|
||||
color: maroon ;
|
||||
text-align: center }
|
||||
|
||||
p.sidebar-title {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold ;
|
||||
font-size: larger }
|
||||
|
||||
p.sidebar-subtitle {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
p.topic-title {
|
||||
font-weight: bold }
|
||||
|
||||
pre.address {
|
||||
margin-bottom: 0 ;
|
||||
margin-top: 0 ;
|
||||
font: inherit }
|
||||
|
||||
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||
margin-left: 2em ;
|
||||
margin-right: 2em }
|
||||
|
||||
pre.code .ln { color: grey; } /* line numbers */
|
||||
pre.code, code { background-color: #eeeeee }
|
||||
pre.code .comment, code .comment { color: #5C6576 }
|
||||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||
|
||||
span.classifier {
|
||||
font-family: sans-serif ;
|
||||
font-style: oblique }
|
||||
|
||||
span.classifier-delimiter {
|
||||
font-family: sans-serif ;
|
||||
font-weight: bold }
|
||||
|
||||
span.interpreted {
|
||||
font-family: sans-serif }
|
||||
|
||||
span.option {
|
||||
white-space: nowrap }
|
||||
|
||||
span.pre {
|
||||
white-space: pre }
|
||||
|
||||
span.problematic {
|
||||
color: red }
|
||||
|
||||
span.section-subtitle {
|
||||
/* font-size relative to parent (h1..h6 element) */
|
||||
font-size: 80% }
|
||||
|
||||
table.citation {
|
||||
border-left: solid 1px gray;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docinfo {
|
||||
margin: 2em 4em }
|
||||
|
||||
table.docutils {
|
||||
margin-top: 0.5em ;
|
||||
margin-bottom: 0.5em }
|
||||
|
||||
table.footnote {
|
||||
border-left: solid 1px black;
|
||||
margin-left: 1px }
|
||||
|
||||
table.docutils td, table.docutils th,
|
||||
table.docinfo td, table.docinfo th {
|
||||
padding-left: 0.5em ;
|
||||
padding-right: 0.5em ;
|
||||
vertical-align: top }
|
||||
|
||||
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||
font-weight: bold ;
|
||||
text-align: left ;
|
||||
white-space: nowrap ;
|
||||
padding-left: 0 }
|
||||
|
||||
/* "booktabs" style (no vertical lines) */
|
||||
table.docutils.booktabs {
|
||||
border: 0px;
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
table.docutils.booktabs * {
|
||||
border: 0px;
|
||||
}
|
||||
table.docutils.booktabs th {
|
||||
border-bottom: thin solid;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||
font-size: 100% }
|
||||
|
||||
ul.auto-toc {
|
||||
list-style-type: none }
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="document" id="storage-image-import">
|
||||
<h1 class="title">Storage Image Import</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:f68662397b0281f5612be18799e059c067aaee708405d05a4c799f97a751164f
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/storage/tree/14.0/storage_image_import"><img alt="OCA/storage" src="https://img.shields.io/badge/github-OCA%2Fstorage-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/storage-14-0/storage-14-0-storage_image_import"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/storage&target_branch=14.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
|
||||
<p>This module allows to import images based on url (from csv file)</p>
|
||||
<p><strong>Table of contents</strong></p>
|
||||
<div class="contents local topic" id="contents">
|
||||
<ul class="simple">
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-1">Usage</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-2">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-3">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-4">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-5">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-6">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">Usage</a></h1>
|
||||
<p>In a csv file, for each image per line only the field “imported_from_url” is mandatory. Since, the name is computed from the url, it could be usefull to add the “alt_name” field as a column.</p>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Bug Tracker</a></h1>
|
||||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/storage/issues">GitHub Issues</a>.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
<a class="reference external" href="https://github.com/OCA/storage/issues/new?body=module:%20storage_image_import%0Aversion:%2014.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||
</div>
|
||||
<div class="section" id="credits">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Akretion</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Contributors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Sébastien Beau <<a class="reference external" href="mailto:sebastien.beau@akretion.com">sebastien.beau@akretion.com</a>></li>
|
||||
<li>Kévin Roche <<a class="reference external" href="mailto:kevin.roche@akretion.com">kevin.roche@akretion.com</a>></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="maintainers">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Maintainers</a></h2>
|
||||
<p>This module is maintained by the OCA.</p>
|
||||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.</p>
|
||||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/storage/tree/14.0/storage_image_import">OCA/storage</a> project on GitHub.</p>
|
||||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
from . import test_import
|
|
@ -0,0 +1 @@
|
|||
from . import fake_image_relation
|
|
@ -0,0 +1,11 @@
|
|||
# Copyright 2021 Camptocamp (http://www.camptocamp.com).
|
||||
# @author Iván Todorovich <ivan.todorovich@gmail.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class FakeImageRelation(models.Model):
|
||||
_name = "fake.image.relation"
|
||||
_inherit = ["image.relation.abstract"]
|
||||
_description = "Fake Image Relation model used in tests"
|