Browse Source

[ADD] document_quick_access_folder_classification

pull/74/head
Enric Tobella 5 years ago
parent
commit
f53a8a0ee5
  1. 2
      .travis.yml
  2. 97
      document_quick_access_folder_auto_classification/README.rst
  3. 2
      document_quick_access_folder_auto_classification/__init__.py
  4. 30
      document_quick_access_folder_auto_classification/__manifest__.py
  5. 25
      document_quick_access_folder_auto_classification/data/config_parameter.xml
  6. 21
      document_quick_access_folder_auto_classification/data/cron_data.xml
  7. 2
      document_quick_access_folder_auto_classification/models/__init__.py
  8. 62
      document_quick_access_folder_auto_classification/models/document_quick_access_missing.py
  9. 182
      document_quick_access_folder_auto_classification/models/document_quick_access_rule.py
  10. 4
      document_quick_access_folder_auto_classification/readme/CONFIGURE.rst
  11. 1
      document_quick_access_folder_auto_classification/readme/CONTRIBUTORS.rst
  12. 2
      document_quick_access_folder_auto_classification/readme/DESCRIPTION.rst
  13. 11
      document_quick_access_folder_auto_classification/readme/USAGE.rst
  14. 2
      document_quick_access_folder_auto_classification/security/ir.model.access.csv
  15. 17
      document_quick_access_folder_auto_classification/security/security.xml
  16. BIN
      document_quick_access_folder_auto_classification/static/description/icon.png
  17. 443
      document_quick_access_folder_auto_classification/static/description/index.html
  18. 1
      document_quick_access_folder_auto_classification/tests/__init__.py
  19. 160
      document_quick_access_folder_auto_classification/tests/test_document_quick_access_auto_classification.py
  20. BIN
      document_quick_access_folder_auto_classification/tests/test_file.pdf
  21. 77
      document_quick_access_folder_auto_classification/views/document_quick_access_missing.xml
  22. 1
      document_quick_access_folder_auto_classification/wizards/__init__.py
  23. 35
      document_quick_access_folder_auto_classification/wizards/document_quick_access_missing_assign.py
  24. 38
      document_quick_access_folder_auto_classification/wizards/document_quick_access_missing_assign.xml
  25. 1
      oca_dependencies.txt
  26. 2
      requirements.txt

2
.travis.yml

@ -12,6 +12,8 @@ addons:
packages:
- expect-dev # provides unbuffer utility
- python-lxml # because pip installation is slow
- libzbar-dev
- poppler-utils
env:
global:

97
document_quick_access_folder_auto_classification/README.rst

@ -0,0 +1,97 @@
================================================
Document Quick Access Folder Auto Classification
================================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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%2Fserver--ux-lightgray.png?logo=github
:target: https://github.com/OCA/server-ux/tree/11.0/document_quick_access_folder_auto_classification
:alt: OCA/server-ux
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/server-ux-11-0/server-ux-11-0-document_quick_access_folder_auto_classification
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/250/11.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4| |badge5|
This module creates a job that scans all files from a folder and attaches them
to its record. The record is found using the document quick access rules.
**Table of contents**
.. contents::
:local:
Configuration
=============
# Create 3 folders on your odoo system. Odoo will use them for Preprocessing,
Store processed (not required), Store failed (not required)
# Access your system parameters and edit the parameters in order to match your
folders
Usage
=====
Users can drop the files on the folder (You may be able to configure your
scanner to send the files directly).
Then, they will be able to see the files attached to the expected record.
If two records matches the rules, it will be attached to both (two QRs).
If the file matches no rules, it will be attached as a non processed documents.
Users should be able to assign which record to use
# Access `Documents to process`
# Select a non processed document
# Assign or reject the document. When assigning it, the record will be asked.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-ux/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/server-ux/issues/new?body=module:%20document_quick_access_folder_auto_classification%0Aversion:%2011.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
~~~~~~~
* Creu Blanca
Contributors
~~~~~~~~~~~~
* Enric Tobella <etobella@creublanca.es>
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/server-ux <https://github.com/OCA/server-ux/tree/11.0/document_quick_access_folder_auto_classification>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

