diff options
| author | Rémi Rahir <rar@odoo.com> | 2018-08-03 12:21:28 +0200 |
|---|---|---|
| committer | Rémi Rahir <rar@odoo.com> | 2018-08-10 18:41:00 +0200 |
| commit | 53cdd68e7cc9e1494a9a99dcd2fff0cc835eab50 (patch) | |
| tree | 34b9c7837603a3e929b52271381a3a6e5bd17018 /addons | |
| parent | a07902ffa81443d242167bd8291f1902b43beb2e (diff) | |
[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
Diffstat (limited to 'addons')
35 files changed, 822 insertions, 510 deletions
diff --git a/addons/account/__manifest__.py b/addons/account/__manifest__.py index c99baf6f130..e79c388f6f3 100644 --- a/addons/account/__manifest__.py +++ b/addons/account/__manifest__.py @@ -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', diff --git a/addons/account/models/__init__.py b/addons/account/models/__init__.py index 144bc6bc2ba..50b2b4adcf0 100644 --- a/addons/account/models/__init__.py +++ b/addons/account/models/__init__.py @@ -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 diff --git a/addons/account/models/account_invoice.py b/addons/account/models/account_invoice.py index 785159896fd..56f1e9c910e 100644 --- a/addons/account/models/account_invoice.py +++ b/addons/account/models/account_invoice.py @@ -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', diff --git a/addons/account/models/company.py b/addons/account/models/company.py index 9929b942aa9..c9a898d626f 100644 --- a/addons/account/models/company.py +++ b/addons/account/models/company.py @@ -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.") diff --git a/addons/account/models/res_company.py b/addons/account/models/res_company.py deleted file mode 100644 index 522bd956fc3..00000000000 --- a/addons/account/models/res_company.py +++ /dev/null @@ -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) diff --git a/addons/account/models/res_config_settings.py b/addons/account/models/res_config_settings.py index 739507e5dc1..438393ca020 100644 --- a/addons/account/models/res_config_settings.py +++ b/addons/account/models/res_config_settings.py @@ -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): diff --git a/addons/account/views/res_config_settings_views.xml b/addons/account/views/res_config_settings_views.xml index daf25279f83..6158e088d3f 100644 --- a/addons/account/views/res_config_settings_views.xml +++ b/addons/account/views/res_config_settings_views.xml @@ -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"> diff --git a/addons/account/wizard/__init__.py b/addons/account/wizard/__init__.py index b7374520988..d8440f9e57c 100644 --- a/addons/account/wizard/__init__.py +++ b/addons/account/wizard/__init__.py @@ -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 diff --git a/addons/account/wizard/account_invoice_send.py b/addons/account/wizard/account_invoice_send.py new file mode 100644 index 00000000000..2bb3c6b8750 --- /dev/null +++ b/addons/account/wizard/account_invoice_send.py @@ -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'} diff --git a/addons/account/wizard/account_invoice_send_views.xml b/addons/account/wizard/account_invoice_send_views.xml new file mode 100644 index 00000000000..c48e7b4dc02 --- /dev/null +++ b/addons/account/wizard/account_invoice_send_views.xml @@ -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> diff --git a/addons/account/wizard/multi_compose_message.py b/addons/account/wizard/multi_compose_message.py deleted file mode 100644 index cdbae7610f2..00000000000 --- a/addons/account/wizard/multi_compose_message.py +++ /dev/null @@ -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() diff --git a/addons/account/wizard/multi_compose_message_views.xml b/addons/account/wizard/multi_compose_message_views.xml deleted file mode 100644 index bed5b6939e8..00000000000 --- a/addons/account/wizard/multi_compose_message_views.xml +++ /dev/null @@ -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> diff --git a/addons/snailmail/__init__.py b/addons/snailmail/__init__.py index 35e7c9600c5..cde864bae21 100644 --- a/addons/snailmail/__init__.py +++ b/addons/snailmail/__init__.py @@ -1,4 +1,3 @@ # -*- coding: utf-8 -*- from . import models -from . import wizard diff --git a/addons/snailmail/__manifest__.py b/addons/snailmail/__manifest__.py index 2a83ff96e5e..eb40e985b9c 100644 --- a/addons/snailmail/__manifest__.py +++ b/addons/snailmail/__manifest__.py @@ -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, } diff --git a/addons/snailmail/data/mail_activity_data.xml b/addons/snailmail/data/mail_activity_data.xml deleted file mode 100644 index 191fb9bc03a..00000000000 --- a/addons/snailmail/data/mail_activity_data.xml +++ /dev/null @@ -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> diff --git a/addons/snailmail/data/snailmail_data.xml b/addons/snailmail/data/snailmail_data.xml new file mode 100644 index 00000000000..354c30aaf53 --- /dev/null +++ b/addons/snailmail/data/snailmail_data.xml @@ -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> diff --git a/addons/snailmail/models/__init__.py b/addons/snailmail/models/__init__.py index 883205a7200..ee0d4f4704c 100644 --- a/addons/snailmail/models/__init__.py +++ b/addons/snailmail/models/__init__.py @@ -2,3 +2,4 @@ from . import res_company from . import res_config_settings +from . import snailmail_letter diff --git a/addons/snailmail/models/res_company.py b/addons/snailmail/models/res_company.py index c4875d754ac..b51147b2ef1 100644 --- a/addons/snailmail/models/res_company.py +++ b/addons/snailmail/models/res_company.py @@ -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) diff --git a/addons/snailmail/models/res_config_settings.py b/addons/snailmail/models/res_config_settings.py index 52468b7a0f5..7b916ce0e1f 100644 --- a/addons/snailmail/models/res_config_settings.py +++ b/addons/snailmail/models/res_config_settings.py @@ -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') diff --git a/addons/snailmail/models/snailmail_letter.py b/addons/snailmail/models/snailmail_letter.py new file mode 100644 index 00000000000..5b09681bead --- /dev/null +++ b/addons/snailmail/models/snailmail_letter.py @@ -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() diff --git a/addons/snailmail/security/ir.model.access.csv b/addons/snailmail/security/ir.model.access.csv new file mode 100644 index 00000000000..3c111854063 --- /dev/null +++ b/addons/snailmail/security/ir.model.access.csv @@ -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
\ No newline at end of file diff --git a/addons/snailmail/views/res_config_settings_views.xml b/addons/snailmail/views/res_config_settings_views.xml index cd5577c8cde..1e8b864d928 100644 --- a/addons/snailmail/views/res_config_settings_views.xml +++ b/addons/snailmail/views/res_config_settings_views.xml @@ -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> diff --git a/addons/snailmail/views/snailmail_views.xml b/addons/snailmail/views/snailmail_views.xml new file mode 100644 index 00000000000..7a230c2f706 --- /dev/null +++ b/addons/snailmail/views/snailmail_views.xml @@ -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> diff --git a/addons/snailmail/wizard/__init__.py b/addons/snailmail/wizard/__init__.py deleted file mode 100644 index eebbf14ea53..00000000000 --- a/addons/snailmail/wizard/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# -*- coding: utf-8 -*- - -from . import multi_compose_message diff --git a/addons/snailmail/wizard/multi_compose_message.py b/addons/snailmail/wizard/multi_compose_message.py deleted file mode 100644 index 14df5467e7e..00000000000 --- a/addons/snailmail/wizard/multi_compose_message.py +++ /dev/null @@ -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 diff --git a/addons/snailmail/wizard/multi_compose_message_views.xml b/addons/snailmail/wizard/multi_compose_message_views.xml deleted file mode 100644 index 47d595e0685..00000000000 --- a/addons/snailmail/wizard/multi_compose_message_views.xml +++ /dev/null @@ -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> diff --git a/addons/snailmail_account/__init__.py b/addons/snailmail_account/__init__.py new file mode 100644 index 00000000000..35e7c9600c5 --- /dev/null +++ b/addons/snailmail_account/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import models +from . import wizard diff --git a/addons/snailmail_account/__manifest__.py b/addons/snailmail_account/__manifest__.py new file mode 100644 index 00000000000..5f71dd9eacf --- /dev/null +++ b/addons/snailmail_account/__manifest__.py @@ -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, +} diff --git a/addons/snailmail_account/models/__init__.py b/addons/snailmail_account/models/__init__.py new file mode 100644 index 00000000000..883205a7200 --- /dev/null +++ b/addons/snailmail_account/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import res_company +from . import res_config_settings diff --git a/addons/snailmail_account/models/res_company.py b/addons/snailmail_account/models/res_company.py new file mode 100644 index 00000000000..1ac0da4e0fb --- /dev/null +++ b/addons/snailmail_account/models/res_company.py @@ -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) diff --git a/addons/snailmail_account/models/res_config_settings.py b/addons/snailmail_account/models/res_config_settings.py new file mode 100644 index 00000000000..06a1eb98373 --- /dev/null +++ b/addons/snailmail_account/models/res_config_settings.py @@ -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') diff --git a/addons/snailmail_account/views/res_config_settings_views.xml b/addons/snailmail_account/views/res_config_settings_views.xml new file mode 100644 index 00000000000..13df8ce7e2e --- /dev/null +++ b/addons/snailmail_account/views/res_config_settings_views.xml @@ -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> diff --git a/addons/snailmail_account/wizard/__init__.py b/addons/snailmail_account/wizard/__init__.py new file mode 100644 index 00000000000..1a67eb15ca1 --- /dev/null +++ b/addons/snailmail_account/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import account_invoice_send diff --git a/addons/snailmail_account/wizard/account_invoice_send.py b/addons/snailmail_account/wizard/account_invoice_send.py new file mode 100644 index 00000000000..e6c08e1e535 --- /dev/null +++ b/addons/snailmail_account/wizard/account_invoice_send.py @@ -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 diff --git a/addons/snailmail_account/wizard/account_invoice_send_views.xml b/addons/snailmail_account/wizard/account_invoice_send_views.xml new file mode 100644 index 00000000000..275f8ea202f --- /dev/null +++ b/addons/snailmail_account/wizard/account_invoice_send_views.xml @@ -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> |
