[MERGE] EDI branch of hmo/odo

bzr revid: fp@tinyerp.com-20111116185443-zu2zpt7qd3lrfw9s
This commit is contained in:
Fabien Pinckaers 2011-11-16 19:54:43 +01:00
commit 6861e526da
64 changed files with 4004 additions and 263 deletions

View File

@ -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:

View File

@ -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',

View File

@ -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"/>

View File

@ -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',

View File

@ -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>

View File

@ -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

View File

@ -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"

View File

@ -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;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Invoice number: <strong>${object.number}</strong><br />
&nbsp;&nbsp;Invoice total: <strong>${object.amount_total} ${object.currency_id.name}</strong><br />
&nbsp;&nbsp;Invoice date: ${object.date_invoice}<br />
% if object.origin:
&nbsp;&nbsp;Order reference: ${object.origin}<br />
% endif
&nbsp;&nbsp;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&amp;business=%s&amp;item_name=%s%%20Invoice%%20%s&amp;" \
"invoice=%s&amp;amount=%s&amp;currency_code=%s&amp;button_subtype=services&amp;no_note=1&amp;bn=OpenERP_Invoice_PayNow_%s" % \
(paypal_account,comp_name,inv_number,inv_number,inv_amount,cur_name,cur_name)
%>
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${paypal_url}">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
% endif
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #FFF;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br/>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br/>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br/>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
%endif
<p></p>
</div>
</div>
]]></field>
<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&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)
%>
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>

View File

@ -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"

View File

@ -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:

View File

@ -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)])

View File

@ -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)])

View File

@ -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)])

View File

@ -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)])

View File

@ -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)])

View File

@ -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)])

View File

@ -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
-

27
addons/edi/__init__.py Normal file
View File

@ -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

57
addons/edi/__openerp__.py Normal file
View File

@ -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:

View File

@ -0,0 +1 @@
import main

View File

@ -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
#

70
addons/edi/edi_service.py Normal file
View File

@ -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()

View File

@ -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

684
addons/edi/models/edi.py Normal file
View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ir_edi_all_read access_ir_edi_all_read model_edi_document 1 0 0 0
3 access_ir_edi_employee_create access_ir_edi_employee_create model_edi_document base.group_user 1 0 1 0

View File

@ -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

View File

@ -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 +
"&currency_code=" + cur_code +
"&button_subtype=services&amp;no_note=1&amp;bn=OpenERP_PayNow_" + cur_code;
return paypal_url;
},
toggle_choice: function(mode) {
return function($e) {
$('.oe_edi_nested_block_'+mode).hide();
$('.'+$e.target.id+'_nested').show();
return true;
}
},
do_print: function(e){
var l = window.location;
window.location = l.protocol + '//' + l.host + "/edi/download_attachment?db=" + this.db + "&token=" + this.token;
},
do_import_existing: function(e) {
var url_download = this.get_download_url();
var $edi_text_server_input = this.$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:

View File

@ -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+'&amp;token='+widget.token : '/web/static/src/img/logo.png')+')'"/></a> </div>
</td>
</tr>
<tr>
<td colspan="2" valign="top" height="100%">
<table cellspacing="0" cellpadding="0" border="0" height="100%" width="100%">
<tr>
<td class="oe_edi_view">
<p class="oe_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>

View File

@ -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 &amp;&amp; (doc.type == 'out_invoice' or doc.type == 'in_refund')">
<t t-if="doc.company_address.paypal_account || doc.company_address.bank_ids">
<p class="oe_edi_sidebar_title">Pay Online</p>
<t t-if="doc.company_address.paypal_account">
<div class="oe_edi_option">
<input type="radio" id="oe_edi_paypal" name="oe_edi_pay" class="oe_edi_pay_choice"/>
<label for="oe_edi_paypal" id="oe_edi_paypal" class="oe_edi_pay_choice_label">Paypal</label>
</div>
<p class="oe_edi_nested_block_pay oe_edi_paypal_nested">
You may directly pay this invoice online via Paypal's secure payment gateway:<br/>
<a t-att-href="widget.get_paypal_url('Invoice','internal_number')" target="_new">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
</p>
</t>
<t t-if="doc.company_address.bank_ids">
<div class="oe_edi_option">
<input type="radio" id="oe_edi_pay_wire" name="oe_edi_pay" class="oe_edi_pay_choice"/>
<label for="oe_edi_pay_wire" id="oe_edi_pay_wire" class="oe_edi_pay_choice_label">Bank Wire Transfer</label>
</div>
<p class="oe_edi_nested_block_pay oe_edi_pay_wire_nested">
Please transfer <strong><t t-esc="_.sprintf('%.2f',doc.amount_total)"/> <t t-esc="doc.currency.code"/></strong> to
<strong><t t-esc="doc.company_id[1]"/></strong> (postal address on the invoice header)
using one of the following bank accounts. Be sure to mention the invoice
reference <strong><t t-esc="doc.internal_number"/></strong> on the transfer:
<br/><br/>
</p>
<ul class="oe_edi_nested_block_pay oe_edi_pay_wire_nested">
<t t-foreach="doc.company_address.bank_ids" t-as="bank_info">
<li><t t-esc="bank_info[1]"/></li>
</t>
</ul>
</t>
</t>
</t>
</t>
</template>