2
document_quick_access_folder_auto_classification/__init__.py

@ -0,0 +1,2 @@
from . import models
from . import wizards

30
document_quick_access_folder_auto_classification/__manifest__.py

@ -0,0 +1,30 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
{
'name': 'Document Quick Access Folder Auto Classification',
'summary': """
Auto classification of Documents after reading a QR""",
'version': '11.0.1.0.0',
'license': 'AGPL-3',
'author': 'Creu Blanca,Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/server-ux',
'depends': [
'document_quick_access',
'queue_job',
],
'data': [
'security/security.xml',
'security/ir.model.access.csv',
'wizards/document_quick_access_missing_assign.xml',
'views/document_quick_access_missing.xml',
'data/config_parameter.xml',
'data/cron_data.xml',
],
'external_dependencies': {
'python': [
'pyzbar',
'pdf2image',
],
},
}

25
document_quick_access_folder_auto_classification/data/config_parameter.xml

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="document_quick_access_auto_classification_path"
model="ir.config_parameter" forcecreate="True">
<field name="key">document_quick_access_auto_classification.path</field>
<field name="value">/opt/qr_data</field>
</record>
<record id="document_quick_access_auto_classification_ok_path"
model="ir.config_parameter">
<field name="key">document_quick_access_auto_classification.ok_path</field>
<field name="value">/opt/ok_qr_data</field>
</record>
<record id="document_quick_access_auto_classification_failure_path"
model="ir.config_parameter">
<field name="key">document_quick_access_auto_classification.failure_path</field>
<field name="value">/opt/ko_qr_data</field>
</record>
<record id="document_quick_access_auto_classification_process_path"
model="ir.config_parameter">
<field name="key">document_quick_access_auto_classification.process_path</field>
<field name="value">/opt/process_data</field>
</record>
</data>
</odoo>

21
document_quick_access_folder_auto_classification/data/cron_data.xml

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
© 2013-2016 Akretion (Alexis de Lattre <alexis.delattre@akretion.com>)
License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
-->
<odoo noupdate="1">
<record id="process_documents" model="ir.cron">
<field name="name">Process documents</field>
<field name="active" eval="True"/>
<field name="model_id"
ref="document_quick_access.model_document_quick_access_rule"/>
<field name="state">code</field>
<field name="code">model.cron_folder_auto_classification()</field>
<field name="interval_number">15</field>
<field name="interval_type">minutes</field>
<field name="numbercall">-1</field>
<field name="user_id" ref="base.user_root"/>
</record>
</odoo>

2
document_quick_access_folder_auto_classification/models/__init__.py

@ -0,0 +1,2 @@
from . import document_quick_access_rule
from . import document_quick_access_missing

62
document_quick_access_folder_auto_classification/models/document_quick_access_missing.py

@ -0,0 +1,62 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class DocumentQuickAccessMissing(models.Model):
_name = 'document.quick.access.missing'
_description = 'Missing Document'
name = fields.Char(required=True, readonly=True)
data = fields.Binary(attachment=True, required=True, readonly=True)
state = fields.Selection([
('pending', 'Pending'),
('processed', 'Processed'),
('deleted', 'Rejected')
], default='pending')
model = fields.Char(readonly=True)
res_id = fields.Integer(readonly=True)
@api.multi
def assign_model(self, model, res_id):
records = self.filtered(lambda r: r.state == 'pending')
res = self.env[model].browse(res_id)
res.ensure_one()
for record in records:
self.env['document.quick.access.rule']._assign_document(
record.name, record.data, res
)
records.write(self._processed_values(model, res_id))
def _processed_values(self, model, res_id):
return {
'state': 'processed',
'model': model,
'res_id': res_id
}
def _deleted_values(self):
return {
'state': 'deleted'
}
def access_resource(self):
self.ensure_one()
if not self.model:
return {}
record = self.env[self.model].browse(self.res_id).exists()
if not record:
return {}
return {
"type": "ir.actions.act_window",
"res_model": record._name,
"views": [[record.get_formview_id(), "form"]],
"res_id": record.id,
}
@api.multi
def reject_assign_document(self):
self.filtered(lambda r: r.state == 'pending').write(
self._deleted_values()
)

