[MERGE] Review/Update EDI features for 7.0:

Some EDI features replaced by the improved 7.0 portal (with auth_signup)
------------------------------------------------------------------------
- remove the static export of EDI document and the persistence of these EDI blobs, replaced by the live portal access to the document (via portal module)
- remove all the qweb templates and web controllers that were previously used to give access to the EDI document preview
- preserve the web controllers/URLs that were use for the direct EDI import (so that a document from a 6.1 system can still be imported in a 7.0 system)
- temporarily remove the EDI export UI, replaced for the time being by the portal view. => It is planned in 7.1 to provide a new EDI export system via email notification and attaching JSON EDI blobs to outgoing emails. Thanks to the mail gateway integration and a whitelist/certificate system it will be possible to setup end-to-end B2B EDI automation.
- EDI email notifications now integrated in Sales/Invoicing flows via an explicit "Send by Email" step
- PDF version of the document now sent along with the notification (instead of a simple link to view it)
- Portal signup link integrated in this notification when the portal module is installed (via glue module sale_portal)

Online Payment (Paypal) features preserved in a different form
--------------------------------------------------------------
- as the EDI preview was removed, the payment button was moved to a new banner in the portal view of relevant documents, as follows:
- addition of a new portal.payment_acquirer model to define online payment gateways such as Paypal, Google Checkout, etc. (Paypal included by default)
- new payment banner added to the portal form view of Sale Orders and Customer Invoices, allowing online payment via defined payment_acquirers
- new settings item in Settings>Invoicing>Banks&Cash to make the payment banner visible to all employees as well (normally it's only visible in the portal)

+ minor cleanup and removal of deprecated fields

bzr revid: odo@openerp.com-20121120170915-cmb77iubyrwmib2l
This commit is contained in:
Olivier Dony 2012-11-20 18:09:15 +01:00
commit 437833ba43
56 changed files with 1425 additions and 1468 deletions

View File

@ -391,29 +391,33 @@ class account_invoice(osv.osv):
'''
This function opens a window to compose an email, with the edi invoice template message loaded by default
'''
mod_obj = self.pool.get('ir.model.data')
template = mod_obj.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')
template_id = template and template[1] or False
res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
res_id = res and res[1] or False
assert len(ids) == 1, 'This option should only be used for a single id at a time.'
ir_model_data = self.pool.get('ir.model.data')
try:
template_id = ir_model_data.get_object_reference(cr, uid, 'account', 'email_template_edi_invoice')[1]
except ValueError:
template_id = False
try:
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError:
compose_form_id = False
ctx = dict(context)
ctx.update({
'default_model': 'account.invoice',
'default_res_id': ids[0],
'default_use_template': True,
'default_use_template': bool(template_id),
'default_template_id': template_id,
'default_composition_mode': 'comment',
})
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(res_id, 'form')],
'view_id': res_id,
'type': 'ir.actions.act_window',
'views': [(compose_form_id, 'form')],
'view_id': compose_form_id,
'target': 'new',
'context': ctx,
'nodestroy': True,
}
def confirm_paid(self, cr, uid, ids, context=None):

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2011 OpenERP S.A. <http://openerp.com>
# Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,9 +19,8 @@
#
##############################################################################
from osv import fields, osv, orm
from openerp.osv import osv
from edi import EDIMixin
from edi.models import edi
INVOICE_LINE_EDI_STRUCT = {
'name': True,
@ -71,16 +70,6 @@ INVOICE_EDI_STRUCT = {
class account_invoice(osv.osv, EDIMixin):
_inherit = 'account.invoice'
def action_invoice_sent(self, cr, uid, ids, context=None):
""""Override this method to add a link to mail"""
if context is None:
context = {}
invoice_objs = self.browse(cr, uid, ids, context=context)
edi_token = self.pool.get('edi.document').export_edi(cr, uid, invoice_objs, context = context)[0]
web_root_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
ctx = dict(context, edi_web_url_view=edi.EDI_VIEW_WEB_URL % (web_root_url, cr.dbname, edi_token))
return super(account_invoice, self).action_invoice_sent(cr, uid, ids, context=ctx)
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
"""Exports a supplier or customer invoice"""
edi_struct = dict(edi_struct or INVOICE_EDI_STRUCT)
@ -111,8 +100,8 @@ class account_invoice(osv.osv, EDIMixin):
return tax_account
def _edi_invoice_account(self, cr, uid, partner_id, invoice_type, context=None):
partner_pool = self.pool.get('res.partner')
partner = partner_pool.browse(cr, uid, partner_id, context=context)
res_partner = self.pool.get('res.partner')
partner = res_partner.browse(cr, uid, partner_id, context=context)
if invoice_type in ('out_invoice', 'out_refund'):
invoice_account = partner.property_account_receivable
else:
@ -136,31 +125,30 @@ class account_invoice(osv.osv, EDIMixin):
self._edi_requires_attributes(('company_id','company_address','type'), edi_document)
res_partner = self.pool.get('res.partner')
src_company_id, src_company_name = edi_document.pop('company_id')
xid, company_name = edi_document.pop('company_id')
# Retrofit address info into a unified partner info (changed in v7 - used to keep them separate)
company_address_edi = edi_document.pop('company_address')
company_address_edi['name'] = company_name
company_address_edi['is_company'] = True
company_address_edi['__import_model'] = 'res.partner'
company_address_edi['__id'] = xid # override address ID, as of v7 they should be the same anyway
if company_address_edi.get('logo'):
company_address_edi['image'] = company_address_edi.pop('logo')
invoice_type = edi_document['type']
partner_value = {}
if invoice_type in ('out_invoice', 'out_refund'):
partner_value.update({'customer': True})
if invoice_type in ('in_invoice', 'in_refund'):
partner_value.update({'supplier': True})
# imported company_address = new partner address
address_info = edi_document.pop('company_address')
if 'name' not in address_info:
address_info['name'] = src_company_name
address_info['type'] = 'invoice'
address_info.update(partner_value)
address_id = res_partner.edi_import(cr, uid, address_info, context=context)
if invoice_type.startswith('out_'):
company_address_edi['customer'] = True
else:
company_address_edi['supplier'] = True
partner_id = res_partner.edi_import(cr, uid, company_address_edi, context=context)
# modify edi_document to refer to new partner
partner_address = res_partner.browse(cr, uid, address_id, context=context)
address_edi_m2o = self.edi_m2o(cr, uid, partner_address, context=context)
edi_document['partner_id'] = address_edi_m2o
edi_document.pop('partner_address', False) # ignored
return address_id
partner = res_partner.browse(cr, uid, partner_id, context=context)
partner_edi_m2o = self.edi_m2o(cr, uid, partner, context=context)
edi_document['partner_id'] = partner_edi_m2o
edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
return partner_id
def edi_import(self, cr, uid, edi_document, context=None):
""" During import, invoices will import the company that is provided in the invoice as
@ -200,7 +188,7 @@ class account_invoice(osv.osv, EDIMixin):
invoice_type = invoice_type.startswith('in_') and invoice_type.replace('in_','out_') or invoice_type.replace('out_','in_')
edi_document['type'] = invoice_type
#import company as a new partner
# import company as a new partner
partner_id = self._edi_import_company(cr, uid, edi_document, context=context)
# Set Account

View File

@ -1,17 +1,6 @@
<?xml version="1.0" ?>
<openerp>
<data>
<!-- EDI Export + Send email Action -->
<record id="ir_actions_server_edi_invoice" model="ir.actions.server">
<field name="code">if (object.type in ('out_invoice', 'out_refund')) and not object.partner_id.opt_out: object.edi_export_and_email(template_ext_id='account.email_template_edi_invoice', context=context)</field>
<field eval="6" name="sequence"/>
<field name="state">code</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="condition">True</field>
<field name="name">Auto-email confirmed invoices</field>
</record>
<!-- EDI related Email Templates menu -->
<record model="ir.actions.act_window" id="action_email_templates">
<field name="name">Email Templates</field>
@ -27,22 +16,19 @@
</data>
<!-- Mail template and workflow bindings are done in a NOUPDATE block
<!-- Mail template are declared in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<!-- bind the mailing server action to invoice open activity -->
<record id="account.act_open" model="workflow.activity">
<field name="action_id" ref="ir_actions_server_edi_invoice"/>
</record>
<!--Email template -->
<record id="email_template_edi_invoice" model="email.template">
<field name="name">Automated Invoice Notification Mail</field>
<field name="name">Invoice - Send by Email</field>
<field name="email_from">${object.user_id.email or object.company_id.email or 'noreply@localhost'}</field>
<field name="subject">${object.company_id.name} Invoice (Ref ${object.number or 'n/a' })</field>
<field name="email_recipients">${object.partner_id.id}</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="auto_delete" eval="True"/>
<field name="report_template" ref="account_invoices"/>
<field name="report_name">Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}</field>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
@ -58,15 +44,11 @@
% if object.origin:
&nbsp;&nbsp;Order reference: ${object.origin}<br />
% endif
% if object.user_id:
&nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Invoice%20${object.number}">${object.user_id.name}</a>
% endif
</p>
<p>
You can view the invoice document, download it and pay online using the following link:
</p>
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #FFF; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="${ctx.get('edi_web_url_view') or ''}">View Invoice</a>
% if object.company_id.paypal_account and object.type in ('out_invoice', 'in_refund'):
<%
comp_name = quote(object.company_id.name)

View File

@ -227,7 +227,7 @@
</div>
</group>
<separator string="Bank &amp; Cash"/>
<group>
<group name="bank_cash">
<label for="id" string="Configuration"/>
<div>
<div>

View File

@ -38,45 +38,46 @@
-
Then I export the customer invoice
-
!python {model: edi.document}: |
!python {model: edi.edi}: |
import json
invoice_pool = self.pool.get('account.invoice')
invoice = invoice_pool.browse(cr, uid, ref("invoice_edi_1"))
token = self.export_edi(cr, uid, [invoice])
assert token, 'Invalid EDI Token'
edi_doc = self.generate_edi(cr, uid, [invoice])
assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict'
-
Then I import a sample EDI document of another customer invoice
Then I import a sample EDI document of another customer invoice from OpenERP 7.0
-
!python {model: account.invoice}: |
import time
edi_document = {
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.random_invoice_763jsms",
"__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.random_invoice_763jsms",
"__module": "account",
"__model": "account.invoice",
"__version": [6,1,0],
"internal_number": time.strftime("SAJ/%Y/002"),
"__version": [7,0,0],
"internal_number": time.strftime("SAJ/%Y/070"),
"company_address": {
"__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.main_address",
"__id": "base:b33adf8a-decd-11f0-a4de-702a04e25700.main_address",
"__module": "base",
"__model": "res.partner",
"city": "Gerompont",
"name": "Company main address",
"zip": "1367",
"country_id": ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.be", "Belgium"],
"country_id": ["base:b33adf8a-decd-11f0-a4de-702a04e25700.be", "Belgium"],
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"bank_ids": [
["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_bank-ZrTWzesfsdDJzGbp","Sample bank: 123465789-156113"]
["base:b33adf8a-decd-11f0-a4de-702a04e25700.res_partner_bank-ZrTWzesfsdDJzGbp","Sample bank: 70-123465789-156113"]
],
},
"company_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_company_test11", "Thomson pvt. ltd."],
"company_id": ["account:b33adf8a-decd-11f0-a4de-702a04e25700.res_company_test11", "Thomson pvt. ltd."],
"currency": {
"__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.EUR",
"__id": "base:b33adf8a-decd-11f0-a4de-702a04e25700.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
"partner_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_test20", "Junjun wala"],
"partner_id": ["account:b33adf8a-decd-11f0-a4de-702a04e25700.res_partner_test20", "Junjun wala"],
"partner_address": {
"__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_address_7wdsjasdjh",
"__module": "base",
@ -91,7 +92,7 @@
"date_invoice": time.strftime('%Y-%m-%d'),
"name": "sample invoice",
"tax_line": [{
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_tax-4g4EutbiEMVl",
"__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.account_invoice_tax-4g4EutbiEMVl",
"__module": "account",
"__model": "account.invoice.tax",
"amount": 1000.0,
@ -102,21 +103,21 @@
"invoice_line": [{
"__module": "account",
"__model": "account.invoice.line",
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-1RP3so",
"uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "Unit"],
"__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.account_invoice_line-1RP3so",
"uos_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_uom_unit", "Unit"],
"name": "PC Assemble SC234",
"price_unit": 10.0,
"product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_3", "[PCSC234] PC Assemble SC234"],
"product_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_product_3", "[PCSC234] PC Assemble SC234"],
"quantity": 1.0
},
{
"__module": "account",
"__model": "account.invoice.line",
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-u2XV5",
"uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "Unit"],
"__id": "account:b33adf8a-decd-11f0-a4de-702a04e25700.account_invoice_line-u2XV5",
"uos_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_uom_unit", "Unit"],
"name": "PC on Demand",
"price_unit": 100.0,
"product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_5", "[PC-DEM] PC on Demand"],
"product_id": ["product:b33adf8a-decd-11f0-a4de-702a04e25700.product_product_5", "[PC-DEM] PC on Demand"],
"quantity": 5.0
}]
}
@ -125,12 +126,13 @@
invoice_new = self.browse(cr, uid, invoice_id)
# check bank info on partner
assert invoice_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a supplier invoice"
assert len(invoice_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = invoice_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Sample bank: 123465789-156113", 'Expected "Sample bank: 123465789-156113", got %s' % bank_info.acc_number
assert bank_info.acc_number == "Sample bank: 70-123465789-156113", 'Expected "Sample bank: 70-123465789-156113", got %s' % bank_info.acc_number
assert invoice_new.partner_id.supplier, 'Imported Partner is not marked as supplier'
assert invoice_new.reference == time.strftime("SAJ/%Y/002"), "internal number is not stored in reference"
assert invoice_new.reference == time.strftime("SAJ/%Y/070"), "internal number is not stored in reference"
assert invoice_new.reference_type == 'none', "reference type is not set to 'none'"
assert invoice_new.internal_number == False, "internal number is not reset"
assert invoice_new.journal_id.id, "journal id is not selected"
@ -152,3 +154,111 @@
for inv_tax in invoice_new.tax_line:
assert inv_tax.manual, "tax line not set to manual"
assert inv_tax.account_id, "missing tax line account"
-
Then I import a sample EDI document of another customer invoice from OpenERP 6.1 (to test backwards compatibility)
-
!python {model: account.invoice}: |
import time
edi_document = {
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.random_invoice_763jsms",
"__module": "account",
"__model": "account.invoice",
"__version": [6,1,0],
"internal_number": time.strftime("SAJ/%Y/061"),
"company_address": {
"__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.main_address",
"__module": "base",
"__model": "res.partner.address",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.be", "Belgium"],
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"bank_ids": [
["base:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_bank-ZrTWzesfsdDJzGbp","Sample bank: 123465789-156113"]
],
},
"company_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_company_test11", "Thomson pvt. ltd."],
"currency": {
"__id": "base:b22acf7a-ddcd-11e0-a4db-701a04e25543.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
"partner_id": ["account:b22acf7a-ddcd-11e0-a4db-701a04e25543.res_partner_test20", "Junjun wala"],
"partner_address": {
"__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_address_7wdsjasdjh",
"__module": "base",
"__model": "res.partner.address",
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
},
"date_invoice": time.strftime('%Y-%m-%d'),
"name": "sample invoice",
"tax_line": [{
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_tax-4g4EutbiEMVl",
"__module": "account",
"__model": "account.invoice.tax",
"amount": 1000.0,
"manual": True,
"name": "sale tax",
}],
"type": "out_invoice",
"invoice_line": [{
"__module": "account",
"__model": "account.invoice.line",
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-1RP3so",
"uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "PCE"],
"name": "Basic PC",
"price_unit": 10.0,
"product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_pc1", "[PC1] Basic PC"],
"quantity": 1.0
},
{
"__module": "account",
"__model": "account.invoice.line",
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.account_invoice_line-u2XV5",
"uos_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_uom_unit", "PCE"],
"name": "Medium PC",
"price_unit": 100.0,
"product_id": ["product:b22acf7a-ddcd-11e0-a4db-701a04e25543.product_product_pc3", "[PC3] Medium PC"],
"quantity": 5.0
}]
}
invoice_id = self.edi_import(cr, uid, edi_document, context=context)
assert invoice_id, 'EDI import failed'
invoice_new = self.browse(cr, uid, invoice_id)
# check bank info on partner
assert invoice_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a supplier invoice"
assert len(invoice_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = invoice_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Sample bank: 123465789-156113", 'Expected "Sample bank: 123465789-156113", got %s' % bank_info.acc_number
assert invoice_new.partner_id.supplier, 'Imported Partner is not marked as supplier'
assert invoice_new.reference == time.strftime("SAJ/%Y/061"), "internal number is not stored in reference"
assert invoice_new.reference_type == 'none', "reference type is not set to 'none'"
assert invoice_new.internal_number == False, "internal number is not reset"
assert invoice_new.journal_id.id, "journal id is not selected"
assert invoice_new.type == 'in_invoice', "Invoice type was not set properly"
assert len(invoice_new.invoice_line) == 2, "invoice lines are not same"
for inv_line in invoice_new.invoice_line:
if inv_line.name == 'Basic PC':
assert inv_line.uos_id.name == "PCE" , "uom is not same"
assert inv_line.price_unit == 10 , "price unit is not same"
assert inv_line.quantity == 1 , "product qty is not same"
assert inv_line.price_subtotal == 10, "price sub total is not same"
elif inv_line.name == 'Medium PC':
assert inv_line.uos_id.name == "PCE" , "uom is not same"
assert inv_line.price_unit == 100 , "price unit is not same"
assert inv_line.quantity == 5 , "product qty is not same"
assert inv_line.price_subtotal == 500, "price sub total is not same"
else:
raise AssertionError('unknown invoice line: %s' % inv_line)
for inv_tax in invoice_new.tax_line:
assert inv_tax.manual, "tax line not set to manual"
assert inv_tax.account_id, "missing tax line account"

View File

@ -34,7 +34,6 @@ class Controller(openerp.addons.web.http.Controller):
def retrieve(self, req, dbname, token):
""" retrieve the user info (name, login or email) corresponding to a signup token """
registry = RegistryManager.get(dbname)
user_info = None
with registry.cursor() as cr:
res_partner = registry.get('res.partner')
user_info = res_partner.signup_retrieve_info(cr, openerp.SUPERUSER_ID, token)

View File

@ -23,9 +23,7 @@ import time
import urllib
import urlparse
import openerp
from openerp.osv import osv, fields
from openerp import SUPERUSER_ID
from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT
from openerp.tools.safe_eval import safe_eval
@ -35,7 +33,7 @@ class SignupError(Exception):
def random_token():
# the token has an entropy of about 120 bits (6 bits/char * 20 chars)
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
return ''.join(random.choice(chars) for i in xrange(20))
return ''.join(random.choice(chars) for _ in xrange(20))
def now():
return time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
@ -51,29 +49,44 @@ class res_partner(osv.Model):
(not partner.signup_expiration or dt <= partner.signup_expiration)
return res
def _get_signup_url(self, cr, uid, ids, name, arg, context=None):
""" determine a signup url for a given partner """
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
# if required, make sure that every partner without user has a valid signup token
if context and context.get('signup_valid'):
unsigned_ids = [p.id for p in self.browse(cr, uid, ids, context) if not p.user_ids]
self.signup_prepare(cr, uid, unsigned_ids, context=context)
def _get_signup_url_for_action(self, cr, uid, ids, action='login', view_type=None, menu_id=None, res_id=None, context=None):
""" generate a signup url for the given partner ids and action, possibly overriding
the url state components (menu_id, id, view_type) """
res = dict.fromkeys(ids, False)
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
for partner in self.browse(cr, uid, ids, context):
# when required, make sure the partner has a valid signup token
if context and context.get('signup_valid') and not partner.user_ids:
self.signup_prepare(cr, uid, [partner.id], context=context)
action_template = None
params = {
'action': urllib.quote(action),
'db': urllib.quote(cr.dbname),
}
if partner.signup_token:
params = (urllib.quote(cr.dbname), urllib.quote(partner.signup_token))
res[partner.id] = urlparse.urljoin(base_url, "#action=login&db=%s&token=%s" % params)
action_template = "?db=%(db)s#action=%(action)s&token=%(token)s"
params['token'] = urllib.quote(partner.signup_token)
elif partner.user_ids:
user = partner.user_ids[0]
params = (urllib.quote(cr.dbname), urllib.quote(user.login))
res[partner.id] = urlparse.urljoin(base_url, "#action=login&db=%s&login=%s" % params)
action_template = "?db=%(db)s#action=%(action)s&db=%(db)s&login=%(login)s"
params['login'] = urllib.quote(partner.user_ids[0].login)
if action_template:
if view_type:
action_template += '&view_type=%s' % urllib.quote(view_type)
if menu_id:
action_template += '&menu_id=%s' % urllib.quote(str(menu_id))
if res_id:
action_template += '&id=%s' % urllib.quote(str(res_id))
res[partner.id] = urlparse.urljoin(base_url, action_template % params)
return res
def _get_signup_url(self, cr, uid, ids, name, arg, context=None):
""" proxy for function field towards actual implementation """
return self._get_signup_url_for_action(cr, uid, ids, context=context)
_columns = {
'signup_token': fields.char(size=24, string='Signup Token'),
'signup_expiration': fields.datetime(string='Signup Expiration'),
'signup_token': fields.char('Signup Token'),
'signup_expiration': fields.datetime('Signup Expiration'),
'signup_valid': fields.function(_get_signup_valid, type='boolean', string='Signup Token is Valid'),
'signup_url': fields.function(_get_signup_url, type='char', string='Signup URL'),
}

View File

@ -63,7 +63,6 @@ class crm_lead_forward_to_partner(osv.osv_memory):
_defaults = {
'history_mode': 'latest',
'content_subtype': lambda self,cr, uid, context={}: 'html',
}
def get_record_data(self, cr, uid, model, res_id, context=None):

View File

@ -18,20 +18,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
import models
import edi_service
from models.edi import EDIMixin, edi_document
_logger = logging.getLogger(__name__)
# web
try:
import controllers
except ImportError:
_logger.warning(
"""Could not load openerp-web section of EDI, EDI will not behave correctly
To fix, launch openerp-web in embedded mode""")
from . import controllers
from . import models
from . import edi_service
from .models.edi import EDIMixin, edi
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -36,12 +36,10 @@ documentation at http://doc.openerp.com.
'website': 'http://www.openerp.com',
'depends': ['base', 'email_template'],
'icon': '/edi/static/src/img/knowledge.png',
'data': ['security/ir.model.access.csv'],
'test': ['test/edi_partner_test.yml'],
'js': ['static/src/js/edi.js'],
'css': ['static/src/css/edi.css'],
'qweb': ['static/src/xml/*.xml'],
'installable': True,
'auto_install': False,
}

View File

@ -1,77 +1,25 @@
import json
import textwrap
import simplejson
import werkzeug.wrappers
import openerp.addons.web.http as openerpweb
import openerp.addons.web.controllers.main as webmain
class EDI(openerpweb.Controller):
# http://hostname:8069/edi/view?db=XXXX&token=XXXXXXXXXXX
# http://hostname:8069/edi/import_url?url=URIEncodedURL
_cp_path = "/edi"
def template(self, req, mods='web,edi'):
d = {}
d["js"] = "\n".join('<script type="text/javascript" src="%s"></script>'%i for i in webmain.manifest_list(req, mods, 'js'))
d["css"] = "\n".join('<link rel="stylesheet" href="%s">'%i for i in webmain.manifest_list(req, mods, 'css'))
d["modules"] = simplejson.dumps(mods.split(','))
return d
@openerpweb.httprequest
def view(self, req, db, token):
d = self.template(req)
d["init"] = 's.edi.edi_view("%s","%s");'%(db,token)
r = webmain.html_template % d
return r
@openerpweb.httprequest
def import_url(self, req, url):
d = self.template(req)
d["init"] = 's.edi.edi_import("%s");'%(url)
r = webmain.html_template % d
return r
@openerpweb.httprequest
def download(self, req, db, token):
result = req.session.proxy('edi').get_edi_document(db, token)
response = werkzeug.wrappers.Response( result, headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))])
return response
@openerpweb.httprequest
def download_attachment(self, req, db, token):
result = req.session.proxy('edi').get_edi_document(db, token)
doc = json.loads(result)[0]
attachment = doc['__attachments'] and doc['__attachments'][0]
if attachment:
result = attachment["content"].decode('base64')
import email.Utils as utils
# Encode as per RFC 2231
filename_utf8 = attachment['file_name']
filename_encoded = "%s=%s" % ('filename*',
utils.encode_rfc2231(filename_utf8, 'utf-8'))
response = werkzeug.wrappers.Response(result, headers=[('Content-Type', 'application/pdf'),
('Content-Disposition', 'inline; ' + filename_encoded),
('Content-Length', len(result))])
return response
@openerpweb.httprequest
def binary(self, req, db, token, field_path="company_address.logo", content_type='image/png'):
result = req.session.proxy('edi').get_edi_document(db, token)
doc = json.loads(result)[0]
for name in field_path.split("."):
doc = doc[name]
result = doc.decode('base64')
response = werkzeug.wrappers.Response(result, headers=[('Content-Type', content_type),
('Content-Length', len(result))])
return response
@openerpweb.jsonrequest
def get_edi_document(self, req, db, token):
result = req.session.proxy('edi').get_edi_document(db, token)
return json.loads(result)
modules = webmain.module_boot(req) + ['edi']
modules_str = ','.join(modules)
modules_json = simplejson.dumps(modules)
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in webmain.manifest_list(req, modules_str, 'js'))
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in webmain.manifest_list(req, modules_str, 'css'))
return webmain.html_template % {
'js': js,
'css': css,
'modules': modules_json,
'init': 's.edi.edi_import("%s");' % url,
}
@openerpweb.jsonrequest
def import_edi_url(self, req, url):
@ -81,6 +29,4 @@ class EDI(openerpweb.Controller):
return {"action": webmain.clean_action(req, result[0][2], context)}
return True
#
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -20,8 +20,8 @@
##############################################################################
import logging
import netsvc
import openerp
import openerp.netsvc as netsvc
_logger = logging.getLogger(__name__)
@ -34,10 +34,10 @@ class edi(netsvc.ExportService):
try:
registry = openerp.modules.registry.RegistryManager.get(db_name)
assert registry, 'Unknown database %s' % db_name
edi_document = registry['edi.document']
edi = registry['edi.edi']
cr = registry.db.cursor()
res = None
res = getattr(edi_document, method_name)(cr, *method_args)
res = getattr(edi, method_name)(cr, *method_args)
cr.commit()
except Exception:
_logger.exception('Failed to execute EDI method %s with args %r.', method_name, method_args)
@ -46,9 +46,6 @@ class edi(netsvc.ExportService):
cr.close()
return res
def exp_get_edi_document(self, db_name, edi_token):
return self._edi_dispatch(db_name, 'get_document', 1, edi_token)
def exp_import_edi_document(self, db_name, uid, passwd, edi_document, context=None):
return self._edi_dispatch(db_name, 'import_edi', uid, edi_document, None)
@ -59,9 +56,6 @@ class edi(netsvc.ExportService):
if method in ['import_edi_document', 'import_edi_url']:
(db, uid, passwd ) = params[0:3]
openerp.service.security.check(db, uid, passwd)
elif method in ['get_edi_document']:
# No security check for these methods
pass
else:
raise KeyError("Method not found: %s." % method)
fn = getattr(self, 'exp_'+method)

View File

@ -24,15 +24,13 @@ import hashlib
import json
import logging
import re
import threading
import time
import urllib2
import openerp
import openerp.release as release
import netsvc
import pooler
from osv import osv,fields,orm
import openerp.netsvc as netsvc
from openerp.osv import osv, fields
from tools.translate import _
from tools.safe_eval import safe_eval as eval
_logger = logging.getLogger(__name__)
@ -74,16 +72,9 @@ def last_update_for(record):
return False
class edi_document(osv.osv):
_name = 'edi.document'
_description = 'EDI Document'
_columns = {
'name': fields.char("EDI token", size = 128, help="Unique identifier for retrieving an EDI document."),
'document': fields.text("Document", help="EDI document content")
}
_sql_constraints = [
('name_uniq', 'unique (name)', 'EDI Tokens must be unique!')
]
class edi(osv.AbstractModel):
_name = 'edi.edi'
_description = 'EDI Subsystem'
def new_edi_token(self, cr, uid, record):
"""Return a new, random unique token to identify this model record,
@ -109,7 +100,7 @@ class edi_document(osv.osv):
"""Generates a final EDI document containing the EDI serialization
of the given records, which should all be instances of a Model
that has the :meth:`~.edi` mixin. The document is not saved in the
database, this is done by :meth:`~.export_edi`.
database.
:param list(browse_record) records: records to export as EDI
:return: UTF-8 encoded string containing the serialized records
@ -120,19 +111,6 @@ class edi_document(osv.osv):
edi_list += record_model_obj.edi_export(cr, uid, [record], context=context)
return self.serialize(edi_list)
def get_document(self, cr, uid, edi_token, context=None):
"""Retrieve the EDI document corresponding to the given edi_token.
:return: EDI document string
:raise: ValueError if requested EDI token does not match any know document
"""
_logger.debug("get_document(%s)", edi_token)
edi_ids = self.search(cr, uid, [('name','=', edi_token)], context=context)
if not edi_ids:
raise ValueError('Invalid EDI token: %s.' % edi_token)
edi = self.browse(cr, uid, edi_ids[0], context=context)
return edi.document
def load_edi(self, cr, uid, edi_documents, context=None):
"""Import the given EDI document structures into the system, using
:meth:`~.import_edi`.
@ -171,38 +149,18 @@ class edi_document(osv.osv):
"""
return json.loads(edi_documents_string)
def export_edi(self, cr, uid, records, context=None):
"""Export the given database records as EDI documents, stores them
permanently with a new unique EDI token, for later retrieval via :meth:`~.get_document`,
and returns the list of the new corresponding ``ir.edi.document`` records.
:param records: list of browse_record of any model
:return: list of IDs of the new ``ir.edi.document`` entries, in the same
order as the provided ``records``.
"""
exported_ids = []
for record in records:
document = self.generate_edi(cr, uid, [record], context)
token = self.new_edi_token(cr, uid, record)
self.create(cr, uid, {
'name': token,
'document': document
}, context=context)
exported_ids.append(token)
return exported_ids
def import_edi(self, cr, uid, edi_document=None, edi_url=None, context=None):
"""Import a JSON serialized EDI Document string into the system, first retrieving it
from the given ``edi_url`` if provided.
:param str|unicode edi_document: UTF-8 string or unicode containing JSON-serialized
:param str|unicode edi: UTF-8 string or unicode containing JSON-serialized
EDI Document to import. Must not be provided if
``edi_url`` is given.
:param str|unicode edi_url: URL where the EDI document (same format as ``edi_document``)
:param str|unicode edi_url: URL where the EDI document (same format as ``edi``)
may be retrieved, without authentication.
"""
if edi_url:
assert not edi_document, 'edi_document must not be provided if edi_url is given.'
assert not edi_document, 'edi must not be provided if edi_url is given.'
edi_document = urllib2.urlopen(edi_url).read()
assert edi_document, 'EDI Document is empty!'
edi_documents = self.deserialize(edi_document)
@ -215,10 +173,10 @@ class EDIMixin(object):
``edi_import()`` and ``edi_export()`` methods to implement their
specific behavior, based on the primitives provided by this mixin."""
def _edi_requires_attributes(self, attributes, edi_document):
model_name = edi_document.get('__imported_model') or edi_document.get('__model') or self._name
def _edi_requires_attributes(self, attributes, edi):
model_name = edi.get('__imported_model') or edi.get('__model') or self._name
for attribute in attributes:
assert edi_document.get(attribute),\
assert edi.get(attribute),\
'Attribute `%s` is required in %s EDI documents.' % (attribute, model_name)
# private method, not RPC-exposed as it creates ir.model.data entries as
@ -318,7 +276,6 @@ class EDIMixin(object):
:return: list of dicts containing boilerplate EDI metadata for each record,
at the corresponding index from ``records``.
"""
data_ids = []
ir_attachment = self.pool.get('ir.attachment')
results = []
for record in records:
@ -398,7 +355,7 @@ class EDIMixin(object):
return [self.edi_m2o(cr, uid, r, context=context) for r in records]
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
"""Returns a list of dicts representing an edi.document containing the
"""Returns a list of dicts representing EDI documents containing the
records, and matching the given ``edi_struct``, if provided.
:param edi_struct: if provided, edi_struct should be a dictionary
@ -443,50 +400,6 @@ class EDIMixin(object):
results.append(edi_dict)
return results
def edi_export_and_email(self, cr, uid, ids, template_ext_id, context=None):
"""Export the given records just like :meth:`~.export_edi`, the render the
given email template, in order to trigger appropriate notifications.
This method is intended to be called as part of business documents'
lifecycle, so it silently ignores any error occurring during the process,
as this is usually non-critical. To avoid any delay, it is also asynchronous
and will spawn a short-lived thread to perform the action.
:param str template_ext_id: external id of the email.template to use for
the mail notifications
:return: True
"""
def email_task():
db = pooler.get_db(cr.dbname)
local_cr = None
try:
time.sleep(3) # lame workaround to wait for commit of parent transaction
# grab a fresh browse_record on local cursor
local_cr = db.cursor()
web_root_url = self.pool.get('ir.config_parameter').get_param(local_cr, uid, 'web.base.url')
if not web_root_url:
_logger.warning('Ignoring EDI mail notification, web.base.url is not defined in parameters.')
return
mail_tmpl = self._edi_get_object_by_external_id(local_cr, uid, template_ext_id, 'email.template', context=context)
if not mail_tmpl:
# skip EDI export if the template was not found
_logger.warning('Ignoring EDI mail notification, template %s cannot be located.', template_ext_id)
return
for edi_record in self.browse(local_cr, uid, ids, context=context):
edi_token = self.pool.get('edi.document').export_edi(local_cr, uid, [edi_record], context = context)[0]
edi_context = dict(context, edi_web_url_view=EDI_VIEW_WEB_URL % (web_root_url, local_cr.dbname, edi_token))
self.pool.get('email.template').send_mail(local_cr, uid, mail_tmpl.id, edi_record.id,
force_send=False, context=edi_context)
_logger.info('EDI export successful for %s #%s, email notification sent.', self._name, edi_record.id)
except Exception:
_logger.warning('Ignoring EDI mail notification, failed to generate it.', exc_info=True)
finally:
if local_cr:
local_cr.commit()
local_cr.close()
threading.Thread(target=email_task, name='EDI ExportAndEmail for %s %r' % (self._name, ids)).start()
return True
def _edi_get_object_by_name(self, cr, uid, name, model_name, context=None):
model = self.pool.get(model_name)
search_results = model.name_search(cr, uid, name, operator='=', context=context)
@ -515,18 +428,20 @@ class EDIMixin(object):
file_name = record.name_get()[0][1]
file_name = re.sub(r'[^a-zA-Z0-9_-]', '_', file_name)
file_name += ".pdf"
ir_attachment = self.pool.get('ir.attachment').create(cr, uid,
{'name': file_name,
'datas': result,
'datas_fname': file_name,
'res_model': self._name,
'res_id': record.id,
'type': 'binary'},
context=context)
self.pool.get('ir.attachment').create(cr, uid,
{
'name': file_name,
'datas': result,
'datas_fname': file_name,
'res_model': self._name,
'res_id': record.id,
'type': 'binary'
},
context=context)
def _edi_import_attachments(self, cr, uid, record_id, edi_document, context=None):
def _edi_import_attachments(self, cr, uid, record_id, edi, context=None):
ir_attachment = self.pool.get('ir.attachment')
for attachment in edi_document.get('__attachments', []):
for attachment in edi.get('__attachments', []):
# check attachment data is non-empty and valid
file_data = None
try:
@ -573,8 +488,10 @@ class EDIMixin(object):
if data_ids:
model = self.pool.get(model)
data = ir_model_data.browse(cr, uid, data_ids[0], context=context)
result = model.browse(cr, uid, data.res_id, context=context)
return result
if model.exists(cr, uid, [data.res_id]):
return model.browse(cr, uid, data.res_id, context=context)
# stale external-id, cleanup to allow re-import, as the corresponding record is gone
ir_model_data.unlink(cr, 1, [data_ids[0]])
def edi_import_relation(self, cr, uid, model, value, external_id, context=None):
"""Imports a M2O/M2M relation EDI specification ``[external_id,value]`` for the
@ -588,6 +505,10 @@ class EDIMixin(object):
* If previous steps gave no result, create a new record with the given
value in the target model, assign it the given external_id, and return
the new database ID
:param str value: display name of the record to import
:param str external_id: fully-qualified external ID of the record
:return: database id of newly-imported or pre-existing record
"""
_logger.debug("%s: Importing EDI relationship [%r,%r]", model, external_id, value)
target = self._edi_get_object_by_external_id(cr, uid, external_id, model, context=context)
@ -602,9 +523,11 @@ class EDIMixin(object):
self._name, external_id, value)
# also need_new_ext_id here, but already been set above
model = self.pool.get(model)
# should use name_create() but e.g. res.partner won't allow it at the moment
res_id = model.create(cr, uid, {model._rec_name: value}, context=context)
res_id, _ = model.name_create(cr, uid, value, context=context)
target = model.browse(cr, uid, res_id, context=context)
else:
_logger.debug("%s: Importing EDI relationship [%r,%r] - record already exists with ID %s, using it",
self._name, external_id, value, target.id)
if need_new_ext_id:
ext_id_members = split_external_id(external_id)
# module name is never used bare when creating ir.model.data entries, in order
@ -614,19 +537,19 @@ class EDIMixin(object):
self._edi_external_id(cr, uid, target, existing_id=ext_id_members['id'], existing_module=module, context=context)
return target.id
def edi_import(self, cr, uid, edi_document, context=None):
"""Imports a dict representing an edi.document into the system.
def edi_import(self, cr, uid, edi, context=None):
"""Imports a dict representing an EDI document into the system.
:param dict edi_document: EDI document to import
:param dict edi: EDI document to import
:return: the database ID of the imported record
"""
assert self._name == edi_document.get('__import_model') or \
('__import_model' not in edi_document and self._name == edi_document.get('__model')), \
assert self._name == edi.get('__import_model') or \
('__import_model' not in edi and self._name == edi.get('__model')), \
"EDI Document Model and current model do not match: '%s' (EDI) vs '%s' (current)." % \
(edi_document['__model'], self._name)
(edi.get('__model'), self._name)
# First check the record is now already known in the database, in which case it is ignored
ext_id_members = split_external_id(edi_document['__id'])
ext_id_members = split_external_id(edi['__id'])
existing = self._edi_get_object_by_external_id(cr, uid, ext_id_members['full'], self._name, context=context)
if existing:
_logger.info("'%s' EDI Document with ID '%s' is already known, skipping import!", self._name, ext_id_members['full'])
@ -634,7 +557,7 @@ class EDIMixin(object):
record_values = {}
o2m_todo = {} # o2m values are processed after their parent already exists
for field_name, field_value in edi_document.iteritems():
for field_name, field_value in edi.iteritems():
# skip metadata and empty fields
if field_name.startswith('__') or field_value is None or field_value is False:
continue
@ -679,7 +602,7 @@ class EDIMixin(object):
dest_model.edi_import(cr, uid, o2m_line, context=context)
# process the attachments, if any
self._edi_import_attachments(cr, uid, record_id, edi_document, context=context)
self._edi_import_attachments(cr, uid, record_id, edi, context=context)
return record_id

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2011 OpenERP S.A. <http://openerp.com>
# Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,7 +19,7 @@
#
##############################################################################
from osv import fields,osv
from openerp.osv import osv
class res_company(osv.osv):
"""Helper subclass for res.company providing util methods for working with

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2011 OpenERP S.A. <http://openerp.com>
# Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,7 +19,7 @@
#
##############################################################################
from osv import fields,osv
from openerp.osv import osv
from edi import EDIMixin
from openerp import SUPERUSER_ID

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2011 OpenERP S.A. <http://openerp.com>
# Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -20,10 +20,9 @@
##############################################################################
import logging
from osv import fields,osv
from openerp.osv import osv
from edi import EDIMixin
from openerp import SUPERUSER_ID
from tools.translate import _
_logger = logging.getLogger(__name__)
RES_PARTNER_EDI_STRUCT = {

View File

@ -1,3 +0,0 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_ir_edi_all_read,access_ir_edi_all_read,model_edi_document,,1,0,0,0
access_ir_edi_employee_create,access_ir_edi_employee_create,model_edi_document,base.group_user,1,0,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_edi_all_read access_ir_edi_all_read model_edi_document 1 0 0 0
3 access_ir_edi_employee_create access_ir_edi_employee_create model_edi_document base.group_user 1 0 1 0

View File

@ -1,210 +0,0 @@
/** EDI content **/
.openerp .company_logo {
background-size: 180px 46px;
}
.oe_edi_view {
width: 65%;
vertical-align: top;
padding: 0px 25px;
border-right: 1px solid #D2CFCF;
}
.oe_edi_sidebar_container {
width: 35%;
padding: 0px 10px;
vertical-align: top;
}
button.oe_edi_action_print {
font-size: 1.5em;
margin-left: 35%;
margin-bottom: 20px;
}
button.oe_edi_action_print img {
vertical-align: bottom;
width: 32px;
height: 32px;
}
/** EDI Sidebar **/
.oe_edi_sidebar_title {
border-bottom: 1px solid #D2CFCF;
font-weight: bold;
font-size: 1.3em;
min-width: 10em;
}
.oe_edi_nested_block, .oe_edi_nested_block_import, .oe_edi_nested_block_pay {
margin: 0px 40px;
min-width: 10em;
display: none; /* made visible by click on parent input/label */
}
.oe_edi_right_top .oe_edi_nested_block label {
float: left;
text-align: right;
margin-right: 0.5em;
line-height: 180%;
font-weight: bold;
min-width: 5em;
}
.oe_edi_option {
padding-left: 5px;
line-height: 2em;
}
.oe_edi_option:hover {
background: #e8e8e8;
}
.oe_edi_import_button {
margin: 2px 10px;
white-space: nowrap;
}
.oe_edi_small, .oe_edi_small input {
font-size: 90%;
}
/** Sidebar bottom **/
.oe_edi_paypal_button {
margin: 6px;
}
/** Paperbox, from http://www.sitepoint.com/pure-css3-paper-curl/ **/
.oe_edi_paperbox {
position: relative;
width: 700px;
padding: 30px;
padding-bottom: 50px;
margin: 20px auto;
background-color: #fff;
-webkit-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1);
-moz-box-shadow: 0 0 4px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 5px rgba(0, 0, 0, 0.2), inset 0 0 50px rgba(0, 0, 0, 0.1);
}
.oe_edi_paperbox:before, .oe_edi_paperbox:after {
position: absolute;
width: 40%;
height: 10px;
content: ' ';
left: 12px;
bottom: 15px;
background: transparent;
-webkit-transform: skew(-5deg) rotate(-5deg);
-moz-transform: skew(-5deg) rotate(-5deg);
-ms-transform: skew(-5deg) rotate(-5deg);
-o-transform: skew(-5deg) rotate(-5deg);
transform: skew(-5deg) rotate(-5deg);
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
-moz-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
z-index: -1;
}
.oe_edi_paperbox:after {
left: auto; right: 12px;
-webkit-transform: skew(5deg) rotate(5deg);
-moz-transform: skew(5deg) rotate(5deg);
-ms-transform: skew(5deg) rotate(5deg);
-o-transform: skew(5deg) rotate(5deg);
transform: skew(5deg) rotate(5deg);
}
/** Sale Order / Purchase Order Preview **/
table.oe_edi_data, .oe_edi_doc_title {
border-collapse: collapse;
clear: both;
}
.oe_edi_data th {
white-space: nowrap;
}
.oe_edi_data .oe_edi_floor {
border-bottom: 1px solid black;
}
.oe_edi_data .oe_edi_ceiling {
border-top: 1px solid black;
}
.oe_edi_data .oe_edi_data_row {
border-bottom: 1px solid #D2CFCF;
}
.oe_edi_data_row td {
vertical-align: top;
}
.oe_edi_inner_note {
font-style: italic;
font-size: 95%;
padding-left: 10px;
/* prevent wide notes from disrupting layout due to <pre> styling */
white-space: pre-line;
width: 90%;
}
.oe_edi_data_row .oe_edi_inner_note {
/* prevent wide notes from disrupting layout due to <pre> styling */
width: 25em;
}
.oe_edi_shade {
background: #e8e8e8;
}
.oe_edi_company_name {
text-transform: uppercase;
font-weight: bold;
}
.oe_edi_address_from {
float: left;
}
.oe_edi_address_to {
float: right;
margin-top: 25px;
margin-bottom: 30px;
}
.oe_edi_company_block_title {
width: 375px;
margin: 0px;
padding: 2px 14px;
background-color: #252525;
border-top-left-radius: 5px 5px;
border-top-right-radius: 5px 5px;
background-repeat: repeat no-repeat;
}
.oe_edi_company_block_title .oe_edi_company_name {
margin: 0px;
font-size: 1em;
color: #FFF;
}
.oe_edi_company_block_body {
width: 375px;
margin: 0px;
padding: 5px 14px;
line-height: 16px;
background-color: rgb(242, 242, 242);
}
.oe_edi_company_block_body p {
color: #222;
margin: 5px 0px;
}
.oe_edi_summary_label {
float: left;
}
.oe_edi_summary_value {
float: right;
}
/** Python code highlighting **/
/* GeSHi (C) 2004 - 2007 Nigel McNie, 2007 - 2008 Benny Baumann
(http://qbnz.com/highlighter/ and http://geshi.org/) */
.python .de1, .python .de2 {font: normal normal 1em/1.2em monospace; margin:0; padding:0; background:none; vertical-align:top;}
.python {font-family:monospace;}
.python .imp {font-weight: bold; color: red;}
.python li, .python .li1 {background: #ffffff; list-style: none;}
.python .ln {width:1px;text-align:right;margin:0;padding:0 2px;vertical-align:top;}
.python .li2 {background: #f8f8f8;}
.python .kw1 {color: #ff7700;font-weight:bold;}
.python .kw2 {color: #008000;}
.python .kw3 {color: #dc143c;}
.python .kw4 {color: #0000cd;}
.python .co1 {color: #808080; font-style: italic;}
.python .coMULTI {color: #808080; font-style: italic;}
.python .es0 {color: #000099; font-weight: bold;}
.python .br0 {color: black;}
.python .sy0 {color: #66cc66;}
.python .st0 {color: #483d8b;}
.python .nu0 {color: #ff4500;}
.python .me1 {color: black;}
.python span.xtra { display:block; }
.python ol { padding: 0px; }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,119 +1,9 @@
openerp.edi = function(openerp) {
openerp.edi = {}
openerp.edi = function(instance) {
var _t = instance.web._t;
instance.edi = {}
openerp.edi.EdiView = openerp.web.Widget.extend({
init: function(parent, db, token) {
this._super();
this.db = db;
this.token = token;
this.session = openerp.session;
this.template = "EdiEmpty";
this.content = "";
this.sidebar = "";
},
start: function() {
this._super();
var self = this;
var param = {"db": self.db, "token": self.token};
return self.rpc('/edi/get_edi_document', param).done(this.on_document_loaded).fail(this.on_document_failed);
},
on_document_loaded: function(docs){
this.doc = docs[0];
var template_content = "Edi." + this.doc.__model + ".content";
var template_sidebar = "Edi." + this.doc.__model + ".sidebar";
var param = {"widget":this, "doc":this.doc};
if (openerp.web.qweb.templates[template_sidebar]) {
this.sidebar = openerp.web.qweb.render(template_sidebar, param);
}
if (openerp.web.qweb.templates[template_content]) {
this.content = openerp.web.qweb.render(template_content, param);
}
this.$el.html(openerp.web.qweb.render("EdiView", param));
this.$el.find('button.oe_edi_action_print').bind('click', this.do_print);
this.$el.find('button#oe_edi_import_existing').bind('click', this.do_import_existing);
this.$el.find('button#oe_edi_import_create').bind('click', this.do_import_create);
this.$el.find('button#oe_edi_download').bind('click', this.do_download);
this.$el.find('.oe_edi_import_choice, .oe_edi_import_choice_label').bind('click', this.toggle_choice('import'));
this.$el.find('.oe_edi_pay_choice, .oe_edi_pay_choice_label').bind('click', this.toggle_choice('pay'));
this.$el.find('#oe_edi_download_show_code').bind('click', this.show_code);
},
on_document_failed: function(response) {
var self = this;
var params = {
error: response,
//TODO: should this be _t() wrapped?
message: "Sorry, this document cannot be located. Perhaps the link you are using has expired?"
}
$(openerp.web.qweb.render("DialogWarning", params)).dialog({
title: "Document not found",
modal: true,
});
},
show_code: function($event) {
$('#oe_edi_download_code').toggle();
},
get_download_url: function() {
var l = window.location;
var url_prefix = l.protocol + '//' + l.host;
return url_prefix +'/edi/download?db=' + this.db + '&token=' + this.token;
},
get_paypal_url: function(document_type, ref_field) {
var comp_name = encodeURIComponent(this.doc.company_id[1]);
var doc_ref = encodeURIComponent(this.doc[ref_field]);
var paypal_account = encodeURIComponent(this.doc.company_address.paypal_account);
var amount = encodeURIComponent(this.doc.amount_total);
var cur_code = encodeURIComponent(this.doc.currency.code);
var paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick" +
"&business=" + paypal_account +
"&item_name=" + document_type + "%20" + comp_name + "%20" + doc_ref +
"&invoice=" + doc_ref +
"&amount=" + amount +
"&currency_code=" + cur_code +
"&button_subtype=services&amp;no_note=1&amp;bn=OpenERP_PayNow_" + cur_code;
return paypal_url;
},
toggle_choice: function(mode) {
return function($e) {
$('.oe_edi_nested_block_'+mode).hide();
$('.'+$e.target.id+'_nested').show();
return true;
}
},
do_print: function(e){
var l = window.location;
window.location = l.protocol + '//' + l.host + "/edi/download_attachment?db=" + this.db + "&token=" + this.token;
},
do_import_existing: function(e) {
var url_download = this.get_download_url();
var $edi_text_server_input = this.$el.find('#oe_edi_txt_server_url');
var server_url = $edi_text_server_input.val();
$edi_text_server_input.removeClass('invalid');
if (!server_url) {
$edi_text_server_input.addClass('invalid');
return false;
}
var protocol = "http://";
if (server_url.toLowerCase().lastIndexOf('http', 0) == 0 ) {
protocol = '';
}
window.location = protocol + server_url + '/edi/import_url?url=' + encodeURIComponent(url_download);
},
do_import_create: function(e){
var url_download = this.get_download_url();
window.location = "https://cc.my.openerp.com/odms/create_edi?url=" + encodeURIComponent(url_download);
},
do_download: function(e){
window.location = this.get_download_url();
}
});
instance.edi.EdiImport = instance.web.Widget.extend({
openerp.edi.edi_view = function (db, token) {
openerp.session.session_bind().done(function () {
new openerp.edi.EdiView(null,db,token).appendTo($("body").addClass('openerp'));
});
}
openerp.edi.EdiImport = openerp.web.Widget.extend({
init: function(parent,url) {
this._super();
this.url = url;
@ -137,7 +27,7 @@ openerp.edi.EdiImport = openerp.web.Widget.extend({
show_login: function() {
this.destroy_content();
this.login = new openerp.web.Login(this);
this.login = new instance.web.Login(this);
this.login.appendTo(this.$el);
},
@ -167,18 +57,18 @@ openerp.edi.EdiImport = openerp.web.Widget.extend({
window.location = "/";
}
}
}).html('The document has been successfully imported!');
}).html(_t('The document has been successfully imported!'));
}
},
on_imported_error: function(response){
var self = this;
var msg = "Sorry, the document could not be imported.";
var msg = _t("Sorry, the document could not be imported.");
if (response.data.fault_code) {
msg += "\n Reason:" + response.data.fault_code;
msg += "\n " + _t("Reason:") + response.data.fault_code;
}
var params = {error: response, message: msg};
$(openerp.web.qweb.render("CrashManagerWarning", params)).dialog({
title: "Document Import Notification",
$(instance.web.qweb.render("CrashManager.warning", params)).dialog({
title: _t("Document Import Notification"),
modal: true,
buttons: {
Ok: function() { $(this).dialog("close"); }
@ -187,9 +77,9 @@ openerp.edi.EdiImport = openerp.web.Widget.extend({
}
});
openerp.edi.edi_import = function (url) {
openerp.session.session_bind().done(function () {
new openerp.edi.EdiImport(null,url).appendTo($("body").addClass('openerp'));
instance.edi.edi_import = function (url) {
instance.session.session_bind().done(function () {
new instance.edi.EdiImport(null,url).appendTo($("body").addClass('openerp'));
});
}

View File

@ -1,93 +0,0 @@
<template>
<t t-name="EdiEmpty">
<div style="height:100%;"></div>
</t>
<t t-name="EdiImport">
<t t-call="WebClient"/>
</t>
<t t-name="EdiView">
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%" id="oe_app" class="oe-application oe_forms oe_semantic_html_override">
<tr>
<td colspan="2" valign="top" id="oe_header" class="header">
<div> <a href="/" class="company_logo_link">
<div class="company_logo"
t-att-style="'background-size: 180px 46px; background: url('+ (doc.company_address ? '/edi/binary?db='+widget.db+'&amp;token='+widget.token : '/web/static/src/img/logo.png')+')'"/></a> </div>
</td>
</tr>
<tr>
<td colspan="2" valign="top" height="100%">
<table cellspacing="0" cellpadding="0" border="0" height="100%" width="100%">
<tr>
<td class="oe_edi_view">
<p class="oe_paragraph"><t t-raw="widget.content"/></p>
<button type="button" class="oe_edi_action_print">
View/Print <img src="/edi/static/src/img/pdf.png"/>
</button>
</td>
<td class="oe_edi_sidebar_container">
<p class="oe_edi_sidebar_title">
Import this document
</p>
<div class="oe_edi_option">
<input type="radio" id="oe_edi_import_openerp" name="oe_edi_import" class="oe_edi_import_choice"/>
<label for="oe_edi_import_openerp" id="oe_edi_import_openerp" class="oe_edi_import_choice_label">Import it into an existing OpenERP instance</label>
</div>
<p class="oe_edi_nested_block_import oe_edi_import_openerp_nested">
<label for="oe_edi_txt_server_url">OpenERP instance address:</label>
<br/>
<input type="text" id="oe_edi_txt_server_url" placeholder="http://example.my.openerp.com/"/><br/>
<button type="button" class="oe_edi_import_button" id="oe_edi_import_existing">Import</button>
</p>
<div class="oe_edi_option">
<input type="radio" id="oe_edi_import_saas" name="oe_edi_import" class="oe_edi_import_choice"/>
<label for="oe_edi_import_saas" id="oe_edi_import_saas" class="oe_edi_import_choice_label">Import it into a new OpenERP Online instance</label>
</div>
<p class="oe_edi_nested_block_import oe_edi_import_saas_nested">
<button type="button" class="oe_edi_import_button" id="oe_edi_import_create">Create my new OpenERP instance</button>
</p>
<div class="oe_edi_option">
<input type="radio" id="oe_edi_import_download" name="oe_edi_import" class="oe_edi_import_choice"/>
<label for="oe_edi_import_download" id="oe_edi_import_download" class="oe_edi_import_choice_label">Import into another application</label>
</div>
<p class="oe_edi_nested_block_import oe_edi_small oe_edi_import_download_nested">
OpenERP's Electronic Data Interchange documents are based on a generic and language
independent <a href="http://json.org">JSON</a> serialization of the document's attribute.
It is usually very quick and straightforward to create a small plug-in for your preferred
application that will be capable of importing any OpenERP EDI document.
You can find out more details about how to do this and what the content of OpenERP EDI documents
is like in the <a href="http://doc.openerp.com/search.html?q=edi">OpenERP documentation</a>.
<br/>
To get started immediately, <a href="#" id="oe_edi_download_show_code">see is all it takes to use this EDI document in Python</a>.
</p>
<div class="python oe_edi_nested_block_import oe_edi_small" id="oe_edi_download_code" t-translation="off">
<ol><li class="li1"><div class="de1"><span class="kw1">import</span> <span class="kw3">urllib2</span><span class="sy0">,</span> simplejson</div></li>
<li class="li1"><div class="de1">edi_document <span class="sy0">=</span> <span class="kw3">urllib2</span>.<span class="me1">urlopen</span><span class="br0">(</span><span class="st0">'<t t-esc="widget.get_download_url()"/>'</span><span class="br0">)</span>.<span class="me1">read</span><span class="br0">(</span><span class="br0">)</span></div></li>
<li class="li2"><div class="de2">document_data <span class="sy0">=</span> simplejson.<span class="me1">loads</span><span class="br0">(</span>edi_document<span class="br0">)</span><span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span></div></li>
<li class="li1"><div class="de1"><span class="kw1">print</span> <span class="st0">"Amount: "</span><span class="sy0">,</span> document_data<span class="br0">[</span><span class="st0">'amount_total'</span><span class="br0">]</span></div></li>
</ol></div>
<p class="oe_edi_nested_block_import oe_edi_small oe_edi_import_download_nested">
You can download the raw EDI document here:<br/>
<input type="text" readonly="readonly" t-att-value="widget.get_download_url()"/>
<button type="button" class="oe_edi_import_button" id="oe_edi_download">Download</button>
</p>
<div class="oe_edi_right_bottom">
<t t-raw="widget.sidebar"/>
</div>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td colspan="2">
<div id="oe_footer" class="oe_footer">
<p class="oe_footer_powered">Powered by <a href="http://www.openerp.com">OpenERP</a></p>
</div>
</td>
</tr>
</table>
</t>
</template>

View File

@ -1,163 +0,0 @@
<template>
<t t-name="Edi.account.invoice.content">
<div class="oe_edi_paperbox">
<div class="oe_edi_address_from">
<div class="oe_edi_company_block_title">
<span class="oe_edi_company_name"><t t-esc="doc.company_id[1]"/></span>
</div>
<div class="oe_edi_company_block_body">
<p>
<t t-if="doc.company_address">
<t t-if="doc.company_address.street" t-esc="doc.company_address.street"/><br/>
<t t-if="doc.company_address.street2"><t t-esc="doc.company_address.street2"/><br/></t>
<t t-if="doc.company_address.zip" t-esc="doc.company_address.zip"/> <t t-if="doc.company_address.city" t-esc="doc.company_address.city"/> <br/>
<t t-if="doc.company_address.country_id"><t t-esc="doc.company_address.country_id[1]"/><br/></t>
</t>
</p>
</div>
</div>
<div class="oe_edi_address_to">
<div class="oe_edi_company_block_title">
<span class="oe_edi_company_name"><t t-esc="doc.partner_id[1]"/></span>
</div>
<div class="oe_edi_company_block_body">
<p>
<t t-if="doc.partner_address">
<t t-if="doc.partner_address.street" t-esc="doc.partner_address.street"/><br/>
<t t-if="doc.partner_address.street2"><t t-esc="doc.partner_address.street2"/><br/></t>
<t t-if="doc.partner_address.zip" t-esc="doc.partner_address.zip"/> <t t-if="doc.partner_address.city" t-esc="doc.partner_address.city"/> <br/>
<t t-if="doc.partner_address.country_id"><t t-esc="doc.partner_address.country_id[1]"/><br/></t>
</t>
</p>
</div>
</div>
<h1 class="oe_edi_doc_title">Invoice <t t-esc="doc.internal_number"/>: <t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/></h1>
<table width="100%" class="oe_edi_data oe_edi_shade">
<tr class="oe_edi_floor">
<th align="left">Description</th>
<th align="left">Date</th>
<th align="left">Your Reference</th>
</tr>
<tr class="oe_edi_data_row">
<td align="left"><t t-if="doc.name" t-esc="doc.name"/></td>
<td align="left"><t t-if="doc.date_invoice" t-esc="doc.date_invoice"/></td>
<td align="left"><t t-if="doc.partner_ref" t-esc="doc.partner_ref"/></td>
</tr>
</table>
<p/>
<table width="100%" class="oe_edi_data">
<tr class="oe_edi_floor">
<th align="left">Product Description</th>
<th align="right">Quantity</th>
<th align="right">Unit Price</th>
<th align="right">Discount</th>
<th align="right">Price</th>
</tr>
<t t-if="doc.invoice_line" t-foreach="doc.invoice_line" t-as="invoice_line">
<tr class="oe_edi_data_row">
<td align="left"><t t-esc="invoice_line.name"/>
<t t-if="invoice_line.note">
<pre class="oe_edi_inner_note"><t t-esc="invoice_line.note"/></pre>
</t>
</td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',invoice_line.quantity)"/> <t t-esc="invoice_line.uos_id[1]"/></td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',invoice_line.price_unit)"/></td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',invoice_line.discount)"/></td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',invoice_line.price_subtotal)"/> <t t-esc="doc.currency.code"/></td>
</tr>
</t>
<tr>
<td colspan="3"></td>
<td colspan="2" class="oe_edi_ceiling">
<div class="oe_edi_summary_label">
Net Total:
</div>
<div class="oe_edi_summary_value">
<t t-esc="_.str.sprintf('%.2f',doc.amount_untaxed)"/> <t t-esc="doc.currency.code"/>
</div>
</td>
</tr>
<tr>
<td colspan="3"></td>
<td colspan="2" class="oe_edi_floor">
<div class="oe_edi_summary_label">
Taxes:
</div>
<div class="oe_edi_summary_value">
<t t-esc="_.str.sprintf('%.2f',doc.amount_tax)"/> <t t-esc="doc.currency.code"/>
</div>
</td>
</tr>
<tr>
<td colspan="3"></td>
<th colspan="2" class="oe_edi_shade">
<div class="oe_edi_summary_label">
Total:
</div>
<div class="oe_edi_summary_value">
<t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/>
</div>
</th>
</tr>
</table>
<t t-if="doc.tax_line">
<table class="oe_edi_data" width="40%">
<tr class="oe_edi_floor">
<th align="left">Tax</th>
<th align="right">Base Amount</th>
<th align="right">Amount</th>
</tr>
<t t-if="doc.tax_line"><t t-foreach="doc.tax_line" t-as="tax_line">
<tr class="oe_edi_data_row">
<td align="left"><t t-esc="tax_line.name"/></td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',tax_line.base_amount)"/> <t t-esc="doc.currency.code"/></td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',tax_line.amount)"/> <t t-esc="doc.currency.code"/></td>
</tr>
</t>
</t>
</table>
</t>
<t t-if="doc.comment">
<p>Notes:</p>
<pre class="oe_edi_inner_note"><t t-esc="doc.comment"/></pre>
</t>
</div>
</t>
<t t-name="Edi.account.invoice.sidebar">
<t t-if="!doc.reconciled &amp;&amp; (doc.type == 'out_invoice' or doc.type == 'in_refund')">
<t t-if="doc.company_address.paypal_account || doc.company_address.bank_ids">
<p class="oe_edi_sidebar_title">Pay Online</p>
<t t-if="doc.company_address.paypal_account">
<div class="oe_edi_option">
<input type="radio" id="oe_edi_paypal" name="oe_edi_pay" class="oe_edi_pay_choice"/>
<label for="oe_edi_paypal" id="oe_edi_paypal" class="oe_edi_pay_choice_label">Paypal</label>
</div>
<p class="oe_edi_nested_block_pay oe_edi_paypal_nested">
You may directly pay this invoice online via Paypal's secure payment gateway:<br/>
<a t-att-href="widget.get_paypal_url('Invoice','internal_number')" target="_new">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
</p>
</t>
<t t-if="doc.company_address.bank_ids">
<div class="oe_edi_option">
<input type="radio" id="oe_edi_pay_wire" name="oe_edi_pay" class="oe_edi_pay_choice"/>
<label for="oe_edi_pay_wire" id="oe_edi_pay_wire" class="oe_edi_pay_choice_label">Bank Wire Transfer</label>
</div>
<p class="oe_edi_nested_block_pay oe_edi_pay_wire_nested">
Please transfer <strong><t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/></strong> to
<strong><t t-esc="doc.company_id[1]"/></strong> (postal address on the invoice header)
using one of the following bank accounts. Be sure to mention the invoice
reference <strong><t t-esc="doc.internal_number"/></strong> on the transfer:
<br/><br/>
</p>
<ul class="oe_edi_nested_block_pay oe_edi_pay_wire_nested">
<t t-foreach="doc.company_address.bank_ids" t-as="bank_info">
<li><t t-esc="bank_info[1]"/></li>
</t>
</ul>
</t>
</t>
</t>
</t>
</template>

View File

@ -1,169 +0,0 @@
<template>
<t t-name="Edi.sale.order.content">
<div class="oe_edi_paperbox">
<div class="oe_edi_address_from">
<div class="oe_edi_company_block_title">
<span class="oe_edi_company_name"><t t-esc="doc.company_id[1]"/></span>
</div>
<div class="oe_edi_company_block_body">
<p>
<t t-if="doc.company_address">
<t t-if="doc.company_address.street" t-esc="doc.company_address.street"/><br/>
<t t-if="doc.company_address.street2"><t t-esc="doc.company_address.street2"/><br/></t>
<t t-if="doc.company_address.zip" t-esc="doc.company_address.zip"/> <t t-if="doc.company_address.city" t-esc="doc.company_address.city"/> <br/>
<t t-if="doc.company_address.country_id"><t t-esc="doc.company_address.country_id[1]"/><br/></t>
</t>
</p>
</div>
</div>
<div class="oe_edi_address_to">
<div class="oe_edi_company_block_title">
<span class="oe_edi_company_name"><t t-esc="doc.partner_id[1]"/></span>
</div>
<div class="oe_edi_company_block_body">
<p>
<t t-if="doc.partner_address">
<t t-if="doc.partner_address.street" t-esc="doc.partner_address.street"/><br/>
<t t-if="doc.partner_address.street2"><t t-esc="doc.partner_address.street2"/><br/></t>
<t t-if="doc.partner_address.zip" t-esc="doc.partner_address.zip"/> <t t-if="doc.partner_address.city" t-esc="doc.partner_address.city"/> <br/>
<t t-if="doc.partner_address.country_id"><t t-esc="doc.partner_address.country_id[1]"/><br/></t>
</t>
</p>
</div>
</div>
<t t-if="(doc.state == 'draft' or doc.state == 'sent') and doc.__model == 'sale.order'">
<h1 class="oe_edi_doc_title">Quotation <t t-esc="doc.name"/>: <t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/></h1>
</t>
<t t-if="(doc.state == 'draft' or doc.state == 'sent') and doc.__model == 'purchase.order'">
<h1 class="oe_edi_doc_title">Request for Quotation <t t-esc="doc.name"/>: <t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/></h1>
</t>
<t t-if="(doc.state != 'draft' and doc.state != 'sent')">
<h1 class="oe_edi_doc_title">Order <t t-esc="doc.name"/>: <t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/></h1>
</t>
<table width="100%" class="oe_edi_data oe_edi_shade">
<tr class="oe_edi_floor">
<th align="left">Your Reference</th>
<th align="left">Date</th>
<th align="left">Salesperson</th>
<th align="left">Payment terms</th>
</tr>
<tr class="oe_edi_data_row">
<td align="left"><t t-if="doc.partner_ref" t-esc="doc.partner_ref"/></td>
<td align="left"><t t-esc="doc.date_order"/></td>
<td align="left"><t t-if="doc.user_id" t-esc="doc.user_id[1]"/></td>
<td align="left">
<t t-if="doc.payment_term" t-esc="doc.payment_term[1]"/>
</td>
</tr>
</table>
<p/>
<table width="100%" class="oe_edi_data">
<tr class="oe_edi_floor">
<th align="left">Product Description</th>
<th align="right">Quantity</th>
<th align="right">Unit Price</th>
<th align="right">Discount(%)</th>
<th align="right">Price</th>
</tr>
<t t-if="doc.order_line" t-foreach="doc.order_line" t-as="doc_line">
<tr class="oe_edi_data_row">
<td align="left"><t t-esc="doc_line.name"/>
<t t-if="doc_line.notes">
<pre class="oe_edi_inner_note"><t t-esc="doc_line.notes"/></pre>
</t>
</td>
<td align="right">
<t t-esc="_.str.sprintf('%.2f',doc_line.product_qty)"/> <t t-esc="doc_line.product_uom[1]"/>
</td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',doc_line.price_unit)"/></td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',doc_line.discount or 0.0)"/></td>
<td align="right"><t t-esc="_.str.sprintf('%.2f',doc_line.price_subtotal)"/> <t t-esc="doc.currency.code"/></td>
</tr>
</t>
<tr>
<td colspan="3"></td>
<td colspan="2" class="oe_edi_ceiling">
<div class="oe_edi_summary_label">
Net Total:
</div>
<div class="oe_edi_summary_value">
<t t-esc="_.str.sprintf('%.2f',doc.amount_untaxed)"/> <t t-esc="doc.currency.code"/>
</div>
</td>
</tr>
<tr>
<td colspan="3"></td>
<td colspan="2" class="oe_edi_floor">
<div class="oe_edi_summary_label">
Taxes:
</div>
<div class="oe_edi_summary_value">
<t t-esc="_.str.sprintf('%.2f',doc.amount_tax)"/> <t t-esc="doc.currency.code"/>
</div>
</td>
</tr>
<tr>
<td colspan="3"></td>
<th colspan="2" class="oe_edi_shade">
<div class="oe_edi_summary_label">
Total:
</div>
<div class="oe_edi_summary_value">
<t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/>
</div>
</th>
</tr>
</table>
<t t-if="doc.notes">
<p>Notes:</p>
<pre class="oe_edi_inner_note"><t t-esc="doc.notes"/></pre>
</t>
</div>
</t>
<t t-name="Edi.sale.order.sidebar">
<t t-if="doc.order_policy and (doc.order_policy == 'prepaid' || doc.order_policy == 'manual') and (doc.state != 'draft' and doc.state != 'sent')">
<t t-if="doc.company_address.paypal_account || doc.company_address.bank_ids">
<p class="oe_edi_sidebar_title">Pay Online</p>
<t t-if="doc.company_address.paypal_account">
<div class="oe_edi_option">
<input type="radio" id="oe_edi_paypal" name="oe_edi_pay" class="oe_edi_pay_choice"/>
<label for="oe_edi_paypal" id="oe_edi_paypal" class="oe_edi_pay_choice_label">Paypal</label>
</div>
<p class="oe_edi_nested_block_pay oe_edi_paypal_nested">
You may directly pay this order online via Paypal's secure payment gateway:<br/>
<a t-att-href="widget.get_paypal_url('Sale Order','name')" target="_new">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
</p>
</t>
<t t-if="doc.company_address.bank_ids">
<div class="oe_edi_option">
<input type="radio" id="oe_edi_pay_wire" name="oe_edi_pay" class="oe_edi_pay_choice"/>
<label for="oe_edi_pay_wire" id="oe_edi_pay_wire" class="oe_edi_pay_choice_label">Bank Wire Transfer</label>
</div>
<p class="oe_edi_nested_block_pay oe_edi_pay_wire_nested">
Please transfer <strong><t t-esc="_.str.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/></strong> to
<strong><t t-esc="doc.company_id[1]"/></strong> (postal address on the order header)
using one of the following bank accounts. Be sure to mention the document
reference <strong><t t-esc="doc.name"/></strong> on the transfer:
<br/><br/>
</p>
<ul class="oe_edi_nested_block_pay oe_edi_pay_wire_nested">
<t t-foreach="doc.company_address.bank_ids" t-as="bank_info">
<li><t t-esc="bank_info[1]"/></li>
</t>
</ul>
</t>
</t>
</t>
</t>
<t t-name="Edi.purchase.order.content">
<t t-call="Edi.sale.order.content"/>
</t>
<t t-name="Edi.purchase.order.sidebar">
<t t-call="Edi.sale.order.sidebar"/>
</t>
</template>

View File

@ -6,11 +6,10 @@
with an attached file, check the result, the alter the data
and reimport it.
-
!python {model: edi.document}: |
!python {model: edi.edi}: |
import json
partner_obj = self.pool.get('res.partner')
tokens = self.export_edi(cr, uid, [partner_obj.browse(cr, uid, ref('base.res_partner_2'))])
doc = self.get_document(cr, uid, tokens[0], context=context)
res_partner = self.pool.get('res.partner')
doc = self.generate_edi(cr, uid, [res_partner.browse(cr, uid, ref('base.res_partner_2'))])
edi_doc, = json.loads(doc)
# check content of the document
@ -36,8 +35,7 @@
"Expected (%r,> %r) after import 1, got %r" % ('res.partner', ref('base.res_partner_2'), result)
# export the same partner we just created, and see if the output matches the input
tokens = self.export_edi(cr, uid, [partner_obj.browse(cr, uid, result[1])])
doc_output = self.get_document(cr, uid, tokens[0], context=context)
doc_output = self.generate_edi(cr, uid, [res_partner.browse(cr, uid, result[1])])
edi_doc_output, = json.loads(doc_output)
for attribute in ('__model', '__module', '__id', 'name', '__attachments'):
assert edi_doc_output.get(attribute) == edi_doc.get(attribute), \

View File

@ -100,25 +100,27 @@ class test_message_compose(test_mail_mockup.TestMailMockups):
# ----------------------------------------
# 1. Comment on pigs
compose_id = mail_compose.create(cr, uid,
{'subject': 'Forget me subject', 'body': 'Dummy body'},
{'default_composition_mode': 'comment',
'default_model': 'mail.group',
'default_res_id': self.group_pigs_id,
'default_use_template': False,
'default_template_id': email_template_id,
'active_ids': [self.group_pigs_id, self.group_bird_id]})
compose = mail_compose.browse(cr, uid, compose_id)
# 2. Perform 'toggle_template', to set use_template and use template_id
mail_compose.toggle_template(cr, uid, [compose_id], {'default_composition_mode': 'comment', 'default_model': 'mail.group'})
context = {
'default_composition_mode': 'comment',
'default_model': 'mail.group',
'default_res_id': self.group_pigs_id,
'default_use_template': False,
'default_template_id': email_template_id,
'active_ids': [self.group_pigs_id, self.group_bird_id]
}
compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context)
compose = mail_compose.browse(cr, uid, compose_id, context)
onchange_res = compose.onchange_template_id(email_template_id, 'comment', 'mail.group', self.group_pigs_id)['value']
onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])]
onchange_res['attachment_ids'] = [(4, attachment_id) for attachment_id in onchange_res.pop('attachment_ids', [])]
compose.write(onchange_res)
compose.refresh()
message_pids = [partner.id for partner in compose.partner_ids]
partner_ids = self.res_partner.search(cr, uid, [('email', 'in', ['b@b.b', 'c@c.c', 'd@d.d'])])
# Test: mail.compose.message: subject, body, content_subtype, partner_ids
# Test: mail.compose.message: subject, body, partner_ids
self.assertEqual(compose.subject, _subject1, 'mail.compose.message subject incorrect')
self.assertEqual(compose.body, _body_html1, 'mail.compose.message body incorrect')
self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message content_subtype incorrect')
self.assertEqual(set(message_pids), set(partner_ids), 'mail.compose.message partner_ids incorrect')
# Test: mail.compose.message: attachments
# Test: mail.message: attachments
@ -128,41 +130,34 @@ class test_message_compose(test_mail_mockup.TestMailMockups):
self.assertIn((attach.name, base64.b64decode(attach.datas)), _attachments_test,
'mail.message attachment name / data incorrect')
# 3. Perform 'toggle_template': template is not set anymore
mail_compose.toggle_template(cr, uid, [compose_id], {'default_composition_mode': 'comment', 'default_model': 'mail.group'})
compose.refresh()
# Test: subject, body, partner_ids
self.assertEqual(compose.subject, False, 'mail.compose.message subject incorrect')
self.assertEqual(compose.body, '', 'mail.compose.message body incorrect')
# ----------------------------------------
# CASE3: mass_mail with template
# ----------------------------------------
# 1. Mass_mail on pigs and bird, with a default_partner_ids set to check he is correctly added
compose_id = mail_compose.create(cr, uid,
{'subject': 'Forget me subject', 'body': 'Dummy body'},
{'default_composition_mode': 'mass_mail',
'default_model': 'mail.group',
'default_res_id': self.group_pigs_id,
'default_use_template': False,
'default_template_id': email_template_id,
'default_partner_ids': [p_a_id],
'active_ids': [self.group_pigs_id, self.group_bird_id]})
compose = mail_compose.browse(cr, uid, compose_id)
# 2. Perform 'toggle_template', to set use_template and use template_id
mail_compose.toggle_template(cr, uid, [compose_id], {'default_composition_mode': 'comment', 'default_model': 'mail.group'})
context = {
'default_composition_mode': 'mass_mail',
'default_model': 'mail.group',
'default_res_id': self.group_pigs_id,
'default_template_id': email_template_id,
'default_partner_ids': [p_a_id],
'active_ids': [self.group_pigs_id, self.group_bird_id]
}
compose_id = mail_compose.create(cr, uid, {'subject': 'Forget me subject', 'body': 'Dummy body'}, context)
compose = mail_compose.browse(cr, uid, compose_id, context)
onchange_res = compose.onchange_template_id(email_template_id, 'mass_mail', 'mail.group', self.group_pigs_id)['value']
onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])]
onchange_res['attachment_ids'] = [(4, attachment_id) for attachment_id in onchange_res.pop('attachment_ids', [])]
compose.write(onchange_res)
compose.refresh()
message_pids = [partner.id for partner in compose.partner_ids]
partner_ids = [p_a_id]
# Test: mail.compose.message: subject, body, content_subtype, partner_ids
self.assertEqual(compose.subject, '${object.name}', 'mail.compose.message subject incorrect')
self.assertEqual(compose.body, '${object.description}', 'mail.compose.message body incorrect')
self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message content_subtype incorrect')
self.assertEqual(set(message_pids), set(partner_ids), 'mail.compose.message partner_ids incorrect')
# 3. Post the comment, get created message
# 2. Post the comment, get created message
mail_compose.send_mail(cr, uid, [compose_id], {'default_res_id': -1, 'active_ids': [self.group_pigs_id, self.group_bird_id]})
group_pigs.refresh()
group_bird.refresh()

View File

@ -19,11 +19,8 @@
#
##############################################################################
import tools
from osv import osv
from osv import fields
from openerp import tools
from openerp.osv import osv, fields
def _reopen(self, res_id, model):
return {'type': 'ir.actions.act_window',
@ -39,7 +36,6 @@ def _reopen(self, res_id, model):
},
}
class mail_compose_message(osv.TransientModel):
_inherit = 'mail.compose.message'
@ -60,37 +56,19 @@ class mail_compose_message(osv.TransientModel):
record_ids = email_template_obj.search(cr, uid, [('model', '=', model)], context=context)
return email_template_obj.name_get(cr, uid, record_ids, context) + [(False, '')]
def default_get(self, cr, uid, fields, context=None):
if context is None:
context = {}
result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
result['template_id'] = context.get('default_template_id', context.get('mail.compose.template_id', False))
# pre-render the template if any
if result.get('use_template') and result.get('template_id'):
onchange_res = self.onchange_use_template(cr, uid, [], result.get('use_template'), result.get('template_id'),
result.get('composition_mode'), result.get('model'), result.get('res_id'), context=context)
result.update(onchange_res['value'])
return result
_columns = {
'use_template': fields.boolean('Use Template'),
# incredible hack of the day: size=-1 means we want an int db column instead of an str one
'template_id': fields.selection(_get_templates, 'Template', size=-1),
}
_defaults = {
'use_template': True,
}
def onchange_template_id(self, cr, uid, ids, use_template, template_id, composition_mode, model, res_id, context=None):
""" - use_template not set: return default_get
- use_template set in mass_mailing: we cannot render, so return the template values
- use_template set: return rendered values """
if use_template and template_id and composition_mode == 'mass_mail':
def onchange_template_id(self, cr, uid, ids, template_id, composition_mode, model, res_id, context=None):
""" - mass_mailing: we cannot render, so return the template values
- normal mode: return rendered values """
if template_id and composition_mode == 'mass_mail':
values = self.pool.get('email.template').read(cr, uid, template_id, ['subject', 'body_html'], context)
values.pop('id')
elif use_template and template_id:
elif template_id:
# FIXME odo: change the mail generation to avoid attachment duplication
values = self.generate_email_for_composer(cr, uid, template_id, res_id, context=context)
# transform attachments into attachment_ids
values['attachment_ids'] = []
@ -102,39 +80,16 @@ class mail_compose_message(osv.TransientModel):
'datas_fname': attach_fname,
'res_model': model,
'res_id': res_id,
'type': 'binary', # overwrite the context default_value
'type': 'binary', # override default_type from context, possibly meant for another model!
}
values['attachment_ids'].append(ir_attach_obj.create(cr, uid, data_attach, context=context))
else:
values = self.default_get(cr, uid, ['body', 'body_html', 'subject', 'partner_ids', 'attachment_ids'], context=context)
values = self.default_get(cr, uid, ['body', 'subject', 'partner_ids', 'attachment_ids'], context=context)
if values.get('body_html'):
values['body'] = values.pop('body_html')
values.update(use_template=use_template, template_id=template_id)
return {'value': values}
def toggle_template(self, cr, uid, ids, context=None):
""" hit toggle template mode button: calls onchange_use_template to
emulate an on_change, then writes the values to update the form. """
for record in self.browse(cr, uid, ids, context=context):
onchange_res = self.onchange_use_template(cr, uid, ids, not record.use_template,
record.template_id, record.composition_mode, record.model, record.res_id, context=context).get('value', {})
# update partner_ids and attachment_ids
onchange_res['partner_ids'] = [(4, partner_id) for partner_id in onchange_res.pop('partner_ids', [])]
onchange_res['attachment_ids'] = [(4, attachment_id) for attachment_id in onchange_res.pop('attachment_ids', [])]
record.write(onchange_res)
return _reopen(self, record.id, record.model)
def onchange_use_template(self, cr, uid, ids, use_template, template_id, composition_mode, model, res_id, context=None):
""" onchange_use_template (values: True or False). If use_template is
False, we do as an onchange with template_id False for values """
values = self.onchange_template_id(cr, uid, ids, use_template,
template_id, composition_mode, model, res_id, context=context)
# force html when using templates
if use_template:
values['value']['content_subtype'] = 'html'
return values
def save_as_template(self, cr, uid, ids, context=None):
""" hit save as template button: current form value will be a new
template attached to the current document. """
@ -155,7 +110,7 @@ class mail_compose_message(osv.TransientModel):
'attachment_ids': [(6, 0, [att.id for att in record.attachment_ids])]
}
template_id = email_template.create(cr, uid, values, context=context)
record.write(record.onchange_template_id(True, template_id, record.composition_mode, record.model, record.res_id)['value'])
record.write(record.onchange_template_id(template_id, record.composition_mode, record.model, record.res_id)['value'])
return _reopen(self, record.id, record.model)
#------------------------------------------------------
@ -167,7 +122,7 @@ class mail_compose_message(osv.TransientModel):
mail.compose.message, transform email_cc and email_to into partner_ids """
template_values = self.pool.get('email.template').generate_email(cr, uid, template_id, res_id, context=context)
# filter template values
fields = ['body', 'body_html', 'subject', 'email_to', 'email_recipients', 'email_cc', 'attachments']
fields = ['body_html', 'subject', 'email_to', 'email_recipients', 'email_cc', 'attachments']
values = dict((field, template_values[field]) for field in fields if template_values.get(field))
values['body'] = values.pop('body_html', '')
# transform email_to, email_cc into partner_ids

View File

@ -7,25 +7,17 @@
<field name="model">mail.compose.message</field>
<field name="inherit_id" ref="mail.email_compose_message_wizard_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//field[@name='content_subtype']" position="after">
<field name="use_template" invisible="1"
on_change="onchange_use_template(use_template, template_id, composition_mode, model, res_id, context)"/>
</xpath>
<xpath expr="//footer" position="inside">
<group class="oe_right" col="1">
<div attrs="{'invisible':[('use_template','=',False)]}">Use template
<field name="template_id" attrs="{'invisible':[('use_template','=',False)]}"
nolabel="1"
on_change="onchange_template_id(use_template, template_id, composition_mode, model, res_id, context)"/>
</div>
<button icon="/email_template/static/src/img/email_template_save.png"
type="object" name="save_as_template" string="Save as new template" class="oe_link"
help="Save as a new template"
attrs="{'invisible':[('content_subtype','!=','html')]}"/>
</group>
</xpath>
</data>
<xpath expr="//footer" position="inside">
<group class="oe_right" col="1">
<div>Use template
<field name="template_id" nolabel="1"
on_change="onchange_template_id(template_id, composition_mode, model, res_id, context)"/>
</div>
<button icon="/email_template/static/src/img/email_template_save.png"
type="object" name="save_as_template" string="Save as new template" class="oe_link"
help="Save as a new template"/>
</group>
</xpath>
</field>
</record>

View File

@ -437,9 +437,8 @@ class test_mail(test_mail_mockup.TestMailMockups):
# Mail data
_subject = 'Pigs'
_body_text = 'Pigs rules'
_msg_reply = 'Re: Pigs'
_msg_body = '<pre>Pigs rules</pre>'
_body = 'Pigs <b>rule</b>'
_reply_subject = 'Re: Pigs'
_attachments = [
{'name': 'First', 'datas_fname': 'first.txt', 'datas': 'My first attachment'.encode('base64')},
{'name': 'Second', 'datas_fname': 'second.txt', 'datas': 'My second attachment'.encode('base64')}
@ -462,9 +461,9 @@ class test_mail(test_mail_mockup.TestMailMockups):
# 1. Comment group_pigs with body_text and subject
compose_id = mail_compose.create(cr, uid,
{'subject': _subject, 'body_text': _body_text, 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
{'subject': _subject, 'body': _body, 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
{'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_pigs_id,
'default_content_subtype': 'plaintext'})
'default_content_subtype': 'plaintext'})
compose = mail_compose.browse(cr, uid, compose_id)
# Test: mail.compose.message: composition_mode, model, res_id
self.assertEqual(compose.composition_mode, 'comment', 'mail.compose.message incorrect composition_mode')
@ -476,8 +475,8 @@ class test_mail(test_mail_mockup.TestMailMockups):
group_pigs.refresh()
message = group_pigs.message_ids[0]
# Test: mail.message: subject, body inside pre
self.assertEqual(message.subject, False, 'mail.message incorrect subject')
self.assertEqual(message.body, _msg_body, 'mail.message incorrect body')
self.assertEqual(message.subject, _subject, 'mail.message incorrect subject')
self.assertEqual(message.body, _body, 'mail.message incorrect body')
# Test: mail.message: notified_partner_ids = entries in mail.notification: group_pigs fans (a, b) + mail.compose.message partner_ids (c, d)
msg_pids = [partner.id for partner in message.notified_partner_ids]
test_pids = [p_b_id, p_c_id, p_d_id]
@ -495,13 +494,12 @@ class test_mail(test_mail_mockup.TestMailMockups):
{'attachment_ids': [(0, 0, _attachments[0]), (0, 0, _attachments[1])]},
{'default_composition_mode': 'reply', 'default_model': 'mail.thread', 'default_res_id': self.group_pigs_id, 'default_parent_id': message.id})
compose = mail_compose.browse(cr, uid, compose_id)
# Test: model, res_id, parent_id, content_subtype
# Test: model, res_id, parent_id
self.assertEqual(compose.model, 'mail.group', 'mail.compose.message incorrect model')
self.assertEqual(compose.res_id, self.group_pigs_id, 'mail.compose.message incorrect res_id')
self.assertEqual(compose.parent_id.id, message.id, 'mail.compose.message incorrect parent_id')
self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message incorrect content_subtype')
# Test: mail.message: subject as Re:.., body in html, parent_id
self.assertEqual(compose.subject, _msg_reply, 'mail.message incorrect subject')
self.assertEqual(compose.subject, _reply_subject, 'mail.message incorrect subject')
# self.assertIn('Administrator wrote:<blockquote><pre>Pigs rules</pre></blockquote>', compose.body, 'mail.message body is incorrect')
self.assertEqual(compose.parent_id and compose.parent_id.id, message.id, 'mail.message parent_id incorrect')
# Test: mail.message: attachments
@ -518,8 +516,6 @@ class test_mail(test_mail_mockup.TestMailMockups):
{'default_composition_mode': 'mass_mail', 'default_model': 'mail.group', 'default_res_id': False,
'active_ids': [self.group_pigs_id, group_bird_id]})
compose = mail_compose.browse(cr, uid, compose_id)
# Test: content_subtype is html
self.assertEqual(compose.content_subtype, 'html', 'mail.compose.message content_subtype incorrect')
# 2. Post the comment, get created message for each group
mail_compose.send_mail(cr, uid, [compose_id],

View File

@ -21,7 +21,7 @@
from openerp.addons.mail.tests import test_mail_mockup
from openerp.osv.orm import except_orm
from openerp.tools.misc import mute_logger
from openerp.tools import mute_logger
class test_mail_access_rights(test_mail_mockup.TestMailMockups):
@ -52,6 +52,7 @@ class test_mail_access_rights(test_mail_mockup.TestMailMockups):
self.user_raoul = self.res_users.browse(cr, uid, self.user_raoul_id)
self.partner_raoul_id = self.user_raoul.partner_id.id
@mute_logger('openerp.addons.base.ir.ir_model','openerp.osv.orm')
def test_00_mail_message_search_access_rights(self):
""" Test mail_message search override about access rights. """
cr, uid, group_pigs_id = self.cr, self.uid, self.group_pigs_id
@ -85,7 +86,7 @@ class test_mail_access_rights(test_mail_mockup.TestMailMockups):
msg_ids = self.mail_message.search(cr, uid, [('subject', 'like', '_Test')])
self.assertEqual(set([msg_id1, msg_id2, msg_id3, msg_id4, msg_id5, msg_id6, msg_id7, msg_id8]), set(msg_ids), 'mail_message search failed')
@mute_logger('openerp.osv.orm')
@mute_logger('openerp.addons.base.ir.ir_model','openerp.osv.orm')
def test_05_mail_message_read_access_rights(self):
""" Test basic mail_message read access rights. """
cr, uid = self.cr, self.uid
@ -133,8 +134,7 @@ class test_mail_access_rights(test_mail_mockup.TestMailMockups):
self.assertRaises(except_orm, self.mail_message.read,
cr, user_bert_id, message_id)
@mute_logger('openerp.addons.base.ir.ir_model')
@mute_logger('openerp.osv.orm')
@mute_logger('openerp.addons.base.ir.ir_model','openerp.osv.orm')
def test_10_mail_flow_access_rights(self):
""" Test a Chatter-looks alike flow. """
cr, uid = self.cr, self.uid
@ -182,14 +182,14 @@ class test_mail_access_rights(test_mail_mockup.TestMailMockups):
# Do: Bert create a mail.compose.message record, because he uses the wizard
compose_id = mail_compose.create(cr, user_bert_id,
{'subject': 'Subject', 'body_text': 'Body text', 'partner_ids': []},
{'subject': 'Subject', 'body': 'Body text', 'partner_ids': []},
# {'subject': 'Subject', 'body_text': 'Body text', 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
{'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_jobs_id})
mail_compose.send_mail(cr, user_bert_id, [compose_id])
self.user_demo_id = self.registry('ir.model.data').get_object_reference(self.cr, self.uid, 'base', 'user_demo')[1]
compose_id = mail_compose.create(cr, self.user_demo_id,
{'subject': 'Subject', 'body_text': 'Body text', 'partner_ids': []},
{'subject': 'Subject', 'body': 'Body text', 'partner_ids': []},
# {'subject': 'Subject', 'body_text': 'Body text', 'partner_ids': [(4, p_c_id), (4, p_d_id)]},
{'default_composition_mode': 'comment', 'default_model': 'mail.group', 'default_res_id': self.group_jobs_id})
mail_compose.send_mail(cr, self.user_demo_id, [compose_id])

View File

@ -81,7 +81,7 @@ class mail_compose_message(osv.TransientModel):
elif composition_mode == 'comment' and model and res_id:
vals = self.get_record_data(cr, uid, model, res_id, context=context)
elif composition_mode == 'mass_mail' and model and active_ids:
vals = {'model': model, 'res_id': res_id, 'content_subtype': 'html'}
vals = {'model': model, 'res_id': res_id}
else:
vals = {'model': model, 'res_id': res_id}
if composition_mode:
@ -106,16 +106,10 @@ class mail_compose_message(osv.TransientModel):
'mail_compose_message_ir_attachments_rel',
'wizard_id', 'attachment_id', 'Attachments'),
'filter_id': fields.many2one('ir.filters', 'Filters'),
'body_text': fields.text('Plain-text Contents'),
'content_subtype': fields.char('Message content subtype', size=32, readonly=1,
help="Type of message, usually 'html' or 'plain', used to select "\
"plain-text or rich-text contents accordingly"),
}
_defaults = {
'composition_mode': 'comment',
'content_subtype': lambda self, cr, uid, ctx={}: 'html',
'body_text': lambda self, cr, uid, ctx={}: False,
'body': lambda self, cr, uid, ctx={}: '',
'subject': lambda self, cr, uid, ctx={}: False,
'partner_ids': lambda self, cr, uid, ctx={}: [],
@ -172,32 +166,9 @@ class mail_compose_message(osv.TransientModel):
'parent_id': message_data.id,
'subject': reply_subject,
'partner_ids': partner_ids,
'content_subtype': 'html',
}
return result
def toggle_content_subtype(self, cr, uid, ids, context=None):
""" toggle content_subtype: calls onchange_formatting to emulate an
on_change, then writes the value to update the form. """
for record in self.browse(cr, uid, ids, context=context):
content_st_new_value = 'plain' if record.content_subtype == 'html' else 'html'
onchange_res = self.onchange_content_subtype(cr, uid, ids, content_st_new_value, record.model, record.res_id, context=context)
self.write(cr, uid, [record.id], onchange_res['value'], context=context)
return True
def onchange_content_subtype(self, cr, uid, ids, value, model, res_id, context=None):
""" This onchange allows to have some specific behavior when switching
between text or html mode. This method can be overridden.
:param values: 'plain' or 'html'
"""
return {'value': {'content_subtype': value}}
def dummy(self, cr, uid, ids, context=None):
""" TDE: defined to have buttons that do basically nothing. It is
currently impossible to have buttons that do nothing special
in views (if type not specified, considered as 'object'). """
return True
#------------------------------------------------------
# Wizard validation and send
#------------------------------------------------------
@ -218,8 +189,8 @@ class mail_compose_message(osv.TransientModel):
for res_id in res_ids:
# default values, according to the wizard options
post_values = {
'subject': wizard.subject if wizard.content_subtype == 'html' else False,
'body': wizard.body if wizard.content_subtype == 'html' else '<pre>%s</pre>' % tools.ustr(wizard.body_text),
'subject': wizard.subject,
'body': wizard.body,
'parent_id': wizard.parent_id and wizard.parent_id.id,
'partner_ids': [(4, partner.id) for partner in wizard.partner_ids],
'attachments': [(attach.datas_fname or attach.name, base64.b64decode(attach.datas)) for attach in wizard.attachment_ids],

View File

@ -12,7 +12,6 @@
<field name="model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="parent_id" invisible="1"/>
<field name="content_subtype" invisible="1"/>
<!-- visible wizard -->
<label for="partner_ids" string="Recipients"/>
<div>
@ -25,8 +24,7 @@
<field name="partner_ids" widget="many2many_tags_email" placeholder="Add contacts to notify..."
context="{'force_email':True}" required="1"/>
</div>
<field name="subject" placeholder="Subject..."
attrs="{'invisible':[('content_subtype', '=', 'plain')]}"/>
<field name="subject" placeholder="Subject..."/>
</group>
<field name="body"/>
<field name="attachment_ids" widget="many2many_binary"/>

View File

@ -22,6 +22,6 @@
import portal
import mail_mail
import wizard
import acquirer
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -49,8 +49,10 @@ very handy when used in combination with the module 'share'.
'portal_view.xml',
'wizard/portal_wizard_view.xml',
'wizard/share_wizard_view.xml',
'acquirer_view.xml',
],
'demo': ['portal_demo.xml'],
'css': ['static/src/css/portal.css'],
'installable': True,
}

98
addons/portal/acquirer.py Normal file
View File

@ -0,0 +1,98 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import logging
from urllib import quote as quote
from openerp.osv import osv, fields
from openerp.tools.translate import _
from openerp.tools import float_repr
_logger = logging.getLogger(__name__)
try:
from mako.template import Template as MakoTemplate
except ImportError:
_logger.warning("payment_acquirer: mako templates not available, payment acquirer will not work!")
class acquirer(osv.Model):
_name = 'portal.payment.acquirer'
_description = 'Online Payment Acquirer'
_columns = {
'name': fields.char('Name', required=True),
'form_template': fields.text('Payment form template (HTML)', translate=True, required=True),
'visible': fields.boolean('Visible', help="Make this payment acquirer available in portal forms (Customer invoices, etc.)"),
}
_default = {
'visible': True,
}
def render(self, cr, uid, id, object, reference, currency, amount, context=None, **kwargs):
""" Renders the form template of the given acquirer as a mako template """
if not isinstance(id, (int,long)):
id = id[0]
this = self.browse(cr, uid, id)
if context is None:
context = {}
try:
i18n_kind = _(object._description) # may fail to translate, but at least we try
result = MakoTemplate(this.form_template).render_unicode(object=object,
reference=reference,
currency=currency,
amount=amount,
kind=i18n_kind,
quote=quote,
# context kw would clash with mako internals
ctx=context,
format_exceptions=True)
return result.strip()
except Exception:
_logger.exception("failed to render mako template value for payment.acquirer %s: %r", this.name, this.form_template)
return
def _wrap_payment_block(self, cr, uid, html_block, amount, currency, context=None):
payment_header = _('Pay safely online')
amount_str = float_repr(amount, self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))
currency_str = currency.symbol or currency.name
amount = u"%s %s" % ((currency_str, amount_str) if currency.position == 'before' else (amount_str, currency_str))
result = """<div class="payment_acquirers">
<div class="payment_header">
<div class="payment_amount">%s</div>
%s
</div>
%%s
</div>""" % (amount, payment_header)
return result % html_block
def render_payment_block(self, cr, uid, object, reference, currency, amount, context=None, **kwargs):
""" Renders all visible payment acquirer forms for the given rendering context, and
return them wrapped in an appropriate HTML block, ready for direct inclusion
in an OpenERP v7 form view """
acquirer_ids = self.search(cr, uid, [('visible', '=', True)])
if not acquirer_ids:
return
html_forms = []
for this in self.browse(cr, uid, acquirer_ids):
html_forms.append(this.render(object, reference, currency, amount, context=context, **kwargs))
html_block = '\n'.join(filter(None,html_forms))
return self._wrap_payment_block(cr, uid, html_block, amount, currency, context=context)

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="acquirer_form" model="ir.ui.view">
<field name="model">portal.payment.acquirer</field>
<field name="arch" type="xml">
<form string="Payment Acquirer" version="7.0">
<group col="1">
<div class="oe_title">
<label for="name" class="oe_edit_only"/><h1><field name="name"/></h1>
<div class="oe_edit_only"><field name="visible"/><label for="visible"/></div>
</div>
<group string="Form Template">
<div>
<p>
This is an HTML form template to submit a payment through this acquirer.
The template will be rendered with <a href="http://www.makotemplates.org/" target="_blank">Mako</a>, so it may use Mako expressions.
The Mako evaluation context provides:
<ul>
<li>reference: the reference number of the document to pay</li>
<li>kind: the kind of document on which the payment form is rendered (translated to user language, e.g. "Invoice")</li>
<li>currency: the currency record in which the document is issued (e.g. currency.name could be EUR)</li>
<li>amount: the total amount to pay, as a float</li>
<li>object: the document on which the payment form is rendered (usually an invoice or sale order record)</li>
<li>quote(): a method to quote special string character to make them suitable for inclusion in a URL</li>
<li>cr: the current database cursor</li>
<li>uid: the current user id</li>
<li>ctx: the current context dictionary</li>
</ul>
If the template renders to an empty result in a certain context it will be ignored, as if it was inactive.
</p>
</div>
<field name="form_template" nolabel="1" colspan="2"/>
</group>
</group>
</form>
</field>
</record>
<record id="acquirer_list" model="ir.ui.view">
<field name="model">portal.payment.acquirer</field>
<field name="arch" type="xml">
<tree string="Payment Acquirers">
<field name="name"/>
<field name="visible"/>
</tree>
</field>
</record>
<record id="acquirer_search" model="ir.ui.view">
<field name="model">portal.payment.acquirer</field>
<field name="arch" type="xml">
<search>
<field name="name"/>
</search>
</field>
</record>
<!-- Acquirers list action is visible in Invoicing Settings -->
<record model="ir.actions.act_window" id="action_acquirer_list">
<field name="name">Payment Acquirers</field>
<field name="res_model">portal.payment.acquirer</field>
</record>
</data>
</openerp>

