[MERGE] EDI branch of hmo/odo
bzr revid: fp@tinyerp.com-20111116185443-zu2zpt7qd3lrfw9s
This commit is contained in:
commit
6861e526da
|
@ -35,5 +35,5 @@ import product
|
|||
import ir_sequence
|
||||
import company
|
||||
import res_currency
|
||||
|
||||
import edi
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -53,7 +53,7 @@ module named account_voucher.
|
|||
'website': 'http://www.openerp.com',
|
||||
'images' : ['images/accounts.jpeg','images/bank_statement.jpeg','images/cash_register.jpeg','images/chart_of_accounts.jpeg','images/customer_invoice.jpeg','images/journal_entries.jpeg'],
|
||||
'init_xml': [],
|
||||
"depends" : ["base_setup", "product", "analytic", "process","board"],
|
||||
"depends" : ["base_setup", "product", "analytic", "process", "board", "edi"],
|
||||
'update_xml': [
|
||||
'security/account_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
@ -123,7 +123,8 @@ module named account_voucher.
|
|||
'board_account_view.xml',
|
||||
"wizard/account_report_profit_loss_view.xml",
|
||||
"wizard/account_report_balance_sheet_view.xml",
|
||||
"account_bank_view.xml"
|
||||
"edi/invoice_action_data.xml",
|
||||
"account_bank_view.xml",
|
||||
],
|
||||
'demo_xml': [
|
||||
'demo/account_demo.xml',
|
||||
|
@ -145,10 +146,9 @@ module named account_voucher.
|
|||
'test/account_fiscalyear_close.yml',
|
||||
'test/account_bank_statement.yml',
|
||||
'test/account_cash_statement.yml',
|
||||
'test/test_edi_invoice.yml',
|
||||
'test/account_report.yml',
|
||||
|
||||
|
||||
],
|
||||
],
|
||||
'installable': True,
|
||||
'active': False,
|
||||
'certificate': '0080331923549',
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
rml="account/report/account_print_invoice.rml"
|
||||
string="Invoices"
|
||||
attachment="(object.state in ('open','paid')) and ('INV'+(object.number or '').replace('/',''))"
|
||||
usage="default"
|
||||
multi="True"/>
|
||||
<report id="account_transfers" model="account.transfer" name="account.transfer" string="Transfers" xml="account/report/transfer.xml" xsl="account/report/transfer.xsl"/>
|
||||
<report auto="False" id="account_intracom" menu="False" model="account.move.line" name="account.intracom" string="IntraCom"/>
|
||||
|
|
|
@ -24,6 +24,7 @@ from osv import fields, osv
|
|||
class res_company(osv.osv):
|
||||
_inherit = "res.company"
|
||||
_columns = {
|
||||
'paypal_account': fields.char("Paypal Account", size=128, help="Paypal username (usually email) for receiving online payments."),
|
||||
'overdue_msg': fields.text('Overdue Payments Message', translate=True),
|
||||
'property_reserve_and_surplus_account': fields.property(
|
||||
'account.account',
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<field name="arch" type="xml">
|
||||
<field name="currency_id" position="after">
|
||||
<field name="property_reserve_and_surplus_account" colspan="2"/>
|
||||
<field name="paypal_account" />
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 invoice
|
|
@ -0,0 +1,273 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 osv import fields, osv, orm
|
||||
from edi import EDIMixin
|
||||
|
||||
INVOICE_LINE_EDI_STRUCT = {
|
||||
'name': True,
|
||||
'origin': True,
|
||||
'uos_id': True,
|
||||
'product_id': True,
|
||||
'price_unit': True,
|
||||
'quantity': True,
|
||||
'discount': True,
|
||||
'note': True,
|
||||
|
||||
# fields used for web preview only - discarded on import
|
||||
'price_subtotal': True,
|
||||
}
|
||||
|
||||
INVOICE_TAX_LINE_EDI_STRUCT = {
|
||||
'name': True,
|
||||
'base': True,
|
||||
'amount': True,
|
||||
'manual': True,
|
||||
'sequence': True,
|
||||
'base_amount': True,
|
||||
'tax_amount': True,
|
||||
}
|
||||
|
||||
INVOICE_EDI_STRUCT = {
|
||||
'name': True,
|
||||
'origin': True,
|
||||
'company_id': True, # -> to be changed into partner
|
||||
'type': True, # -> reversed at import
|
||||
'internal_number': True, # -> reference at import
|
||||
'comment': True,
|
||||
'date_invoice': True,
|
||||
'date_due': True,
|
||||
'partner_id': True,
|
||||
'payment_term': True,
|
||||
#custom: currency_id
|
||||
'invoice_line': INVOICE_LINE_EDI_STRUCT,
|
||||
'tax_line': INVOICE_TAX_LINE_EDI_STRUCT,
|
||||
|
||||
# fields used for web preview only - discarded on import
|
||||
#custom: 'partner_ref'
|
||||
'amount_total': True,
|
||||
'amount_untaxed': True,
|
||||
'amount_tax': True,
|
||||
}
|
||||
|
||||
class account_invoice(osv.osv, EDIMixin):
|
||||
_inherit = 'account.invoice'
|
||||
|
||||
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)
|
||||
res_company = self.pool.get('res.company')
|
||||
res_partner_address = self.pool.get('res.partner.address')
|
||||
edi_doc_list = []
|
||||
for invoice in records:
|
||||
# generate the main report
|
||||
self._edi_generate_report_attachment(cr, uid, invoice, context=context)
|
||||
edi_doc = super(account_invoice,self).edi_export(cr, uid, [invoice], edi_struct, context)[0]
|
||||
edi_doc.update({
|
||||
'company_address': res_company.edi_export_address(cr, uid, invoice.company_id, context=context),
|
||||
'company_paypal_account': invoice.company_id.paypal_account,
|
||||
'partner_address': res_partner_address.edi_export(cr, uid, [invoice.address_invoice_id], context=context)[0],
|
||||
|
||||
'currency': self.pool.get('res.currency').edi_export(cr, uid, [invoice.currency_id], context=context)[0],
|
||||
'partner_ref': invoice.reference or False,
|
||||
})
|
||||
edi_doc_list.append(edi_doc)
|
||||
return edi_doc_list
|
||||
|
||||
def _edi_tax_account(self, cr, uid, invoice_type='out_invoice', context=None):
|
||||
#TODO/FIXME: should select proper Tax Account
|
||||
account_pool = self.pool.get('account.account')
|
||||
account_ids = account_pool.search(cr, uid, [('type','<>','view'),('type','<>','income'), ('type', '<>', 'closed')])
|
||||
tax_account = False
|
||||
if account_ids:
|
||||
tax_account = account_pool.browse(cr, uid, account_ids[0])
|
||||
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)
|
||||
if invoice_type in ('out_invoice', 'out_refund'):
|
||||
invoice_account = partner.property_account_receivable
|
||||
else:
|
||||
invoice_account = partner.property_account_payable
|
||||
return invoice_account
|
||||
|
||||
def _edi_product_account(self, cr, uid, product_id, invoice_type, context=None):
|
||||
product_pool = self.pool.get('product.product')
|
||||
product = product_pool.browse(cr, uid, product_id, context=context)
|
||||
if invoice_type in ('out_invoice','out_refund'):
|
||||
account = product.property_account_income or product.categ_id.property_account_income_categ
|
||||
else:
|
||||
account = product.property_account_expense or product.categ_id.property_account_expense_categ
|
||||
return account
|
||||
|
||||
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','type'), edi_document)
|
||||
res_partner_address = self.pool.get('res.partner.address')
|
||||
res_partner = self.pool.get('res.partner')
|
||||
|
||||
# imported company = new partner
|
||||
src_company_id, src_company_name = edi_document.pop('company_id')
|
||||
partner_id = self.edi_import_relation(cr, uid, 'res.partner', src_company_name,
|
||||
src_company_id, context=context)
|
||||
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})
|
||||
res_partner.write(cr, uid, [partner_id], partner_value, context=context)
|
||||
|
||||
# imported company_address = new partner address
|
||||
address_info = edi_document.pop('company_address')
|
||||
address_info['partner_id'] = (src_company_id, src_company_name)
|
||||
address_info['type'] = 'invoice'
|
||||
address_id = res_partner_address.edi_import(cr, uid, address_info, context=context)
|
||||
|
||||
# modify edi_document to refer to new partner
|
||||
partner_address = res_partner_address.browse(cr, uid, address_id, context=context)
|
||||
edi_document['partner_id'] = (src_company_id, src_company_name)
|
||||
edi_document.pop('partner_address', False) # ignored
|
||||
edi_document['address_invoice_id'] = self.edi_m2o(cr, uid, partner_address, context=context)
|
||||
|
||||
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
|
||||
a new partner (e.g. supplier company for a customer invoice will be come a supplier
|
||||
record for the new invoice.
|
||||
Summary of tasks that need to be done:
|
||||
- import company as a new partner, if type==in then supplier=1, else customer=1
|
||||
- partner_id field is modified to point to the new partner
|
||||
- company_address data used to add address to new partner
|
||||
- change type: out_invoice'<->'in_invoice','out_refund'<->'in_refund'
|
||||
- reference: should contain the value of the 'internal_number'
|
||||
- reference_type: 'none'
|
||||
- internal number: reset to False, auto-generated
|
||||
- journal_id: should be selected based on type: simply put the 'type'
|
||||
in the context when calling create(), will be selected correctly
|
||||
- payment_term: if set, create a default one based on name...
|
||||
- for invoice lines, the account_id value should be taken from the
|
||||
product's default, i.e. from the default category, as it will not
|
||||
be provided.
|
||||
- for tax lines, we disconnect from the invoice.line, so all tax lines
|
||||
will be of type 'manual', and default accounts should be picked based
|
||||
on the tax config of the DB where it is imported.
|
||||
"""
|
||||
if context is None:
|
||||
context = {}
|
||||
self._edi_requires_attributes(('company_id','company_address','type','invoice_line','currency'), edi_document)
|
||||
|
||||
# extract currency info
|
||||
res_currency = self.pool.get('res.currency')
|
||||
currency_info = edi_document.pop('currency')
|
||||
currency_id = res_currency.edi_import(cr, uid, currency_info, context=context)
|
||||
currency = res_currency.browse(cr, uid, currency_id)
|
||||
edi_document['currency_id'] = self.edi_m2o(cr, uid, currency, context=context)
|
||||
|
||||
# change type: out_invoice'<->'in_invoice','out_refund'<->'in_refund'
|
||||
invoice_type = edi_document['type']
|
||||
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
|
||||
partner_id = self._edi_import_company(cr, uid, edi_document, context=context)
|
||||
|
||||
# Set Account
|
||||
invoice_account = self._edi_invoice_account(cr, uid, partner_id, invoice_type, context=context)
|
||||
edi_document['account_id'] = invoice_account and self.edi_m2o(cr, uid, invoice_account, context=context) or False
|
||||
|
||||
# reference: should contain the value of the 'internal_number'
|
||||
edi_document['reference'] = edi_document.get('internal_number', False)
|
||||
# reference_type: 'none'
|
||||
edi_document['reference_type'] = 'none'
|
||||
|
||||
# internal number: reset to False, auto-generated
|
||||
edi_document['internal_number'] = False
|
||||
|
||||
# discard web preview fields, if present
|
||||
edi_document.pop('partner_ref', None)
|
||||
|
||||
# journal_id: should be selected based on type: simply put the 'type' in the context when calling create(), will be selected correctly
|
||||
context.update(type=invoice_type)
|
||||
|
||||
# for invoice lines, the account_id value should be taken from the product's default, i.e. from the default category, as it will not be provided.
|
||||
for edi_invoice_line in edi_document['invoice_line']:
|
||||
product_info = edi_invoice_line['product_id']
|
||||
product_id = self.edi_import_relation(cr, uid, 'product.product', product_info[1],
|
||||
product_info[0], context=context)
|
||||
account = self._edi_product_account(cr, uid, product_id, invoice_type, context=context)
|
||||
# TODO: could be improved with fiscal positions perhaps
|
||||
# account = fpos_obj.map_account(cr, uid, fiscal_position_id, account.id)
|
||||
edi_invoice_line['account_id'] = self.edi_m2o(cr, uid, account, context=context) if account else False
|
||||
|
||||
# discard web preview fields, if present
|
||||
edi_invoice_line.pop('price_subtotal', None)
|
||||
|
||||
# for tax lines, we disconnect from the invoice.line, so all tax lines will be of type 'manual', and default accounts should be picked based
|
||||
# on the tax config of the DB where it is imported.
|
||||
tax_account = self._edi_tax_account(cr, uid, context=context)
|
||||
tax_account_info = self.edi_m2o(cr, uid, tax_account, context=context)
|
||||
for edi_tax_line in edi_document.get('tax_line', []):
|
||||
edi_tax_line['account_id'] = tax_account_info
|
||||
edi_tax_line['manual'] = True
|
||||
|
||||
return super(account_invoice,self).edi_import(cr, uid, edi_document, context=context)
|
||||
|
||||
|
||||
def _edi_record_display_action(self, cr, uid, id, context=None):
|
||||
"""Returns an appropriate action definition dict for displaying
|
||||
the record with ID ``rec_id``.
|
||||
|
||||
:param int id: database ID of record to display
|
||||
:return: action definition dict
|
||||
"""
|
||||
action = super(account_invoice,self)._edi_record_display_action(cr, uid, id, context=context)
|
||||
try:
|
||||
invoice = self.browse(cr, uid, id, context=context)
|
||||
if 'out_' in invoice.type:
|
||||
view_ext_id = 'invoice_form'
|
||||
journal_type = 'sale'
|
||||
else:
|
||||
view_ext_id = 'invoice_supplier_form'
|
||||
journal_type = 'purchase'
|
||||
ctx = "{'type': '%s', 'journal_type': '%s'}" % (invoice.type, journal_type)
|
||||
action.update(context=ctx)
|
||||
view_id = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'account', view_ext_id)[1]
|
||||
action.update(views=[(view_id,'form'), (False, 'tree')])
|
||||
except ValueError:
|
||||
# ignore if views are missing
|
||||
pass
|
||||
return action
|
||||
|
||||
|
||||
class account_invoice_line(osv.osv, EDIMixin):
|
||||
_inherit='account.invoice.line'
|
||||
|
||||
class account_invoice_tax(osv.osv, EDIMixin):
|
||||
_inherit = "account.invoice.tax"
|
||||
|
||||
|
|
@ -0,0 +1,186 @@
|
|||
<?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>
|
||||
<field name="res_model">email.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form,tree</field>
|
||||
<field name="view_id" ref="email_template.email_template_tree" />
|
||||
<field name="search_view_id" ref="email_template.view_email_template_search"/>
|
||||
<field name="context">{'search_default_model_id':'account.invoice'}</field>
|
||||
<field name="context" eval="{'search_default_model_id': ref('account.model_account_invoice')}"/>
|
||||
</record>
|
||||
<menuitem id="menu_email_templates" parent="menu_configuration_misc" action="action_email_templates" sequence="30"/>
|
||||
|
||||
</data>
|
||||
|
||||
<!-- Mail template and workflow bindings are done 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="email_from">${object.user_id.user_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_to">${object.address_invoice_id.email or ''}</field>
|
||||
<field name="model_id" ref="account.model_account_invoice"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<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.address_invoice_id.name and ' ' or ''}${object.address_invoice_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;">
|
||||
<strong>REFERENCES</strong><br />
|
||||
Invoice number: <strong>${object.number}</strong><br />
|
||||
Invoice total: <strong>${object.amount_total} ${object.currency_id.name}</strong><br />
|
||||
Invoice date: ${object.date_invoice}<br />
|
||||
% if object.origin:
|
||||
Order reference: ${object.origin}<br />
|
||||
% endif
|
||||
Your contact: <a href="mailto:${object.user_id.user_email or ''}?subject=Invoice%20${object.number}">${object.user_id.name}</a>
|
||||
</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)
|
||||
inv_number = quote(object.number)
|
||||
paypal_account = quote(object.company_id.paypal_account)
|
||||
inv_amount = quote(str(object.amount_total))
|
||||
cur_name = quote(object.currency_id.name)
|
||||
paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Invoice%%20%s&" \
|
||||
"invoice=%s&amount=%s&currency_code=%s&button_subtype=services&no_note=1&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: ${object.company_id.phone}
|
||||
</div>
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
<div>
|
||||
Web : <a href="${object.company_id.website}">${object.company_id.website}</a>
|
||||
</div>
|
||||
%endif
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
]]></field>
|
||||
<field name="body_text"><![CDATA[
|
||||
Hello${object.address_invoice_id.name and ' ' or ''}${object.address_invoice_id.name or ''},
|
||||
|
||||
A new invoice is available for ${object.partner_id.name}:
|
||||
| Invoice number: *${object.number}*
|
||||
| Invoice total: *${object.amount_total} ${object.currency_id.name}*
|
||||
| Invoice date: ${object.date_invoice}
|
||||
% if object.origin:
|
||||
| Order reference: ${object.origin}
|
||||
% endif
|
||||
| Your contact: ${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''}
|
||||
|
||||
You can view the invoice document, download it and pay online using the following link:
|
||||
${ctx.get('edi_web_url_view') or 'n/a'}
|
||||
|
||||
% 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_total))
|
||||
cur_name = quote(object.currency_id.name)
|
||||
paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Invoice%%20%s"\
|
||||
"&invoice=%s&amount=%s¤cy_code=%s&button_subtype=services&no_note=1&bn=OpenERP_Invoice_PayNow_%s" % \
|
||||
(paypal_account,comp_name,inv_number,inv_number,inv_amount,cur_name,cur_name)
|
||||
%>
|
||||
It is also possible to directly pay with Paypal:
|
||||
${paypal_url}
|
||||
% endif
|
||||
|
||||
If you have any question, do not hesitate to contact us.
|
||||
|
||||
|
||||
Thank you for choosing ${object.company_id.name}!
|
||||
|
||||
|
||||
--
|
||||
${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''}
|
||||
${object.company_id.name}
|
||||
% if object.company_id.street:
|
||||
${object.company_id.street or ''}
|
||||
% endif
|
||||
% if object.company_id.street2:
|
||||
${object.company_id.street2}
|
||||
% endif
|
||||
% if object.company_id.city or object.company_id.zip:
|
||||
${object.company_id.zip or ''} ${object.company_id.city or ''}
|
||||
% 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 ''}
|
||||
% endif
|
||||
% if object.company_id.phone:
|
||||
Phone: ${object.company_id.phone}
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
${object.company_id.website or ''}
|
||||
% endif
|
||||
]]></field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
|
@ -0,0 +1,152 @@
|
|||
-
|
||||
In order to test the EDI export features of Invoices
|
||||
-
|
||||
First I create a draft customer invoice
|
||||
-
|
||||
!record {model: account.invoice, id: invoice_edi_1}:
|
||||
journal_id: 1
|
||||
partner_id: base.res_partner_agrolait
|
||||
currency_id: base.EUR
|
||||
address_invoice_id: base.res_partner_address_8invoice
|
||||
company_id: 1
|
||||
account_id: account.a_pay
|
||||
date_invoice: '2011-06-22'
|
||||
name: selling product
|
||||
type: 'out_invoice'
|
||||
invoice_line:
|
||||
- product_id: product.product_product_pc1
|
||||
uos_id: 1
|
||||
quantity: 1.0
|
||||
price_unit: 10.0
|
||||
name: 'basic pc'
|
||||
account_id: account.a_pay
|
||||
invoice_line:
|
||||
- product_id: product.product_product_pc3
|
||||
uos_id: 1
|
||||
quantity: 5.0
|
||||
price_unit: 100.0
|
||||
name: 'Medium PC'
|
||||
account_id: account.a_pay
|
||||
tax_line:
|
||||
- name: sale tax
|
||||
account_id: account.a_pay
|
||||
manual: True
|
||||
amount: 1000.00
|
||||
-
|
||||
I confirm and open the invoice
|
||||
-
|
||||
!workflow {model: account.invoice, ref: invoice_edi_1, action: invoice_open}
|
||||
-
|
||||
Then I export the customer invoice
|
||||
-
|
||||
!python {model: edi.document}: |
|
||||
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'
|
||||
-
|
||||
Then I import a sample EDI document of another customer invoice
|
||||
-
|
||||
!python {model: account.invoice}: |
|
||||
edi_document = {
|
||||
"__id": "account:b22acf7a-ddcd-11e0-a4db-701a04e25543.random_invoice_763jsms",
|
||||
"__module": "account",
|
||||
"__model": "account.invoice",
|
||||
"__version": [6,1,0],
|
||||
"internal_number": "SAJ/2011/002",
|
||||
"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": "2011-06-22",
|
||||
"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 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 == "SAJ/2011/002", "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"
|
|
@ -66,6 +66,7 @@ import account_change_currency
|
|||
|
||||
import account_report_balance_sheet
|
||||
import account_report_profit_loss
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
||||
|
|
|
@ -49,18 +49,6 @@
|
|||
company_id: base.main_company
|
||||
view_id: account.account_journal_bank_view
|
||||
|
||||
-
|
||||
I create new partner Mark Strauss.
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_strauss0}:
|
||||
address:
|
||||
- city: paris
|
||||
country_id: base.fr
|
||||
name: Mark Strauss
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '75016'
|
||||
name: Mr. Mark Strauss
|
||||
-
|
||||
I create the first invoice on 1st January for 200 USD
|
||||
-
|
||||
|
@ -80,7 +68,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_strauss0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -115,7 +103,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_strauss0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -139,21 +127,21 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_strauss0"), ref('bank_journal_USD'), 240.00, 2, ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 240.00, 2, ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 240.00,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.USD'),
|
||||
'journal_id': ref('bank_journal_USD'),
|
||||
'partner_id': ref('res_partner_strauss0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-%m-%d"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'First payment',
|
||||
'name': 'First payment: Case 1 USD/USD',
|
||||
}
|
||||
vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-03-01'), ref('base.USD'), 240)['value'])
|
||||
if not res['value']['line_cr_ids']:
|
||||
|
@ -171,7 +159,7 @@
|
|||
I check that writeoff amount computed is 10.0
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.writeoff_amount == 10.0), "Writeoff amount is not 10.0"
|
||||
-
|
||||
|
@ -179,14 +167,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my first voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -200,7 +188,7 @@
|
|||
I check that my write-off is correct. 9 debit and 10 amount_currency
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 1 USD/USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -237,7 +225,7 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_strauss0"), ref('bank_journal_USD'), 45.00, 2, ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 45.00, 2, ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 45.00,
|
||||
|
@ -245,14 +233,14 @@
|
|||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.USD'),
|
||||
'journal_id': ref('bank_journal_USD'),
|
||||
'partner_id': ref('res_partner_strauss0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-%m-%d"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'Second payment',
|
||||
'name': 'Second payment: Case 1',
|
||||
}
|
||||
vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 45.0)['value'])
|
||||
if not res['value']['line_cr_ids']:
|
||||
|
@ -270,7 +258,7 @@
|
|||
I check that writeoff amount computed is 5.0
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.writeoff_amount == 5.0), "Writeoff amount is not 5.0"
|
||||
-
|
||||
|
@ -278,14 +266,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my second voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -303,7 +291,7 @@
|
|||
I check that my writeoff is correct. 4.75 debit and 5 amount_currency
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_strauss0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 1'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
|
|
@ -29,18 +29,6 @@
|
|||
currency: base.USD
|
||||
company_id: base.main_company
|
||||
view_id: account.account_journal_bank_view
|
||||
-
|
||||
I create a new partner Robert Clements
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_clements0}:
|
||||
address:
|
||||
- city: marseille
|
||||
country_id: base.fr
|
||||
name: Robert
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '13016'
|
||||
name: Mr.Robert Clements
|
||||
-
|
||||
I create the first invoice on 1st January for 200 USD
|
||||
-
|
||||
|
@ -61,7 +49,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_clements0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
check_total : 200
|
||||
-
|
||||
|
@ -98,7 +86,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_clements0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
check_total : 100.0
|
||||
-
|
||||
|
@ -122,21 +110,21 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_clements0"), ref('bank_journal_EUR'), 240.0, 2, ttype='payment', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 240.0, 2, ttype='payment', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 240.0,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.EUR'),
|
||||
'journal_id': ref('bank_journal_EUR'),
|
||||
'partner_id': ref('res_partner_clements0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'payment',
|
||||
'date': time.strftime("%Y-03-01"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'First payment',
|
||||
'name': 'First payment: Case 2 SUPPL USD/EUR',
|
||||
}
|
||||
if not res['value']['line_dr_ids']:
|
||||
res['value']['line_dr_ids'] = [{'type': 'dr', 'account_id': ref('account.a_pay'),}]
|
||||
|
@ -153,14 +141,14 @@
|
|||
I check that writeoff amount computed is -15.0
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.writeoff_amount == -15.0), "Writeoff amount is not -15.0"
|
||||
-
|
||||
I check that currency rate difference is 34.0
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.currency_rate_difference == 34.0), "Currency rate difference is not 34.0"
|
||||
-
|
||||
|
@ -168,14 +156,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -191,7 +179,7 @@
|
|||
I check that my writeoff is correct. -15 in credit with no amount_currency
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -229,21 +217,21 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_clements0"), ref('bank_journal_EUR'), 45.0, 2, ttype='payment', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 45.0, 2, ttype='payment', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 45.0,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.USD'),
|
||||
'journal_id': ref('bank_journal_USD'),
|
||||
'partner_id': ref('res_partner_clements0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'payment',
|
||||
'date': time.strftime("%Y-%m-%d"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'Second payment',
|
||||
'name': 'Second payment: Case 2 SUPPL USD/EUR',
|
||||
}
|
||||
vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 45.0)['value'])
|
||||
if not res['value']['line_dr_ids']:
|
||||
|
@ -261,14 +249,14 @@
|
|||
I check that writeoff amount computed is -5.0
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.writeoff_amount == 5.0), "Writeoff amount is not 5.0"
|
||||
-
|
||||
I check that currency rate difference is 8.50
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.currency_rate_difference == 8.50), "Currency rate difference is not 8.50"
|
||||
-
|
||||
|
@ -276,14 +264,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that my voucher state is posted
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert voucher_id.state == 'posted', "Voucher state is not posted"
|
||||
-
|
||||
|
@ -296,7 +284,7 @@
|
|||
I check that my writeoff is correct. 4.75 in credit and 5 in amount_currency
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_clements0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
|
|
@ -66,18 +66,6 @@
|
|||
currency: base.USD
|
||||
company_id: base.main_company
|
||||
view_id: account.account_journal_bank_view
|
||||
-
|
||||
I create a new partner Michael Geller
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_michael0}:
|
||||
address:
|
||||
- city: paris
|
||||
country_id: base.fr
|
||||
name: Michael
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '75016'
|
||||
name: Mr.Michael Geller
|
||||
-
|
||||
I create the first invoice on 1st January for 200 USD
|
||||
-
|
||||
|
@ -97,7 +85,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_michael0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -132,7 +120,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_michael0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -155,21 +143,21 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michael0"), ref('bank_journal_EUR'), 240.0, False, ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 240.0, False, ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 200.0,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': False,
|
||||
'journal_id': ref('bank_journal_EUR'),
|
||||
'partner_id': ref('res_partner_michael0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-03-01"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'First payment',
|
||||
'name': 'First payment: Case 2 USD/EUR DR EUR',
|
||||
}
|
||||
if not res['value']['line_cr_ids']:
|
||||
res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}]
|
||||
|
@ -187,14 +175,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michael0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michael0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -206,7 +194,7 @@
|
|||
I check that the debtor account has 2 new lines with 0 in amount_currency columns and their credit columns are 130 and 70
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michael0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -237,14 +225,14 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michael0"), ref('bank_journal_USD'), 80.0, 2, ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 80.0, 2, ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 80.0,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.USD'),
|
||||
'journal_id': ref('bank_journal_USD'),
|
||||
'partner_id': ref('res_partner_michael0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-%m-%d"),
|
||||
|
@ -252,7 +240,7 @@
|
|||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'exchange_acc_id': ref('account.o_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'Second payment',
|
||||
'name': 'Second payment: Case 2 SUPPL USD/EUR DR EUR',
|
||||
}
|
||||
vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 80.0)['value'])
|
||||
if not res['value']['line_cr_ids']:
|
||||
|
@ -270,7 +258,7 @@
|
|||
I check that writeoff amount computed is 2.22
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (round(voucher_id.writeoff_amount, 2) == 2.22), "Writeoff amount is not 2.22$"
|
||||
-
|
||||
|
@ -278,14 +266,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that my voucher state is posted
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert voucher_id.state == 'posted', "Voucher state is not posted"
|
||||
-
|
||||
|
@ -298,7 +286,7 @@
|
|||
I check that my writeoff is correct. 2.11 in credit and 2.22 in amount_currency
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michael0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR EUR'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
|
|
@ -66,18 +66,6 @@
|
|||
currency: base.USD
|
||||
company_id: base.main_company
|
||||
view_id: account.account_journal_bank_view
|
||||
-
|
||||
I create a new partner Michal Gallette
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_michal1}:
|
||||
address:
|
||||
- city: paris
|
||||
country_id: base.fr
|
||||
name: Michal
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '75016'
|
||||
name: Mr.Michal Gallette
|
||||
-
|
||||
I create the first invoice on 1st January for 200 USD
|
||||
-
|
||||
|
@ -97,7 +85,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_michal1
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -132,7 +120,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_michal1
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -155,21 +143,21 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michal1"), ref('bank_journal_EUR'), 200.0, False, ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 200.0, False, ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 200.0,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.EUR'),
|
||||
'journal_id': ref('bank_journal_EUR'),
|
||||
'partner_id': ref('res_partner_michal1'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-03-01"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'First payment',
|
||||
'name': 'First payment: Case 2 USD/EUR DR USD',
|
||||
}
|
||||
if not res['value']['line_cr_ids']:
|
||||
res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}]
|
||||
|
@ -187,14 +175,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michal1'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michal1'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -206,7 +194,7 @@
|
|||
I check that the debtor account has 2 new lines with 144.44 and 77.78 in amount_currency columns and their credit columns are 130 and 70
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_michal1'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 2 USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -240,14 +228,14 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_michal1"), ref('bank_journal_USD'), 80.0, ref('base.USD'), ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_USD'), 80.0, ref('base.USD'), ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 80.0,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.USD'),
|
||||
'journal_id': ref('bank_journal_USD'),
|
||||
'partner_id': ref('res_partner_michal1'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-%m-%d"),
|
||||
|
@ -255,7 +243,7 @@
|
|||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'exchange_acc_id': ref('account.o_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'Second payment',
|
||||
'name': 'Second payment: Case 2 SUPPL USD/EUR DR USD',
|
||||
}
|
||||
vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-04-01'), ref('base.USD'), 80.0)['value'])
|
||||
if not res['value']['line_cr_ids']:
|
||||
|
@ -273,7 +261,7 @@
|
|||
I check that writeoff amount computed is 2.22
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (round(voucher_id.writeoff_amount, 2) == 2.22), "Writeoff amount is not 2.22$"
|
||||
-
|
||||
|
@ -281,14 +269,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that my voucher state is posted
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert voucher_id.state == 'posted', "Voucher state is not posted"
|
||||
-
|
||||
|
@ -301,7 +289,7 @@
|
|||
I check that my writeoff is correct. 2.11 in credit and 2.22 in amount_currency
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_michal1'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 2 SUPPL USD/EUR DR USD'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
|
|
@ -29,18 +29,6 @@
|
|||
currency: base.EUR
|
||||
company_id: base.main_company
|
||||
view_id: account.account_journal_bank_view
|
||||
-
|
||||
I create a new partner Peter Lawson.
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_peter0}:
|
||||
address:
|
||||
- city: paris
|
||||
country_id: base.fr
|
||||
name: Peter
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '75016'
|
||||
name: Mr.Peter Lawson
|
||||
-
|
||||
I create the first invoice on 1st January for 150 EUR
|
||||
-
|
||||
|
@ -60,7 +48,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_peter0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -95,7 +83,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_peter0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -118,21 +106,21 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_peter0"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 120.00,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.EUR'),
|
||||
'journal_id': ref('bank_journal_EUR'),
|
||||
'partner_id': ref('res_partner_peter0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-03-01"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'First payment',
|
||||
'name': 'First payment: Case 3',
|
||||
}
|
||||
if not res['value']['line_cr_ids']:
|
||||
res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}]
|
||||
|
@ -149,7 +137,7 @@
|
|||
I check that writeoff amount computed is 0.00
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.writeoff_amount == 0.00), "Writeoff amount is not 0.00"
|
||||
-
|
||||
|
@ -157,14 +145,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my first voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -176,7 +164,7 @@
|
|||
I check that the debtor account has 2 new lines with 0.00 and 0.00 in amount_currency columns and their credit are 20 and 100 respectively
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'),('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -210,21 +198,21 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_peter0"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_EUR'), 120.00, False, ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 120.00,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.EUR'),
|
||||
'journal_id': ref('bank_journal_EUR'),
|
||||
'partner_id': ref('res_partner_peter0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-04-01"),
|
||||
'payment_option': 'with_writeoff',
|
||||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'Second payment',
|
||||
'name': 'Second payment: Case 3',
|
||||
}
|
||||
if not res['value']['line_cr_ids']:
|
||||
res['value']['line_cr_ids'] = [{'type': 'cr', 'account_id': ref('account.a_recv'),}]
|
||||
|
@ -241,7 +229,7 @@
|
|||
I check that writeoff amount computed is 0.00
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'),('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'),('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (voucher_id.writeoff_amount == 0.00), "Writeoff amount is not 0"
|
||||
-
|
||||
|
@ -249,14 +237,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my second voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -268,7 +256,7 @@
|
|||
I check that the debtor account has 2 new lines with 0.00 and 0.00 in amount_currency columns and their credit are 70 and 50
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment'), ('partner_id', '=', ref('res_partner_peter0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'Second payment: Case 3'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
|
|
@ -43,18 +43,6 @@
|
|||
currency: base.CHF
|
||||
company_id: base.main_company
|
||||
view_id: account.account_journal_bank_view
|
||||
-
|
||||
I create a new partner John Armani.
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_john0}:
|
||||
address:
|
||||
- city: paris
|
||||
country_id: base.fr
|
||||
name: John
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '75016'
|
||||
name: Mr.John Armani
|
||||
-
|
||||
I create the first invoice on 1st January for 200 CAD
|
||||
-
|
||||
|
@ -74,7 +62,7 @@
|
|||
product_id: product.product_product_pc1
|
||||
uos_id: product.product_uom_unit
|
||||
journal_id: account.sales_journal
|
||||
partner_id: res_partner_john0
|
||||
partner_id: base.res_partner_seagate
|
||||
reference_type: none
|
||||
-
|
||||
I Validate invoice by clicking on Validate button
|
||||
|
@ -97,14 +85,14 @@
|
|||
!python {model: account.voucher}: |
|
||||
import netsvc, time
|
||||
vals = {}
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("res_partner_john0"), ref('bank_journal_CHF'), 200.00, ref('base.CHF'), ttype='receipt', date=False)
|
||||
res = self.onchange_partner_id(cr, uid, [], ref("base.res_partner_seagate"), ref('bank_journal_CHF'), 200.00, ref('base.CHF'), ttype='receipt', date=False)
|
||||
vals = {
|
||||
'account_id': ref('account.cash'),
|
||||
'amount': 200.00,
|
||||
'company_id': ref('base.main_company'),
|
||||
'currency_id': ref('base.CHF'),
|
||||
'journal_id': ref('bank_journal_CHF'),
|
||||
'partner_id': ref('res_partner_john0'),
|
||||
'partner_id': ref('base.res_partner_seagate'),
|
||||
'period_id': ref('account.period_3'),
|
||||
'type': 'receipt',
|
||||
'date': time.strftime("%Y-%m-%d"),
|
||||
|
@ -112,7 +100,7 @@
|
|||
'writeoff_acc_id': ref('account.a_expense'),
|
||||
'exchange_acc_id': ref('account.o_expense'),
|
||||
'comment': 'Write Off',
|
||||
'name': 'First payment',
|
||||
'name': 'First payment: Case 4',
|
||||
}
|
||||
vals.update(self.onchange_date(cr, uid, [], time.strftime('%Y-03-01'), ref('base.CHF'), 200.0)['value'])
|
||||
if not res['value']['line_cr_ids']:
|
||||
|
@ -128,7 +116,7 @@
|
|||
I check that writeoff amount computed is 13.26
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
assert (round(voucher_id.writeoff_amount,2) == 13.26), "Writeoff amount is not 13.26 CHF"
|
||||
-
|
||||
|
@ -136,14 +124,14 @@
|
|||
-
|
||||
!python {model: account.voucher}: |
|
||||
import netsvc
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.voucher', voucher[0], 'proforma_voucher', cr)
|
||||
-
|
||||
I check that the move of my voucher is valid
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
@ -160,7 +148,7 @@
|
|||
I check that my writeoff is correct. 11.05 credit and 13.26 amount_currency
|
||||
-
|
||||
!python {model: account.voucher}: |
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment'), ('partner_id', '=', ref('res_partner_john0'))])
|
||||
voucher = self.search(cr, uid, [('name', '=', 'First payment: Case 4'), ('partner_id', '=', ref('base.res_partner_seagate'))])
|
||||
voucher_id = self.browse(cr, uid, voucher[0])
|
||||
move_line_obj = self.pool.get('account.move.line')
|
||||
move_lines = move_line_obj.search(cr, uid, [('move_id', '=', voucher_id.move_id.id)])
|
||||
|
|
|
@ -34,21 +34,8 @@
|
|||
-
|
||||
!record {model: account.journal, id: account.sales_journal}:
|
||||
analytic_journal_id: account.cose_journal_sale
|
||||
-
|
||||
I'm creating new Seller "Mr. Pinakin" with him email "info@mycustomer.com".
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_mrpinakin0}:
|
||||
address:
|
||||
- city: Namur
|
||||
country_id: base.be
|
||||
phone: (+32).10.45.18.77
|
||||
street: 23, street ways
|
||||
type: default
|
||||
zip: '2324324'
|
||||
email: 'info@mycustomer.com'
|
||||
name: Mr. Pinakin
|
||||
|
||||
-
|
||||
-
|
||||
I'm creating new Buyer "Mr. Patel" with his email "info@myinfobid.com".
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_mrpatel0}:
|
||||
|
@ -61,21 +48,7 @@
|
|||
email: 'info@myinfobid.com'
|
||||
name: Mr. Patel
|
||||
|
||||
-
|
||||
I'm creating new Buyer "Mr. Johnson" with his email "info@mrkjohnson.com".
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_mrkjohnson0}:
|
||||
address:
|
||||
- city: paris
|
||||
country_id: base.fr
|
||||
name: Mark Johnson
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '75016'
|
||||
email: 'info@mrkjohnson.com'
|
||||
name: Mr. Mark Johnson
|
||||
|
||||
-
|
||||
-
|
||||
I'm creating new Buyer "Mr. Rahi" with his email "info@poalrahi.com".
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_poalrahi0}:
|
||||
|
@ -140,7 +113,7 @@
|
|||
date_dep: !eval "'%s-08-01' %(datetime.now().year)"
|
||||
method: keep
|
||||
name: AD/006
|
||||
partner_id: res_partner_mrpinakin0
|
||||
partner_id: base.res_partner_maxtor
|
||||
specific_cost_ids:
|
||||
- account: auction.auction_expense
|
||||
amount: 200.0
|
||||
|
@ -149,7 +122,7 @@
|
|||
I create a new object wooden-chair which is to be auctioned.
|
||||
-
|
||||
!record {model: auction.lots, id: auction_lots_woodenchair0}:
|
||||
ach_uid: res_partner_mrkjohnson0
|
||||
ach_uid: base.res_partner_seagate
|
||||
artist_id: auction_artists_vincentvangogh0
|
||||
auction_id: auction_dates_antiquefurnitureexhibition0
|
||||
bord_vnd_id: auction_deposit_ad0
|
||||
|
@ -199,12 +172,12 @@
|
|||
price: 3200.0
|
||||
|
||||
-
|
||||
I create another bid for an object "wooden-chair" bid by a Mr.Johnson
|
||||
I create another bid for an object "wooden-chair" bid by a Mr.Seagate
|
||||
-
|
||||
!record {model: auction.bid, id: auction_bid_bid2}:
|
||||
auction_id: auction_dates_antiquefurnitureexhibition0
|
||||
name: bid/003
|
||||
partner_id: res_partner_mrkjohnson0
|
||||
partner_id: base.res_partner_seagate
|
||||
-
|
||||
I create a bid line.
|
||||
-
|
||||
|
@ -215,7 +188,7 @@
|
|||
lot_id: auction.auction_lots_woodenchair0
|
||||
price: 4000.0
|
||||
-
|
||||
Mr. MarkJohnson bid are selected as the Finalist Bid with 4000 Euro
|
||||
Mr. Seagate bids are selected as the Finalist Bid with 4000 Euro
|
||||
-
|
||||
I check that buyer price and seller price gets bound with the value
|
||||
-
|
||||
|
@ -250,7 +223,7 @@
|
|||
-
|
||||
!record {model: auction.lots.make.invoice.buyer, id: auction_lots_make_invoice_buyer_0}:
|
||||
amount: 3090.0
|
||||
buyer_id: res_partner_mrkjohnson0
|
||||
buyer_id: base.res_partner_seagate
|
||||
number: !eval "'%s/003' %(datetime.now().year)"
|
||||
objects: 1
|
||||
-
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 models
|
||||
import edi_service
|
||||
from models.edi import EDIMixin, edi_document
|
||||
|
||||
# web
|
||||
import controllers
|
|
@ -0,0 +1,57 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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/>.
|
||||
#
|
||||
##############################################################################
|
||||
{
|
||||
'name': 'Electronic Data Interchange (EDI)',
|
||||
'version': '1.0',
|
||||
'category': 'Tools',
|
||||
'complexity': "easy",
|
||||
'description': """
|
||||
Provides a common EDI platform that other Applications can use
|
||||
==============================================================
|
||||
|
||||
OpenERP specifies a generic EDI format for exchanging business
|
||||
documents between different systems, and provides generic
|
||||
mechanisms to import and export them.
|
||||
|
||||
More details about OpenERP's EDI format may be found in the
|
||||
technical OpenERP documentation at http://doc.openerp.com
|
||||
""",
|
||||
'author': 'OpenERP SA',
|
||||
'website': 'http://www.openerp.com',
|
||||
'depends': ['base', 'email_template'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
'test': [
|
||||
'test/edi_partner_test.yml',
|
||||
],
|
||||
'js': [
|
||||
'static/src/js/edi.js',
|
||||
],
|
||||
"css": [
|
||||
"static/src/css/edi.css"
|
||||
],
|
||||
'installable': True,
|
||||
'active': False,
|
||||
'certificate': '002046536359186',
|
||||
}
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1 @@
|
|||
import main
|
|
@ -0,0 +1,84 @@
|
|||
import json
|
||||
import textwrap
|
||||
|
||||
import simplejson
|
||||
import werkzeug.wrappers
|
||||
|
||||
import web.common.http as openerpweb
|
||||
import web.controllers.main
|
||||
|
||||
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'):
|
||||
self.wc = openerpweb.controllers_path['/web/webclient']
|
||||
d = {}
|
||||
d["js"] = "\n".join('<script type="text/javascript" src="%s"></script>'%i for i in self.wc.manifest_list(req, mods, 'js'))
|
||||
d["css"] = "\n".join('<link rel="stylesheet" href="%s">'%i for i in self.wc.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"] = 'new s.edi.EdiView(null,"%s","%s").appendTo($("body"));'%(db,token)
|
||||
r = web.controllers.main.html_template % d
|
||||
return r
|
||||
|
||||
@openerpweb.httprequest
|
||||
def import_url(self, req, url):
|
||||
d = self.template(req)
|
||||
d["init"] = 'new s.edi.EdiImport(null,"%s").appendTo($("body"));'%(url)
|
||||
r = web.controllers.main.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)
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def import_edi_url(self, req, url):
|
||||
result = req.session.proxy('edi').import_edi_url(req.session._db, req.session._uid, req.session._password, url)
|
||||
if len(result) == 1:
|
||||
return {"action": web.controllers.main.clean_action(req, result[0][2])}
|
||||
return True
|
||||
|
||||
#
|
|
@ -0,0 +1,70 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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
|
||||
|
||||
import netsvc
|
||||
import openerp
|
||||
|
||||
_logger = logging.getLogger('edi.service')
|
||||
|
||||
class edi(netsvc.ExportService):
|
||||
|
||||
def __init__(self, name="edi"):
|
||||
netsvc.ExportService.__init__(self, name)
|
||||
|
||||
def _edi_dispatch(self, db_name, method_name, *method_args):
|
||||
try:
|
||||
registry = openerp.modules.registry.RegistryManager.get(db_name)
|
||||
assert registry, 'Unknown database %s' % db_name
|
||||
edi_document = registry['edi.document']
|
||||
cr = registry.db.cursor()
|
||||
res = None
|
||||
res = getattr(edi_document, method_name)(cr, *method_args)
|
||||
cr.commit()
|
||||
except Exception:
|
||||
_logger.exception('Failed to execute EDI method %s with args %r', method_name, method_args)
|
||||
raise
|
||||
finally:
|
||||
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)
|
||||
|
||||
def exp_import_edi_url(self, db_name, uid, passwd, edi_url, context=None):
|
||||
return self._edi_dispatch(db_name, 'import_edi', uid, None, edi_url)
|
||||
|
||||
def dispatch(self, method, params):
|
||||
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)
|
||||
return fn(*params)
|
||||
|
||||
edi()
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 edi
|
||||
import res_partner
|
||||
import res_company
|
||||
import res_currency
|
|
@ -0,0 +1,684 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 base64
|
||||
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
|
||||
from tools.translate import _
|
||||
from tools.safe_eval import safe_eval as eval
|
||||
|
||||
EXTERNAL_ID_PATTERN = re.compile(r'^([^.:]+)(?::([^.]+))?\.(\S+)$')
|
||||
EDI_VIEW_WEB_URL = '%s/edi/view?debug=1&db=%s&token=%s'
|
||||
EDI_PROTOCOL_VERSION = 1 # arbitrary ever-increasing version number
|
||||
EDI_GENERATOR = 'OpenERP ' + release.major_version
|
||||
EDI_GENERATOR_VERSION = release.version_info
|
||||
|
||||
def split_external_id(ext_id):
|
||||
match = EXTERNAL_ID_PATTERN.match(ext_id)
|
||||
assert match, \
|
||||
_("'%s' is an invalid external ID") % (ext_id)
|
||||
return {'module': match.group(1),
|
||||
'db_uuid': match.group(2),
|
||||
'id': match.group(3),
|
||||
'full': match.group(0)}
|
||||
|
||||
def safe_unique_id(database_id, model, record_id):
|
||||
"""Generate a unique string to represent a (database_uuid,model,record_id) pair
|
||||
without being too long, and with a very low probability of collisions.
|
||||
"""
|
||||
msg = "%s-%s-%s-%s" % (time.time(), database_id, model, record_id)
|
||||
digest = hashlib.sha1(msg).digest()
|
||||
# fold the sha1 20 bytes digest to 9 bytes
|
||||
digest = ''.join(chr(ord(x) ^ ord(y)) for (x,y) in zip(digest[:9], digest[9:-2]))
|
||||
# b64-encode the 9-bytes folded digest to a reasonable 12 chars ASCII ID
|
||||
digest = base64.urlsafe_b64encode(digest)
|
||||
return '%s-%s' % (model.replace('.','_'), digest)
|
||||
|
||||
def last_update_for(record):
|
||||
"""Returns the last update timestamp for the given record,
|
||||
if available, otherwise False
|
||||
"""
|
||||
if record._model._log_access:
|
||||
record_log = record.perm_read()[0]
|
||||
return record_log.get('write_date') or record_log.get('create_date') or False
|
||||
return False
|
||||
|
||||
_logger = logging.getLogger('edi')
|
||||
|
||||
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!')
|
||||
]
|
||||
|
||||
def new_edi_token(self, cr, uid, record):
|
||||
"""Return a new, random unique token to identify this model record,
|
||||
and to be used as token when exporting it as an EDI document.
|
||||
|
||||
:param browse_record record: model record for which a token is needed
|
||||
"""
|
||||
db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
|
||||
edi_token = hashlib.sha256('%s-%s-%s-%s' % (time.time(), db_uuid, record._name, record.id)).hexdigest()
|
||||
return edi_token
|
||||
|
||||
def serialize(self, edi_documents):
|
||||
"""Serialize the given EDI document structures (Python dicts holding EDI data),
|
||||
using JSON serialization.
|
||||
|
||||
:param [dict] edi_documents: list of EDI document structures to serialize
|
||||
:return: UTF-8 encoded string containing the serialized document
|
||||
"""
|
||||
serialized_list = json.dumps(edi_documents)
|
||||
return serialized_list
|
||||
|
||||
def generate_edi(self, cr, uid, records, context=None):
|
||||
"""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`.
|
||||
|
||||
:param list(browse_record) records: records to export as EDI
|
||||
:return: UTF-8 encoded string containing the serialized records
|
||||
"""
|
||||
edi_list = []
|
||||
for record in records:
|
||||
record_model_obj = self.pool.get(record._name)
|
||||
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`.
|
||||
|
||||
:param edi_documents: list of Python dicts containing the deserialized
|
||||
version of EDI documents
|
||||
:return: list of (model, id, action) tuple containing the model and database ID
|
||||
of all records that were imported in the system, plus a suggested
|
||||
action definition dict for displaying each document.
|
||||
"""
|
||||
ir_module = self.pool.get('ir.module.module')
|
||||
res = []
|
||||
for edi_document in edi_documents:
|
||||
module = edi_document.get('__import_module') or edi_document.get('__module')
|
||||
assert module, 'a `__module` or `__import_module` attribute is required in each EDI document'
|
||||
if module != 'base' and not ir_module.search(cr, uid, [('name','=',module),('state','=','installed')]):
|
||||
raise osv.except_osv(_('Missing Application'),
|
||||
_("The document you are trying to import requires the OpenERP `%s` application. "
|
||||
"You can install it by connecting as the administrator and opening the configuration assistant.")%(module,))
|
||||
model = edi_document.get('__import_model') or edi_document.get('__model')
|
||||
assert model, 'a `__model` or `__import_model` attribute is required in each EDI document'
|
||||
model_obj = self.pool.get(model)
|
||||
assert model_obj, 'model `%s` cannot be found, despite module `%s` being available - '\
|
||||
'this EDI document seems invalid or unsupported' % (model,module)
|
||||
record_id = model_obj.edi_import(cr, uid, edi_document, context=context)
|
||||
record_action = model_obj._edi_record_display_action(cr, uid, record_id, context=context)
|
||||
res.append((model, record_id, record_action))
|
||||
return res
|
||||
|
||||
def deserialize(self, edi_documents_string):
|
||||
"""Return deserialized version of the given EDI Document string.
|
||||
|
||||
:param str|unicode edi_documents_string: UTF-8 string (or unicode) containing
|
||||
JSON-serialized EDI document(s)
|
||||
:return: Python object representing the EDI document(s) (usually a list of dicts)
|
||||
"""
|
||||
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
|
||||
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``)
|
||||
may be retrieved, without authentication.
|
||||
"""
|
||||
if edi_url:
|
||||
assert not edi_document, 'edi_document 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)
|
||||
return self.load_edi(cr, uid, edi_documents, context=context)
|
||||
|
||||
|
||||
class EDIMixin(object):
|
||||
"""Mixin class for Model objects that want be exposed as EDI documents.
|
||||
Classes that inherit from this mixin class should override the
|
||||
``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
|
||||
for attribute in attributes:
|
||||
assert edi_document.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
|
||||
# SUPERUSER based on its parameters
|
||||
def _edi_external_id(self, cr, uid, record, existing_id=None, existing_module=None,
|
||||
context=None):
|
||||
"""Generate/Retrieve unique external ID for ``record``.
|
||||
Each EDI record and each relationship attribute in it is identified by a
|
||||
unique external ID, which includes the database's UUID, as a way to
|
||||
refer to any record within any OpenERP instance, without conflict.
|
||||
|
||||
For OpenERP records that have an existing "External ID" (i.e. an entry in
|
||||
ir.model.data), the EDI unique identifier for this record will be made of
|
||||
"%s:%s:%s" % (module, database UUID, ir.model.data ID). The database's
|
||||
UUID MUST NOT contain a colon characters (this is guaranteed by the
|
||||
UUID algorithm).
|
||||
|
||||
For records that have no existing ir.model.data entry, a new one will be
|
||||
created during the EDI export. It is recommended that the generated external ID
|
||||
contains a readable reference to the record model, plus a unique value that
|
||||
hides the database ID. If ``existing_id`` is provided (because it came from
|
||||
an import), it will be used instead of generating a new one.
|
||||
If ``existing_module`` is provided (because it came from
|
||||
an import), it will be used instead of using local values.
|
||||
|
||||
:param browse_record record: any browse_record needing an EDI external ID
|
||||
:param string existing_id: optional existing external ID value, usually coming
|
||||
from a just-imported EDI record, to be used instead
|
||||
of generating a new one
|
||||
:param string existing_module: optional existing module name, usually in the
|
||||
format ``module:db_uuid`` and coming from a
|
||||
just-imported EDI record, to be used instead
|
||||
of local values
|
||||
:return: the full unique External ID to use for record
|
||||
"""
|
||||
ir_model_data = self.pool.get('ir.model.data')
|
||||
db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
|
||||
ext_id = record.get_external_id()[record.id]
|
||||
if not ext_id:
|
||||
ext_id = existing_id or safe_unique_id(db_uuid, record._name, record.id)
|
||||
# ID is unique cross-db thanks to db_uuid (already included in existing_module)
|
||||
module = existing_module or "%s:%s" % (record._original_module, db_uuid)
|
||||
_logger.debug("%s: Generating new external ID `%s.%s` for %r", self._name,
|
||||
module, ext_id, record)
|
||||
ir_model_data.create(cr, openerp.SUPERUSER_ID,
|
||||
{'name': ext_id,
|
||||
'model': record._name,
|
||||
'module': module,
|
||||
'res_id': record.id})
|
||||
else:
|
||||
module, ext_id = ext_id.split('.')
|
||||
if not ':' in module:
|
||||
# this record was not previously EDI-imported
|
||||
if not module == record._original_module:
|
||||
# this could happen for data records defined in a module that depends
|
||||
# on the module that owns the model, e.g. purchase defines
|
||||
# product.pricelist records.
|
||||
_logger.debug('Mismatching module: expected %s, got %s, for %s',
|
||||
module, record._original_module, record)
|
||||
# ID is unique cross-db thanks to db_uuid
|
||||
module = "%s:%s" % (module, db_uuid)
|
||||
|
||||
return '%s.%s' % (module, ext_id)
|
||||
|
||||
def _edi_record_display_action(self, cr, uid, id, context=None):
|
||||
"""Returns an appropriate action definition dict for displaying
|
||||
the record with ID ``rec_id``.
|
||||
|
||||
:param int id: database ID of record to display
|
||||
:return: action definition dict
|
||||
"""
|
||||
return {'type': 'ir.actions.act_window',
|
||||
'view_mode': 'form,tree',
|
||||
'view_type': 'form',
|
||||
'res_model': self._name,
|
||||
'res_id': id}
|
||||
|
||||
def edi_metadata(self, cr, uid, records, context=None):
|
||||
"""Return a list containing the boilerplate EDI structures for
|
||||
exporting ``records`` as EDI, including
|
||||
the metadata fields
|
||||
|
||||
The metadata fields always include::
|
||||
|
||||
{
|
||||
'__model': 'some.model', # record model
|
||||
'__module': 'module', # require module
|
||||
'__id': 'module:db-uuid:model.id', # unique global external ID for the record
|
||||
'__last_update': '2011-01-01 10:00:00', # last update date in UTC!
|
||||
'__version': 1, # EDI spec version
|
||||
'__generator' : 'OpenERP', # EDI generator
|
||||
'__generator_version' : [6,1,0], # server version, to check compatibility.
|
||||
'__attachments_':
|
||||
}
|
||||
|
||||
:param list(browse_record) records: records to export
|
||||
: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:
|
||||
ext_id = self._edi_external_id(cr, uid, record, context=context)
|
||||
edi_dict = {
|
||||
'__id': ext_id,
|
||||
'__last_update': last_update_for(record),
|
||||
'__model' : record._name,
|
||||
'__module' : record._original_module,
|
||||
'__version': EDI_PROTOCOL_VERSION,
|
||||
'__generator': EDI_GENERATOR,
|
||||
'__generator_version': EDI_GENERATOR_VERSION,
|
||||
}
|
||||
attachment_ids = ir_attachment.search(cr, uid, [('res_model','=', record._name), ('res_id', '=', record.id)])
|
||||
if attachment_ids:
|
||||
attachments = []
|
||||
for attachment in ir_attachment.browse(cr, uid, attachment_ids, context=context):
|
||||
attachments.append({
|
||||
'name' : attachment.name,
|
||||
'content': attachment.datas, # already base64 encoded!
|
||||
'file_name': attachment.datas_fname,
|
||||
})
|
||||
edi_dict.update(__attachments=attachments)
|
||||
results.append(edi_dict)
|
||||
return results
|
||||
|
||||
def edi_m2o(self, cr, uid, record, context=None):
|
||||
"""Return a m2o EDI representation for the given record.
|
||||
|
||||
The EDI format for a many2one is::
|
||||
|
||||
['unique_external_id', 'Document Name']
|
||||
"""
|
||||
edi_ext_id = self._edi_external_id(cr, uid, record, context=context)
|
||||
relation_model = record._model
|
||||
name = relation_model.name_get(cr, uid, [record.id], context=context)
|
||||
name = name and name[0][1] or False
|
||||
return [edi_ext_id, name]
|
||||
|
||||
def edi_o2m(self, cr, uid, records, edi_struct=None, context=None):
|
||||
"""Return a list representing a O2M EDI relationship containing
|
||||
all the given records, according to the given ``edi_struct``.
|
||||
This is basically the same as exporting all the record using
|
||||
:meth:`~.edi_export` with the given ``edi_struct``, and wrapping
|
||||
the results in a list.
|
||||
|
||||
Example::
|
||||
|
||||
[ # O2M fields would be a list of dicts, with their
|
||||
{ '__id': 'module:db-uuid.id', # own __id.
|
||||
'__last_update': 'iso date', # update date
|
||||
'name': 'some name',
|
||||
#...
|
||||
},
|
||||
# ...
|
||||
],
|
||||
"""
|
||||
result = []
|
||||
for record in records:
|
||||
result += record._model.edi_export(cr, uid, [record], edi_struct=edi_struct, context=context)
|
||||
return result
|
||||
|
||||
def edi_m2m(self, cr, uid, records, context=None):
|
||||
"""Return a list representing a M2M EDI relationship directed towards
|
||||
all the given records.
|
||||
This is basically the same as exporting all the record using
|
||||
:meth:`~.edi_m2o` and wrapping the results in a list.
|
||||
|
||||
Example::
|
||||
|
||||
# M2M fields are exported as a list of pairs, like a list of M2O values
|
||||
[
|
||||
['module:db-uuid.id1', 'Task 01: bla bla'],
|
||||
['module:db-uuid.id2', 'Task 02: bla bla']
|
||||
]
|
||||
"""
|
||||
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
|
||||
records, and matching the given ``edi_struct``, if provided.
|
||||
|
||||
:param edi_struct: if provided, edi_struct should be a dictionary
|
||||
with a skeleton of the fields to export.
|
||||
Basic fields can have any key as value, but o2m
|
||||
values should have a sample skeleton dict as value,
|
||||
to act like a recursive export.
|
||||
For example, for a res.partner record::
|
||||
|
||||
edi_struct: {
|
||||
'name': True,
|
||||
'company_id': True,
|
||||
'address': {
|
||||
'name': True,
|
||||
'street': True,
|
||||
}
|
||||
}
|
||||
|
||||
Any field not specified in the edi_struct will not
|
||||
be included in the exported data. Fields with no
|
||||
value (False) will be omitted in the EDI struct.
|
||||
If edi_struct is omitted, no fields will be exported
|
||||
"""
|
||||
if edi_struct is None:
|
||||
edi_struct = {}
|
||||
fields_to_export = edi_struct.keys()
|
||||
results = []
|
||||
for record in records:
|
||||
edi_dict = self.edi_metadata(cr, uid, [record], context=context)[0]
|
||||
for field in fields_to_export:
|
||||
column = self._all_columns[field].column
|
||||
value = getattr(record, field)
|
||||
if not value and value not in ('', 0):
|
||||
continue
|
||||
elif column._type == 'many2one':
|
||||
value = self.edi_m2o(cr, uid, value, context=context)
|
||||
elif column._type == 'many2many':
|
||||
value = self.edi_m2m(cr, uid, value, context=context)
|
||||
elif column._type == 'one2many':
|
||||
value = self.edi_o2m(cr, uid, value, edi_struct=edi_struct.get(field, {}), context=context)
|
||||
edi_dict[field] = value
|
||||
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 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)
|
||||
if len(search_results) == 1:
|
||||
return model.browse(cr, uid, search_results[0][0], context=context)
|
||||
return False
|
||||
|
||||
def _edi_generate_report_attachment(self, cr, uid, record, context=None):
|
||||
"""Utility method to generate the first PDF-type report declared for the
|
||||
current model with ``usage`` attribute set to ``default``.
|
||||
This must be called explicitly by models that need it, usually
|
||||
at the beginning of ``edi_export``, before the call to ``super()``."""
|
||||
ir_actions_report = self.pool.get('ir.actions.report.xml')
|
||||
matching_reports = ir_actions_report.search(cr, uid, [('model','=',self._name),
|
||||
('report_type','=','pdf'),
|
||||
('usage','=','default')])
|
||||
if matching_reports:
|
||||
report = ir_actions_report.browse(cr, uid, matching_reports[0])
|
||||
report_service = 'report.' + report.report_name
|
||||
service = netsvc.LocalService(report_service)
|
||||
(result, format) = service.create(cr, uid, [record.id], {'model': self._name}, context=context)
|
||||
eval_context = {'time': time, 'object': record}
|
||||
if not report.attachment or not eval(report.attachment, eval_context):
|
||||
# no auto-saving of report as attachment, need to do it manually
|
||||
result = base64.b64encode(result)
|
||||
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},
|
||||
context=context)
|
||||
|
||||
def _edi_import_attachments(self, cr, uid, record_id, edi_document, context=None):
|
||||
ir_attachment = self.pool.get('ir.attachment')
|
||||
for attachment in edi_document.get('__attachments', []):
|
||||
# check attachment data is non-empty and valid
|
||||
file_data = None
|
||||
try:
|
||||
file_data = base64.b64decode(attachment.get('content'))
|
||||
except TypeError:
|
||||
pass
|
||||
assert file_data, 'Incorrect/Missing attachment file content'
|
||||
assert attachment.get('name'), 'Incorrect/Missing attachment name'
|
||||
assert attachment.get('file_name'), 'Incorrect/Missing attachment file name'
|
||||
assert attachment.get('file_name'), 'Incorrect/Missing attachment file name'
|
||||
ir_attachment.create(cr, uid, {'name': attachment['name'],
|
||||
'datas_fname': attachment['file_name'],
|
||||
'res_model': self._name,
|
||||
'res_id': record_id,
|
||||
# should be pure 7bit ASCII
|
||||
'datas': str(attachment['content']),
|
||||
}, context=context)
|
||||
|
||||
|
||||
def _edi_get_object_by_external_id(self, cr, uid, external_id, model, context=None):
|
||||
"""Returns browse_record representing object identified by the model and external_id,
|
||||
or None if no record was found with this external id.
|
||||
|
||||
:param external_id: fully qualified external id, in the EDI form
|
||||
``module:db_uuid:identifier``.
|
||||
:param model: model name the record belongs to.
|
||||
"""
|
||||
ir_model_data = self.pool.get('ir.model.data')
|
||||
# external_id is expected to have the form: ``module:db_uuid:model.random_name``
|
||||
ext_id_members = split_external_id(external_id)
|
||||
db_uuid = self.pool.get('ir.config_parameter').get_param(cr, uid, 'database.uuid')
|
||||
module = ext_id_members['module']
|
||||
ext_id = ext_id_members['id']
|
||||
modules = []
|
||||
ext_db_uuid = ext_id_members['db_uuid']
|
||||
if ext_db_uuid:
|
||||
modules.append('%s:%s' % (module, ext_id_members['db_uuid']))
|
||||
if ext_db_uuid is None or ext_db_uuid == db_uuid:
|
||||
# local records may also be registered without the db_uuid
|
||||
modules.append(module)
|
||||
data_ids = ir_model_data.search(cr, uid, [('model','=',model),
|
||||
('name','=',ext_id),
|
||||
('module','in',modules)])
|
||||
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
|
||||
|
||||
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
|
||||
given model, returning the corresponding database ID:
|
||||
|
||||
* First, checks if the ``external_id`` is already known, in which case the corresponding
|
||||
database ID is directly returned, without doing anything else;
|
||||
* If the ``external_id`` is unknown, attempts to locate an existing record
|
||||
with the same ``value`` via name_search(). If found, the given external_id will
|
||||
be assigned to this local record (in addition to any existing one)
|
||||
* 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
|
||||
"""
|
||||
_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)
|
||||
need_new_ext_id = False
|
||||
if not target:
|
||||
_logger.debug("%s: Importing EDI relationship [%r,%r] - ID not found, trying name_get",
|
||||
self._name, external_id, value)
|
||||
target = self._edi_get_object_by_name(cr, uid, value, model, context=context)
|
||||
need_new_ext_id = True
|
||||
if not target:
|
||||
_logger.debug("%s: Importing EDI relationship [%r,%r] - name not found, creating it!",
|
||||
self._name, external_id, value)
|
||||
# also need_new_ext_id here, but already been set above
|
||||
model = self.pool.get(model)
|
||||
res_id, name = model.name_create(cr, uid, value, context=context)
|
||||
target = model.browse(cr, uid, res_id, context=context)
|
||||
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
|
||||
# to avoid being taken as part of the module's data, and cleanup up at next update
|
||||
module = "%s:%s" % (ext_id_members['module'], ext_id_members['db_uuid'])
|
||||
# create a new ir.model.data entry for this value
|
||||
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.
|
||||
|
||||
:param dict edi_document: 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')), \
|
||||
"EDI Document Model and current model do not match: '%s' (EDI) vs '%s' (current)" % \
|
||||
(edi_document['__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'])
|
||||
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'])
|
||||
return existing.id
|
||||
|
||||
record_values = {}
|
||||
o2m_todo = {} # o2m values are processed after their parent already exists
|
||||
for field_name, field_value in edi_document.iteritems():
|
||||
# skip metadata and empty fields
|
||||
if field_name.startswith('__') or field_value is None or field_value is False:
|
||||
continue
|
||||
field_info = self._all_columns.get(field_name)
|
||||
if not field_info:
|
||||
_logger.warning('Ignoring unknown field `%s` when importing `%s` EDI document', field_name, self._name)
|
||||
continue
|
||||
field = field_info.column
|
||||
# skip function/related fields
|
||||
if isinstance(field, fields.function):
|
||||
_logger.warning("Unexpected function field value found in '%s' EDI document: '%s'" % (self._name, field_name))
|
||||
continue
|
||||
relation_model = field._obj
|
||||
if field._type == 'many2one':
|
||||
record_values[field_name] = self.edi_import_relation(cr, uid, relation_model,
|
||||
field_value[1], field_value[0],
|
||||
context=context)
|
||||
elif field._type == 'many2many':
|
||||
record_values[field_name] = [self.edi_import_relation(cr, uid, relation_model, m2m_value[1],
|
||||
m2m_value[0], context=context)
|
||||
for m2m_value in field_value]
|
||||
elif field._type == 'one2many':
|
||||
# must wait until parent report is imported, as the parent relationship
|
||||
# is often required in o2m child records
|
||||
o2m_todo[field_name] = field_value
|
||||
else:
|
||||
record_values[field_name] = field_value
|
||||
|
||||
module_ref = "%s:%s" % (ext_id_members['module'], ext_id_members['db_uuid'])
|
||||
record_id = self.pool.get('ir.model.data')._update(cr, uid, self._name, module_ref, record_values,
|
||||
xml_id=ext_id_members['id'], context=context)
|
||||
|
||||
record_display, = self.name_get(cr, uid, [record_id], context=context)
|
||||
|
||||
# process o2m values, connecting them to their parent on-the-fly
|
||||
for o2m_field, o2m_value in o2m_todo.iteritems():
|
||||
field = self._all_columns[o2m_field].column
|
||||
dest_model = self.pool.get(field._obj)
|
||||
for o2m_line in o2m_value:
|
||||
# link to parent record: expects an (ext_id, name) pair
|
||||
o2m_line[field._fields_id] = (ext_id_members['full'], record_display[1])
|
||||
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)
|
||||
|
||||
return record_id
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,58 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 osv import fields,osv
|
||||
|
||||
class res_company(osv.osv):
|
||||
"""Helper subclass for res.company providing util methods for working with
|
||||
companies in the context of EDI import/export. The res.company object
|
||||
itself is not EDI-exportable"""
|
||||
_inherit = "res.company"
|
||||
|
||||
def edi_export_address(self, cr, uid, company, edi_address_struct=None, context=None):
|
||||
"""Returns a dict representation of the address of the company record, suitable for
|
||||
inclusion in an EDI document, and matching the given edi_address_struct if provided.
|
||||
The first found address is returned, in order of preference: invoice, contact, default.
|
||||
|
||||
:param browse_record company: company to export
|
||||
:return: dict containing the address representation for the company record, or
|
||||
an empty dict if no address can be found
|
||||
"""
|
||||
res_partner = self.pool.get('res.partner')
|
||||
res_partner_address = self.pool.get('res.partner.address')
|
||||
addresses = res_partner.address_get(cr, uid, [company.partner_id.id], ['default', 'contact', 'invoice'])
|
||||
addr_id = addresses['invoice'] or addresses['contact'] or addresses['default']
|
||||
result = {}
|
||||
if addr_id:
|
||||
address = res_partner_address.browse(cr, uid, addr_id, context=context)
|
||||
result = res_partner_address.edi_export(cr, uid, [address], edi_struct=edi_address_struct, context=context)[0]
|
||||
if company.logo:
|
||||
result['logo'] = company.logo # already base64-encoded
|
||||
if company.paypal_account:
|
||||
result['paypal_account'] = company.paypal_account
|
||||
# bank info: include only bank account supposed to be displayed in document footers
|
||||
res_partner_bank = self.pool.get('res.partner.bank')
|
||||
bank_ids = res_partner_bank.search(cr, uid, [('company_id','=',company.id),('footer','=',True)], context=context)
|
||||
if bank_ids:
|
||||
result['bank_ids'] = res_partner_address.edi_m2m(cr, uid,
|
||||
res_partner_bank.browse(cr, uid, bank_ids, context=context),
|
||||
context=context)
|
||||
return result
|
|
@ -0,0 +1,64 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 osv import fields,osv
|
||||
from edi import EDIMixin
|
||||
from openerp import SUPERUSER_ID
|
||||
|
||||
RES_CURRENCY_EDI_STRUCT = {
|
||||
#custom: 'code'
|
||||
'symbol': True,
|
||||
'rate': True,
|
||||
}
|
||||
|
||||
class res_currency(osv.osv, EDIMixin):
|
||||
_inherit = "res.currency"
|
||||
|
||||
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
|
||||
edi_struct = dict(edi_struct or RES_CURRENCY_EDI_STRUCT)
|
||||
edi_doc_list = []
|
||||
for currency in records:
|
||||
# Get EDI doc based on struct. The result will also contain all metadata fields and attachments.
|
||||
edi_doc = super(res_currency,self).edi_export(cr, uid, [currency], edi_struct, context)[0]
|
||||
edi_doc.update(code=currency.name)
|
||||
edi_doc_list.append(edi_doc)
|
||||
return edi_doc_list
|
||||
|
||||
def edi_import(self, cr, uid, edi_document, context=None):
|
||||
self._edi_requires_attributes(('code','symbol'), edi_document)
|
||||
external_id = edi_document['__id']
|
||||
existing_currency = self._edi_get_object_by_external_id(cr, uid, external_id, 'res_currency', context=context)
|
||||
if existing_currency:
|
||||
return existing_currency.id
|
||||
|
||||
# find with unique ISO code
|
||||
existing_ids = self.search(cr, uid, [('name','=',edi_document['code'])])
|
||||
if existing_ids:
|
||||
return existing_ids[0]
|
||||
|
||||
# nothing found, create a new one
|
||||
currency_id = self.create(cr, SUPERUSER_ID, {'name': edi_document['code'],
|
||||
'symbol': edi_document['symbol']}, context=context)
|
||||
rate = edi_document.pop('rate')
|
||||
if rate:
|
||||
self.pool.get('res.currency.rate').create(cr, SUPERUSER_ID, {'currency_id': currency_id,
|
||||
'rate': rate}, context=context)
|
||||
return currency_id
|
|
@ -0,0 +1,104 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 osv import fields,osv
|
||||
from edi import EDIMixin
|
||||
from openerp import SUPERUSER_ID
|
||||
from tools.translate import _
|
||||
|
||||
RES_PARTNER_ADDRESS_EDI_STRUCT = {
|
||||
'name': True,
|
||||
'email': True,
|
||||
'street': True,
|
||||
'street2': True,
|
||||
'zip': True,
|
||||
'city': True,
|
||||
'country_id': True,
|
||||
'state_id': True,
|
||||
'phone': True,
|
||||
'fax': True,
|
||||
'mobile': True,
|
||||
}
|
||||
|
||||
RES_PARTNER_EDI_STRUCT = {
|
||||
'name': True,
|
||||
'ref': True,
|
||||
'lang': True,
|
||||
'website': True,
|
||||
'address': RES_PARTNER_ADDRESS_EDI_STRUCT
|
||||
}
|
||||
|
||||
class res_partner(osv.osv, EDIMixin):
|
||||
_inherit = "res.partner"
|
||||
|
||||
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
|
||||
return super(res_partner,self).edi_export(cr, uid, records,
|
||||
edi_struct or dict(RES_PARTNER_EDI_STRUCT),
|
||||
context=context)
|
||||
|
||||
class res_partner_address(osv.osv, EDIMixin):
|
||||
_inherit = "res.partner.address"
|
||||
|
||||
def _get_bank_type(self, cr, uid, context=None):
|
||||
# first option: the "normal" bank type, installed by default
|
||||
res_partner_bank_type = self.pool.get('res.partner.bank.type')
|
||||
try:
|
||||
return self.pool.get('ir.model.data').get_object(cr, uid, 'base', 'bank_normal', context=context).code
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# second option: create a new custom type for EDI or use it if already created, as IBAN type is
|
||||
# not always appropriate: we need a free-form bank type for max flexibility (users can correct
|
||||
# data manually after import)
|
||||
code, label = 'edi_generic', 'Generic Bank Type (auto-created for EDI)'
|
||||
bank_code_ids = res_partner_bank_type.search(cr, uid, [('code','=',code)], context=context)
|
||||
if not bank_code_ids:
|
||||
logging.getLogger('edi.res_partner').info('Normal bank account type is missing, creating '
|
||||
'a generic bank account type for EDI.')
|
||||
self.res_partner_bank_type.create(cr, SUPERUSER_ID, {'name': label,
|
||||
'code': label})
|
||||
return code
|
||||
|
||||
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
|
||||
return super(res_partner_address,self).edi_export(cr, uid, records,
|
||||
edi_struct or dict(RES_PARTNER_ADDRESS_EDI_STRUCT),
|
||||
context=context)
|
||||
|
||||
def edi_import(self, cr, uid, edi_document, context=None):
|
||||
# handle bank info, if any
|
||||
edi_bank_ids = edi_document.pop('bank_ids', None)
|
||||
address_id = super(res_partner_address,self).edi_import(cr, uid, edi_document, context=context)
|
||||
if edi_bank_ids:
|
||||
address = self.browse(cr, uid, address_id, context=context)
|
||||
import_ctx = dict((context or {}),
|
||||
default_partner_id=address.partner_id.id,
|
||||
default_state=self._get_bank_type(cr, uid, context))
|
||||
for ext_bank_id, bank_name in edi_bank_ids:
|
||||
try:
|
||||
self.edi_import_relation(cr, uid, 'res.partner.bank',
|
||||
bank_name, ext_bank_id, context=import_ctx)
|
||||
except osv.except_osv:
|
||||
# failed to import it, try again with unrestricted default type
|
||||
logging.getLogger('edi.res_partner').warning('Failed to import bank account using'
|
||||
'bank type: %s, ignoring', import_ctx['default_state'],
|
||||
exc_info=True)
|
||||
return address_id
|
|
@ -0,0 +1,3 @@
|
|||
"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
|
|
|
@ -0,0 +1,220 @@
|
|||
/** 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;
|
||||
}
|
||||
|
||||
.openerp input.invalid {
|
||||
background-color: #F66 !important;
|
||||
border: 1px solid #D00 !important;
|
||||
color: #000;
|
||||
}
|
||||
/* following browse-specific hacks need to be separate rules, as browsers are
|
||||
required to ignore rules with unknown selectors. At time of writing, there
|
||||
is no standard way to style HTML5 placeholders */
|
||||
.openerp input.invalid:-moz-placeholder {
|
||||
color: #FFF;
|
||||
}
|
||||
.openerp input.invalid::-webkit-input-placeholder {
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
/** 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/ **/
|
||||
body {
|
||||
background: #EEE; /* contrast with paper */
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.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.
After Width: | Height: | Size: 6.8 KiB |
|
@ -0,0 +1,175 @@
|
|||
openerp.edi = function(openerp) {
|
||||
openerp.web.qweb.add_template("/web/static/src/xml/base.xml");
|
||||
openerp.web.qweb.add_template("/edi/static/src/xml/edi.xml");
|
||||
openerp.web.qweb.add_template("/edi/static/src/xml/edi_account.xml");
|
||||
openerp.web.qweb.add_template("/edi/static/src/xml/edi_sale_purchase.xml");
|
||||
openerp.edi = {}
|
||||
|
||||
openerp.edi.EdiView = openerp.web.Widget.extend({
|
||||
init: function(parent, db, token) {
|
||||
this._super();
|
||||
this.db = db;
|
||||
this.token = token;
|
||||
this.session = new openerp.web.Connection();
|
||||
this.template = "EdiEmpty";
|
||||
this.content = "";
|
||||
this.sidebar = "";
|
||||
},
|
||||
start: function() {
|
||||
this._super();
|
||||
var param = {"db": this.db, "token": this.token};
|
||||
this.rpc('/edi/get_edi_document', param, this.on_document_loaded, 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.$element.html(openerp.web.qweb.render("EdiView", param));
|
||||
this.$element.find('button.oe_edi_action_print').bind('click', this.do_print);
|
||||
this.$element.find('button#oe_edi_import_existing').bind('click', this.do_import_existing);
|
||||
this.$element.find('button#oe_edi_import_create').bind('click', this.do_import_create);
|
||||
this.$element.find('button#oe_edi_download').bind('click', this.do_download);
|
||||
this.$element.find('.oe_edi_import_choice, .oe_edi_import_choice_label').bind('click', this.toggle_choice('import'));
|
||||
this.$element.find('.oe_edi_pay_choice, .oe_edi_pay_choice_label').bind('click', this.toggle_choice('pay'));
|
||||
this.$element.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 +
|
||||
"¤cy_code=" + cur_code +
|
||||
"&button_subtype=services&no_note=1&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.$element.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();
|
||||
}
|
||||
});
|
||||
|
||||
openerp.edi.EdiImport = openerp.web.Widget.extend({
|
||||
init: function(parent,url) {
|
||||
this._super();
|
||||
this.url = url;
|
||||
var params = {};
|
||||
|
||||
this.template = "EdiImport";
|
||||
this.session = new openerp.web.Connection();
|
||||
this.login = new openerp.web.Login(this);
|
||||
this.header = new openerp.web.Header(this);
|
||||
this.header.on_logout.add(this.login.on_logout);
|
||||
},
|
||||
start: function() {
|
||||
this.session.on_session_invalid.add_last(this.do_ask_login)
|
||||
this.session.on_session_valid.add_last(this.do_import);
|
||||
this.header.appendTo($("#oe_header"));
|
||||
this.session.start();
|
||||
this.login.appendTo($('#oe_login'));
|
||||
},
|
||||
do_ask_login: function() {
|
||||
this.login.do_ask_login(this.do_import);
|
||||
},
|
||||
do_import: function() {
|
||||
this.rpc('/edi/import_edi_url', {url: this.url}, this.on_imported, this.on_imported_error);
|
||||
},
|
||||
on_imported: function(response) {
|
||||
if ('action' in response) {
|
||||
this.rpc("/web/session/save_session_action", {the_action: response.action}, function(key) {
|
||||
window.location = "/web/webclient/home?debug=1&s_action="+encodeURIComponent(key);
|
||||
});
|
||||
}
|
||||
else {
|
||||
$('<div>').dialog({
|
||||
modal: true,
|
||||
title: 'Import Successful!',
|
||||
buttons: {
|
||||
Ok: function() {
|
||||
$(this).dialog("close");
|
||||
window.location = "/web/webclient/home";
|
||||
}
|
||||
}
|
||||
}).html('The document has been successfully imported!');
|
||||
}
|
||||
},
|
||||
on_imported_error: function(response){
|
||||
var self = this;
|
||||
var msg = "Sorry, the document could not be imported.";
|
||||
if (response.data.fault_code) {
|
||||
msg += "\n Reason:" + response.data.fault_code;
|
||||
}
|
||||
var params = {error: response, message: msg};
|
||||
$(openerp.web.qweb.render("DialogWarning", params)).dialog({
|
||||
title: "Document Import Notification",
|
||||
modal: true,
|
||||
buttons: {
|
||||
Ok: function() { $(this).dialog("close"); }
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
|
|
@ -0,0 +1,93 @@
|
|||
<template>
|
||||
<t t-name="EdiEmpty">
|
||||
<div style="height:100%;"></div>
|
||||
</t>
|
||||
<t t-name="EdiImport">
|
||||
<t t-call="Interface"/>
|
||||
</t>
|
||||
<t t-name="EdiView">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%" height="100%" id="oe_app" class="oe-application oe_forms">
|
||||
<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: url('+ (doc.company_address ? '/edi/binary?db='+widget.db+'&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_form_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">
|
||||
<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>
|
|
@ -0,0 +1,163 @@
|
|||
<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="_.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-esc="doc.name"/></td>
|
||||
<td align="left"><t t-esc="doc.date_invoice"/></td>
|
||||
<td align="left"><t 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="_.sprintf('%.2f',invoice_line.quantity)"/> <t t-esc="invoice_line.uos_id[1]"/></td>
|
||||
<td align="right"><t t-esc="_.sprintf('%.2f',invoice_line.price_unit)"/></td>
|
||||
<td align="right"><t t-esc="_.sprintf('%.2f',invoice_line.discount)"/></td>
|
||||
<td align="right"><t t-esc="_.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="_.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="_.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="_.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="_.sprintf('%.2f',tax_line.base_amount)"/> <t t-esc="doc.currency.code"/></td>
|
||||
<td align="right"><t t-esc="_.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 && (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="_.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>
|
|
@ -0,0 +1,161 @@
|
|||
<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>
|
||||
|
||||
<h1 class="oe_edi_doc_title">Order <t t-esc="doc.name"/>: <t t-esc="_.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">Your Reference</th>
|
||||
<th align="left">Date</th>
|
||||
<th align="left">Salesman</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="_.sprintf('%.2f',doc_line.product_qty)"/> <t t-esc="doc_line.product_uom[1]"/>
|
||||
</td>
|
||||
<td align="right"><t t-esc="_.sprintf('%.2f',doc_line.price_unit)"/></td>
|
||||
<td align="right"><t t-esc="_.sprintf('%.2f',doc_line.discount or 0.0)"/></td>
|
||||
<td align="right"><t t-esc="_.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="_.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="_.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="_.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 && (doc.order_policy == 'prepaid' || doc.order_policy == 'manual')">
|
||||
<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="_.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>
|
|
@ -0,0 +1,44 @@
|
|||
-
|
||||
In order to test the basic EDI system, I will export a partner,
|
||||
modify the exported EDI document to add an attachment and change
|
||||
the data, and then re-import it and re-export it.
|
||||
|
||||
with an attached file, check the result, the alter the data
|
||||
and reimport it.
|
||||
-
|
||||
!python {model: edi.document}: |
|
||||
import json
|
||||
partner_obj = self.pool.get('res.partner')
|
||||
tokens = self.export_edi(cr, uid, [partner_obj.browse(cr, uid, ref('base.res_partner_agrolait'))])
|
||||
doc = self.get_document(cr, uid, tokens[0], context=context)
|
||||
edi_doc, = json.loads(doc)
|
||||
|
||||
# check content of the document
|
||||
assert edi_doc.get('__id').endswith('.res_partner_agrolait'), 'Incorrect external ID'
|
||||
assert edi_doc.get('__model') == 'res.partner', 'Incorrect/Missing __model'
|
||||
assert edi_doc.get('__module') == 'base', 'Incorrect/Missing __module'
|
||||
assert edi_doc.get('__last_update'), 'Missing __last_update'
|
||||
|
||||
# try to import the document after changing the name and id, and attaching a
|
||||
# file, and check that a new partner is returned
|
||||
edi_doc['__id'] = 'base:xxd37f8a-xx55-11e0-xxdd-xx81124c8b50.res_partner_xxx'
|
||||
edi_doc['name'] = 'AgroMilk'
|
||||
attachment = {
|
||||
'name': 'Test file',
|
||||
'file_name': 'test.png',
|
||||
# base64 standard requires blocks of 57bytes=76chars, NL-separated
|
||||
'content': 'iVBORw0KGgoAAAANSUhEUgAAAA4AAAAKCAYAAACE2W/HAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A\n/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9oIDQ84BkjLWAYAAACNSURBVCjP\njZKxDUJBDEOf/wbUbEHDhkxAzRjUDECBhMQYjIApuPBNpK+PpUinOPYll5Nt1iCJXifbSPpJZtHg\nXLUdu0FWpMEt87a/xtsmqjjNetPFyhuWYFuj7RcgQBNwXNGdw2CqY85yXWi5z7YBnmRyEHvg0YSX\nrLE9P30nwugOHHr+s5va4x+fofAGm1+JjnJICm0AAAAASUVORK5CYII=\n',
|
||||
}
|
||||
edi_doc['__attachments'] = [attachment]
|
||||
doc = json.dumps([edi_doc])
|
||||
result, = self.import_edi(cr, uid, edi_document=doc)
|
||||
assert result[0] == 'res.partner' and result[1] > ref('base.res_partner_agrolait'),\
|
||||
"Expected (%r,> %r) after import 1, got %r" % ('res.partner', ref('base.res_partner_agrolait'), 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)
|
||||
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), \
|
||||
'Incorrect value for %s, expected %r, got %r' % (attribute, edi_doc.get(attribute), edi_doc_output.get(attribute))
|
|
@ -21,5 +21,6 @@
|
|||
##############################################################################
|
||||
import email_template
|
||||
import wizard
|
||||
import res_partner
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -61,9 +61,13 @@ Openlabs was kept
|
|||
"data": [
|
||||
'wizard/email_template_preview_view.xml',
|
||||
'email_template_view.xml',
|
||||
'res_partner_view.xml',
|
||||
'wizard/email_compose_message_view.xml',
|
||||
'security/ir.model.access.csv'
|
||||
],
|
||||
"demo": [
|
||||
'res_partner_demo.yml',
|
||||
],
|
||||
"installable": True,
|
||||
"active": False,
|
||||
"certificate" : "00817073628967384349",
|
||||
|
|
|
@ -28,6 +28,7 @@ from osv import osv
|
|||
from osv import fields
|
||||
import tools
|
||||
from tools.translate import _
|
||||
from urllib import quote as quote
|
||||
|
||||
try:
|
||||
from mako.template import Template as MakoTemplate
|
||||
|
@ -66,6 +67,7 @@ class email_template(osv.osv):
|
|||
user=user,
|
||||
# context kw would clash with mako internals
|
||||
ctx=context,
|
||||
quote=quote,
|
||||
format_exceptions=True)
|
||||
if result == u'False':
|
||||
result = u''
|
||||
|
@ -305,6 +307,7 @@ class email_template(osv.osv):
|
|||
'attachment_ids': False,
|
||||
'message_id': False,
|
||||
'state': 'outgoing',
|
||||
'subtype': 'plain',
|
||||
}
|
||||
if not template_id:
|
||||
return values
|
||||
|
@ -319,6 +322,9 @@ class email_template(osv.osv):
|
|||
template.model, res_id, context=context) \
|
||||
or False
|
||||
|
||||
if values['body_html']:
|
||||
values.update(subtype='html')
|
||||
|
||||
if template.user_signature:
|
||||
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
|
||||
values['body_text'] += '\n\n' + signature
|
||||
|
@ -355,20 +361,24 @@ class email_template(osv.osv):
|
|||
values['attachments'] = attachments
|
||||
return values
|
||||
|
||||
def send_mail(self, cr, uid, template_id, res_id, context=None):
|
||||
def send_mail(self, cr, uid, template_id, res_id, force_send=False, context=None):
|
||||
"""Generates a new mail message for the given template and record,
|
||||
and schedule it for delivery through the ``mail`` module's scheduler.
|
||||
and schedules it for delivery through the ``mail`` module's scheduler.
|
||||
|
||||
:param int template_id: id of the template to render
|
||||
:param int res_id: id of the record to render the template with
|
||||
(model is taken from the template)
|
||||
:param bool force_send: if True, the generated mail.message is
|
||||
immediately sent after being created, as if the scheduler
|
||||
was executed for this message only.
|
||||
:returns: id of the mail.message that was created
|
||||
"""
|
||||
mail_message = self.pool.get('mail.message')
|
||||
ir_attachment = self.pool.get('ir.attachment')
|
||||
template = self.browse(cr, uid, template_id, context)
|
||||
values = self.generate_email(cr, uid, template_id, res_id, context=context)
|
||||
attachments = values.pop('attachments') or {}
|
||||
message_id = mail_message.create(cr, uid, values, context=context)
|
||||
msg_id = mail_message.create(cr, uid, values, context=context)
|
||||
# link attachments
|
||||
attachment_ids = []
|
||||
for fname, fcontent in attachments.iteritems():
|
||||
|
@ -377,10 +387,13 @@ class email_template(osv.osv):
|
|||
'datas_fname': fname,
|
||||
'datas': fcontent,
|
||||
'res_model': mail_message._name,
|
||||
'res_id': message_id,
|
||||
'res_id': msg_id,
|
||||
}
|
||||
if context.has_key('default_type'):
|
||||
del context['default_type']
|
||||
attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context))
|
||||
attachment_ids.append(ir_attachment.create(cr, uid, attachment_data, context=context))
|
||||
if force_send:
|
||||
mail_message.send(cr, uid, [msg_id], context=context)
|
||||
return msg_id
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -155,9 +155,8 @@
|
|||
<field name="search_view_id" ref="view_email_template_search"/>
|
||||
</record>
|
||||
|
||||
<menuitem name="Templates" id="menu_email_template_all_tools"
|
||||
parent="mail.menu_email_message_tools" action="action_email_template_tree_all" />
|
||||
|
||||
<menuitem id="menu_email_templates" parent="base.menu_email" action="action_email_template_tree_all"
|
||||
sequence="20"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 osv import fields,osv
|
||||
|
||||
class res_partner(osv.osv):
|
||||
"""Inherit res.partner to add a generic opt-out field that can be used
|
||||
to restrict usage of automatic email templates.
|
||||
This field is unused by default. """
|
||||
_inherit = 'res.partner'
|
||||
|
||||
_columns = {
|
||||
'opt_out': fields.boolean('Opt-out', help="If checked, this partner will not receive any automated email " \
|
||||
"notifications, such as the availability of invoices."),
|
||||
}
|
||||
|
||||
_defaults = {
|
||||
'opt_out': False,
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
-
|
||||
Set opt-out to True on all demo partners
|
||||
-
|
||||
!python {model: res.partner}: |
|
||||
partner_ids = self.search(cr, uid, [])
|
||||
# assume partners with an external ID come from demo data
|
||||
ext_ids = self._get_external_ids(cr, uid, partner_ids)
|
||||
ids_to_update = [k for (k,v) in ext_ids.iteritems() if v]
|
||||
self.write(cr, uid, ids_to_update, {'opt_out': True})
|
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="res_partner_opt_out_form">
|
||||
<field name="name">res.partner.opt_out.form</field>
|
||||
<field name="model">res.partner</field>
|
||||
<field name="type">form</field>
|
||||
<field name="inherit_id" ref="base.view_partner_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="/form/notebook/page/field[@name='active']" position="after">
|
||||
<field name="opt_out" groups="base.group_extended"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
|
@ -87,6 +87,8 @@ class email_template_preview(osv.osv_memory):
|
|||
signature = self.pool.get('res.users').browse(cr, uid, uid, context).signature
|
||||
description += '\n' + signature
|
||||
vals['body_text'] = description
|
||||
if template.body_html:
|
||||
vals['body_html'] = self.render_template(cr, uid, template.body_html, model, res_id, context) or ''
|
||||
vals['report_name'] = self.render_template(cr, uid, template.report_name, model, res_id, context)
|
||||
return {'value': vals}
|
||||
|
||||
|
|
|
@ -479,11 +479,22 @@ class mail_message(osv.osv):
|
|||
attachments = []
|
||||
for attach in message.attachment_ids:
|
||||
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
|
||||
|
||||
body = message.body_html if message.subtype == 'html' else message.body_text
|
||||
body_alternative = None
|
||||
subtype_alternative = None
|
||||
if message.subtype == 'html' and message.body_text:
|
||||
# we have a plain text alternative prepared, pass it to
|
||||
# build_message instead of letting it build one
|
||||
body_alternative = message.body_text
|
||||
subtype_alternative = 'plain'
|
||||
|
||||
msg = ir_mail_server.build_email(
|
||||
email_from=message.email_from,
|
||||
email_to=to_email(message.email_to),
|
||||
subject=message.subject,
|
||||
body=message.body_html if message.subtype == 'html' else message.body_text,
|
||||
body=body,
|
||||
body_alternative=body_alternative,
|
||||
email_cc=to_email(message.email_cc),
|
||||
email_bcc=to_email(message.email_bcc),
|
||||
reply_to=message.reply_to,
|
||||
|
@ -491,6 +502,7 @@ class mail_message(osv.osv):
|
|||
references = message.references,
|
||||
object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
|
||||
subtype=message.subtype,
|
||||
subtype_alternative=subtype_alternative,
|
||||
headers=message.headers and literal_eval(message.headers))
|
||||
res = ir_mail_server.send_email(cr, uid, msg,
|
||||
mail_server_id=message.mail_server_id.id,
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<?xml version="1.0"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<menuitem name="Configuration" parent="base.menu_tools" id="base.menu_lunch_survey_root" sequence="20"/>
|
||||
|
||||
<record model="ir.ui.view" id="view_email_message_form">
|
||||
<field name="name">mail.message.form</field>
|
||||
<field name="model">mail.message</field>
|
||||
|
@ -144,14 +141,10 @@
|
|||
src_model="res.partner"
|
||||
view_id="view_email_message_tree"/>
|
||||
|
||||
<menuitem name="Emails" id="menu_email_message_tools" parent="base.menu_tools" />
|
||||
|
||||
<menuitem name="Messages"
|
||||
id="menu_email_message"
|
||||
parent="menu_email_message_tools"
|
||||
parent="base.menu_email"
|
||||
action="action_view_mail_message" />
|
||||
|
||||
<menuitem name="Emails" id="menu_config_email" parent="base.menu_lunch_survey_root" sequence="20"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -13,36 +13,22 @@
|
|||
list_price: 80.00
|
||||
|
||||
- |
|
||||
"Mark Johnson" want to join "Gold Membership".
|
||||
- |
|
||||
I'm creating new member "Mark Johnson".
|
||||
"Seagate" want to join "Gold Membership", so I'm checking "Current Membership State" of "Seagate". It is an "Non Member" or not.
|
||||
-
|
||||
!record {model: res.partner, id: res_partner_markjohnson0}:
|
||||
address:
|
||||
- city: paris
|
||||
country_id: base.fr
|
||||
name: Mark Johnson
|
||||
street: 1 rue Rockfeller
|
||||
type: invoice
|
||||
zip: '75016'
|
||||
name: Mark Johnson
|
||||
- |
|
||||
I'm checking "Current Membership State" of "Mark Johnson". It is an "Non Member" or not.
|
||||
-
|
||||
!assert {model: res.partner, id: res_partner_markjohnson0}:
|
||||
!assert {model: res.partner, id: base.res_partner_seagate}:
|
||||
- membership_state == 'none', 'Member should be has "Current Membership State" in "Non Member".'
|
||||
- |
|
||||
I'm doing to make membership invoice for "Mark Johnson" on joining "Gold Membership".
|
||||
I'm doing to make membership invoice for "Seagate" on joining "Gold Membership".
|
||||
-
|
||||
!python {model: res.partner}: |
|
||||
self.create_membership_invoice(cr, uid, [ref("res_partner_markjohnson0")], product_id=ref("product_product_membershipproduct0"), datas={"amount":80.00})
|
||||
self.create_membership_invoice(cr, uid, [ref("base.res_partner_seagate")], product_id=ref("product_product_membershipproduct0"), datas={"amount":80.00})
|
||||
- |
|
||||
I'm checking "Current Membership State" of "Mark Johnson". It is an "Waiting Member" or not.
|
||||
I'm checking "Current Membership State" of "Seagate". It is an "Waiting Member" or not.
|
||||
-
|
||||
!assert {model: res.partner, id: res_partner_markjohnson0}:
|
||||
!assert {model: res.partner, id: base.res_partner_seagate}:
|
||||
- membership_state == 'waiting', 'Member should be has "Current Membership State" in "Waiting Member".'
|
||||
- |
|
||||
I'm Opening that Invoice which is created for "Mark Johnson".
|
||||
I'm Opening that Invoice which is created for "Seagate".
|
||||
-
|
||||
!python {model: res.partner}: |
|
||||
import netsvc
|
||||
|
@ -52,16 +38,16 @@
|
|||
membership_line_pool = self.pool.get('membership.membership_line')
|
||||
membership_pool = self.pool.get('product.product')
|
||||
|
||||
membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('res_partner_markjohnson0'))])
|
||||
membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('base.res_partner_seagate'))])
|
||||
membership_lines = membership_line_pool.browse(cr, uid, membership_line_ids)
|
||||
assert membership_lines, _('Membership is not registrated.')
|
||||
membership_line = membership_lines[0]
|
||||
wf_service = netsvc.LocalService("workflow")
|
||||
wf_service.trg_validate(uid, 'account.invoice', membership_line.account_invoice_id.id, 'invoice_open', cr)
|
||||
- |
|
||||
I'm checking "Current membership state" of "Mark Johnson". It is an "Invoiced Member" or not.
|
||||
I'm checking "Current membership state" of "Seagate". It is an "Invoiced Member" or not.
|
||||
-
|
||||
!assert {model: res.partner, id: res_partner_markjohnson0}:
|
||||
!assert {model: res.partner, id: base.res_partner_seagate}:
|
||||
- membership_state == 'invoiced', 'Member should be has "Current Membership State" in "Invoiced Member".'
|
||||
|
||||
- |
|
||||
|
@ -85,10 +71,10 @@
|
|||
- membership_state == 'free', 'Member should be has "Current Membership State" in "Free Member".'
|
||||
|
||||
- |
|
||||
I'm set "Mark Johnson" as a associated member of "Ms. Johnson" and also set Non free member.
|
||||
I'm set "Seagate" as a associated member of "Ms. Johnson" and also set Non free member.
|
||||
-
|
||||
!python {model: res.partner}: |
|
||||
self.write(cr, uid, [ref("res_partner_msjohnson0")], {'free_member': False, 'associate_member': ref("res_partner_markjohnson0")})
|
||||
self.write(cr, uid, [ref("res_partner_msjohnson0")], {'free_member': False, 'associate_member': ref("base.res_partner_seagate")})
|
||||
|
||||
- |
|
||||
I'm checking "Current membership state" of "Ms. Johnson". It is an "Paid Member" or not.
|
||||
|
@ -108,17 +94,17 @@
|
|||
type: service
|
||||
list_price: 50.00
|
||||
- |
|
||||
I'm making invoice of "Mark Johnson" member on joining new membership "Regular Membership".
|
||||
I'm making invoice of "Seagate" member on joining new membership "Regular Membership".
|
||||
-
|
||||
!python {model: res.partner}: |
|
||||
self.create_membership_invoice(cr, uid, [ref("res_partner_markjohnson0")], product_id=ref("product_product_membershipproduct1"), datas={"amount":50.00})
|
||||
self.create_membership_invoice(cr, uid, [ref("base.res_partner_seagate")], product_id=ref("product_product_membershipproduct1"), datas={"amount":50.00})
|
||||
- |
|
||||
I'm checking "Current membership state" of "Mark Johnson". It is an "Old Member" or not.
|
||||
I'm checking "Current membership state" of "Seagate". It is an "Old Member" or not.
|
||||
-
|
||||
!assert {model: res.partner, id: res_partner_markjohnson0}:
|
||||
!assert {model: res.partner, id: base.res_partner_seagate}:
|
||||
- membership_state == 'old', 'Member should be has "Current Membership State" in "Old Member".'
|
||||
- |
|
||||
I'm doing to make credit note of invoice which is paid by "Mark Johnson" to cancel membership.
|
||||
I'm doing to make credit note of invoice which is paid by "Seagate" to cancel membership.
|
||||
-
|
||||
!python {model: account.invoice}: |
|
||||
from tools.translate import _
|
||||
|
@ -128,15 +114,15 @@
|
|||
membership_pool = self.pool.get('product.product')
|
||||
invoice_refund_pool = self.pool.get('account.invoice.refund')
|
||||
|
||||
membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('res_partner_markjohnson0'))])
|
||||
membership_line_ids = membership_line_pool.search(cr, uid, [('membership_id','=',ref('product_product_membershipproduct0')),('partner','=',ref('base.res_partner_seagate'))])
|
||||
membership_lines = membership_line_pool.browse(cr, uid, membership_line_ids)
|
||||
assert membership_lines, _('Membership is not registrated.')
|
||||
membership_line = membership_lines[0]
|
||||
refund_id = invoice_refund_pool.create(cr, uid, {'description': 'Refund of Membership', 'filter_refund': 'refund'}, {'active_id': membership_line.account_invoice_id.id})
|
||||
invoice_refund_pool.invoice_refund(cr, uid, [refund_id], {'active_id': membership_line.account_invoice_id.id, 'active_ids': [membership_line.account_invoice_id.id]})
|
||||
- |
|
||||
I'm checking "Current membership state" of "Mark Johnson". It is an "Cancelled Member" or not.
|
||||
I'm checking "Current membership state" of "Seagate". It is an "Cancelled Member" or not.
|
||||
-
|
||||
!assert {model: res.partner, id: res_partner_markjohnson0}:
|
||||
!assert {model: res.partner, id: base.res_partner_seagate}:
|
||||
- membership_state == 'canceled', 'Member should be has "Current Membership State" in "Cancelled Member".'
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import wizard
|
|||
import report
|
||||
import stock
|
||||
import company
|
||||
import edi
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ Dashboard for purchase management that includes:
|
|||
'author': 'OpenERP SA',
|
||||
'website': 'http://www.openerp.com',
|
||||
'images' : ['images/purchase_order.jpeg', 'images/purchase_analysis.jpeg', 'images/request_for_quotation.jpeg'],
|
||||
'depends': ['base', 'account', 'stock', 'process', 'procurement'],
|
||||
'depends': ['stock', 'process', 'procurement'],
|
||||
'data': [
|
||||
'security/purchase_security.xml',
|
||||
'security/ir.model.access.csv',
|
||||
|
@ -58,15 +58,17 @@ Dashboard for purchase management that includes:
|
|||
'process/purchase_process.xml',
|
||||
'report/purchase_report_view.xml',
|
||||
'board_purchase_view.xml',
|
||||
'edi/purchase_order_action_data.xml',
|
||||
],
|
||||
'test': [
|
||||
'test/process/cancel_order.yml',
|
||||
'test/process/cancel_order.yml',
|
||||
'test/process/rfq2order2done.yml',
|
||||
'test/process/generate_invoice_from_reception.yml',
|
||||
'test/process/run_scheduler.yml',
|
||||
'test/process/merge_order.yml',
|
||||
'test/process/run_scheduler.yml',
|
||||
'test/process/merge_order.yml',
|
||||
'test/process/edi_purchase_order.yml',
|
||||
'test/ui/print_report.yml',
|
||||
'test/ui/duplicate_order.yml',
|
||||
'test/ui/duplicate_order.yml',
|
||||
'test/ui/delete_order.yml',
|
||||
],
|
||||
'demo': [
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 purchase_order
|
|
@ -0,0 +1,202 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from osv import fields, osv, orm
|
||||
from edi import EDIMixin
|
||||
from tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
from tools.translate import _
|
||||
|
||||
PURCHASE_ORDER_LINE_EDI_STRUCT = {
|
||||
'name': True,
|
||||
'date_planned': True,
|
||||
'product_id': True,
|
||||
'product_uom': True,
|
||||
'price_unit': True,
|
||||
'product_qty': True,
|
||||
'notes': True,
|
||||
|
||||
# fields used for web preview only - discarded on import
|
||||
'price_subtotal': True,
|
||||
}
|
||||
|
||||
PURCHASE_ORDER_EDI_STRUCT = {
|
||||
'company_id': True, # -> to be changed into partner
|
||||
'name': True,
|
||||
'partner_ref': True,
|
||||
'origin': True,
|
||||
'date_order': True,
|
||||
'partner_id': True,
|
||||
#custom: 'partner_address',
|
||||
'notes': True,
|
||||
'order_line': PURCHASE_ORDER_LINE_EDI_STRUCT,
|
||||
#custom: currency_id
|
||||
|
||||
# fields used for web preview only - discarded on import
|
||||
'amount_total': True,
|
||||
'amount_untaxed': True,
|
||||
'amount_tax': True,
|
||||
}
|
||||
|
||||
class purchase_order(osv.osv, EDIMixin):
|
||||
_inherit = 'purchase.order'
|
||||
|
||||
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)
|
||||
res_company = self.pool.get('res.company')
|
||||
res_partner_address = self.pool.get('res.partner.address')
|
||||
edi_doc_list = []
|
||||
for order in records:
|
||||
# generate the main report
|
||||
self._edi_generate_report_attachment(cr, uid, order, context=context)
|
||||
|
||||
# Get EDI doc based on struct. The result will also contain all metadata fields and attachments.
|
||||
edi_doc = super(purchase_order,self).edi_export(cr, uid, [order], edi_struct, context)[0]
|
||||
edi_doc.update({
|
||||
# force trans-typing to purchase.order upon import
|
||||
'__import_model': 'sale.order',
|
||||
'__import_module': 'sale',
|
||||
|
||||
'company_address': res_company.edi_export_address(cr, uid, order.company_id, context=context),
|
||||
'partner_address': res_partner_address.edi_export(cr, uid, [order.partner_address_id], context=context)[0],
|
||||
'currency': self.pool.get('res.currency').edi_export(cr, uid, [order.pricelist_id.currency_id],
|
||||
context=context)[0],
|
||||
})
|
||||
if edi_doc.get('order_line'):
|
||||
for line in edi_doc['order_line']:
|
||||
line['__import_model'] = 'sale.order.line'
|
||||
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_address = self.pool.get('res.partner.address')
|
||||
res_partner = self.pool.get('res.partner')
|
||||
|
||||
# imported company = as a new partner
|
||||
src_company_id, src_company_name = edi_document.pop('company_id')
|
||||
partner_id = self.edi_import_relation(cr, uid, 'res.partner', src_company_name,
|
||||
src_company_id, context=context)
|
||||
partner_value = {'customer': True}
|
||||
res_partner.write(cr, uid, [partner_id], partner_value, context=context)
|
||||
|
||||
# imported company_address = new partner address
|
||||
address_info = edi_document.pop('company_address')
|
||||
address_info['partner_id'] = (src_company_id, src_company_name)
|
||||
address_info['type'] = 'default'
|
||||
address_id = res_partner_address.edi_import(cr, uid, address_info, context=context)
|
||||
|
||||
# modify edi_document to refer to new partner/address
|
||||
partner_address = res_partner_address.browse(cr, uid, address_id, context=context)
|
||||
edi_document['partner_id'] = (src_company_id, src_company_name)
|
||||
edi_document.pop('partner_address', False) # ignored
|
||||
edi_document['partner_address_id'] = self.edi_m2o(cr, uid, partner_address, context=context)
|
||||
|
||||
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
|
||||
partner_model = self.pool.get('res.partner')
|
||||
partner = partner_model.browse(cr, uid, partner_id, context=context)
|
||||
pricelist = partner.property_product_pricelist_purchase
|
||||
if not pricelist:
|
||||
pricelist = self.pool.get('ir.model.data').get_object(cr, uid, 'purchase', 'list0', context=context)
|
||||
|
||||
if not pricelist.currency_id == currency:
|
||||
# look for a pricelist with the right type and currency, or make a new one
|
||||
pricelist_type = 'purchase'
|
||||
product_pricelist = self.pool.get('product.pricelist')
|
||||
match_pricelist_ids = product_pricelist.search(cr, uid,[('type','=',pricelist_type),
|
||||
('currency_id','=',currency.id)])
|
||||
if match_pricelist_ids:
|
||||
pricelist_id = match_pricelist_ids[0]
|
||||
else:
|
||||
pricelist_name = _('EDI Pricelist (%s)') % (currency.name,)
|
||||
pricelist_id = product_pricelist.create(cr, uid, {'name': pricelist_name,
|
||||
'type': pricelist_type,
|
||||
'currency_id': currency.id,
|
||||
})
|
||||
self.pool.get('product.pricelist.version').create(cr, uid, {'name': pricelist_name,
|
||||
'pricelist_id': pricelist_id})
|
||||
pricelist = product_pricelist.browse(cr, uid, pricelist_id)
|
||||
|
||||
return self.edi_m2o(cr, uid, pricelist, context=context)
|
||||
|
||||
def _edi_get_location(self, cr, uid, partner_id, context=None):
|
||||
partner_model = self.pool.get('res.partner')
|
||||
partner = partner_model.browse(cr, uid, partner_id, context=context)
|
||||
location = partner.property_stock_customer
|
||||
if not location:
|
||||
location = self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'stock_location_stock', context=context)
|
||||
return self.edi_m2o(cr, uid, location, context=context)
|
||||
|
||||
def edi_import(self, cr, uid, edi_document, context=None):
|
||||
self._edi_requires_attributes(('company_id','company_address','order_line','date_order','currency'), edi_document)
|
||||
|
||||
#import company as a new partner
|
||||
partner_id = self.edi_import_company(cr, uid, edi_document, context=context)
|
||||
|
||||
# currency for rounding the discount calculations and for the pricelist
|
||||
res_currency = self.pool.get('res.currency')
|
||||
currency_info = edi_document.pop('currency')
|
||||
currency_id = res_currency.edi_import(cr, uid, currency_info, context=context)
|
||||
order_currency = res_currency.browse(cr, uid, currency_id)
|
||||
|
||||
partner_ref = edi_document.pop('partner_ref', False)
|
||||
edi_document['partner_ref'] = edi_document['name']
|
||||
edi_document['name'] = partner_ref or edi_document['name']
|
||||
edi_document['pricelist_id'] = self._edi_get_pricelist(cr, uid, partner_id, order_currency, context=context)
|
||||
edi_document['location_id'] = self._edi_get_location(cr, uid, partner_id, context=context)
|
||||
|
||||
# discard web preview fields, if present
|
||||
edi_document.pop('amount_total', None)
|
||||
edi_document.pop('amount_tax', None)
|
||||
edi_document.pop('amount_untaxed', None)
|
||||
edi_document.pop('payment_term', None)
|
||||
edi_document.pop('order_policy', None)
|
||||
edi_document.pop('user_id', None)
|
||||
|
||||
for order_line in edi_document['order_line']:
|
||||
self._edi_requires_attributes(('date_planned', 'product_id', 'product_uom', 'product_qty', 'price_unit'), order_line)
|
||||
# original sale order contains unit price and discount, but not final line price
|
||||
discount = order_line.pop('discount', 0.0)
|
||||
if discount:
|
||||
order_line['price_unit'] = res_currency.round(cr, uid, order_currency,
|
||||
(order_line['price_unit'] * (1 - (discount or 0.0) / 100.0)))
|
||||
# sale order lines have sequence numbers, not purchase order lines
|
||||
order_line.pop('sequence', None)
|
||||
|
||||
# discard web preview fields, if present
|
||||
order_line.pop('price_subtotal', None)
|
||||
return super(purchase_order,self).edi_import(cr, uid, edi_document, context=context)
|
||||
|
||||
class purchase_order_line(osv.osv, EDIMixin):
|
||||
_inherit='purchase.order.line'
|
||||
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,156 @@
|
|||
<?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>
|
||||
<field name="res_model">email.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form,tree</field>
|
||||
<field name="view_id" ref="email_template.email_template_tree" />
|
||||
<field name="search_view_id" ref="email_template.view_email_template_search"/>
|
||||
<field name="context" eval="{'search_default_model_id': ref('purchase.model_purchase_order')}"/>
|
||||
</record>
|
||||
<menuitem id="menu_configuration_misc" name="Miscellaneous" parent="menu_purchase_config_purchase" sequence="30"/>
|
||||
<menuitem id="menu_email_templates" parent="menu_configuration_misc" action="action_email_templates" sequence="30"/>
|
||||
</data>
|
||||
|
||||
<!-- Mail template and workflow bindings are done 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="email_from">${object.validator.user_email or ''}</field>
|
||||
<field name="subject">${object.company_id.name} Order (Ref ${object.name or 'n/a' })</field>
|
||||
<field name="email_to">${object.partner_address_id.email}</field>
|
||||
<field name="model_id" ref="purchase.model_purchase_order"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<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_address_id.name and ' ' or ''}${object.partner_address_id.name or ''},</p>
|
||||
|
||||
<p>Here is a purchase order confirmation from ${object.company_id.name}: </p>
|
||||
|
||||
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
|
||||
<strong>REFERENCES</strong><br />
|
||||
Order number: <strong>${object.name}</strong><br />
|
||||
Order total: <strong>${object.amount_total} ${object.pricelist_id.currency_id.name}</strong><br />
|
||||
Order date: ${object.date_order}<br />
|
||||
% if object.origin:
|
||||
Order reference: ${object.origin}<br />
|
||||
% endif
|
||||
% if object.partner_ref:
|
||||
Your reference: ${object.partner_ref}<br />
|
||||
% endif
|
||||
Your contact: <a href="mailto:${object.validator.user_email or ''}?subject=Order%20${object.name}">${object.validator.name}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can view the 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>
|
||||
<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: ${object.company_id.phone}
|
||||
</div>
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
<div>
|
||||
Web : <a href="${object.company_id.website}">${object.company_id.website}</a>
|
||||
</div>
|
||||
%endif
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
]]></field>
|
||||
<field name="body_text"><![CDATA[
|
||||
Hello${object.partner_address_id.name and ' ' or ''}${object.partner_address_id.name or ''},
|
||||
|
||||
Here is a purchase order confirmation from ${object.company_id.name}:
|
||||
| Order number: *${object.name}*
|
||||
| Order total: *${object.amount_total} ${object.pricelist_id.currency_id.name}*
|
||||
| Order date: ${object.date_order}
|
||||
% if object.origin:
|
||||
| Order reference: ${object.origin}
|
||||
% endif
|
||||
% if object.partner_ref:
|
||||
| Your reference: ${object.partner_ref}<br />
|
||||
% endif
|
||||
| Your contact: ${object.validator.name} ${object.validator.user_email and '<%s>'%(object.validator.user_email) or ''}
|
||||
|
||||
You can view the order confirmation and download it using the following link:
|
||||
${ctx.get('edi_web_url_view') or 'n/a'}
|
||||
|
||||
If you have any question, do not hesitate to contact us.
|
||||
|
||||
Thank you!
|
||||
|
||||
|
||||
--
|
||||
${object.validator.name} ${object.validator.user_email and '<%s>'%(object.validator.user_email) or ''}
|
||||
${object.company_id.name}
|
||||
% if object.company_id.street:
|
||||
${object.company_id.street or ''}
|
||||
% endif
|
||||
% if object.company_id.street2:
|
||||
${object.company_id.street2}
|
||||
% endif
|
||||
% if object.company_id.city or object.company_id.zip:
|
||||
${object.company_id.zip or ''} ${object.company_id.city or ''}
|
||||
% 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 ''}
|
||||
% endif
|
||||
% if object.company_id.phone:
|
||||
Phone: ${object.company_id.phone}
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
${object.company_id.website or ''}
|
||||
% endif
|
||||
]]></field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
|
@ -1,7 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<report auto="False" id="report_purchase_quotation" model="purchase.order" name="purchase.quotation" rml="purchase/report/request_quotation.rml" string="Request for Quotation"/>
|
||||
<report auto="False" id="report_purchase_order" model="purchase.order" name="purchase.order" rml="purchase/report/order.rml" string="Purchase Order"/>
|
||||
<report auto="False" id="report_purchase_quotation" model="purchase.order"
|
||||
name="purchase.quotation" rml="purchase/report/request_quotation.rml"
|
||||
string="Request for Quotation"/>
|
||||
<report auto="False" id="report_purchase_order" model="purchase.order"
|
||||
name="purchase.order" rml="purchase/report/order.rml"
|
||||
usage="default" string="Purchase Order"/>
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
-
|
||||
I create a draft Purchase Order
|
||||
-
|
||||
!record {model: purchase.order, id: purchase_order_edi_1}:
|
||||
partner_id: base.res_partner_agrolait
|
||||
partner_address_id: base.res_partner_address_8invoice
|
||||
location_id: stock.stock_location_3
|
||||
pricelist_id: 1
|
||||
order_line:
|
||||
- product_id: product.product_product_pc1
|
||||
product_qty: 1.0
|
||||
product_uom: 1
|
||||
price_unit: 150.0
|
||||
name: 'Basic PC'
|
||||
date_planned: '2011-08-31'
|
||||
order_line:
|
||||
- product_id: product.product_product_pc3
|
||||
product_qty: 10.0
|
||||
product_uom: 1
|
||||
price_unit: 20.0
|
||||
name: 'Medium PC'
|
||||
date_planned: '2011-08-31'
|
||||
|
||||
-
|
||||
I confirm the purchase order
|
||||
-
|
||||
!workflow {model: purchase.order, ref: purchase_order_edi_1, action: purchase_confirm}
|
||||
-
|
||||
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'
|
||||
|
||||
-
|
||||
Then I import a sample EDI document of a sale order
|
||||
-
|
||||
!python {model: edi.document}: |
|
||||
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": "SO008",
|
||||
"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","Guys bank: 123477777-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 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
|
||||
|
||||
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)
|
|
@ -28,5 +28,6 @@ import stock
|
|||
import wizard
|
||||
import report
|
||||
import company
|
||||
import edi
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -83,9 +83,11 @@ Dashboard for Sales Manager that includes:
|
|||
'stock_view.xml',
|
||||
'process/sale_process.xml',
|
||||
'board_sale_view.xml',
|
||||
'edi/sale_order_action_data.xml',
|
||||
],
|
||||
'demo_xml': ['sale_demo.xml'],
|
||||
'test': [
|
||||
'test/edi_sale_order.yml',
|
||||
'test/data_test.yml',
|
||||
'test/manual_order_policy.yml',
|
||||
'test/prepaid_order_policy.yml',
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 sale_order
|
|
@ -0,0 +1,222 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# OpenERP, Open Source Business Applications
|
||||
# Copyright (c) 2011 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 datetime import datetime, timedelta
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
||||
from osv import fields, osv, orm
|
||||
from edi import EDIMixin
|
||||
from tools import DEFAULT_SERVER_DATE_FORMAT
|
||||
|
||||
SALE_ORDER_LINE_EDI_STRUCT = {
|
||||
'sequence': True,
|
||||
'name': True,
|
||||
#custom: 'date_planned'
|
||||
'product_id': True,
|
||||
'product_uom': True,
|
||||
'price_unit': True,
|
||||
#custom: 'product_qty'
|
||||
'discount': True,
|
||||
'notes': True,
|
||||
|
||||
# fields used for web preview only - discarded on import
|
||||
'price_subtotal': True,
|
||||
}
|
||||
|
||||
SALE_ORDER_EDI_STRUCT = {
|
||||
'name': True,
|
||||
'origin': True,
|
||||
'company_id': True, # -> to be changed into partner
|
||||
#custom: 'partner_ref'
|
||||
'date_order': True,
|
||||
'partner_id': True,
|
||||
#custom: 'partner_address'
|
||||
#custom: 'notes'
|
||||
'order_line': SALE_ORDER_LINE_EDI_STRUCT,
|
||||
|
||||
# fields used for web preview only - discarded on import
|
||||
'amount_total': True,
|
||||
'amount_untaxed': True,
|
||||
'amount_tax': True,
|
||||
'payment_term': True,
|
||||
'order_policy': True,
|
||||
'user_id': True,
|
||||
}
|
||||
|
||||
class sale_order(osv.osv, EDIMixin):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
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)
|
||||
res_company = self.pool.get('res.company')
|
||||
res_partner_address = self.pool.get('res.partner.address')
|
||||
edi_doc_list = []
|
||||
for order in records:
|
||||
# generate the main report
|
||||
self._edi_generate_report_attachment(cr, uid, order, context=context)
|
||||
|
||||
# Get EDI doc based on struct. The result will also contain all metadata fields and attachments.
|
||||
edi_doc = super(sale_order,self).edi_export(cr, uid, [order], edi_struct, context)[0]
|
||||
edi_doc.update({
|
||||
# force trans-typing to purchase.order upon import
|
||||
'__import_model': 'purchase.order',
|
||||
'__import_module': 'purchase',
|
||||
|
||||
'company_address': res_company.edi_export_address(cr, uid, order.company_id, context=context),
|
||||
'partner_address': res_partner_address.edi_export(cr, uid, [order.partner_order_id], context=context)[0],
|
||||
|
||||
'currency': self.pool.get('res.currency').edi_export(cr, uid, [order.pricelist_id.currency_id],
|
||||
context=context)[0],
|
||||
'partner_ref': order.client_order_ref or False,
|
||||
'notes': order.note or False,
|
||||
})
|
||||
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_address = self.pool.get('res.partner.address')
|
||||
res_partner = self.pool.get('res.partner')
|
||||
|
||||
# imported company = as a new partner
|
||||
src_company_id, src_company_name = edi_document.pop('company_id')
|
||||
partner_id = self.edi_import_relation(cr, uid, 'res.partner', src_company_name,
|
||||
src_company_id, context=context)
|
||||
partner_value = {'supplier': True}
|
||||
res_partner.write(cr, uid, [partner_id], partner_value, context=context)
|
||||
|
||||
# imported company_address = new partner address
|
||||
address_info = edi_document.pop('company_address')
|
||||
address_info['partner_id'] = (src_company_id, src_company_name)
|
||||
address_info['type'] = 'default'
|
||||
address_id = res_partner_address.edi_import(cr, uid, address_info, context=context)
|
||||
|
||||
# modify edi_document to refer to new partner/address
|
||||
partner_address = res_partner_address.browse(cr, uid, address_id, context=context)
|
||||
edi_document['partner_id'] = (src_company_id, src_company_name)
|
||||
edi_document.pop('partner_address', False) # ignored
|
||||
address_edi_m2o = self.edi_m2o(cr, uid, partner_address, context=context)
|
||||
edi_document['partner_order_id'] = address_edi_m2o
|
||||
edi_document['partner_invoice_id'] = address_edi_m2o
|
||||
edi_document['partner_shipping_id'] = address_edi_m2o
|
||||
|
||||
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
|
||||
partner_model = self.pool.get('res.partner')
|
||||
partner = partner_model.browse(cr, uid, partner_id, context=context)
|
||||
pricelist = partner.property_product_pricelist
|
||||
if not pricelist:
|
||||
pricelist = self.pool.get('ir.model.data').get_object(cr, uid, 'product', 'list0', context=context)
|
||||
|
||||
if not pricelist.currency_id == currency:
|
||||
# look for a pricelist with the right type and currency, or make a new one
|
||||
pricelist_type = 'sale'
|
||||
product_pricelist = self.pool.get('product.pricelist')
|
||||
match_pricelist_ids = product_pricelist.search(cr, uid,[('type','=',pricelist_type),
|
||||
('currency_id','=',currency.id)])
|
||||
if match_pricelist_ids:
|
||||
pricelist_id = match_pricelist_ids[0]
|
||||
else:
|
||||
pricelist_name = _('EDI Pricelist (%s)') % (currency.name,)
|
||||
pricelist_id = product_pricelist.create(cr, uid, {'name': pricelist_name,
|
||||
'type': pricelist_type,
|
||||
'currency_id': currency.id,
|
||||
})
|
||||
self.pool.get('product.pricelist.version').create(cr, uid, {'name': pricelist_name,
|
||||
'pricelist_id': pricelist_id})
|
||||
pricelist = product_pricelist.browse(cr, uid, pricelist_id)
|
||||
|
||||
return self.edi_m2o(cr, uid, pricelist, context=context)
|
||||
|
||||
def edi_import(self, cr, uid, edi_document, context=None):
|
||||
self._edi_requires_attributes(('company_id','company_address','order_line','date_order','currency'), edi_document)
|
||||
|
||||
#import company as a new partner
|
||||
partner_id = self._edi_import_company(cr, uid, edi_document, context=context)
|
||||
|
||||
# currency for rounding the discount calculations and for the pricelist
|
||||
res_currency = self.pool.get('res.currency')
|
||||
currency_info = edi_document.pop('currency')
|
||||
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']
|
||||
edi_document['note'] = edi_document.pop('notes', False)
|
||||
edi_document['pricelist_id'] = self._edi_get_pricelist(cr, uid, partner_id, order_currency, context=context)
|
||||
|
||||
# discard web preview fields, if present
|
||||
edi_document.pop('amount_total', None)
|
||||
edi_document.pop('amount_tax', None)
|
||||
edi_document.pop('amount_untaxed', None)
|
||||
|
||||
order_lines = edi_document['order_line']
|
||||
for order_line in order_lines:
|
||||
self._edi_requires_attributes(('date_planned', 'product_id', 'product_uom', 'product_qty', 'price_unit'), order_line)
|
||||
order_line['product_uom_qty'] = order_line['product_qty']
|
||||
del order_line['product_qty']
|
||||
date_planned = order_line.pop('date_planned')
|
||||
delay = 0
|
||||
if date_order and date_planned:
|
||||
# no security_days buffer, this is the promised date given by supplier
|
||||
delay = (datetime.strptime(date_planned, DEFAULT_SERVER_DATE_FORMAT) - \
|
||||
datetime.strptime(date_order, DEFAULT_SERVER_DATE_FORMAT)).days
|
||||
order_line['delay'] = delay
|
||||
|
||||
# discard web preview fields, if present
|
||||
order_line.pop('price_subtotal', None)
|
||||
return super(sale_order,self).edi_import(cr, uid, edi_document, context=context)
|
||||
|
||||
class sale_order_line(osv.osv, EDIMixin):
|
||||
_inherit='sale.order.line'
|
||||
|
||||
def edi_export(self, cr, uid, records, edi_struct=None, context=None):
|
||||
"""Overridden to provide sale order line fields with the expected names
|
||||
(sale and purchase orders have different column names)"""
|
||||
edi_struct = dict(edi_struct or SALE_ORDER_LINE_EDI_STRUCT)
|
||||
edi_doc_list = []
|
||||
for line in records:
|
||||
edi_doc = super(sale_order_line,self).edi_export(cr, uid, [line], edi_struct, context)[0]
|
||||
edi_doc['__import_model'] = 'purchase.order.line'
|
||||
edi_doc['product_qty'] = line.product_uom_qty
|
||||
if line.product_uos:
|
||||
edi_doc.update(product_uom=line.product_uos,
|
||||
product_qty=line.product_uos_qty)
|
||||
|
||||
# company.security_days is for internal use, so customer should only
|
||||
# see the expected date_planned based on line.delay
|
||||
date_planned = datetime.strptime(line.order_id.date_order, DEFAULT_SERVER_DATE_FORMAT) + \
|
||||
relativedelta(days=line.delay or 0.0)
|
||||
edi_doc['date_planned'] = date_planned.strftime(DEFAULT_SERVER_DATE_FORMAT)
|
||||
edi_doc_list.append(edi_doc)
|
||||
return edi_doc_list
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
|
@ -0,0 +1,193 @@
|
|||
<?xml version="1.0" ?>
|
||||
<openerp>
|
||||
<data>
|
||||
<!-- EDI Export + Send email Action -->
|
||||
<record id="ir_actions_server_edi_sale" model="ir.actions.server">
|
||||
<field name="code">if not object.partner_id.opt_out: object.edi_export_and_email(template_ext_id='sale.email_template_edi_sale', context=context)</field>
|
||||
<field name="state">code</field>
|
||||
<field name="type">ir.actions.server</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="condition">True</field>
|
||||
<field name="name">Auto-email confirmed sale orders</field>
|
||||
</record>
|
||||
|
||||
<!-- EDI related Email Templates menu -->
|
||||
<record model="ir.actions.act_window" id="action_email_templates">
|
||||
<field name="name">Email Templates</field>
|
||||
<field name="res_model">email.template</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form,tree</field>
|
||||
<field name="view_id" ref="email_template.email_template_tree" />
|
||||
<field name="search_view_id" ref="email_template.view_email_template_search"/>
|
||||
<field name="context" eval="{'search_default_model_id': ref('sale.model_sale_order')}"/>
|
||||
</record>
|
||||
<menuitem id="menu_sales_configuration_misc" name="Miscellaneous" parent="base.menu_base_config" sequence="30"/>
|
||||
<menuitem id="menu_email_templates" parent="menu_sales_configuration_misc" action="action_email_templates" sequence="30"/>
|
||||
</data>
|
||||
|
||||
|
||||
<!-- Mail template and workflow bindings are done in a NOUPDATE block
|
||||
so users can freely customize/delete them -->
|
||||
<data noupdate="1">
|
||||
<!-- bind the mailing server action to sale.order confirmed activity -->
|
||||
<record id="sale.act_wait_ship" model="workflow.activity">
|
||||
<field name="action_id" ref="ir_actions_server_edi_sale"/>
|
||||
</record>
|
||||
|
||||
|
||||
<!--Email template -->
|
||||
<record id="email_template_edi_sale" model="email.template">
|
||||
<field name="name">Automated Sale Order Notification Mail</field>
|
||||
<field name="email_from">${object.user_id.user_email or ''}</field>
|
||||
<field name="subject">${object.company_id.name} Order (Ref ${object.name or 'n/a' })</field>
|
||||
<field name="email_to">${object.partner_invoice_id.email}</field>
|
||||
<field name="model_id" ref="sale.model_sale_order"/>
|
||||
<field name="auto_delete" eval="True"/>
|
||||
<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_order_id.name and ' ' or ''}${object.partner_order_id.name or ''},</p>
|
||||
|
||||
<p>Here is your order confirmation for ${object.partner_id.name}: </p>
|
||||
|
||||
<p style="border-left: 1px solid #8e0000; margin-left: 30px;">
|
||||
<strong>REFERENCES</strong><br />
|
||||
Order number: <strong>${object.name}</strong><br />
|
||||
Order total: <strong>${object.amount_total} ${object.pricelist_id.currency_id.name}</strong><br />
|
||||
Order date: ${object.date_order}<br />
|
||||
% if object.origin:
|
||||
Order reference: ${object.origin}<br />
|
||||
% endif
|
||||
% if object.client_order_ref:
|
||||
Your reference: ${object.client_order_ref}<br />
|
||||
% endif
|
||||
Your contact: <a href="mailto:${object.user_id.user_email or ''}?subject=Order%20${object.name}">${object.user_id.name}</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can view the 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:
|
||||
<%
|
||||
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.amount_total))
|
||||
cur_name = quote(object.pricelist_id.currency_id.name)
|
||||
paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Order%%20%s" \
|
||||
"&invoice=%s&amount=%s&currency_code=%s&button_subtype=services&no_note=1" \
|
||||
"&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: ${object.company_id.phone}
|
||||
</div>
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
<div>
|
||||
Web : <a href="${object.company_id.website}">${object.company_id.website}</a>
|
||||
</div>
|
||||
%endif
|
||||
<p></p>
|
||||
</div>
|
||||
</div>
|
||||
]]></field>
|
||||
<field name="body_text"><![CDATA[
|
||||
Hello${object.partner_order_id.name and ' ' or ''}${object.partner_order_id.name or ''},
|
||||
|
||||
Here is your order confirmation for ${object.partner_id.name}:
|
||||
| Order number: *${object.name}*
|
||||
| Order total: *${object.amount_total} ${object.pricelist_id.currency_id.name}*
|
||||
| Order date: ${object.date_order}
|
||||
% if object.origin:
|
||||
| Order reference: ${object.origin}
|
||||
% endif
|
||||
% if object.client_order_ref:
|
||||
| Your reference: ${object.client_order_ref}<br />
|
||||
% endif
|
||||
| Your contact: ${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''}
|
||||
|
||||
You can view the order confirmation, download it and even pay online using the following link:
|
||||
${ctx.get('edi_web_url_view') or 'n/a'}
|
||||
|
||||
% if object.order_policy in ('prepaid','manual') and object.company_id.paypal_account:
|
||||
<%
|
||||
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.amount_total))
|
||||
cur_name = quote(object.pricelist_id.currency_id.name)
|
||||
paypal_url = "https://www.paypal.com/cgi-bin/webscr?cmd=_xclick&business=%s&item_name=%s%%20Order%%20%s&invoice=%s&amount=%s" \
|
||||
"¤cy_code=%s&button_subtype=services&no_note=1&bn=OpenERP_Order_PayNow_%s" % \
|
||||
(paypal_account,comp_name,order_name,order_name,order_amount,cur_name,cur_name)
|
||||
%>
|
||||
It is also possible to directly pay with Paypal:
|
||||
${paypal_url}
|
||||
% endif
|
||||
|
||||
If you have any question, do not hesitate to contact us.
|
||||
|
||||
|
||||
Thank you for choosing ${object.company_id.name}!
|
||||
|
||||
|
||||
--
|
||||
${object.user_id.name} ${object.user_id.user_email and '<%s>'%(object.user_id.user_email) or ''}
|
||||
${object.company_id.name}
|
||||
% if object.company_id.street:
|
||||
${object.company_id.street or ''}
|
||||
% endif
|
||||
% if object.company_id.street2:
|
||||
${object.company_id.street2}
|
||||
% endif
|
||||
% if object.company_id.city or object.company_id.zip:
|
||||
${object.company_id.zip or ''} ${object.company_id.city or ''}
|
||||
% 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 ''}
|
||||
% endif
|
||||
% if object.company_id.phone:
|
||||
Phone: ${object.company_id.phone}
|
||||
% endif
|
||||
% if object.company_id.website:
|
||||
${object.company_id.website or ''}
|
||||
% endif
|
||||
]]></field>
|
||||
</record>
|
||||
</data>
|
||||
</openerp>
|
|
@ -217,7 +217,7 @@ class sale_order(osv.osv):
|
|||
'partner_shipping_id': fields.many2one('res.partner.address', 'Shipping Address', readonly=True, required=True, states={'draft': [('readonly', False)]}, help="Shipping address for current sales order."),
|
||||
|
||||
'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="Incoterm which stands for 'International Commercial terms' implies its a series of sales terms which are used in the commercial transaction."),
|
||||
'picking_policy': fields.selection([('direct', 'Deliver each products when available'), ('one', 'Deliver all products at once')],
|
||||
'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
|
||||
'Picking Policy', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="""If you don't have enough stock available to deliver all at once, do you accept partial shipments or not?"""),
|
||||
'order_policy': fields.selection([
|
||||
('prepaid', 'Pay before delivery'),
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<openerp>
|
||||
<data>
|
||||
|
||||
<report auto="False" id="report_sale_order" model="sale.order" name="sale.order" rml="sale/report/sale_order.rml" string="Quotation / Order"/>
|
||||
<report auto="False" id="report_sale_order" model="sale.order" name="sale.order"
|
||||
rml="sale/report/sale_order.rml" string="Quotation / Order"
|
||||
usage="default"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
|
|
@ -174,6 +174,7 @@
|
|||
category_id:
|
||||
- res_partner_category_customers0
|
||||
name: Cleartrail
|
||||
opt_out: True
|
||||
-
|
||||
I create contact address for Cleartrail.
|
||||
-
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
-
|
||||
I create a draft Sale Order
|
||||
-
|
||||
!record {model: sale.order, id: sale_order_edi_1}:
|
||||
partner_id: base.res_partner_agrolait
|
||||
partner_invoice_id: base.res_partner_address_8invoice
|
||||
partner_order_id: base.res_partner_address_8invoice
|
||||
partner_shipping_id: base.res_partner_address_8invoice
|
||||
pricelist_id: 1
|
||||
order_line:
|
||||
- product_id: product.product_product_pc1
|
||||
product_uom_qty: 1.0
|
||||
product_uom: 1
|
||||
price_unit: 150.0
|
||||
name: 'Basic pc'
|
||||
order_line:
|
||||
- product_id: product.product_product_pc3
|
||||
product_uom_qty: 10.0
|
||||
product_uom: 1
|
||||
price_unit: 200.0
|
||||
name: 'Medium pc'
|
||||
-
|
||||
I confirm the sale order
|
||||
-
|
||||
!workflow {model: sale.order, ref: sale_order_edi_1, action: order_confirm}
|
||||
-
|
||||
Then I export the sale order via EDI
|
||||
-
|
||||
!python {model: edi.document}: |
|
||||
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'
|
||||
-
|
||||
Then I import a sample EDI document of a purchase order
|
||||
-
|
||||
!python {model: edi.document}: |
|
||||
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",
|
||||
"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 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.delay == 18 , "incorrect delay: got %s, expected 18"%(sale_line.delay,)
|
||||
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.delay == 3 , "incorrect delay: got %s, expected 3"%(sale_line.delay,)
|
||||
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)
|
|
@ -25,4 +25,4 @@ import product
|
|||
import report
|
||||
import wizard
|
||||
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||
|
|
|
@ -79,7 +79,7 @@ Thanks to the double entry management, the inventory controlling is powerful and
|
|||
"partner_view.xml",
|
||||
"report/report_stock_move_view.xml",
|
||||
"report/report_stock_view.xml",
|
||||
"board_warehouse_view.xml"
|
||||
"board_warehouse_view.xml",
|
||||
],
|
||||
'test': [
|
||||
'test/stock_test.yml',
|
||||
|
|
Loading…
Reference in New Issue