[IMP] account,snailmail: support batch calls
- reworked the multi.compose.message model to manage several documents simultaneously - Added a letter model for asynchronous processing - usability improvement of the modal
This commit is contained in:
@@ -72,11 +72,11 @@ You could use this simplified accounting in case you work with an (external) acc
|
||||
'views/res_users_views.xml',
|
||||
'data/service_cron_reverse.xml',
|
||||
'views/account_fiscal_year_view.xml',
|
||||
'wizard/multi_compose_message_views.xml',
|
||||
'views/account_incoterms_view.xml',
|
||||
'data/account_incoterms_data.xml',
|
||||
'views/res_company_view.xml',
|
||||
'views/digest_views.xml',
|
||||
'wizard/account_invoice_send_views.xml',
|
||||
],
|
||||
'demo': [
|
||||
'demo/account_demo.xml',
|
||||
|
||||
@@ -16,6 +16,5 @@ from . import res_config_settings
|
||||
from . import account_cash_rounding
|
||||
from . import reconciliation_widget
|
||||
from . import res_users
|
||||
from . import res_company
|
||||
from . import account_incoterms
|
||||
from . import digest
|
||||
|
||||
@@ -574,8 +574,7 @@ class AccountInvoice(models.Model):
|
||||
""" Print the invoice and mark it as sent, so that we can see more
|
||||
easily the next step of the workflow
|
||||
"""
|
||||
self.ensure_one()
|
||||
self.sent = True
|
||||
self.write({'sent': True})
|
||||
if self.user_has_groups('account.group_account_invoice'):
|
||||
return self.env.ref('account.account_invoices').report_action(self)
|
||||
else:
|
||||
@@ -588,7 +587,7 @@ class AccountInvoice(models.Model):
|
||||
"""
|
||||
self.ensure_one()
|
||||
template = self.env.ref('account.email_template_edi_invoice', False)
|
||||
compose_form = self.env.ref('account.multi_compose_message_wizard_form', False)
|
||||
compose_form = self.env.ref('account.account_invoice_send_wizard_form', False)
|
||||
ctx = dict(
|
||||
default_model='account.invoice',
|
||||
default_res_id=self.id,
|
||||
@@ -600,11 +599,11 @@ class AccountInvoice(models.Model):
|
||||
force_email=True
|
||||
)
|
||||
return {
|
||||
'name': _('Compose Email'),
|
||||
'name': _('Send Invoice'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_type': 'form',
|
||||
'view_mode': 'form',
|
||||
'res_model': 'multi.compose.message',
|
||||
'res_model': 'account.invoice.send',
|
||||
'views': [(compose_form.id, 'form')],
|
||||
'view_id': compose_form.id,
|
||||
'target': 'new',
|
||||
|
||||
@@ -71,6 +71,9 @@ Best Regards,'''))
|
||||
qr_code_payment_journal_id = fields.Many2one('account.journal', string='SEPA QR Code Bank Journal account')
|
||||
qr_code_valid = fields.Boolean(string='Has all required arguments', related="qr_code_payment_journal_id.bank_account_id.qr_code_valid")
|
||||
|
||||
invoice_is_email = fields.Boolean('Email by default', default=True)
|
||||
invoice_is_print = fields.Boolean('Print by default', default=True)
|
||||
|
||||
#Fields of the setup step for opening move
|
||||
account_opening_move_id = fields.Many2one(string='Opening Journal Entry', comodel_name='account.move', help="The journal entry containing the initial balance of all this company's accounts.")
|
||||
account_opening_journal_id = fields.Many2one(string='Opening Journal', comodel_name='account.journal', related='account_opening_move_id.journal_id', help="Journal where the opening entry of this company's accounting has been posted.")
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResCompany(models.Model):
|
||||
_inherit = 'res.company'
|
||||
|
||||
is_email = fields.Boolean('Email by default', default=True)
|
||||
is_print = fields.Boolean('Print by default', default=True)
|
||||
@@ -86,6 +86,8 @@ class ResConfigSettings(models.TransientModel):
|
||||
qr_code = fields.Boolean(string='Display SEPA QR code', related='company_id.qr_code')
|
||||
qr_code_payment_journal_id = fields.Many2one('account.journal', related='company_id.qr_code_payment_journal_id', string="Payment Journal", domain="['&',('type', '=', 'bank'), ('currency_id.name','=','EUR')]")
|
||||
qr_code_valid = fields.Boolean(string='Has all required arguments', related="qr_code_payment_journal_id.bank_account_id.qr_code_valid")
|
||||
invoice_is_print = fields.Boolean(string='Print', related='company_id.invoice_is_print')
|
||||
invoice_is_email = fields.Boolean(string='Send Email', related='company_id.invoice_is_email')
|
||||
|
||||
@api.multi
|
||||
def set_values(self):
|
||||
|
||||
@@ -450,6 +450,27 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Default Sending Options</h2>
|
||||
<div class="row mt16 o_settings_container" id="send_default">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="invoice_is_print"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="invoice_is_print"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="invoice_is_email"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="invoice_is_email"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2>Invoices</h2>
|
||||
<div class="row mt16 o_settings_container" id="invoicing_settings">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
|
||||
@@ -21,4 +21,4 @@ from . import account_report_aged_partner_balance
|
||||
from . import setup_wizards
|
||||
from . import wizard_tax_adjustments
|
||||
from . import account_report_tax
|
||||
from . import multi_compose_message
|
||||
from . import account_invoice_send
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class AccountInvoiceSend(models.TransientModel):
|
||||
_name = 'account.invoice.send'
|
||||
_inherits = {'mail.compose.message':'composer_id'}
|
||||
|
||||
is_email = fields.Boolean('Email', default=lambda self: self.env.user.company_id.invoice_is_email)
|
||||
is_print = fields.Boolean('Print', default=lambda self: self.env.user.company_id.invoice_is_print)
|
||||
printed = fields.Boolean('Is Printed', default=False)
|
||||
invoice_ids = fields.Many2many('account.invoice', 'account_invoice_account_invoice_send_rel', string='Invoices')
|
||||
composer_id = fields.Many2one('mail.compose.message', string='Composer', required=True, ondelete='cascade')
|
||||
template_id = fields.Many2one(
|
||||
'mail.template', 'Use template', index=True,
|
||||
domain="[('model', '=', 'account.invoice')]"
|
||||
)
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields):
|
||||
res = super(AccountInvoiceSend, self).default_get(fields)
|
||||
res_ids = self._context.get('active_ids')
|
||||
composer = self.env['mail.compose.message'].create({
|
||||
'composition_mode': 'comment' if len(res_ids) == 1 else 'mass_mail',
|
||||
})
|
||||
res.update({
|
||||
'invoice_ids': res_ids,
|
||||
'composer_id': composer.id,
|
||||
})
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
@api.onchange('invoice_ids')
|
||||
def _compute_composition_mode(self):
|
||||
for wizard in self:
|
||||
wizard.composition_mode = 'comment' if len(wizard.invoice_ids) == 1 else 'mass_mail'
|
||||
|
||||
@api.onchange('template_id')
|
||||
def onchange_template_id(self):
|
||||
if self.composer_id:
|
||||
self.composer_id.template_id = self.template_id.id
|
||||
self.composer_id.onchange_template_id_wrapper()
|
||||
|
||||
@api.multi
|
||||
def _send_email(self):
|
||||
if self.is_email:
|
||||
self.composer_id.send_mail()
|
||||
|
||||
@api.multi
|
||||
def _print_document(self):
|
||||
""" to override for each type of models that will use this composer."""
|
||||
self.ensure_one()
|
||||
return self.invoice_ids.invoice_print()
|
||||
|
||||
@api.multi
|
||||
def send_and_print_action(self):
|
||||
self.ensure_one()
|
||||
self._send_email()
|
||||
if self.is_print:
|
||||
return self._print_document()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record model="ir.ui.view" id="account_invoice_send_wizard_form">
|
||||
<field name="name">account.invoice.send.form</field>
|
||||
<field name="model">account.invoice.send</field>
|
||||
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Invoice send & Print">
|
||||
<!-- truly invisible fields for control and options -->
|
||||
<field name="composition_mode" invisible="1"/>
|
||||
<field name="composer_id" invisible="1"/>
|
||||
<field name="printed" invisible="1"/>
|
||||
<field name="invoice_ids" invisible="1"/>
|
||||
<div name="option_print">
|
||||
<field name="is_print" />
|
||||
<b><label for="is_print"/></b>
|
||||
<div name="info_form" attrs="{'invisible': ['|', ('is_print', '=', False), ('composition_mode', '=', 'mass_mail')]}" class="text-center text-muted d-inline-block">
|
||||
Preview as a PDF
|
||||
</div>
|
||||
</div>
|
||||
<div name="option_email">
|
||||
<field name="is_email" />
|
||||
<b><label for="is_email"/></b>
|
||||
</div>
|
||||
<div name="mail_form" attrs="{'invisible': [('is_email', '=', False)]}">
|
||||
<!-- visible wizard -->
|
||||
<div attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
|
||||
<group>
|
||||
<label for="partner_ids" string="Recipients" groups="base.group_user"/>
|
||||
<div groups="base.group_user">
|
||||
<span attrs="{'invisible': [('composition_mode', '!=', 'mass_mail')]}">
|
||||
<strong>Email mass mailing</strong> on
|
||||
<span>the selected records</span>
|
||||
</span>
|
||||
<span>Followers of the document and</span>
|
||||
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
|
||||
context="{'force_email':True, 'show_email':True}" attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}"/>
|
||||
</div>
|
||||
<field name="subject" placeholder="Subject..." attrs="{'required': [('is_email', '=', True), ('composition_mode', '=', 'comment')]}"/>
|
||||
</group>
|
||||
<field name="body" style="border:none;" options="{'style-inline': true}"/>
|
||||
</div>
|
||||
<group>
|
||||
<group attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}">
|
||||
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" nolabel="1" colspan="2" attrs="{'invisible': [('composition_mode', '=', 'mass_mail')]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="template_id" options="{'no_create': True, 'no_edit': True}"
|
||||
context="{'default_model': 'account.invoice'}"/>
|
||||
</group>
|
||||
</group>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<button string="Send & Print"
|
||||
attrs="{'invisible': ['|', '|', ('printed', '=', True), '|', ('is_email', '=', False), ('is_print', '=', False)]}"
|
||||
name="send_and_print_action" type="object" class="send_and_print btn-primary o_mail_send"/>
|
||||
<button string="Send"
|
||||
attrs="{'invisible': ['|', ('is_print', '=', True), ('is_email', '=', False)]}"
|
||||
name="send_and_print_action" type="object" class="send btn-primary o_mail_send"/>
|
||||
<button string="Print"
|
||||
attrs="{'invisible': ['|', '|', ('printed', '=', True), ('is_print', '=', False), ('is_email', '=', True)]}"
|
||||
name="send_and_print_action" type="object" class="print btn-primary o_mail_send"/>
|
||||
<button string="Done" attrs="{'invisible': [('printed', '=', False)]}" special="cancel" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
<button icon="fa-lg fa-save" type="object" name="save_as_template" string="Save as new template"
|
||||
attrs="{'invisible': ['|', '|', '|', ('composition_mode', '=', 'mass_mail'), ('printed', '=', True), ('is_email', '=', False)]}"
|
||||
class="pull-right btn-secondary" help="Save as a new template" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<act_window
|
||||
name="Send"
|
||||
id="invoice_send"
|
||||
multi="True"
|
||||
key2="client_action_multi"
|
||||
res_model="account.invoice.send"
|
||||
src_model="account.invoice"
|
||||
view_mode="form"
|
||||
target="new"
|
||||
view_type="form"
|
||||
context="{
|
||||
'default_template_id': ref('account.email_template_edi_invoice'),
|
||||
'default_is_print': False}"
|
||||
groups="account.group_account_invoice"/>
|
||||
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
@@ -1,79 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class MultiComposer(models.TransientModel):
|
||||
|
||||
_name = 'multi.compose.message'
|
||||
_inherit = 'mail.compose.message'
|
||||
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True,
|
||||
default=lambda self: self.env.user.company_id.id)
|
||||
is_email = fields.Boolean('Email', default=lambda self: self.env.user.company_id.is_email)
|
||||
is_print = fields.Boolean('Print', default=lambda self: self.env.user.company_id.is_print)
|
||||
printed = fields.Boolean('Is Printed', default=False)
|
||||
partner_ids = fields.Many2many(
|
||||
'res.partner', 'account_compose_message_res_partner_rel',
|
||||
'wizard_id', 'partner_id', 'Additional Contacts')
|
||||
attachment_ids = fields.Many2many(
|
||||
'ir.attachment', 'account_compose_message_ir_attachments_rel',
|
||||
'wizard_id', 'attachment_id', 'Attachments')
|
||||
print_attachment_id = fields.Many2one('ir.attachment', string='Print Attachment')
|
||||
|
||||
@api.multi
|
||||
def _update_company(self):
|
||||
for rec in self:
|
||||
values = {}
|
||||
for field in ['is_print', 'is_email']:
|
||||
if rec.company_id[field] != rec[field]:
|
||||
values.update({field: rec[field]})
|
||||
if values:
|
||||
rec.company_id.write(values)
|
||||
|
||||
@api.multi
|
||||
def write(self, vals):
|
||||
res = super(MultiComposer, self).write(vals)
|
||||
self._update_company()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
@api.onchange('template_id')
|
||||
def onchange_template_id_wrapper(self):
|
||||
self.ensure_one()
|
||||
super(MultiComposer,self).onchange_template_id_wrapper()
|
||||
self.print_attachment_id = self.attachment_ids[:1]
|
||||
|
||||
def _send_email(self):
|
||||
if self.is_email:
|
||||
self.send_mail()
|
||||
|
||||
@api.multi
|
||||
def _print_document(self):
|
||||
""" to override for each type of models that will use this composer."""
|
||||
self.ensure_one()
|
||||
action = {
|
||||
'type': 'ir.actions.act_url',
|
||||
'url': '/web/content/%s?download=true' % (self.print_attachment_id.id),
|
||||
'target': 'new',
|
||||
}
|
||||
self.printed = True
|
||||
return action
|
||||
|
||||
@api.multi
|
||||
def send_and_print_action(self):
|
||||
self.ensure_one()
|
||||
self._send_email()
|
||||
return self._print_document()
|
||||
|
||||
@api.multi
|
||||
def send_action(self):
|
||||
for wizard in self:
|
||||
wizard._send_email()
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
|
||||
@api.multi
|
||||
def print_action(self):
|
||||
self.ensure_one()
|
||||
return self._print_document()
|
||||
@@ -1,66 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="multi_compose_message_wizard_form">
|
||||
<field name="name">multi.compose.message.form</field>
|
||||
<field name="model">multi.compose.message</field>
|
||||
<field name="groups_id" eval="[(4,ref('base.group_user'))]"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Compose Email & Print">
|
||||
<field name="print_attachment_id" invisible="1"/>
|
||||
<div name="option_print" class="col-md-12 mb16">
|
||||
<field name="is_print" widget="boolean_toggle"/>
|
||||
<b><label for="is_print"/></b>
|
||||
</div>
|
||||
<div class="col-md-12 mb16" style="border-bottom: 1px solid #e5e5e5;"></div>
|
||||
<div name="option_email" class="col-md-12 mb16">
|
||||
<field name="is_email" widget="boolean_toggle"/>
|
||||
<b><label for="is_email"/></b>
|
||||
</div>
|
||||
<div name="mail_form" attrs="{'invisible': [('is_email', '=', False)]}">
|
||||
<group>
|
||||
<!-- truly invisible fields for control and options -->
|
||||
<field name="printed" invisible="1"/>
|
||||
<field name="model" invisible="1"/>
|
||||
<field name="res_id" invisible="1"/>
|
||||
<field name="is_log" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<!-- visible wizard -->
|
||||
<label for="partner_ids" string="Recipients" groups="base.group_user"/>
|
||||
<div groups="base.group_user">
|
||||
<span attrs="{'invisible':['|', ('model', '=', False)]}">Followers of the document and</span>
|
||||
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
|
||||
context="{'force_email':True, 'show_email':True}"/>
|
||||
</div>
|
||||
<field name="subject" placeholder="Subject..." required="True"/>
|
||||
</group>
|
||||
<field name="body" options="{'style-inline': true}"/>
|
||||
<group col="4">
|
||||
<field name="attachment_ids" widget="many2many_binary" string="Attach a file" nolabel="1" colspan="2"/>
|
||||
<field name="template_id" options="{'no_create': True}"
|
||||
context="{'default_model': model, 'default_body_html': body, 'default_subject': subject}"/>
|
||||
</group>
|
||||
</div>
|
||||
<footer>
|
||||
<button string="Send & Print"
|
||||
attrs="{'invisible': ['|', '|', ('is_log', '=', True), ('printed', '=', True), '|', ('is_email', '=', False), ('is_print', '=', False)]}"
|
||||
name="send_and_print_action" type="object" class="btn-primary o_mail_send"/>
|
||||
<button string="Send"
|
||||
attrs="{'invisible': ['|', ('is_print', '=', True), ('is_email', '=', False)]}"
|
||||
name="send_action" type="object" class="btn-primary o_mail_send"/>
|
||||
<button string="Print"
|
||||
attrs="{'invisible': ['|', '|', '|', ('is_log', '=', True), ('printed', '=', True), ('is_print', '=', False), ('is_email', '=', True)]}"
|
||||
name="print_action" type="object" class="btn-primary o_mail_send"/>
|
||||
<button string="Done" attrs="{'invisible': [('printed', '=', False)]}" special="cancel" class="btn-primary"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" />
|
||||
<button icon="fa-lg fa-save" type="object" name="save_as_template" string="Save as new template"
|
||||
attrs="{'invisible': ['|', '|', '|', ('is_log', '=', True), ('printed', '=', True), ('is_email', '=', False)]}"
|
||||
class="float-right btn-secondary" help="Save as a new template" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
</odoo>
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
|
||||
@@ -2,15 +2,17 @@
|
||||
{
|
||||
'name': "Snail Mail",
|
||||
'description': """
|
||||
Allows users to send invoices by post
|
||||
Allows users to send documents by post
|
||||
=====================================================
|
||||
""",
|
||||
'category': 'Tools',
|
||||
'version': '0.1',
|
||||
'depends': ['account', 'iap'],
|
||||
'depends': ['iap', 'base_setup'],
|
||||
'data': [
|
||||
'data/mail_activity_data.xml',
|
||||
'wizard/multi_compose_message_views.xml',
|
||||
'data/snailmail_data.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/snailmail_views.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="mail_activity_data_snailmail" model="mail.activity.type">
|
||||
<field name="name">Snailmail</field>
|
||||
<field name="icon">fa-address-card</field>
|
||||
<field name="sequence">15</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.cron" id="snailmail_print">
|
||||
<field name="name">Snaimail: process letters queue</field>
|
||||
<field name="model_id" ref="model_snailmail_letter"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model._snailmail_cron()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">hours</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="mail_activity_data_snailmail" model="mail.activity.type">
|
||||
<field name="name">Snailmail</field>
|
||||
<field name="icon">fa-address-card</field>
|
||||
<field name="sequence">15</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -2,3 +2,4 @@
|
||||
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import snailmail_letter
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
snailmail_color = fields.Boolean(string='Color', default=False)
|
||||
snailmail_duplex = fields.Boolean(string='Both sides', default=False)
|
||||
snailmail_send_by_letter = fields.Boolean(string='Send by Letter by default', default=True)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, api
|
||||
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
snailmail_color = fields.Boolean(string='color', related='company_id.snailmail_color')
|
||||
snailmail_duplex = fields.Boolean(string='Both sides', related='company_id.snailmail_duplex')
|
||||
|
||||
snailmail_color = fields.Boolean(string='Print In Color', related='company_id.snailmail_color')
|
||||
snailmail_duplex = fields.Boolean(string='Print Both sides', related='company_id.snailmail_duplex')
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import re
|
||||
import base64
|
||||
import datetime
|
||||
|
||||
from odoo import fields, models, api, _, tools
|
||||
from odoo.addons.iap import jsonrpc
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://iap-snailmail.odoo.com'
|
||||
ESTIMATE_ENDPOINT = '/iap/snailmail/1/estimate'
|
||||
PRINT_ENDPOINT = '/iap/snailmail/1/print'
|
||||
|
||||
|
||||
class SnailmailLetter(models.Model):
|
||||
_name = 'snailmail.letter'
|
||||
|
||||
user_id = fields.Many2one('res.users', 'User sending the letter')
|
||||
model = fields.Char('Model', required=True)
|
||||
res_id = fields.Integer('Document ID', required=True)
|
||||
partner_id = fields.Many2one('res.partner', string='Recipient', required=True)
|
||||
company_id = fields.Many2one('res.company', string='Company', required=True, readonly=True,
|
||||
default=lambda self: self.env.user.company_id.id)
|
||||
report_template = fields.Many2one('ir.actions.report', 'Optional report to print and attach')
|
||||
|
||||
attachment_id = fields.Many2one('ir.attachment', string='Attachment', ondelete='cascade')
|
||||
color = fields.Boolean(string='Color', default=lambda self: self.env.user.company_id.snailmail_color)
|
||||
duplex = fields.Boolean(string='Both side', default=lambda self: self.env.user.company_id.snailmail_duplex)
|
||||
state = fields.Selection([
|
||||
('draft', 'Draft'),
|
||||
('pending', 'In Queue'),
|
||||
('sent', 'Sent'),
|
||||
('error', 'Error'),
|
||||
('canceled', 'Canceled')
|
||||
], 'Status', readonly=True, copy=False, default='draft',
|
||||
help="When a letter is created, the status is 'Draft'.\n"
|
||||
"If the letter is correctly sent, the status goes in 'Sent',\n"
|
||||
"If not, it will got in state 'Error' and the error message will be displayed in the field 'Error Message'.")
|
||||
info_msg = fields.Char('Information')
|
||||
|
||||
@api.multi
|
||||
def _fetch_attachment(self):
|
||||
"""
|
||||
This method will check if we have any existent attachement matching the model
|
||||
and res_ids and create them if not found.
|
||||
"""
|
||||
self.ensure_one()
|
||||
obj = self.env[self.model].browse(self.res_id)
|
||||
if not self.attachment_id:
|
||||
report = self.report_template
|
||||
if not report:
|
||||
report_name = self.env.context.get('report_name')
|
||||
report = self.env['ir.actions.report']._get_report_from_name(report_name)
|
||||
if not report:
|
||||
return False
|
||||
else:
|
||||
self.write({'report_template': report.id})
|
||||
# report = self.env.ref('account.account_invoices')
|
||||
if report.print_report_name:
|
||||
report_name = safe_eval(report.print_report_name, {'object': obj})
|
||||
elif report.attachment:
|
||||
report_name = safe_eval(report.attachment, {'object': obj})
|
||||
else:
|
||||
report_name = 'Document'
|
||||
filename = "%s.%s" % (report_name, "pdf")
|
||||
pdf_bin, _ = report.render_qweb_pdf(self.res_id)
|
||||
attachment = self.env['ir.attachment'].create({
|
||||
'name': filename,
|
||||
'datas': base64.b64encode(pdf_bin),
|
||||
'datas_fname': filename,
|
||||
'res_model': 'snailmail.letter',
|
||||
'res_id': self.id,
|
||||
'type': 'binary', # override default_type from context, possibly meant for another model!
|
||||
})
|
||||
self.write({'attachment_id': attachment.id})
|
||||
|
||||
return self.attachment_id
|
||||
|
||||
def _count_pages_pdf(self, bin_pdf):
|
||||
""" Count the number of pages of the given pdf file.
|
||||
:param bin_pdf : binary content of the pdf file
|
||||
"""
|
||||
pages = 0
|
||||
for match in re.compile(b"/Count\s+(\d+)").finditer(bin_pdf):
|
||||
pages = int(match.group(1))
|
||||
return pages
|
||||
|
||||
@api.multi
|
||||
def _snailmail_create(self, route):
|
||||
"""
|
||||
Create a dictionnary object to send to snailmail server.
|
||||
|
||||
:return: Dict in the form:
|
||||
{
|
||||
account_token: string, //IAP Account token of the user
|
||||
documents: [{
|
||||
pages: int,
|
||||
pdf_bin: pdf file
|
||||
res_id: int (client-side res_id),
|
||||
res_model: char (client-side res_model),
|
||||
address: {
|
||||
name: char,
|
||||
street: char,
|
||||
street2: char (OPTIONAL),
|
||||
zip: int,
|
||||
city: char,
|
||||
state: char (state code (OPTIONAL)),
|
||||
country_code: char (country code)
|
||||
}
|
||||
return_address: {
|
||||
name: char,
|
||||
street: char,
|
||||
street2: char (OPTIONAL),
|
||||
zip: int,
|
||||
city: char,at
|
||||
state: char (state code (OPTIONAL)),
|
||||
country_code: char (country code)
|
||||
}
|
||||
}],
|
||||
options: {
|
||||
color: boolean (true if color, false if black-white),
|
||||
duplex: boolean (true if duplex, false otherwise),
|
||||
}
|
||||
}
|
||||
"""
|
||||
account_token = self.env['iap.account'].get('snailmail').account_token
|
||||
documents = []
|
||||
|
||||
batch = len(self) > 1
|
||||
for letter in self:
|
||||
document = {
|
||||
# generic informations to send
|
||||
'letter_id': letter.id,
|
||||
'res_model': letter.model,
|
||||
'res_id': letter.res_id,
|
||||
'address': {
|
||||
'name': letter.partner_id.name,
|
||||
'street': letter.partner_id.street,
|
||||
'street2': letter.partner_id.street2,
|
||||
'zip': letter.partner_id.zip,
|
||||
'state': letter.partner_id.state_id.code if letter.partner_id.state_id else False,
|
||||
'city': letter.partner_id.city,
|
||||
'country_code': letter.partner_id.country_id.code
|
||||
},
|
||||
'return_address': {
|
||||
'name': letter.company_id.partner_id.name,
|
||||
'street': letter.company_id.partner_id.street,
|
||||
'street2': letter.company_id.partner_id.street2,
|
||||
'zip': letter.company_id.partner_id.zip,
|
||||
'state': letter.company_id.partner_id.state_id.code if letter.company_id.partner_id.state_id else False,
|
||||
'city': letter.company_id.partner_id.city,
|
||||
'country_code': letter.company_id.partner_id.country_id.code,
|
||||
}
|
||||
}
|
||||
# Specific to each case:
|
||||
# If we are estimating the price: batch: 1 obj = 1 page else: we generate the attachment to have the exact price.
|
||||
# If we are printing -> attach the pdf
|
||||
if route == 'estimate' and batch:
|
||||
document.update(pages=1)
|
||||
else:
|
||||
attachment = letter._fetch_attachment()
|
||||
if attachment:
|
||||
document.update({
|
||||
'pdf_bin': route == 'print' and attachment.datas,
|
||||
'pages': route == 'estimate' and self._count_pages_pdf(base64.b64decode(attachment.datas)),
|
||||
})
|
||||
else:
|
||||
letter.write({
|
||||
'info_msg': 'The attachment could not be generated.',
|
||||
'state': 'error',
|
||||
})
|
||||
continue
|
||||
documents.append(document)
|
||||
|
||||
return {
|
||||
'account_token': account_token,
|
||||
'documents': documents,
|
||||
'options': {
|
||||
'color': self and self[0].color,
|
||||
'duplex': self and self[0].duplex,
|
||||
},
|
||||
# this will not raise the InsufficientCreditError which is the behaviour we want for now
|
||||
'batch': True,
|
||||
}
|
||||
|
||||
def _get_error_message(self, error):
|
||||
if error == 'CREDIT_ERROR':
|
||||
link = self.env['iap.account'].get_credits_url(service_name='snailmail')
|
||||
return _('You don\'t have enough credits to perform this operation.<br>Please go to your <a href=%s target="new">iap account</a>.' % link)
|
||||
if error == 'NO_PRICE_AVAILABLE':
|
||||
return _('The country of the partner is not covered by Snailmail.')
|
||||
if error == 'MISSING_REQUIRED_FIELDS':
|
||||
return _('One or more required fields are empty.')
|
||||
if error == 'SOMETHING_IS_WRONG':
|
||||
return _('An unknown error happened. Please contact the support.')
|
||||
return error
|
||||
|
||||
@api.multi
|
||||
def _snailmail_print(self):
|
||||
"""
|
||||
get response
|
||||
{
|
||||
'request_code': RESPONSE_OK, # because we receive 200 if good or fail
|
||||
'total_cost': total_cost,
|
||||
'credit_error': credit_error,
|
||||
'request': {
|
||||
'documents': documents,
|
||||
'options': options
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
self.write({'state': 'pending'})
|
||||
endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
|
||||
params = self._snailmail_create('print')
|
||||
response = jsonrpc(endpoint + PRINT_ENDPOINT, params=params)
|
||||
for doc in response['request']['documents']:
|
||||
letter = self.browse(doc['letter_id'])
|
||||
record = self.env[doc['res_model']].browse(doc['res_id'])
|
||||
if doc.get('sent') and response['request_code'] == 200:
|
||||
if hasattr(record, '_message_log'):
|
||||
message = _('The document was correctly sent by post.<br>The tracking id is %s' % doc['send_id'])
|
||||
record._message_log(body=message)
|
||||
letter.write({'info_msg': message, 'state': 'sent'})
|
||||
else:
|
||||
note = _('An error occured when sending the document by post.<br>Error: %s' % \
|
||||
self._get_error_message(doc['error'] if response['request_code'] == 200 else response['reason']))
|
||||
activity_data = {
|
||||
'res_id': letter.res_id,
|
||||
'res_model_id': self.env['ir.model']._get(letter.model).id,
|
||||
'activity_type_id': self.env.ref('snailmail.mail_activity_data_snailmail').id,
|
||||
'summary': _('Post letter: an error occured.'),
|
||||
'note': note,
|
||||
'user_id': letter.user_id.id,
|
||||
}
|
||||
self.env['mail.activity'].create(activity_data)
|
||||
letter.write({'info_msg': note})
|
||||
|
||||
self.env.cr.commit()
|
||||
|
||||
@api.multi
|
||||
def snailmail_print(self):
|
||||
self._snailmail_print()
|
||||
|
||||
@api.multi
|
||||
def cancel(self):
|
||||
self.write({'state': 'canceled'})
|
||||
|
||||
@api.multi
|
||||
def _snailmail_estimate(self):
|
||||
"""
|
||||
Send a request to estimate the cost of sending all the documents with
|
||||
the differents options.
|
||||
|
||||
The JSON object sent is the one generated from self._snailmail_create()
|
||||
|
||||
arguments sent:
|
||||
{
|
||||
"documents":[{
|
||||
pages: int,
|
||||
res_id: int (client_side, optional),
|
||||
res_model: int (client_side, optional),
|
||||
address: {
|
||||
country_code: char (country name)
|
||||
}
|
||||
}],
|
||||
'color': # color on the letter,
|
||||
'duplex': # one/two side printing,
|
||||
}
|
||||
|
||||
The answer of the server is the same JSON object with some additionnal fields:
|
||||
{
|
||||
"total_cost": integer, //The cost of sending ALL the documents
|
||||
body: JSON object (same as body param except new param 'cost' in each documents)
|
||||
}
|
||||
"""
|
||||
endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
|
||||
params = self._snailmail_create('estimate')
|
||||
req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params)
|
||||
# The cost is sent in hunderth of eurocents, we change it to euros and then convert it to the company currency
|
||||
cost = int(req['total_cost'])/10000.0
|
||||
currency_eur = self.env.ref('base.EUR')
|
||||
|
||||
return currency_eur._convert(cost, self[0].company_id.currency_id, self[0].company_id, fields.Datetime.now())
|
||||
|
||||
@api.model
|
||||
def _snailmail_cron(self):
|
||||
letters_send = self.search([('state', 'in', ['pending', 'error'])])
|
||||
if letters_send:
|
||||
letters_send._snailmail_print()
|
||||
limit_date = datetime.datetime.utcnow() - datetime.timedelta(days=1)
|
||||
limit_date_str = datetime.datetime.strftime(limit_date, tools.DEFAULT_SERVER_DATETIME_FORMAT)
|
||||
letters_canceled = self.search([
|
||||
'|',
|
||||
('state', '=', 'canceled'),
|
||||
'&',
|
||||
('state' ,'=', 'draft'),
|
||||
('write_date', '<', limit_date_str),
|
||||
])
|
||||
letters_canceled.unlink()
|
||||
@@ -0,0 +1,5 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_snailmail_letter_all,snailmail.letter.all,model_snailmail_letter,,1,0,0,0
|
||||
access_snailmail_letter_portal,snailmail.letter.portal,model_snailmail_letter,base.group_portal,1,0,1,0
|
||||
access_snailmail_letter_user,snailmail.letter.user,model_snailmail_letter,base.group_user,1,1,1,0
|
||||
access_snailmail_letter_system,snailmail.letter.system,model_snailmail_letter,base.group_system,1,1,1,1
|
||||
|
@@ -1,41 +1,41 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.snailmail</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="100"/>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='multi_company']" position="after">
|
||||
<h2>Postal Printings</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="100"/>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='multi_company']" position="after">
|
||||
<h2>Postal Printings</h2>
|
||||
<div class="row mt16 o_settings_container">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="snailmail_duplex"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="snailmail_duplex"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
|
||||
<div class="text-muted">
|
||||
Choose the layout to print documents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="snailmail_duplex"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="snailmail_duplex"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/>
|
||||
<div class="text-muted">
|
||||
Choose the layout to print documents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="snailmail_color"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="snailmail_color"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
|
||||
<div class="text-muted">
|
||||
Choose the ink used to print documents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="snailmail_color"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="snailmail_color"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/>
|
||||
<div class="text-muted">
|
||||
Choose the ink used to print documents
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record model="ir.ui.view" id="snailmail_letter_list">
|
||||
<field name="name">snailmail.letter.tree</field>
|
||||
<field name="model">snailmail.letter</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree decoration-danger="state=='error'" decoration-muted="state=='sent'" string="Letters">
|
||||
<field name="attachment_id"/>
|
||||
<field name="model"/>
|
||||
<field name="res_id"/>
|
||||
<field name="company_id"/>
|
||||
<field name="state"/>
|
||||
<field name="info_msg" widget="html"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="snailmail_letter_form">
|
||||
<field name="name">snailmail.letter.form</field>
|
||||
<field name="model">snailmail.letter</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<header>
|
||||
<button name="snailmail_print" string="Send Now" type="object" states="pending,error" class="oe_highlight"/>
|
||||
<button name="cancel" string="Cancel" type="object" states="pending,error"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="pending,sent,cancel"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="attachment_id"/>
|
||||
<field name="partner_id"/>
|
||||
<field name="user_id"/>
|
||||
<field name="model"/>
|
||||
<field name="res_id"/>
|
||||
<field name="color"/>
|
||||
<field name="duplex"/>
|
||||
<field name="info_msg" widget="html" readonly="1"/>
|
||||
<field name="company_id"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="action_mail_letters">
|
||||
<field name="name">Letters</field>
|
||||
<field name="res_model">snailmail.letter</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form,tree</field>
|
||||
<field name="domain">[('state', '!=', 'draft')]</field>
|
||||
<field name="view_id" ref="snailmail_letter_list" />
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_snailmail_letters" parent="base.menu_email" action="action_mail_letters"
|
||||
sequence="20"/>
|
||||
</odoo>
|
||||
@@ -1,3 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import multi_compose_message
|
||||
@@ -1,244 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import re
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.iap import jsonrpc
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://iap-services.odoo.com'
|
||||
ESTIMATE_ENDPOINT = '/iap/snailmail/1/estimate'
|
||||
PRINT_ENDPOINT = '/iap/snailmail/1/print'
|
||||
|
||||
|
||||
class MultiComposer(models.TransientModel):
|
||||
|
||||
_name = 'multi.compose.message'
|
||||
_inherit = 'multi.compose.message'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', string='Partner')
|
||||
snailmail_color = fields.Boolean(string='Color', default=lambda self: self.env.user.company_id.snailmail_color)
|
||||
snailmail_duplex = fields.Boolean(string='Both side', default=lambda self: self.env.user.company_id.snailmail_duplex)
|
||||
snailmail_send_by_letter = fields.Boolean('Post', default=lambda self: self.env.user.company_id.snailmail_send_by_letter)
|
||||
|
||||
snailmail_cost = fields.Float(string='Credits', compute='_snailmail_estimate', store=True, readonly=True)
|
||||
currency_id = fields.Many2one('res.currency', default=lambda self: self.env.user.company_id.currency_id.id, string="Currency")
|
||||
|
||||
@api.multi
|
||||
def _update_company(self):
|
||||
super(MultiComposer,self)._update_company()
|
||||
for rec in self:
|
||||
if rec.company_id.snailmail_send_by_letter != rec.snailmail_send_by_letter:
|
||||
rec.company_id.write({
|
||||
'snailmail_send_by_letter': rec.snailmail_send_by_letter,
|
||||
})
|
||||
|
||||
@api.multi
|
||||
def _snailmail_create(self, route, batch=False):
|
||||
"""
|
||||
Create a dictionnary object to send to snailmail server.
|
||||
|
||||
:return: Dict in the form:
|
||||
{
|
||||
account_token: string, //IAP Account token of the user
|
||||
documents: [{
|
||||
pages: int,
|
||||
pdf_bin: pdf file
|
||||
res_id: int (client-side res_id),
|
||||
res_model: char (client-side res_model),
|
||||
address: {
|
||||
name: char,
|
||||
street: char,
|
||||
street2: char (OPTIONAL),
|
||||
zip: int,
|
||||
city: char,
|
||||
state: char (state code (OPTIONAL)),
|
||||
country_code: char (country code)
|
||||
}
|
||||
return_address: {
|
||||
name: char,
|
||||
street: char,
|
||||
street2: char (OPTIONAL),
|
||||
zip: int,
|
||||
city: char,
|
||||
state: char (state code (OPTIONAL)),
|
||||
country_code: char (country code)
|
||||
}
|
||||
}],
|
||||
options: {
|
||||
color: boolean (true if color, false if black-white),
|
||||
duplex: boolean (true if duplex, false otherwise),
|
||||
}
|
||||
}
|
||||
"""
|
||||
account_token = self.env['iap.account'].get('snailmail').account_token
|
||||
documents = []
|
||||
pdf = self.print_attachment_id.datas
|
||||
if pdf:
|
||||
documents.append({
|
||||
'pdf_bin': route == 'print' and pdf,
|
||||
'pages': route == 'estimate' and self._count_pages_pdf(pdf),
|
||||
'address': {
|
||||
'name': self.partner_id.name,
|
||||
'street': self.partner_id.street,
|
||||
'street2': self.partner_id.street2,
|
||||
'zip': self.partner_id.zip,
|
||||
'state': self.partner_id.state_id.code if self.partner_id.state_id else False,
|
||||
'city': self.partner_id.city,
|
||||
'country_code': self.partner_id.country_id.code
|
||||
},
|
||||
'return_address': {
|
||||
'name': self.company_id.partner_id.name,
|
||||
'street': self.company_id.partner_id.street,
|
||||
'street2': self.company_id.partner_id.street2,
|
||||
'zip': self.company_id.partner_id.zip,
|
||||
'state': self.company_id.partner_id.state_id.code if self.company_id.partner_id.state_id else False,
|
||||
'city': self.company_id.partner_id.city,
|
||||
'country_code': self.company_id.partner_id.country_id.code,
|
||||
}
|
||||
})
|
||||
return {
|
||||
'account_token': account_token,
|
||||
'documents': documents,
|
||||
'options': {
|
||||
'color': self.snailmail_color,
|
||||
'duplex': self.snailmail_duplex,
|
||||
},
|
||||
'batch': batch,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def action_snailmail_print(self):
|
||||
"""
|
||||
get response
|
||||
{
|
||||
'request_code': RESPONSE_OK, # 200 if good or fail
|
||||
'total_cost': total_cost,
|
||||
'credit_error': credit_error,
|
||||
'request': {
|
||||
'documents': documents,
|
||||
'options': options
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
self.ensure_one()
|
||||
endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
|
||||
params = self._snailmail_create('print', batch=True)
|
||||
response = jsonrpc(endpoint + PRINT_ENDPOINT, params=params)
|
||||
|
||||
error = False
|
||||
doc = response['request']['documents'][0]
|
||||
record = self.env[self.model].browse(self.res_id)
|
||||
if doc.get('sent') and response['request_code'] == 200:
|
||||
if hasattr(record, '_message_log'):
|
||||
message = _('The document was correctly sent by post.<br>The tracking id is %s' % doc['send_id'])
|
||||
record._message_log(body=message)
|
||||
else:
|
||||
activity_data = {
|
||||
'res_id': self.res_id,
|
||||
'res_model_id': self.env['ir.model']._get(self.model).id,
|
||||
'activity_type_id': self.env.ref('snailmail.mail_activity_data_snailmail').id,
|
||||
'summary': _('Post letter: an error occured.'),
|
||||
'note': _('An error occured when sending the document by post.<br>Error: %s' % \
|
||||
self._get_error_message(doc['error'] if response['request_code'] == 200 else response['reason'])),
|
||||
'user_id': self.env.user.id,
|
||||
}
|
||||
self.env['mail.activity'].create(activity_data)
|
||||
error = True
|
||||
if not error:
|
||||
notification = _('Post letter: document sent.')
|
||||
else:
|
||||
notification = _('Post letter: an error occured. Please check the document for more information.')
|
||||
# as we interact with the exterior world, we commit
|
||||
self.env.cr.commit()
|
||||
self._update_company()
|
||||
return {
|
||||
'error': error,
|
||||
'message': notification,
|
||||
}
|
||||
|
||||
@api.multi
|
||||
@api.depends('print_attachment_id')
|
||||
@api.onchange('snailmail_send_by_letter')
|
||||
def _snailmail_estimate(self):
|
||||
"""
|
||||
Send a request to estimate the cost of sending all the documents with
|
||||
the differents options.
|
||||
|
||||
The JSON object sent is the one generated from self._snailmail_create()
|
||||
|
||||
arguments sent:
|
||||
{
|
||||
"documents":[{
|
||||
pages: int,
|
||||
res_id: int (client_side, optional),
|
||||
res_model: int (client_side, optional),
|
||||
address: {
|
||||
country_code: char (country name)
|
||||
}
|
||||
}],
|
||||
'color': # color on the letter,
|
||||
'duplex': # one/two side printing,
|
||||
}
|
||||
|
||||
The answer of the server is the same JSON object with some additionnal fields:
|
||||
{
|
||||
"total_cost": integer, //The cost of sending ALL the documents
|
||||
body: JSON object (same as body param except new param 'cost' in each documents)
|
||||
}
|
||||
"""
|
||||
for composer in self:
|
||||
if not composer.partner_id:
|
||||
composer.partner_id = self.env[self.model].browse(self.res_id).partner_id
|
||||
|
||||
if composer.snailmail_send_by_letter and composer.print_attachment_id :# and not composer.is_estimated:
|
||||
endpoint = self.env['ir.config_parameter'].sudo().get_param('snailmail.endpoint', DEFAULT_ENDPOINT)
|
||||
|
||||
params = composer._snailmail_create('estimate')
|
||||
req = jsonrpc(endpoint + '/iap/snailmail/1/estimate', params=params)
|
||||
composer.is_estimated = True
|
||||
|
||||
# The cost is sent in eurocents, we change it to euros and then convert it to the company currency
|
||||
cost = int(req['total_cost'])/100.0
|
||||
currency_eur = self.env.ref('base.EUR')
|
||||
|
||||
composer.snailmail_cost = currency_eur._convert(cost, self.currency_id, self.company_id, fields.Datetime.now())
|
||||
|
||||
# all the print stuff will go to the cron onsnailail print orders
|
||||
|
||||
def _count_pages_pdf(self, bin_pdf):
|
||||
""" Count the number of pages of the given pdf file.
|
||||
:param bin_pdf : binary content of the pdf file
|
||||
"""
|
||||
pages = 0
|
||||
for match in re.compile(b"/Count\s+(\d+)").finditer(bin_pdf):
|
||||
pages = int(match.group(1))
|
||||
return pages
|
||||
|
||||
def _get_error_message(self, error):
|
||||
if error == 'CREDIT_ERROR':
|
||||
link = self.env['iap.account'].get_credits_url(service_name='snailmail')
|
||||
return _('You don\'t have enough credits to perform this operation.<br>Please go to your <a href=%s target="new">iap account</a>.' % link)
|
||||
if error == 'NO_PRICE_AVAILABLE':
|
||||
return _('The country of the partner is not covered by Snailmail.')
|
||||
if error == 'MISSING_REQUIRED_FIELDS':
|
||||
return _('One or more required fields are empty.')
|
||||
if error == 'SOMETHING_IS_WRONG':
|
||||
return _('An unknown error happened. Please contact the support.')
|
||||
return error
|
||||
|
||||
|
||||
@api.multi
|
||||
def send_and_print_action(self):
|
||||
res = super(MultiComposer,self).send_and_print_action()
|
||||
if self.snailmail_send_by_letter:
|
||||
self.action_snailmail_print()
|
||||
return res
|
||||
|
||||
@api.multi
|
||||
def send_action(self):
|
||||
res = super(MultiComposer,self).send_action()
|
||||
for wizard in self:
|
||||
if wizard.snailmail_send_by_letter:
|
||||
wizard.action_snailmail_print()
|
||||
return res
|
||||
@@ -1,39 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="multi_compose_message_inherit_account_wizard_form">
|
||||
<field name="name">multi.compose.message.form.inherited.snailmail</field>
|
||||
<field name="model">multi.compose.message</field>
|
||||
<field name="inherit_id" ref="account.multi_compose_message_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='option_email']" position='before'>
|
||||
<div name="option_letter" class="col-md-12 mb16">
|
||||
<div name="option" class="float-left">
|
||||
<field name="snailmail_send_by_letter" widget="boolean_toggle"/>
|
||||
<b><label for="snailmail_send_by_letter"/></b>
|
||||
</div>
|
||||
<div class="float-right" name='cost' attrs="{'invisible': [('snailmail_send_by_letter','=',False)]}">
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<b><field name="snailmail_cost" widget='monetary' options="{'currency_field': 'currency_id'}"/></b>
|
||||
</div>
|
||||
</div>
|
||||
<div name="address_form" attrs="{'invisible': [('snailmail_send_by_letter', '=', False)]}">
|
||||
<group>
|
||||
<field string="Delivery Address" name="partner_id" readonly="1" force_save="1" context="{'show_address': 1}" options="{'always_reload': True, 'no_quick_create': True}" />
|
||||
</group>
|
||||
</div>
|
||||
<div class="col-md-12 mb16" style="border-bottom: 1px solid #e5e5e5;"></div>
|
||||
</xpath>
|
||||
<xpath expr="//footer/button[@name='send_and_print_action']" position='attributes'>
|
||||
<attribute name="attrs">{'invisible': ['|', '|', ('is_log', '=', True), ('printed', '=', True), '|', ('is_print', '=', False), '&', '&', ('is_print', '=', True), ('snailmail_send_by_letter', '=', False), ('is_email', '=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//footer/button[@name='send_action']" position='attributes'>
|
||||
<attribute name="attrs">{'invisible': ['|' , ('is_log', '=', True), '|', ('is_print', '=', True), '&', '&', ('is_print', '=', False), ('snailmail_send_by_letter', '=', False), ('is_email', '=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//footer/button[@name='print_action']" position='attributes'>
|
||||
<attribute name="attrs">{'invisible': ['|', '|', '|', '|', ('printed', '=', True), ('is_log', '=', True), ('is_print', '=', False), ('snailmail_send_by_letter', '=', True), ('is_email', '=', True)]}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -0,0 +1,16 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': "snailmail_account",
|
||||
'description': """
|
||||
Allows users to send invoices by post
|
||||
=====================================================
|
||||
""",
|
||||
'category': 'Tools',
|
||||
'version': '0.1',
|
||||
'depends': ['account', 'snailmail'],
|
||||
'data': [
|
||||
'views/res_config_settings_views.xml',
|
||||
'wizard/account_invoice_send_views.xml',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import res_company
|
||||
from . import res_config_settings
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
invoice_is_snailmail = fields.Boolean(string='Send by Letter by default', default=True)
|
||||
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
invoice_is_snailmail = fields.Boolean(string='Send by Post', related='company_id.invoice_is_snailmail')
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.snailmail.account</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="100"/>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@id='send_default']" position="inside">
|
||||
<div class="col-12 col-lg-6 o_setting_box">
|
||||
<div class="o_setting_left_pane">
|
||||
<field name="invoice_is_snailmail"/>
|
||||
</div>
|
||||
<div class="o_setting_right_pane">
|
||||
<label for="invoice_is_snailmail"/>
|
||||
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific."/>
|
||||
<div class="content-group" attrs="{'invisible': [('invoice_is_snailmail' ,'=', False)]}">
|
||||
<div class="mt16">
|
||||
<field name="snailmail_duplex" class="oe_inline"/>
|
||||
<label string="Print on Both Sides" class="o_light_label" for="snailmail_duplex"/>
|
||||
</div>
|
||||
<div>
|
||||
<field name="snailmail_color" class="oe_inline"/>
|
||||
<label string="Print In Color" class="o_light_label" for="snailmail_color"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import account_invoice_send
|
||||
@@ -0,0 +1,76 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class AccountInvoiceSend(models.TransientModel):
|
||||
_name = 'account.invoice.send'
|
||||
_inherit = 'account.invoice.send'
|
||||
|
||||
partner_id = fields.Many2one('res.partner', compute='_get_partner', string='Partner')
|
||||
snailmail_is_letter = fields.Boolean('Send by Post', help='Allows to send the document by snail mail (coventional posting delivery service)', default=lambda self: self.env.user.company_id.snailmail_is_letter)
|
||||
snailmail_cost = fields.Float(string='Credits', compute='_snailmail_estimate', store=True, readonly=True)
|
||||
currency_id = fields.Many2one('res.currency', compute='_get_invoice_currency', string="Currency")
|
||||
letter_ids = fields.Many2many('snailmail.letter', 'snailmail_letter_account_invoice_send_rel', ondelete='cascade')
|
||||
|
||||
@api.multi
|
||||
@api.onchange('invoice_ids')
|
||||
def _get_invoice_currency(self):
|
||||
for wizard in self:
|
||||
if wizard.invoice_ids:
|
||||
wizard.currency_id = wizard.invoice_ids.mapped('currency_id')[0].id
|
||||
|
||||
@api.multi
|
||||
@api.onchange('invoice_ids')
|
||||
def _get_partner(self):
|
||||
for wizard in self:
|
||||
if wizard.invoice_ids and len(wizard.invoice_ids) == 1:
|
||||
wizard.partner_id = wizard.invoice_ids.partner_id.id
|
||||
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
for agc in self:
|
||||
agc.letter_ids.filtered(lambda l: l.state == 'draft').unlink()
|
||||
return super(AccountInvoiceSend, self).unlink()
|
||||
|
||||
@api.multi
|
||||
def _fetch_letters(self):
|
||||
self.ensure_one()
|
||||
if not self.letter_ids:
|
||||
|
||||
letters = self.env['snailmail.letter']
|
||||
for invoice in self.invoice_ids:
|
||||
letter = letters.create({
|
||||
'partner_id': invoice.partner_id.id,
|
||||
'model': 'account.invoice',
|
||||
'res_id': invoice.id,
|
||||
'user_id': self.env.user.id,
|
||||
'company_id': invoice.company_id.id,
|
||||
'report_template': self.env.ref('account.account_invoices').id
|
||||
})
|
||||
letters |= letter
|
||||
self.letter_ids = [(4, l.id) for l in letters]
|
||||
return self.letter_ids
|
||||
|
||||
@api.onchange('snailmail_is_letter')
|
||||
def _snailmail_estimate(self):
|
||||
for wizard in self:
|
||||
if wizard.snailmail_is_letter:
|
||||
letters = wizard._fetch_letters()
|
||||
wizard.snailmail_cost = letters._snailmail_estimate()
|
||||
|
||||
@api.multi
|
||||
def snailmail_print_action(self):
|
||||
for wizard in self:
|
||||
letters = wizard._fetch_letters()
|
||||
letters.write({'state': 'pending'})
|
||||
if len(letters) == 1:
|
||||
letters._snailmail_print()
|
||||
|
||||
@api.multi
|
||||
def send_and_print_action(self):
|
||||
res = super(AccountInvoiceSend, self).send_and_print_action()
|
||||
if self.snailmail_is_letter:
|
||||
self.snailmail_print_action()
|
||||
return res
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="account_invoice_send_inherit_account_wizard_form">
|
||||
<field name="name">account.invoice.send.form.inherited.snailmail</field>
|
||||
<field name="model">account.invoice.send</field>
|
||||
<field name="inherit_id" ref="account.account_invoice_send_wizard_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//div[@name='option_email']" position='before'>
|
||||
<div name="option_letter">
|
||||
<field name="letter_ids" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<div name="option" class="text-left d-inline-block">
|
||||
<field name="snailmail_is_letter" />
|
||||
<b><label for="snailmail_is_letter"/></b>
|
||||
</div>
|
||||
<div class="text-left d-inline-block mr8" name='cost' attrs="{'invisible': [('snailmail_is_letter','=', False)]}">
|
||||
<b>(
|
||||
<field name="snailmail_cost" class="mr4" widget='monetary' options="{'currency_field': 'currency_id'}"/>
|
||||
<i class="fa fa-info-circle" role="img" aria-label="Warning" title="
|
||||
The letter will be sent using the IAP service from Odoo. Make sure you have enough credits on your account or proceed to a recharge. In case of multiple sending, the price is estimated based on 1 page per document.
|
||||
"/>
|
||||
)</b>
|
||||
</div>
|
||||
<div class="text-right text-muted d-inline-block" name="address" attrs="{'invisible': ['|', ('composition_mode', '=', 'mass_mail'), ('is_print','=', False)]}">
|
||||
<span> to: </span>
|
||||
<field name="partner_id" readonly="1" force_save="1" context="{'show_address': 1, 'address_inline': 1}" options="{'always_reload': True, 'no_quick_create': True}"/>
|
||||
</div>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//footer/button[hasclass('send_and_print')]" position='attributes'>
|
||||
<attribute name="attrs">{'invisible': ['|', ('printed', '=', True), '|', ('is_print', '=', False), '&', '&', ('is_print', '=', True), ('snailmail_is_letter', '=', False), ('is_email', '=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//footer/button[hasclass('send')]" position='attributes'>
|
||||
<attribute name="attrs">{'invisible': ['|', ('is_print', '=', True), '&', '&', ('is_print', '=', False), ('snailmail_is_letter', '=', False), ('is_email', '=', False)]}</attribute>
|
||||
</xpath>
|
||||
<xpath expr="//footer/button[hasclass('print')]" position='attributes'>
|
||||
<attribute name="attrs">{'invisible': ['|', '|', '|', ('printed', '=', True), ('is_print', '=', False), ('snailmail_is_letter', '=', True), ('is_email', '=', True)]}</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user