View File

@ -27,6 +27,23 @@
<field name="res_id" ref="company_jobs"/>
<field name="view_mode">form</field>
</record>
<record id="paypal_acquirer" model="portal.payment.acquirer">
<field name="name">Paypal</field>
<field name="form_template"><![CDATA[
% if object.company_id.paypal_account:
<form action="https://www.paypal.com/cgi-bin/webscr" method="post" target="_blank">
<input type="hidden" name="cmd" value="_xclick"/>
<input type="hidden" name="business" value="${object.company_id.paypal_account}"/>
<input type="hidden" name="item_name" value="${object.company_id.name} ${kind.title()} ${reference}"/>
<input type="hidden" name="amount" value="${amount}"/>
<input type="hidden" name="currency_code" value="${currency.name}"/>
<input type="image" name="submit" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</form>
% endif
]]></field>
</record>
</data>
</openerp>

View File

@ -4,3 +4,5 @@ access_res_partner,res.partner,base.model_res_partner,portal.group_portal,1,0,0,
access_res_partner_address,res.partner_address,base.model_res_partner_address,portal.group_portal,1,0,0,0
access_res_partner_category,res.partner_category,base.model_res_partner_category,portal.group_portal,1,0,0,0
access_res_partner_title,res.partner_title,base.model_res_partner_title,portal.group_portal,1,0,0,0
access_acquirer,portal.payment.acquirer,portal.model_portal_payment_acquirer,,1,0,0,0
access_acquirer_all,portal.payment.acquirer,portal.model_portal_payment_acquirer,base.group_system,1,1,1,1

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
4 access_res_partner_address res.partner_address base.model_res_partner_address portal.group_portal 1 0 0 0
5 access_res_partner_category res.partner_category base.model_res_partner_category portal.group_portal 1 0 0 0
6 access_res_partner_title res.partner_title base.model_res_partner_title portal.group_portal 1 0 0 0
7 access_acquirer portal.payment.acquirer portal.model_portal_payment_acquirer 1 0 0 0
8 access_acquirer_all portal.payment.acquirer portal.model_portal_payment_acquirer base.group_system 1 1 1 1

