[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:
Rémi Rahir
2018-08-10 18:41:00 +02:00
parent a07902ffa8
commit 53cdd68e7c
35 changed files with 822 additions and 510 deletions
+1 -1
View File
@@ -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',
-1
View File
@@ -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
+4 -5
View File
@@ -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',
+3
View File
@@ -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.")
-11
View File
@@ -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">
+1 -1
View File
@@ -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 &amp; 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 &amp; 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 &amp; 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 &amp; 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
View File
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard
+6 -4
View File
@@ -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>
+21
View File
@@ -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>
+1
View File
@@ -2,3 +2,4 @@
from . import res_company
from . import res_config_settings
from . import snailmail_letter
+1 -4
View File
@@ -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')
+302
View File
@@ -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 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_snailmail_letter_all snailmail.letter.all model_snailmail_letter 1 0 0 0
3 access_snailmail_letter_portal snailmail.letter.portal model_snailmail_letter base.group_portal 1 0 1 0
4 access_snailmail_letter_user snailmail.letter.user model_snailmail_letter base.group_user 1 1 1 0
5 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>
-3
View File
@@ -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), '&amp;', '&amp;', ('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), '&amp;', '&amp;', ('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>
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
from . import models
from . import wizard
+16
View File
@@ -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.&#10;Make sure you have enough credits on your account or proceed to a recharge.&#10;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), '&amp;', '&amp;', ('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), '&amp;', '&amp;', ('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>