View File

@ -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 &amp;&amp; (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>

View File

@ -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))

View File

@ -21,5 +21,6 @@
##############################################################################
import email_template
import wizard
import res_partner
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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",

View File

@ -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:

View File

@ -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>

View File

@ -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,
}

View File

@ -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})

View File

@ -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>

View File

@ -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}

View File

@ -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,

View File

@ -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>

View File

@ -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".'

View File

@ -26,6 +26,7 @@ import wizard
import report
import stock
import company
import edi
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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': [

View File

@ -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

View File

@ -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:

View File

@ -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;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Order number: <strong>${object.name}</strong><br />
&nbsp;&nbsp;Order total: <strong>${object.amount_total} ${object.pricelist_id.currency_id.name}</strong><br />
&nbsp;&nbsp;Order date: ${object.date_order}<br />
% if object.origin:
&nbsp;&nbsp;Order reference: ${object.origin}<br />
% endif
% if object.partner_ref:
&nbsp;&nbsp;Your reference: ${object.partner_ref}<br />
% endif
&nbsp;&nbsp;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:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
%endif
<p></p>
</div>
</div>
]]></field>
<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>

View File

@ -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>

View File

@ -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)

View File

@ -28,5 +28,6 @@ import stock
import wizard
import report
import company
import edi
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -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',

View File

@ -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

View File

@ -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:

View File

@ -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;">
&nbsp;&nbsp;<strong>REFERENCES</strong><br />
&nbsp;&nbsp;Order number: <strong>${object.name}</strong><br />
&nbsp;&nbsp;Order total: <strong>${object.amount_total} ${object.pricelist_id.currency_id.name}</strong><br />
&nbsp;&nbsp;Order date: ${object.date_order}<br />
% if object.origin:
&nbsp;&nbsp;Order reference: ${object.origin}<br />
% endif
% if object.client_order_ref:
&nbsp;&nbsp;Your reference: ${object.client_order_ref}<br />
% endif
&nbsp;&nbsp;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&amp;business=%s&amp;item_name=%s%%20Order%%20%s" \
"&amp;invoice=%s&amp;amount=%s&amp;currency_code=%s&amp;button_subtype=services&amp;no_note=1" \
"&amp;bn=OpenERP_Order_PayNow_%s" % \
(paypal_account,comp_name,order_name,order_name,order_amount,cur_name,cur_name)
%>
<br/>
<p>It is also possible to directly pay with Paypal:</p>
<a style="margin-left: 120px;" href="${paypal_url}">
<img class="oe_edi_paypal_button" src="https://www.paypal.com/en_US/i/btn/btn_paynowCC_LG.gif"/>
</a>
% endif
<br/>
<p>If you have any question, do not hesitate to contact us.</p>
<p>Thank you for choosing ${object.company_id.name or 'us'}!</p>
<br/>
<br/>
<div style="width: 375px; margin: 0px; padding: 0px; background-color: #8E0000; border-top-left-radius: 5px 5px; border-top-right-radius: 5px 5px; background-repeat: repeat no-repeat;">
<h3 style="margin: 0px; padding: 2px 14px; font-size: 12px; color: #FFF;">
<strong style="text-transform:uppercase;">${object.company_id.name}</strong></h3>
</div>
<div style="width: 347px; margin: 0px; padding: 5px 14px; line-height: 16px; background-color: #F2F2F2;">
<span style="color: #222; margin-bottom: 5px; display: block; ">
% if object.company_id.street:
${object.company_id.street}<br/>
% endif
% if object.company_id.street2:
${object.company_id.street2}<br/>
% endif
% if object.company_id.city or object.company_id.zip:
${object.company_id.zip} ${object.company_id.city}<br/>
% endif
% if object.company_id.country_id:
${object.company_id.state_id and ('%s, ' % object.company_id.state_id.name) or ''} ${object.company_id.country_id.name or ''}<br/>
% endif
</span>
% if object.company_id.phone:
<div style="margin-top: 0px; margin-right: 0px; margin-bottom: 0px; margin-left: 0px; padding-top: 0px; padding-right: 0px; padding-bottom: 0px; padding-left: 0px; ">
Phone:&nbsp; ${object.company_id.phone}
</div>
% endif
% if object.company_id.website:
<div>
Web :&nbsp;<a href="${object.company_id.website}">${object.company_id.website}</a>
</div>
%endif
<p></p>
</div>
</div>
]]></field>
<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" \
"&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)
%>
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>

View File

@ -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'),

View File

@ -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>

View File

@ -174,6 +174,7 @@
category_id:
- res_partner_category_customers0
name: Cleartrail
opt_out: True
-
I create contact address for Cleartrail.
-

View File

@ -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)

View File

@ -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:

View File

@ -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',