View File

@ -0,0 +1,81 @@
.openerp .oe_application .oe_form_sheetbg {
/* Establish a stacking context on top of which the
payment_acquirers::before element can be positioned */
position: relative;
z-index: 0;
}
.openerp .payment_acquirers {
margin: -40px 0 -32px -24px;
position: relative;
padding: 10px 15px;
right: -153px;
background: #729FCF;
background-image: -webkit-gradient(linear, left top, left bottom, from(#729FCF), to(#3465A4));
background-image: -webkit-linear-gradient(top, #729FCF, #3465A4);
background-image: -moz-linear-gradient(top, #729FCF, #3465A4);
background-image: -ms-linear-gradient(top, #729FCF, #3465A4);
background-image: -o-linear-gradient(top, #729FCF, #3465A4);
background-image: linear-gradient(to bottom, #729FCF, #3465A4);
border-bottom: 1px solid #043574;
-webkit-box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.45);
}
.openerp .payment_acquirers form {
display: inline-block;
vertical-align: top;
}
.openerp .payment_acquirers form input,
.openerp .payment_acquirers form textarea,
.openerp .payment_acquirers form select
{
-webkit-box-shadow: none;
box-shadow: none;
background: transparent;
border: none;
padding: none;
}
.openerp .payment_acquirers::after {
content: " ";
display: block;
width: 10px;
height: 20px;
position: absolute;
bottom: 0;
right: 1px;
margin-bottom: -6px;
background: #043574;
-webkit-transform: skewY(-45deg);
-moz-transform: skewY(-45deg);
-ms-transform: skewY(-45deg);
-o-transform: skewY(-45deg);
transform: skewY(-45deg);
-webkit-box-shadow: inset 1px -1px 2px black, -1px 1px 3px black;
box-shadow: inset 1px -1px 2px black, -1px 1px 3px black;
/* push it under all its siblings, just on top of its root
in the z-index stack: div.oe_form_sheetbg */
z-index: -1;
}
.openerp .payment_acquirers .payment_header {
display: inline-block;
font-weight: bold;
font-size: 110%;
padding-right: 15px;
color: white;
text-shadow: 0 1px 1px #729FCF, 0 -1px 1px #3465A4;
}
.openerp .payment_acquirers .payment_header .payment_amount {
font-size: 130%;
padding: 6px 0px;
}

View File

@ -19,3 +19,5 @@
#
##############################################################################
import portal_sale
import res_config

View File

@ -26,18 +26,34 @@
'category': 'Tools',
'complexity': 'easy',
'description': """
This module adds sale menu and features to your portal if sale and portal are installed.
========================================================================================
This module adds a Sales menu to your portal as soon as sale and portal are installed.
======================================================================================
After installing this module, portal users will be able to access their own documents
via the following menus:
- Quotations
- Sale Orders
- Delivery Orders
- Products (public ones)
- Invoices
- Payments/Refunds
If online payment acquirers are configured, portal users will also be given the opportunity to
pay online on their Sale Orders and Invoices that are not paid yet. Paypal is included
by default, you simply need to configure a Paypal account in the Accounting/Invoicing settings.
""",
'author': 'OpenERP SA',
'depends': ['sale_stock','portal'],
'data': [
'security/portal_security.xml',
'portal_sale_view.xml',
'portal_sale_data.xml',
'res_config_view.xml',
'security/ir.model.access.csv',
],
'installable': True,
'auto_install': True,
'category': 'Hidden',
}
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import osv, fields
class sale_order(osv.Model):
_inherit = 'sale.order'
# make the real method inheritable
_payment_block_proxy = lambda self,*a,**kw: self._portal_payment_block(*a, **kw)
_columns = {
'portal_payment_options': fields.function(_payment_block_proxy, type="html", string="Portal Payment Options"),
}
def _portal_payment_block(self, cr, uid, ids, fieldname, arg, context=None):
result = dict.fromkeys(ids, False)
payment_acquirer = self.pool.get('portal.payment.acquirer')
for this in self.browse(cr, uid, ids, context=context):
if this.state not in ('draft','cancel') and not this.invoiced:
result[this.id] = payment_acquirer.render_payment_block(cr, uid, this, this.name,
this.pricelist_id.currency_id, this.amount_total, context=context)
return result
def action_quotation_send(self, cr, uid, ids, context=None):
''' Override to use a modified template that includes a portal signup link '''
action_dict = super(sale_order, self).action_quotation_send(cr, uid, ids, context=context)
try:
template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal_sale', 'email_template_edi_sale')[1]
# assume context is still a dict, as prepared by super
ctx = action_dict['context']
ctx['default_template_id'] = template_id
ctx['default_use_template'] = True
except Exception:
pass
return action_dict
class account_invoice(osv.Model):
_inherit = 'account.invoice'
# make the real method inheritable
_payment_block_proxy = lambda self,*a,**kw: self._portal_payment_block(*a, **kw)
_columns = {
'portal_payment_options': fields.function(_payment_block_proxy, type="html", string="Portal Payment Options"),
}
def _portal_payment_block(self, cr, uid, ids, fieldname, arg, context=None):
result = dict.fromkeys(ids, False)
payment_acquirer = self.pool.get('portal.payment.acquirer')
for this in self.browse(cr, uid, ids, context=context):
if this.state not in ('draft','done') and not this.reconciled:
result[this.id] = payment_acquirer.render_payment_block(cr, uid, this, this.number,
this.currency_id, this.residual, context=context)
return result
def action_invoice_sent(self, cr, uid, ids, context=None):
''' Override to use a modified template that includes a portal signup link '''
action_dict = super(account_invoice, self).action_invoice_sent(cr, uid, ids, context=context)
try:
template_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal_sale', 'email_template_edi_invoice')[1]
# assume context is still a dict, as prepared by super
ctx = action_dict['context']
ctx['default_template_id'] = template_id
ctx['default_use_template'] = True
except Exception:
pass
return action_dict

View File

@ -0,0 +1,212 @@
<openerp>
<!-- Mail template is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<!--Email template -->
<record id="email_template_edi_sale" model="email.template">
<field name="name">Sale Order - Send by Email (Portal)</field>
<field name="email_from">${object.user_id.email or ''}</field>
<field name="subject">${object.company_id.name} ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'} (Ref ${object.name or 'n/a' })</field>
<field name="email_recipients">${object.partner_invoice_id.id}</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="auto_delete" eval="True"/>
<field name="report_template" ref="sale.report_sale_order"/>
<field name="report_name">${(object.name or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}</field>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
<p>Hello${object.partner_id.name and ' ' or ''}${object.partner_id.name or ''},</p>
<p>Here is your ${object.state in ('draft', 'sent') and 'quotation' or 'order confirmation'} from ${object.company_id.name}: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Order number: <strong>${object.name}</strong><br />
&nbsp;&nbsp;Order total: <strong>${object.amount_total} ${object.pricelist_id.currency_id.name}</strong><br />
&nbsp;&nbsp;Order date: ${object.date_order}<br />
% if object.origin:
&nbsp;&nbsp;Order reference: ${object.origin}<br />
% endif
% if object.client_order_ref:
&nbsp;&nbsp;Your reference: ${object.client_order_ref}<br />
% endif
% if object.user_id:
&nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Order%20${object.name}">${object.user_id.name}</a>
% endif
</p>
<%
action = 'portal_sale.action_quotations_portal' if object.state in ('draft', 'sent') else 'portal_sale.action_orders_portal'
object.partner_id.signup_prepare()
signup_url = object.partner_id._get_signup_url_for_action(action=action,view_type='form',res_id=object.id)[object.partner_id.id]
%>
% if signup_url:
<p>
You can access this document and pay online via our Customers Portal:
</p>
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="${signup_url}">View ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'}</a>
% endif
% if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account and object.state != 'draft':
<%
comp_name = quote(object.company_id.name)
order_name = quote(object.name)
paypal_account = quote(object.company_id.paypal_account)
order_amount = quote(str(object.residual))
cur_name = quote(object.pricelist_id.currency_id.name)
paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&amp;business=%s&amp;item_name=%s%%20Order%%20%s" \
"&amp;invoice=%s&amp;amount=%s&amp;currency_code=%s&amp;button_subtype=services&amp;no_note=1" \
"&amp;bn=OpenERP_Order_PayNow_%s" % \
(paypal_account,comp_name,order_name,order_name,order_amount,cur_name,cur_name)
%>
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${paypal_url}">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
% endif
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #FFF;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br/>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br/>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br/>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
%endif
<p></p>
</div>
</div>
]]></field>
</record>
<record id="email_template_edi_invoice" model="email.template">
<field name="name">Invoice - Send by Email (Portal)</field>
<field name="email_from">${object.user_id.email or object.company_id.email or 'noreply@localhost'}</field>
<field name="subject">${object.company_id.name} Invoice (Ref ${object.number or 'n/a' })</field>
<field name="email_recipients">${object.partner_id.id}</field>
<field name="model_id" ref="account.model_account_invoice"/>
<field name="auto_delete" eval="True"/>
<field name="report_template" ref="account.account_invoices"/>
<field name="report_name">Invoice_${(object.number or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}</field>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
<p>Hello${object.partner_id.name and ' ' or ''}${object.partner_id.name or ''},</p>
<p>A new invoice is available for ${object.partner_id.name}: </p>
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Invoice number: <strong>${object.number}</strong><br />
&nbsp;&nbsp;Invoice total: <strong>${object.amount_total} ${object.currency_id.name}</strong><br />
&nbsp;&nbsp;Invoice date: ${object.date_invoice}<br />
% if object.origin:
&nbsp;&nbsp;Order reference: ${object.origin}<br />
% endif
% if object.user_id:
&nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Invoice%20${object.number}">${object.user_id.name}</a>
% endif
</p>
<%
action = 'portal_sale.portal_action_invoices'
object.partner_id.signup_prepare()
signup_url = object.partner_id._get_signup_url_for_action(action=action,view_type='form',res_id=object.id)[object.partner_id.id]
%>
% if signup_url:
<p>
You can access the invoice document and pay online via our Customers Portal:
</p>
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #DDD; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="${signup_url}">View Invoice</a>
% endif
% if object.company_id.paypal_account and object.type in ('out_invoice', 'in_refund'):
<%
comp_name = quote(object.company_id.name)
inv_number = quote(object.number)
paypal_account = quote(object.company_id.paypal_account)
inv_amount = quote(str(object.amount_residual))
cur_name = quote(object.currency_id.name)
paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&amp;business=%s&amp;item_name=%s%%20Invoice%%20%s&amp;" \
"invoice=%s&amp;amount=%s&amp;currency_code=%s&amp;button_subtype=services&amp;no_note=1&amp;bn=OpenERP_Invoice_PayNow_%s" % \
(paypal_account,comp_name,inv_number,inv_number,inv_amount,cur_name,cur_name)
%>
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${paypal_url}">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
% endif
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #FFF;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br/>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br/>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br/>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
%endif
<p></p>
</div>
</div>
]]></field>
</record>
</data>
</openerp>

View File

@ -1,12 +1,35 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Add payment options to sale.order and invoice forms -->
<record model="ir.ui.view" id="sale_order_form_payment">
<field name="name">sale.order.form.payment</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<notebook version="7.0" position="before">
<field name="portal_payment_options" groups="portal_sale.group_payment_options"/>
</notebook>
</field>
</record>
<record model="ir.ui.view" id="invoice_form_payment">
<field name="name">account.invoice.form.payment</field>
<field name="model">account.invoice</field>
<field name="inherit_id" ref="account.invoice_form"/>
<field name="arch" type="xml">
<notebook version="7.0" position="before">
<field name="portal_payment_options" groups="portal_sale.group_payment_options"/>
</notebook>
</field>
</record>
<!--
Override the original action to set another help field and/or
another context field, more suited for portal members
-->
<record id="action_order_tree5" model="ir.actions.act_window">
<record id="action_quotations_portal" model="ir.actions.act_window">
<field name="name">Quotations</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
@ -16,8 +39,8 @@
<field name="help">You don't have any quotation.</field>
</record>
<record id="action_order_form" model="ir.actions.act_window">
<field name="name">Sales Orders</field>
<record id="action_orders_portal" model="ir.actions.act_window">
<field name="name">Sale Orders</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
<field name="view_mode">tree,form,calendar,graph</field>
@ -48,8 +71,8 @@
<field name="help">There are no public products.</field>
</record>
<record id="action_invoice_tree1" model="ir.actions.act_window">
<field name="name">Customer Invoices</field>
<record id="portal_action_invoices" model="ir.actions.act_window">
<field name="name">Invoices</field>
<field name="res_model">account.invoice</field>
<field name="view_mode">tree,form,calendar,graph</field>
<field name="domain">[('type','=','out_invoice')]</field>
@ -57,9 +80,21 @@
<field name="search_view_id" ref="account.view_account_invoice_filter"/>
<field name="help">You don't have any invoice.</field>
</record>
<record id="portal_action_invoices_tree_spec" model="ir.actions.act_window.view">
<field name="act_window_id" ref="portal_action_invoices"/>
<field name="view_id" ref="account.invoice_tree"/>
<field name="view_mode">tree</field>
<field name="sequence" eval="0"/>
</record>
<record id="portal_action_invoices_form_spec" model="ir.actions.act_window.view">
<field name="act_window_id" ref="portal_action_invoices"/>
<field name="view_id" ref="account.invoice_form"/>
<field name="view_mode">form</field>
<field name="sequence" eval="1"/>
</record>
<record id="action_vendor_receipt" model="ir.actions.act_window">
<field name="name">Customer Payment</field>
<record id="portal_action_vouchers" model="ir.actions.act_window">
<field name="name">Refunds/Payments</field>
<field name="res_model">account.voucher</field>
<field name="domain">[('journal_id.type', 'in', ['bank', 'cash']), ('type','=','receipt')]</field>
<field name="context">{'type':'receipt'}</field>
@ -68,19 +103,17 @@
<field name="help">You don't have any refunds or payments.</field>
</record>
<menuitem name="Quotations" id="portal_quotations" parent="portal.portal_orders"
action="action_order_tree5" sequence="10"/>
<menuitem name="Sales Orders" id="portal_sales_orders" parent="portal.portal_orders"
action="action_order_form" sequence="20"/>
<menuitem name="Delivery Orders" id="portal_delivery" parent="portal.portal_orders"
<menuitem id="portal_quotations" parent="portal.portal_orders"
action="action_quotations_portal" sequence="10"/>
<menuitem id="portal_sales_orders" parent="portal.portal_orders"
action="action_orders_portal" sequence="20"/>
<menuitem id="portal_delivery" parent="portal.portal_orders"
action="action_picking_tree" sequence="30"/>
<menuitem name="Products" id="portal_products" parent="portal.portal_orders"
<menuitem id="portal_products" parent="portal.portal_orders"
action="product_normal_action" sequence="40"/>
<menuitem name="Invoice" id="portal_invoices" parent="portal.portal_invoices_payements"
action="action_invoice_tree1" sequence="10"/>
<menuitem name="Refund/Payments" id="portal_payments" parent="portal.portal_invoices_payements"
action="action_vendor_receipt" sequence="20"/>
<menuitem id="portal_invoices" parent="portal.portal_invoices_payements"
action="portal_action_invoices" sequence="10"/>
<menuitem id="portal_payments" parent="portal.portal_invoices_payements"
action="portal_action_vouchers" sequence="20"/>
</data>
</openerp>

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.osv import fields, osv
class sale_portal_config_settings(osv.TransientModel):
_inherit = 'account.config.settings'
_columns = {
'group_payment_options': fields.boolean('Show payment buttons to employees too',
implied_group='portal_sale.group_payment_options',
help="Show online payment options on Sale Orders and Customer Invoices to employees. "
"If not checked, these options are only visible to portal users."),
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0"?>
<openerp>
<data>
<!-- Add payment options to sale.order and invoice forms -->
<record model="ir.ui.view" id="portal_sale_payment_option_config">
<field name="model">account.config.settings</field>
<field name="inherit_id" ref="account.view_account_config_settings"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='bank_cash']/div" version="7.0" position="inside">
<div>
<field name="group_payment_options" class="oe_inline"/>
<label for="group_payment_options"/>
<button name='%(portal.action_acquirer_list)d' type="action"
string="Configure payment acquiring methods" class="oe_link"/>
</div>
</xpath>
</field>
</record>
</data>
</openerp>

View File

@ -2,6 +2,18 @@
<openerp>
<data noupdate="1">
<record id="group_payment_options" model="res.groups">
<field name="name">View Online Payment Options</field>
<field name="category_id" ref="base.module_category_hidden"/>
<field name="comment">Members of this group see the online payment options
on Sale Orders and Customer Invoices. These options are meant for customers who are accessing
their documents through the portal.</field>
</record>
<!-- Make the payment_options group implied for all portal users -->
<record id="portal.group_portal" model="res.groups">
<field name="implied_ids" eval="[(4,ref('group_payment_options'))]"/>
</record>
<!-- Sale Portal Access Rules -->
<record id="portal_sale_order_user_rule" model="ir.rule">
<field name="name">Portal Personal Quotations/Sales Orders</field>

View File

@ -100,6 +100,23 @@ class product_uom(osv.osv):
def _factor_inv_write(self, cursor, user, id, name, value, arg, context=None):
return self.write(cursor, user, id, {'factor': self._compute_factor_inv(value)}, context=context)
def name_create(self, cr, uid, name, context=None):
""" The UoM category and factor are required, so we'll have to add temporary values
for imported UoMs """
uom_categ = self.pool.get('product.uom.categ')
# look for the category based on the english name, i.e. no context on purpose!
# TODO: should find a way to have it translated but not created until actually used
categ_misc = 'Unsorted/Imported Units'
categ_id = uom_categ.search(cr, uid, [('name', '=', categ_misc)])
if categ_id:
categ_id = categ_id[0]
else:
categ_id, _ = uom_categ.name_create(cr, uid, categ_misc)
uom_id = self.create(cr, uid, {self._rec_name: name,
'category_id': categ_id,
'factor': 1})
return self.name_get(cr, uid, [uom_id], context=context)[0]
def create(self, cr, uid, data, context=None):
if 'factor_inv' in data:
if data['factor_inv'] <> 1:

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2011 OpenERP S.A. <http://openerp.com>
# Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,13 +19,8 @@
#
##############################################################################
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from osv import fields, osv, orm
from openerp.osv import osv
from edi import EDIMixin
from edi.models import edi
from tools import DEFAULT_SERVER_DATE_FORMAT
from tools.translate import _
PURCHASE_ORDER_LINE_EDI_STRUCT = {
@ -62,16 +57,6 @@ PURCHASE_ORDER_EDI_STRUCT = {
class purchase_order(osv.osv, EDIMixin):
_inherit = 'purchase.order'
def wkf_send_rfq(self, cr, uid, ids, context=None):
""""Override this method to add a link to mail"""
if context is None:
context = {}
purchase_objs = self.browse(cr, uid, ids, context=context)
edi_token = self.pool.get('edi.document').export_edi(cr, uid, purchase_objs, context = context)[0]
web_root_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
ctx = dict(context, edi_web_url_view=edi.EDI_VIEW_WEB_URL % (web_root_url, cr.dbname, edi_token))
return super(purchase_order, self).wkf_send_rfq(cr, uid, ids, context=ctx)
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
"""Exports a purchase order"""
edi_struct = dict(edi_struct or PURCHASE_ORDER_EDI_STRUCT)
@ -106,22 +91,26 @@ class purchase_order(osv.osv, EDIMixin):
# the desired company among the user's allowed companies
self._edi_requires_attributes(('company_id','company_address'), edi_document)
res_partner_obj = self.pool.get('res.partner')
res_partner = self.pool.get('res.partner')
# imported company_address = new partner address
src_company_id, src_company_name = edi_document.pop('company_id')
address_info = edi_document.pop('company_address')
address_info['customer'] = True
if 'name' not in address_info:
address_info['name'] = src_company_name
address_id = res_partner_obj.edi_import(cr, uid, address_info, context=context)
xid, company_name = edi_document.pop('company_id')
# Retrofit address info into a unified partner info (changed in v7 - used to keep them separate)
company_address_edi = edi_document.pop('company_address')
company_address_edi['name'] = company_name
company_address_edi['is_company'] = True
company_address_edi['__import_model'] = 'res.partner'
company_address_edi['__id'] = xid # override address ID, as of v7 they should be the same anyway
if company_address_edi.get('logo'):
company_address_edi['image'] = company_address_edi.pop('logo')
company_address_edi['supplier'] = True
partner_id = res_partner.edi_import(cr, uid, company_address_edi, context=context)
# modify edi_document to refer to new partner/address
partner_address = res_partner_obj.browse(cr, uid, address_id, context=context)
edi_document.pop('partner_address', False) # ignored
edi_document['partner_id'] = self.edi_m2o(cr, uid, partner_address, context=context)
return address_id
# modify edi_document to refer to new partner
partner = res_partner.browse(cr, uid, partner_id, context=context)
partner_edi_m2o = self.edi_m2o(cr, uid, partner, context=context)
edi_document['partner_id'] = partner_edi_m2o
edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
return partner_id
def _edi_get_pricelist(self, cr, uid, partner_id, currency, context=None):
# TODO: refactor into common place for purchase/sale, e.g. into product module

View File

@ -1,18 +1,6 @@
<?xml version="1.0" ?>
<openerp>
<data>
<!--Export edi document -->
<!--
<record id="ir_actions_server_edi_purchase" model="ir.actions.server">
<field name="code">if not object.partner_id.opt_out: object.edi_export_and_email(template_ext_id='purchase.email_template_edi_purchase', context=context)</field>
<field name="state">code</field>
<field name="type">ir.actions.server</field>
<field name="model_id" ref="purchase.model_purchase_order"/>
<field name="condition">True</field>
<field name="name">Auto-email confirmed purchase orders</field>
</record>
-->
<!-- EDI related Email Templates menu -->
<record model="ir.actions.act_window" id="action_email_templates">
<field name="name">Email Templates</field>
@ -25,24 +13,19 @@
</record>
</data>
<!-- Mail template and workflow bindings are done in a NOUPDATE block
<!-- Mail template are declared in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<!-- bind the mailing server action to purchase.order confirmed activity -->
<!--
<record id="purchase.act_confirmed" model="workflow.activity">
<field name="action_id" ref="ir_actions_server_edi_purchase"/>
</record>
-->
<!--Email template -->
<record id="email_template_edi_purchase" model="email.template">
<field name="name">Automated Purchase Order Notification Mail</field>
<field name="name">Purchase Order - Send by mail</field>
<field name="email_from">${object.validator.email or ''}</field>
<field name="subject">${object.company_id.name} Order (Ref ${object.name or 'n/a' })</field>
<field name="email_recipients">${object.partner_id.id}</field>
<field name="model_id" ref="purchase.model_purchase_order"/>
<field name="auto_delete" eval="True"/>
<field name="report_template" ref="report_purchase_quotation"/>
<field name="report_name">RFQ_${(object.name or '').replace('/','_')}</field>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
@ -61,15 +44,11 @@
% if object.partner_ref:
&nbsp;&nbsp;Your reference: ${object.partner_ref}<br />
% endif
% if object.user_id:
&nbsp;&nbsp;Your contact: <a href="mailto:${object.validator.email or ''}?subject=Order%20${object.name}">${object.validator.name}</a>
% endif
</p>
<p>
You can view the ${object.state in ('draft', 'sent') and 'request for quotation' or 'order confirmation'} document and download it using the following link:
</p>
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #FFF; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="${ctx.get('edi_web_url_view') or ''}">View Order</a>
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you!</p>

View File

@ -384,29 +384,32 @@ class purchase_order(osv.osv):
'''
This function opens a window to compose an email, with the edi purchase template message loaded by default
'''
mod_obj = self.pool.get('ir.model.data')
template = mod_obj.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')
template_id = template and template[1] or False
res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
res_id = res and res[1] or False
ir_model_data = self.pool.get('ir.model.data')
try:
template_id = ir_model_data.get_object_reference(cr, uid, 'purchase', 'email_template_edi_purchase')[1]
except ValueError:
template_id = False
try:
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError:
compose_form_id = False
ctx = dict(context)
ctx.update({
'default_model': 'purchase.order',
'default_res_id': ids[0],
'default_use_template': True,
'default_use_template': bool(template_id),
'default_template_id': template_id,
'default_composition_mode': 'comment',
})
})
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(res_id, 'form')],
'view_id': res_id,
'type': 'ir.actions.act_window',
'views': [(compose_form_id, 'form')],
'view_id': compose_form_id,
'target': 'new',
'context': ctx,
'nodestroy': True,
}
#TODO: implement messages system

View File

@ -27,36 +27,36 @@
-
Then I export the purchase order via EDI
-
!python {model: edi.document}: |
order_pool = self.pool.get('purchase.order')
order = order_pool.browse(cr, uid, ref("purchase_order_edi_1"))
token = self.export_edi(cr, uid, [order])
assert token, 'Invalid EDI Token'
!python {model: edi.edi}: |
import json
order_pool = self.pool.get('purchase.order')
order = order_pool.browse(cr, uid, ref("purchase_order_edi_1"))
edi_doc = self.generate_edi(cr, uid, [order])
assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict'
-
Then I import a sample EDI document of a sale order
Then I import a sample EDI document of a sale order (v7.0)
-
!python {model: edi.document}: |
!python {model: edi.edi}: |
purchase_order_pool = self.pool.get('purchase.order')
edi_document = {
"__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_test",
"__id": "sale:724f9v70-dv70-1v70-8v70-701a04e25v70.sale_order_test",
"__module": "sale",
"__model": "sale.order",
"__import_module": "purchase",
"__import_model": "purchase.order",
"__version": [6,1,0],
"__version": [7,0,0],
"name": "SO008",
"currency": {
"__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.EUR",
"__id": "base:724f9v70-dv70-1v70-8v70-701a04e25v70.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
"date_order": "2011-09-13",
"partner_id": ["sale:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_test22", "Junjun wala"],
"partner_id": ["sale:724f9v70-dv70-1v70-8v70-701a04e25v70.res_partner_test22", "Junjun wala"],
"partner_address": {
"__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_address_7wdsjasdjh",
"__id": "base:724f9v70-dv70-1v70-8v70-701a04e25v70.res_partner_address_7wdsjasdjh",
"__module": "base",
"__model": "res.partner",
"phone": "(+32).81.81.37.00",
@ -65,48 +65,48 @@
"zip": "1367",
"country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
},
"company_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_company", "Supplier S.A."],
"company_id": ["base:724f9v70-dv70-1v70-8v70-701a04e25v70.main_company", "Supplier S.A."],
"company_address": {
"__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_address",
"__id": "base:724f9v70-dv70-1v70-8v70-701a04e25v70.main_address",
"__module": "base",
"__model": "res.partner",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.be", "Belgium"],
"country_id": ["base:724f9v70-dv70-1v70-8v70-701a04e25v70.be", "Belgium"],
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"street2": "mailbox 34",
"bank_ids": [
["base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_bank-XiwqnxKWzGbp","Guys bank: 123477777-156113"]
["base:724f9v70-dv70-1v70-8v70-701a04e25v70.res_partner_bank-XiwqnxKWzGbp","Guys bank: 123477777-156113"]
],
},
"order_line": [{
"__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeuI-SSP0",
"__id": "sale:724f9v70-dv70-1v70-8v70-701a04e25v70.sale_order_line-LXEqeuI-SSP0",
"__module": "sale",
"__model": "sale.order.line",
"__import_module": "purchase",
"__import_model": "purchase.order.line",
"name": "PC Assemble SC234",
"product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "Unit"],
"product_uom": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_uom_unit", "Unit"],
"product_qty": 1.0,
"date_planned": "2011-09-30",
"sequence": 10,
"price_unit": 150.0,
"product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_3", "[PCSC234] PC Assemble SC234"],
"product_id": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_product_3", "[PCSC234] PC Assemble SC234"],
},
{
"__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeadasdad",
"__id": "sale:724f9v70-dv70-1v70-8v70-701a04e25v70.sale_order_line-LXEqeadasdad",
"__module": "sale",
"__model": "sale.order.line",
"__import_module": "purchase",
"__import_model": "purchase.order.line",
"name": "PC on Demand",
"product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "Unit"],
"product_uom": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_uom_unit", "Unit"],
"product_qty": 10.0,
"sequence": 11,
"date_planned": "2011-09-15",
"price_unit": 20.0,
"product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_5", "[PC-DEM] PC on Demand"],
"product_id": ["product:724f9v70-dv70-1v70-8v70-701a04e25v70.product_product_5", "[PC-DEM] PC on Demand"],
}],
}
new_purchase_order_id = purchase_order_pool.edi_import(cr, uid, edi_document, context=context)
@ -114,6 +114,7 @@
order_new = purchase_order_pool.browse(cr, uid, new_purchase_order_id)
# check bank info on partner
assert order_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a purchase order"
assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = order_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Guys bank: 123477777-156113", 'Expected "Guys bank: 123477777-156113", got %s' % bank_info.acc_number
@ -133,3 +134,104 @@
assert purchase_line.product_qty == 10 , "product qty is not same"
else:
raise AssertionError('unknown order line: %s' % purchase_line)
-
"Then I import a sample EDI document of a sale order (v6.1 - to test backwards compatibility)"
-
!python {model: edi.edi}: |
purchase_order_pool = self.pool.get('purchase.order')
edi_document = {
"__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_test",
"__module": "sale",
"__model": "sale.order",
"__import_module": "purchase",
"__import_model": "purchase.order",
"__version": [6,1,0],
"name": "SO08v61",
"currency": {
"__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
"date_order": "2011-09-13",
"partner_id": ["sale:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_test22", "Junjun wala"],
"partner_address": {
"__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_address_7wdsjasdjh",
"__module": "base",
"__model": "res.partner.address",
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
},
"company_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_company", "Supplier S.A."],
"company_address": {
"__id": "base:724f93ec-ddd0-11e0-88ec-701a04e25543.main_address",
"__module": "base",
"__model": "res.partner.address",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:724f93ec-ddd0-11e0-88ec-701a04e25543.be", "Belgium"],
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"street2": "mailbox 34",
"bank_ids": [
["base:724f93ec-ddd0-11e0-88ec-701a04e25543.res_partner_bank-XiwqnxKWzGbp","Another bank: 123477700-156113"]
],
},
"order_line": [{
"__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeuI-SSP0",
"__module": "sale",
"__model": "sale.order.line",
"__import_module": "purchase",
"__import_model": "purchase.order.line",
"name": "Basic PC",
"product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"],
"product_qty": 1.0,
"date_planned": "2011-09-30",
"sequence": 10,
"price_unit": 150.0,
"product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc1", "[PC1] Basic PC"],
},
{
"__id": "sale:724f93ec-ddd0-11e0-88ec-701a04e25543.sale_order_line-LXEqeadasdad",
"__module": "sale",
"__model": "sale.order.line",
"__import_module": "purchase",
"__import_model": "purchase.order.line",
"name": "Medium PC",
"product_uom": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_uom_unit", "PCE"],
"product_qty": 10.0,
"sequence": 11,
"date_planned": "2011-09-15",
"price_unit": 20.0,
"product_id": ["product:724f93ec-ddd0-11e0-88ec-701a04e25543.product_product_pc3", "[PC3] Medium PC"],
}],
}
new_purchase_order_id = purchase_order_pool.edi_import(cr, uid, edi_document, context=context)
assert new_purchase_order_id, 'Purchase order import failed'
order_new = purchase_order_pool.browse(cr, uid, new_purchase_order_id)
# check bank info on partner
assert order_new.partner_id.supplier, "Imported partner should be a supplier, as we just imported the document as a purchase order"
assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = order_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Another bank: 123477700-156113", 'Expected "Another bank: 123477700-156113", got %s' % bank_info.acc_number
assert order_new.pricelist_id.name == 'Default Purchase Pricelist' , "Default Purchase Pricelist was not automatically assigned"
assert order_new.amount_total == 350, "Amount total is not same"
assert order_new.amount_untaxed == 350, "untaxed amount is not same"
assert len(order_new.order_line) == 2, "Purchase order lines number mismatch"
for purchase_line in order_new.order_line:
if purchase_line.name == 'Basic PC':
assert purchase_line.product_uom.name == "PCE" , "uom is not same"
assert purchase_line.price_unit == 150 , "unit price is not same, got %s, expected 150"%(purchase_line.price_unit,)
assert purchase_line.product_qty == 1 , "product qty is not same"
elif purchase_line.name == 'Medium PC':
assert purchase_line.product_uom.name == "PCE" , "uom is not same"
assert purchase_line.price_unit == 20 , "unit price is not same, got %s, expected 20"%(purchase_line.price_unit,)
assert purchase_line.product_qty == 10 , "product qty is not same"
else:
raise AssertionError('unknown order line: %s' % purchase_line)

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2011 OpenERP S.A. <http://openerp.com>
# Copyright (c) 2011-2012 OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,13 +19,8 @@
#
##############################################################################
from datetime import datetime, timedelta
from dateutil.relativedelta import relativedelta
from osv import fields, osv, orm
from openerp.osv import osv
from edi import EDIMixin
from edi.models import edi
from tools import DEFAULT_SERVER_DATE_FORMAT
SALE_ORDER_LINE_EDI_STRUCT = {
'sequence': True,
@ -65,15 +60,6 @@ SALE_ORDER_EDI_STRUCT = {
class sale_order(osv.osv, EDIMixin):
_inherit = 'sale.order'
def action_quotation_send(self, cr, uid, ids, context=None):
if context is None:
context = {}
sale_objs = self.browse(cr, uid, ids, context=context)
edi_token = self.pool.get('edi.document').export_edi(cr, uid, sale_objs, context = context)[0]
web_root_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url')
ctx = dict(context, edi_web_url_view=edi.EDI_VIEW_WEB_URL % (web_root_url, cr.dbname, edi_token))
return super(sale_order, self).action_quotation_send(cr, uid, ids, context=ctx)
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
"""Exports a Sale order"""
edi_struct = dict(edi_struct or SALE_ORDER_EDI_STRUCT)
@ -102,34 +88,36 @@ class sale_order(osv.osv, EDIMixin):
edi_doc_list.append(edi_doc)
return edi_doc_list
def _edi_import_company(self, cr, uid, edi_document, context=None):
# TODO: for multi-company setups, we currently import the document in the
# user's current company, but we should perhaps foresee a way to select
# the desired company among the user's allowed companies
self._edi_requires_attributes(('company_id','company_address'), edi_document)
res_partner_obj = self.pool.get('res.partner')
res_partner = self.pool.get('res.partner')
# imported company_address = new partner address
src_company_id, src_company_name = edi_document.pop('company_id')
xid, company_name = edi_document.pop('company_id')
# Retrofit address info into a unified partner info (changed in v7 - used to keep them separate)
company_address_edi = edi_document.pop('company_address')
company_address_edi['name'] = company_name
company_address_edi['is_company'] = True
company_address_edi['__import_model'] = 'res.partner'
company_address_edi['__id'] = xid # override address ID, as of v7 they should be the same anyway
if company_address_edi.get('logo'):
company_address_edi['image'] = company_address_edi.pop('logo')
company_address_edi['customer'] = True
partner_id = res_partner.edi_import(cr, uid, company_address_edi, context=context)
address_info = edi_document.pop('company_address')
address_info['supplier'] = True
if 'name' not in address_info:
address_info['name'] = src_company_name
# modify edi_document to refer to new partner
partner = res_partner.browse(cr, uid, partner_id, context=context)
partner_edi_m2o = self.edi_m2o(cr, uid, partner, context=context)
edi_document['partner_id'] = partner_edi_m2o
edi_document['partner_invoice_id'] = partner_edi_m2o
edi_document['partner_shipping_id'] = partner_edi_m2o
address_id = res_partner_obj.edi_import(cr, uid, address_info, context=context)
edi_document.pop('partner_address', None) # ignored, that's supposed to be our own address!
return partner_id
# modify edi_document to refer to new partner/address
partner_address = res_partner_obj.browse(cr, uid, address_id, context=context)
edi_document.pop('partner_address', False) # ignored
address_edi_m2o = self.edi_m2o(cr, uid, partner_address, context=context)
edi_document['partner_id'] = address_edi_m2o
edi_document['partner_invoice_id'] = address_edi_m2o
edi_document['partner_shipping_id'] = address_edi_m2o
return address_id
def _edi_get_pricelist(self, cr, uid, partner_id, currency, context=None):
# TODO: refactor into common place for purchase/sale, e.g. into product module
@ -171,7 +159,6 @@ class sale_order(osv.osv, EDIMixin):
currency_id = res_currency.edi_import(cr, uid, currency_info, context=context)
order_currency = res_currency.browse(cr, uid, currency_id)
date_order = edi_document['date_order']
partner_ref = edi_document.pop('partner_ref', False)
edi_document['client_order_ref'] = edi_document['name']
edi_document['name'] = partner_ref or edi_document['name']
@ -185,7 +172,7 @@ class sale_order(osv.osv, EDIMixin):
order_lines = edi_document['order_line']
for order_line in order_lines:
self._edi_requires_attributes(( 'product_id', 'product_uom', 'product_qty', 'price_unit'), order_line)
self._edi_requires_attributes(('product_id', 'product_uom', 'product_qty', 'price_unit'), order_line)
order_line['product_uom_qty'] = order_line['product_qty']
del order_line['product_qty']

View File

@ -1,7 +1,6 @@
<?xml version="1.0" ?>
<openerp>
<data>
<!-- EDI related Email Templates menu -->
<record model="ir.actions.act_window" id="action_email_templates">
<field name="name">Email Templates</field>
@ -15,19 +14,20 @@
<menuitem id="base.menu_sales_configuration_misc" name="Miscellaneous" parent="base.menu_base_config" sequence="75"/>
</data>
<!-- Mail template is done in a NOUPDATE block
so users can freely customize/delete them -->
<data noupdate="1">
<!--Email template -->
<record id="email_template_edi_sale" model="email.template">
<field name="name">Automated Sale Order Notification Mail</field>
<field name="name">Sale Order - Send by Email</field>
<field name="email_from">${object.user_id.email or ''}</field>
<field name="subject">${object.company_id.name} Order (Ref ${object.name or 'n/a' })</field>
<field name="subject">${object.company_id.name} ${object.state in ('draft', 'sent') and 'Quotation' or 'Order'} (Ref ${object.name or 'n/a' })</field>
<field name="email_recipients">${object.partner_invoice_id.id}</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="auto_delete" eval="True"/>
<field name="report_template" ref="report_sale_order"/>
<field name="report_name">${(object.name or '').replace('/','_')}_${object.state == 'draft' and 'draft' or ''}</field>
<field name="body_html"><![CDATA[
<div style="font-family: 'Lucica Grande', Ubuntu, Arial, Verdana, sans-serif; font-size: 12px; color: rgb(34, 34, 34); background-color: rgb(255, 255, 255); ">
@ -46,16 +46,12 @@
% if object.client_order_ref:
&nbsp;&nbsp;Your reference: ${object.client_order_ref}<br />
% endif
% if object.user_id:
&nbsp;&nbsp;Your contact: <a href="mailto:${object.user_id.email or ''}?subject=Order%20${object.name}">${object.user_id.name}</a>
% endif
</p>
<p>
You can view the ${object.state in ('draft', 'sent') and 'quotation' or 'order confirmation'} document, download it and pay online using the following link:
</p>
<a style="display:block; width: 150px; height:20px; margin-left: 120px; color: #FFF; font-family: 'Lucida Grande', Helvetica, Arial, sans-serif; font-size: 13px; font-weight: bold; text-align: center; text-decoration: none !important; line-height: 1; padding: 5px 0px 0px 0px; background-color: #8E0000; border-radius: 5px 5px; background-repeat: repeat no-repeat;"
href="${ctx.get('edi_web_url_view') or ''}">View Order</a>
% if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account and object.state not in ('draft', 'sent'):
% if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account and object.state != 'draft':
<%
comp_name = quote(object.company_id.name)
order_name = quote(object.name)

View File

@ -636,30 +636,33 @@ class sale_order(osv.osv):
This function opens a window to compose an email, with the edi sale template message loaded by default
'''
assert len(ids) == 1, 'This option should only be used for a single id at a time.'
mod_obj = self.pool.get('ir.model.data')
template = mod_obj.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')
template_id = template and template[1] or False
res = mod_obj.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')
res_id = res and res[1] or False
ir_model_data = self.pool.get('ir.model.data')
try:
template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
except ValueError:
template_id = False
try:
compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
except ValueError:
compose_form_id = False
ctx = dict(context)
ctx.update({
'default_model': 'sale.order',
'default_res_id': ids[0],
'default_use_template': True,
'default_use_template': bool(template_id),
'default_template_id': template_id,
'default_composition_mode': 'comment',
'mark_so_as_sent': True
})
return {
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
'res_model': 'mail.compose.message',
'views': [(res_id, 'form')],
'view_id': res_id,
'type': 'ir.actions.act_window',
'views': [(compose_form_id, 'form')],
'view_id': compose_form_id,
'target': 'new',
'context': ctx,
'nodestroy': True,
}
def action_done(self, cr, uid, ids, context=None):

View File

@ -166,8 +166,8 @@
<header>
<button name="invoice_recreate" states="invoice_except" string="Recreate Invoice" groups="base.group_user"/>
<button name="invoice_corrected" states="invoice_except" string="Ignore Exception" groups="base.group_user"/>
<button name="action_quotation_send" string="Send by Mail" type="object" states="draft" class="oe_highlight" groups="base.group_user"/>
<button name="action_quotation_send" string="Send by Mail" type="object" states="sent" groups="base.group_user"/>
<button name="action_quotation_send" string="Send by Email" type="object" states="draft" class="oe_highlight" groups="base.group_user"/>
<button name="action_quotation_send" string="Send by Email" type="object" states="sent" groups="base.group_user"/>
<button name="print_quotation" string="Print" type="object" states="draft" class="oe_highlight" groups="base.group_user"/>
<button name="print_quotation" string="Print" type="object" states="sent" groups="base.group_user"/>
<button name="action_button_confirm" states="draft" string="Confirm Sale" type="object" groups="base.group_user"/>
@ -358,7 +358,7 @@
</field>
</record>
<record id="action_order_form" model="ir.actions.act_window">
<record id="action_orders" model="ir.actions.act_window">
<field name="name">Sale Orders</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
@ -377,9 +377,9 @@
</p>
</field>
</record>
<menuitem action="action_order_form" id="menu_sale_order" parent="base.menu_sales" sequence="5" groups="base.group_sale_salesman,base.group_sale_manager"/>
<menuitem action="action_orders" id="menu_sale_order" parent="base.menu_sales" sequence="5" groups="base.group_sale_salesman,base.group_sale_manager"/>
<record id="action_order_tree2" model="ir.actions.act_window">
<record id="action_orders_exception" model="ir.actions.act_window">
<field name="name">Sales in Exception</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
@ -390,7 +390,7 @@
<field name="search_view_id" ref="view_sales_order_filter"/>
</record>
<record id="action_order_tree4" model="ir.actions.act_window">
<record id="action_orders_in_progress" model="ir.actions.act_window">
<field name="name">Sales Order in Progress</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
@ -401,7 +401,7 @@
</record>
<record id="action_order_tree5" model="ir.actions.act_window">
<record id="action_quotations" model="ir.actions.act_window">
<field name="name">Quotations</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">sale.order</field>
@ -427,7 +427,7 @@
</record>
<menuitem id="menu_sale_quotations"
action="action_order_tree5" parent="base.menu_sales"
action="action_quotations" parent="base.menu_sales"
sequence="4"/>
<record id="action_order_tree" model="ir.actions.act_window">

View File

@ -25,70 +25,71 @@
-
Then I export the sale order via EDI
-
!python {model: edi.document}: |
!python {model: edi.edi}: |
import json
sale_order = self.pool.get('sale.order')
so = sale_order.browse(cr, uid, ref("sale_order_edi_1"))
token = self.export_edi(cr, uid, [so])
assert token, 'Invalid EDI Token'
edi_doc = self.generate_edi(cr, uid, [so])
assert isinstance(json.loads(edi_doc)[0], dict), 'EDI doc should be a JSON dict'
-
Then I import a sample EDI document of a purchase order
"Then I import a sample EDI document of a purchase order (v7.0)"
-
!python {model: edi.document}: |
!python {model: edi.edi}: |
sale_order_pool = self.pool.get('sale.order')
edi_document = {
"__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_test",
"__id": "purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.purchase_order_test",
"__module": "purchase",
"__model": "purchase.order",
"__import_module": "sale",
"__import_model": "sale.order",
"__version": [6,1,0],
"__version": [7,0,0],
"name": "PO00011",
"date_order": "2011-09-12",
"currency": {
"__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.EUR",
"__id": "base:5af12v70-dv70-1v70-bv70-701a04e25v70.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
"company_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.main_company", "Client S.A."],
"company_id": ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.main_company", "Client S.A."],
"company_address": {
"__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.some_address",
"__id": "base:5af12v70-dv70-1v70-bv70-701a04e25v70.some_address",
"__module": "base",
"__model": "res.partner",
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
"country_id": ["base:5af12v70-dv70-1v70-bv70-701a04e25v70.be", "Belgium"],
"bank_ids": [
["base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_bank-adaWadsadasdDJzGbp","Ladies bank: 032465789-156113"]
["base:5af12v70-dv70-1v70-bv70-701a04e25v70.res_partner_bank-adaWadsadasdDJzGbp","Another bank: 032465700-156700"]
],
},
"partner_id": ["purchase:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_test20", "jones white"],
"partner_id": ["purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.res_partner_test20", "jones white"],
"order_line": [{
"__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-AlhsVDZGoKvJ",
"__id": "purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.purchase_order_line-AlhsVDZGoKvJ",
"__module": "purchase",
"__model": "purchase.order.line",
"__import_module": "sale",
"__import_model": "sale.order.line",
"name": "PC Assemble SC234",
"price_unit": 150.0,
"product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_3", "[PCSC234] PC Assemble SC234"],
"product_id": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_product_3", "[PCSC234] PC Assemble SC234"],
"product_qty": 1.0,
"product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "Unit"],
"product_uom": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_uom_unit", "Unit"],
},
{
"__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-Alsads33e",
"__id": "purchase:5af12v70-dv70-1v70-bv70-701a04e25v70.purchase_order_line-Alsads33e",
"__module": "purchase",
"__model": "purchase.order.line",
"__import_module": "sale",
"__import_model": "sale.order.line",
"name": "PC on Demand",
"price_unit": 100.0,
"product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_5", "[PC-DEM] PC on Demand"],
"product_id": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_product_5", "[PC-DEM] PC on Demand"],
"product_qty": 2.0,
"product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "Unit"],
"product_uom": ["product:5af12v70-dv70-1v70-bv70-701a04e25v70.product_uom_unit", "Unit"],
}],
}
new_sale_order_id = sale_order_pool.edi_import(cr, uid, edi_document, context=context)
@ -96,9 +97,10 @@
order_new = sale_order_pool.browse(cr, uid, new_sale_order_id)
# check bank info on partner
assert order_new.partner_id.customer, "Imported partner should be a customer, as we just imported the document as a sale order"
assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = order_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Ladies bank: 032465789-156113", 'Expected "Ladies bank: 032465789-156113", got %s' % bank_info.acc_number
assert bank_info.acc_number == "Another bank: 032465700-156700", 'Expected "Another bank: 032465700-156700", got %s' % bank_info.acc_number
assert order_new.pricelist_id.name == 'Public Pricelist' , "Public Price list was not automatically assigned"
assert order_new.amount_total == 350, "Amount total is wrong"
@ -115,3 +117,101 @@
assert sale_line.product_uom_qty == 2 , "product qty is not same"
else:
raise AssertionError('unknown order line: %s' % sale_line)
-
"Then I import a sample EDI document of a purchase order (v6.1 - to test backwards compatibility)"
-
!python {model: edi.edi}: |
sale_order_pool = self.pool.get('sale.order')
edi_document = {
"__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_test",
"__module": "purchase",
"__model": "purchase.order",
"__import_module": "sale",
"__import_model": "sale.order",
"__version": [6,1,0],
"name": "PO00011-v61",
"date_order": "2011-09-12",
"currency": {
"__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.EUR",
"__module": "base",
"__model": "res.currency",
"code": "EUR",
"symbol": "€",
},
"company_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.main_company", "Client S.A."],
"company_address": {
"__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.some_address",
"__module": "base",
"__model": "res.partner.address",
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
"bank_ids": [
["base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_bank-adaWadsadasdDJzGbp","Ladies bank: 032465789-156113"]
],
},
"partner_id": ["purchase:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_test20", "jones white"],
"partner_address": {
"__id": "base:5af1272e-dd26-11e0-b65e-701a04e25543.res_partner_address_7wdsjasdjh",
"__module": "base",
"__model": "res.partner.address",
"phone": "(+32).81.81.37.00",
"street": "Chaussee de Namur 40",
"city": "Gerompont",
"zip": "1367",
"country_id": ["base:5af1272e-dd26-11e0-b65e-701a04e25543.be", "Belgium"],
},
"order_line": [{
"__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-AlhsVDZGoKvJ",
"__module": "purchase",
"__model": "purchase.order.line",
"__import_module": "sale",
"__import_model": "sale.order.line",
"name": "Basic PC",
"date_planned": "2011-09-30",
"price_unit": 150.0,
"product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_pc1", "[PC1] Basic PC"],
"product_qty": 1.0,
"product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "PCE"],
},
{
"__id": "purchase:5af1272e-dd26-11e0-b65e-701a04e25543.purchase_order_line-Alsads33e",
"__module": "purchase",
"__model": "purchase.order.line",
"__import_module": "sale",
"__import_model": "sale.order.line",
"name": "Medium PC",
"date_planned": "2011-09-15",
"price_unit": 100.0,
"product_id": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_product_pc3", "[PC3] Medium PC"],
"product_qty": 2.0,
"product_uom": ["product:5af1272e-dd26-11e0-b65e-701a04e25543.product_uom_unit", "PCE"],
}],
}
new_sale_order_id = sale_order_pool.edi_import(cr, uid, edi_document, context=context)
assert new_sale_order_id, 'Sale order import failed'
order_new = sale_order_pool.browse(cr, uid, new_sale_order_id)
# check bank info on partner
assert order_new.partner_id.customer, "Imported partner should be a customer, as we just imported the document as a sale order"
assert len(order_new.partner_id.bank_ids) == 1, "Expected 1 bank entry related to partner"
bank_info = order_new.partner_id.bank_ids[0]
assert bank_info.acc_number == "Ladies bank: 032465789-156113", 'Expected "Ladies bank: 032465789-156113", got %s' % bank_info.acc_number
assert order_new.pricelist_id.name == 'Public Pricelist' , "Public Price list was not automatically assigned"
assert order_new.amount_total == 350, "Amount total is wrong"
assert order_new.amount_untaxed == 350, "Untaxed amount is wrong"
assert len(order_new.order_line) == 2, "Sale order lines mismatch"
for sale_line in order_new.order_line:
if sale_line.name == 'Basic PC':
assert sale_line.product_uom.name == "PCE" , "uom is not same"
assert sale_line.price_unit == 150 , "unit price is not same, got %s, expected 150"%(sale_line.price_unit,)
assert sale_line.product_uom_qty == 1 , "product qty is not same"
elif sale_line.name == 'Medium PC':
assert sale_line.product_uom.name == "PCE" , "uom is not same"
assert sale_line.price_unit == 100 , "unit price is not same, got %s, expected 100"%(sale_line.price_unit,)
assert sale_line.product_uom_qty == 2 , "product qty is not same"
else:
raise AssertionError('unknown order line: %s' % sale_line)