182
document_quick_access_folder_auto_classification/models/document_quick_access_rule.py

@ -0,0 +1,182 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from io import StringIO
import os
import base64
import mimetypes
import shutil
from odoo import api, models
from odoo.addons.queue_job.job import job
from odoo.modules.registry import Registry
import logging
import traceback
_logger = logging.getLogger(__name__)
try:
from pyzbar.pyzbar import decode, ZBarSymbol
except (ImportError, IOError) as err:
_logger.warning(err)
try:
import pdf2image
from pdf2image.exceptions import (
PDFInfoNotInstalledError,
PDFPageCountError,
PDFSyntaxError
)
except (ImportError, IOError) as err:
_logger.warning(err)
class OCRException(Exception):
def __init__(self, name):
self.name = name
class DocumentQuickAccessRule(models.Model):
_inherit = 'document.quick.access.rule'
@api.model
def cron_folder_auto_classification(
self, path=False, processing_path=False, limit=False
):
if not path:
path = self.env['ir.config_parameter'].sudo().get_param(
'document_quick_access_auto_classification.path',
default=False)
if not path:
return False
if not processing_path and not self.env.context.get(
'ignore_process_path'
):
processing_path = self.env[
'ir.config_parameter'
].sudo().get_param(
'document_quick_access_auto_classification.process_path',
default=False)
elements = [os.path.join(
path, f
) for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
if limit:
elements = elements[:limit]
for element in elements:
obj = self
new_element = element
if processing_path:
new_cr = Registry(self.env.cr.dbname).cursor()
try:
if processing_path:
new_element = os.path.join(
processing_path, os.path.basename(element))
shutil.copy(element, new_element)
obj = api.Environment(
new_cr, self.env.uid, self.env.context
)[self._name].browse().with_delay(**self._delay_vals())
obj._process_document(new_element)
if processing_path:
new_cr.commit()
except Exception:
if processing_path:
os.unlink(new_element)
new_cr.rollback()
raise
finally:
if processing_path:
new_cr.close()
if processing_path:
os.unlink(element)
return True
@api.model
def _delay_vals(self):
return {}
@api.model
@job(default_channel='root.document_quick_access_classification')
def _process_document(self, element):
try:
results = self._search_document(element)
return self._postprocess_document(element, results)
except OCRException:
_logger.warning('Element %s was corrupted' % element)
os.unlink(element)
@api.model
def _postprocess_document(self, path, results):
filename = os.path.basename(path)
datas = base64.b64encode(open(path, 'rb').read())
if results:
for result in results:
self._assign_document(filename, datas, result)
new_path = self.env['ir.config_parameter'].sudo().get_param(
'document_quick_access_auto_classification.ok_path',
default=False)
else:
new_path = self.env['ir.config_parameter'].sudo().get_param(
'document_quick_access_auto_classification.failure_path',
default=False)
self.env['document.quick.access.missing'].create({
'name': filename,
'data': datas,
})
if new_path:
shutil.copy(path, os.path.join(new_path, filename))
os.unlink(path)
return bool(results)
def _get_attachment_vals(self, filename, datas, record):
return {
'name': filename,
'datas': datas,
'datas_fname': filename,
'res_model': record._name,
'res_id': record.id,
'mimetype': mimetypes.guess_type(filename)
}
def _assign_document(self, filename, datas, record):
self.env['ir.attachment'].create(
self._get_attachment_vals(filename, datas, record)
)
@api.model
def _search_document_pdf(self, path):
records = []
try:
images = pdf2image.convert_from_bytes(
open(path, 'rb').read())
except (
PDFInfoNotInstalledError, PDFPageCountError, PDFSyntaxError
) as e:
buff = StringIO()
traceback.print_exc(file=buff)
_logger.warning(buff.getvalue())
raise OCRException(str(e))
for im in images:
records += self._search_pil_image(im)
return records
@api.model
def _search_pil_image(self, image):
results = decode(image, symbols=[ZBarSymbol.QRCODE])
records = []
for result in results:
record = self.with_context(
no_raise_document_access=True).read_code(result)
if record:
records += record
return records
@api.model
def _search_document(self, path):
filename, extension = os.path.splitext(path)
if extension == '.pdf':
return self._search_document_pdf(path)
return []
@api.model
def read_code(self, code):
try:
return super().read_code(code)
except Exception:
if self.env.context.get('no_raise_document_access', False):
return False
raise

4
document_quick_access_folder_auto_classification/readme/CONFIGURE.rst

@ -0,0 +1,4 @@
# Create 3 folders on your odoo system. Odoo will use them for Preprocessing,
Store processed (not required), Store failed (not required)
# Access your system parameters and edit the parameters in order to match your
folders

1
document_quick_access_folder_auto_classification/readme/CONTRIBUTORS.rst

@ -0,0 +1 @@
* Enric Tobella <etobella@creublanca.es>

2
document_quick_access_folder_auto_classification/readme/DESCRIPTION.rst

@ -0,0 +1,2 @@
This module creates a job that scans all files from a folder and attaches them
to its record. The record is found using the document quick access rules.

11
document_quick_access_folder_auto_classification/readme/USAGE.rst

@ -0,0 +1,11 @@
Users can drop the files on the folder (You may be able to configure your
scanner to send the files directly).
Then, they will be able to see the files attached to the expected record.
If two records matches the rules, it will be attached to both (two QRs).
If the file matches no rules, it will be attached as a non processed documents.
Users should be able to assign which record to use
# Access `Documents to process`
# Select a non processed document
# Assign or reject the document. When assigning it, the record will be asked.

2
document_quick_access_folder_auto_classification/security/ir.model.access.csv

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_document_quick_access_missing,access_document_quick_access_missing,model_document_quick_access_missing,group_missing_document,1,1,1,0

17
document_quick_access_folder_auto_classification/security/security.xml

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="ir_module_category_missing_document"
model="ir.module.category">
<field name="name">Missing Documents</field>
</record>
<record id="group_missing_document" model="res.groups">
<field name="name">Assigner</field>
<field name="category_id" ref="ir_module_category_missing_document"/>
</record>
<record model="res.users" id="base.user_root">
<field name="groups_id"
eval="[(4,ref('document_quick_access_folder_auto_classification.group_missing_document'))]"/>
</record>
</odoo>

BIN
document_quick_access_folder_auto_classification/static/description/icon.png

After

Width: 128  |  Height: 128  |  Size: 9.2 KiB

443
document_quick_access_folder_auto_classification/static/description/index.html

@ -0,0 +1,443 @@
<?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 0.14: http://docutils.sourceforge.net/" />
<title>Document Quick Access Folder Auto Classification</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/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="document-quick-access-folder-auto-classification">
<h1 class="title">Document Quick Access Folder Auto Classification</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" 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" 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" href="https://github.com/OCA/server-ux/tree/11.0/document_quick_access_folder_auto_classification"><img alt="OCA/server-ux" src="https://img.shields.io/badge/github-OCA%2Fserver--ux-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-ux-11-0/server-ux-11-0-document_quick_access_folder_auto_classification"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/250/11.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module creates a job that scans all files from a folder and attaches them
to its record. The record is found using the document quick access rules.</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="id1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
<dl class="docutils">
<dt># Create 3 folders on your odoo system. Odoo will use them for Preprocessing,</dt>
<dd>Store processed (not required), Store failed (not required)</dd>
<dt># Access your system parameters and edit the parameters in order to match your</dt>
<dd>folders</dd>
</dl>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
<p>Users can drop the files on the folder (You may be able to configure your
scanner to send the files directly).
Then, they will be able to see the files attached to the expected record.
If two records matches the rules, it will be attached to both (two QRs).</p>
<p>If the file matches no rules, it will be attached as a non processed documents.
Users should be able to assign which record to use</p>
<p># Access <cite>Documents to process</cite>
# Select a non processed document
# Assign or reject the document. When assigning it, the record will be asked.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-ux/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 smashing it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/server-ux/issues/new?body=module:%20document_quick_access_folder_auto_classification%0Aversion:%2011.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="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>Creu Blanca</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
<ul class="simple">
<li>Enric Tobella &lt;<a class="reference external" href="mailto:etobella&#64;creublanca.es">etobella&#64;creublanca.es</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id7">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/server-ux/tree/11.0/document_quick_access_folder_auto_classification">OCA/server-ux</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>

1
document_quick_access_folder_auto_classification/tests/__init__.py

@ -0,0 +1 @@
from . import test_document_quick_access_auto_classification

160
document_quick_access_folder_auto_classification/tests/test_document_quick_access_auto_classification.py

@ -0,0 +1,160 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import os
from odoo import tools
from odoo.tools import mute_logger
from odoo.tests.common import TransactionCase
from tempfile import TemporaryDirectory
from mock import patch
class TestDocumentQuickAccessClassification(TransactionCase):
def setUp(self):
super().setUp()
self.tmpdir = TemporaryDirectory()
self.ok_tmpdir = TemporaryDirectory()
self.no_ok_tmpdir = TemporaryDirectory()
self.env['ir.config_parameter'].set_param(
'document_quick_access_auto_classification.path',
self.tmpdir.name
)
self.env['ir.config_parameter'].set_param(
'document_quick_access_auto_classification.ok_path',
self.ok_tmpdir.name
)
self.env['ir.config_parameter'].set_param(
'document_quick_access_auto_classification.failure_path',
self.no_ok_tmpdir.name
)
self.model_id = self.env.ref('base.model_res_partner')
def tearDown(self):
super().tearDown()
self.tmpdir.cleanup()
self.ok_tmpdir.cleanup()
self.no_ok_tmpdir.cleanup()
def test_ok_pdf(self):
partner = self.env['res.partner'].create({
'name': 'Partner',
})
file = tools.file_open(
'test_file.pdf',
mode="rb",
subdir="addons/document_quick_access_folder_auto_classification"
"/tests"
).read()
self.env['document.quick.access.rule'].create({
'model_id': self.model_id.id,
'name': 'PARTNER',
'priority': 1,
'barcode_format': 'standard',
})
with open(os.path.join(self.tmpdir.name, 'test_file.pdf'), 'wb') as f:
f.write(file)
code = partner.get_quick_access_code()
with patch(
'odoo.addons.document_quick_access_folder_auto_classification.'
'models.document_quick_access_rule.decode'
) as ptch:
ptch.return_value = [code]
self.env['document.quick.access.rule'].with_context(
ignore_process_path=True
).cron_folder_auto_classification()
ptch.assert_called()
self.assertTrue(self.env['ir.attachment'].search([
('res_model', '=', partner._name),
('res_id', '=', partner.id)
]))
self.assertTrue(os.path.exists(
os.path.join(self.ok_tmpdir.name, 'test_file.pdf')))
def test_no_ok_assign(self):
file = tools.file_open(
'test_file.pdf',
mode="rb",
subdir="addons/document_quick_access_folder_auto_classification/"
"tests"
).read()
with open(os.path.join(self.tmpdir.name, 'test_file.pdf'), 'wb') as f:
f.write(file)
self.env['document.quick.access.rule'].with_context(
ignore_process_path=True
).cron_folder_auto_classification()
self.assertTrue(os.path.exists(
os.path.join(self.no_ok_tmpdir.name, 'test_file.pdf')))
partner = self.env['res.partner'].create({
'name': 'Partner',
})
missing = self.env['document.quick.access.missing'].search([
('name', '=', 'test_file.pdf'),
('state', '=', 'pending')
])
self.assertTrue(missing)
action = missing.access_resource()
self.assertFalse(action.keys())
self.env['document.quick.access.rule'].create({
'model_id': self.model_id.id,
'name': 'PARTNER',
'priority': 1,
'barcode_format': 'standard',
})
wizard = self.env['document.quick.access.missing.assign'].create({
'object_id': '%s,%s' % (partner._name, partner.id),
'missing_document_id': missing.id,
})
wizard.doit()
self.assertEqual(missing.state, 'processed')
action = missing.access_resource()
self.assertEqual(partner._name, action['res_model'])
self.assertEqual(partner.id, action['res_id'])
def test_no_ok_reject(self):
file = tools.file_open(
'test_file.pdf',
mode="rb",
subdir="addons/document_quick_access_folder_auto_classification/"
"tests"
).read()
with open(os.path.join(self.tmpdir.name, 'test_file.pdf'), 'wb') as f:
f.write(file)
self.env['document.quick.access.rule'].with_context(
ignore_process_path=True
).cron_folder_auto_classification()
self.assertTrue(os.path.exists(
os.path.join(self.no_ok_tmpdir.name, 'test_file.pdf')))
missing = self.env['document.quick.access.missing'].search([
('name', '=', 'test_file.pdf'),
('state', '=', 'pending')
])
self.assertTrue(missing)
missing.reject_assign_document()
self.assertEqual(missing.state, 'deleted')
def test_corrupted(self):
file = tools.file_open(
'test_file.pdf',
mode="rb",
subdir="addons/document_quick_access_folder_auto_classification/"
"tests"
).read()
with open(os.path.join(self.tmpdir.name, 'test_file.pdf'), 'wb') as f:
f.write(file[:int(len(file)/2)])
with mute_logger(
'odoo.addons.document_quick_access_folder_auto_classification.'
'models.document_quick_access_rule'
):
self.env['document.quick.access.rule'].with_context(
ignore_process_path=True
).cron_folder_auto_classification()
self.assertFalse(
os.path.exists(os.path.join(self.ok_tmpdir.name, 'test_file.pdf'))
)
self.assertFalse(os.path.exists(
os.path.join(self.no_ok_tmpdir.name, 'test_file.pdf')))
self.assertFalse(
os.path.exists(os.path.join(self.tmpdir.name, 'test_file.pdf'))
)

BIN
document_quick_access_folder_auto_classification/tests/test_file.pdf

77
document_quick_access_folder_auto_classification/views/document_quick_access_missing.xml

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="document_quick_access_missing_form_view">
<field name="name">document.quick.access.missing.form (in document_quick_access_folder_auto_classification)</field>
<field name="model">document.quick.access.missing</field>
<field name="arch" type="xml">
<form create="false" edit="false">
<header>
<button name="%(document_quick_access_folder_auto_classification.document_quick_access_missing_assign_act_window)s"
type="action" class="primary" string="Assign"
states="pending"
context="{'default_missing_document_id': active_id}"
/>
<button name="reject_assign_document" type="object" string="Reject"
states="pending"/>
<button name="access_resource" type="object" string="Access"
attrs="{'invisible': ['|', ('res_id', '=', False), ('model', '=', False)]}"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<h1>
<field name="name"/>
</h1>
<group>
<field name="data" filename="name"/>
<field name="res_id" invisible="1"/>
<field name="model" invisible="1"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="document_quick_access_missing_search_view">
<field name="name">document.quick.access.missing.search (in document_quick_access_folder_auto_classification)</field>
<field name="model">document.quick.access.missing</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
<separator/>
<filter domain="[('state', '=', 'pending')]" help="Pending" name="pending"/>
<filter domain="[('state', '=', 'processed')]" help="Processed" name="processed"/>
<filter domain="[('state', '=', 'deleted')]" help="Rejected" name="deleted"/>
</search>
</field>
</record>
<record model="ir.ui.view" id="document_quick_access_missing_tree_view">
<field name="name">document.quick.access.missing.tree (in document_quick_access_folder_auto_classification)</field>
<field name="model">document.quick.access.missing</field>
<field name="arch" type="xml">
<tree create="false" delete="false">
<field name="name"/>
<field name="state"/>
</tree>
</field>
</record>
<record model="ir.actions.act_window" id="document_quick_access_missing_act_window">
<field name="name">Document Quick Access Missing</field>
<field name="res_model">document.quick.access.missing</field>
<field name="view_mode">tree,form</field>
<field name="domain">[]</field>
<field name="context">{'search_default_pending': 1}</field>
</record>
<record model="ir.ui.menu" id="document_quick_access_missing_menu">
<field name="name">Documents to process</field>
<field name="action" ref="document_quick_access_missing_act_window"/>
<field name="sequence" eval="16"/>
</record>
</odoo>

1
document_quick_access_folder_auto_classification/wizards/__init__.py

@ -0,0 +1 @@
from . import document_quick_access_missing_assign

35
document_quick_access_folder_auto_classification/wizards/document_quick_access_missing_assign.py

@ -0,0 +1,35 @@
# Copyright 2019 Creu Blanca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class DocumentQuickAccessMissingAssign(models.TransientModel):
_name = 'document.quick.access.missing.assign'
@api.model
def document_quick_access_models(self):
models = self.env['document.quick.access.rule'].search([]).mapped(
'model_id'
)
res = []
for model in models:
res.append((model.model, model.name))
return res
object_id = fields.Reference(
selection=lambda r: r.document_quick_access_models(),
required=True,
)
missing_document_id = fields.Many2one(
'document.quick.access.missing',
required=True,
)
@api.multi
def doit(self):
self.ensure_one()
self.missing_document_id.assign_model(
self.object_id._name, self.object_id.id)
return True

38
document_quick_access_folder_auto_classification/wizards/document_quick_access_missing_assign.xml

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2019 Creu Blanca
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record model="ir.ui.view" id="document_quick_access_missing_assign_form_view">
<field name="name">document.quick.access.missing.assign.form (in document_quick_access_folder_auto_classification)</field>
<field name="model">document.quick.access.missing.assign</field>
<field name="arch" type="xml">
<form string="Document Quick Access Missing Assign">
<group>
<field name="object_id"/>
<field name="missing_document_id" invisible="1"/>
</group>
<footer>
<button name="doit"
string="OK"
class="btn-primary"
type="object"/>
<button string="Cancel"
class="btn-default"
special="cancel"/>
</footer>
</form>
</field>
</record>
<record model="ir.actions.act_window" id="document_quick_access_missing_assign_act_window">
<field name="name">Document Quick Access Missing Assign</field> <!-- TODO -->
<field name="res_model">document.quick.access.missing.assign</field>
<field name="view_mode">form</field>
<field name="context">{}</field>
<field name="target">new</field>
</record>
</odoo>

1
oca_dependencies.txt

@ -1 +1,2 @@
queue
server-ux-doc https://github.com/etobella/server-ux 11.0-add-document_quick_access

2
requirements.txt

@ -0,0 +1,2 @@
pyzbar
pdf2image
Loading…
Cancel
Save