diff --git a/addons/account_analytic_analysis/i18n/nl.po b/addons/account_analytic_analysis/i18n/nl.po index bfff9f3ec71..c3f12aef08c 100644 --- a/addons/account_analytic_analysis/i18n/nl.po +++ b/addons/account_analytic_analysis/i18n/nl.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0_rc3\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-12-11 12:37+0000\n" -"Last-Translator: Pieter J. Kersten (EduSense BV) \n" +"PO-Revision-Date: 2010-02-01 15:46+0000\n" +"Last-Translator: Jan Verlaan (Veritos) \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:16+0000\n" +"X-Launchpad-Export-Date: 2010-02-02 04:48+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: account_analytic_analysis @@ -191,7 +191,7 @@ msgstr "Berekend met de formule: gefactureerd bedrag - totale kosten." #: model:ir.actions.act_window,name:account_analytic_analysis.action_account_analytic_managed #: model:ir.ui.menu,name:account_analytic_analysis.menu_analytic_account_managed msgid "My Accounts" -msgstr "Mijn rekeningen" +msgstr "Mijn kostenplaatsen" #. module: account_analytic_analysis #: model:ir.module.module,description:account_analytic_analysis.module_meta_information @@ -335,7 +335,7 @@ msgstr "Kostenplaats" #: model:ir.actions.act_window,name:account_analytic_analysis.action_account_analytic_managed_overpassed #: model:ir.ui.menu,name:account_analytic_analysis.menu_action_account_analytic_managed_overpassed msgid "Overpassed Accounts" -msgstr "Overbrugde Kostenplaatsen" +msgstr "Overgehevelde kostenplaatsen" #. module: account_analytic_analysis #: model:ir.actions.act_window,name:account_analytic_analysis.action_hr_tree_invoiced_all diff --git a/addons/account_anglo_saxon/__init__.py b/addons/account_anglo_saxon/__init__.py new file mode 100644 index 00000000000..dd9a9262710 --- /dev/null +++ b/addons/account_anglo_saxon/__init__.py @@ -0,0 +1,25 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +import product +import stock +import purchase +import invoice +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/account_anglo_saxon/__terp__.py b/addons/account_anglo_saxon/__terp__.py new file mode 100644 index 00000000000..31818e213ef --- /dev/null +++ b/addons/account_anglo_saxon/__terp__.py @@ -0,0 +1,39 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## +{ + "name" : "Stock Account", + "version" : "1.1", + "author" : "Tiny", + "website" : "http://tinyerp.com", + "description" : """This module will support the Anglo-Saxons accounting methodology by + changing the accounting logic with stock transactions. The difference between the Anglo-Saxon accounting countries + and the Rhine or also called Continental accounting countries is the moment of taking the Cost of Goods Sold versus Cost of Sales. + Anglo-Saxons accounting does take the cost when sales invoice is created, Continental accounting will take the cost at he moment the goods are shipped. + This module will add this functionality by using a interim account, to store the value of shipped goods and will contra book this interim account + when the invoice is created to transfer this amount to the debtor or creditor account.""", + "depends" : ["product", "account", "sale", "purchase"], + "category" : "Generic Modules/Inventory Control", + "init_xml" : [], + "demo_xml" : [], + "update_xml" : ["product_view.xml",], + "active": False, + "installable": True +} +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/account_anglo_saxon/i18n/account_anglo_saxon.pot b/addons/account_anglo_saxon/i18n/account_anglo_saxon.pot new file mode 100644 index 00000000000..a2d316633a4 --- /dev/null +++ b/addons/account_anglo_saxon/i18n/account_anglo_saxon.pot @@ -0,0 +1,54 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * account_anglo_saxon +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 5.0.6\n" +"Report-Msgid-Bugs-To: support@openerp.com\n" +"POT-Creation-Date: 2009-11-24 12:50:50+0000\n" +"PO-Revision-Date: 2009-11-24 12:50:50+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_anglo_saxon +#: view:product.category:0 +msgid " Accounting Property" +msgstr "" + +#. module: account_anglo_saxon +#: model:ir.module.module,description:account_anglo_saxon.module_meta_information +msgid "This module will support the Anglo-Saxons accounting methodology by \n" +" changing the accounting logic with stock transactions. The difference between the Anglo-Saxon accounting countries \n" +" and the Rhine or also called Continental accounting countries is the moment of taking the Cost of Goods Sold versus Cost of Sales. \n" +" Anglo-Saxons accounting does take the cost when sales invoice is created, Continental accounting will take the cost at he moment the goods are shipped.\n" +" This module will add this functionality by using a interim account, to store the value of shipped goods and will contra book this interim account \n" +" when the invoice is created to transfer this amount to the debtor or creditor account." +msgstr "" + +#. module: account_anglo_saxon +#: model:ir.module.module,shortdesc:account_anglo_saxon.module_meta_information +msgid "Stock Account" +msgstr "" + +#. module: account_anglo_saxon +#: constraint:ir.ui.view:0 +msgid "Invalid XML for View Architecture!" +msgstr "" + +#. module: account_anglo_saxon +#: field:product.category,property_account_creditor_price_difference_categ:0 +#: field:product.template,property_account_creditor_price_difference:0 +msgid "Price Difference Account" +msgstr "" + +#. module: account_anglo_saxon +#: help:product.category,property_account_creditor_price_difference_categ:0 +#: help:product.template,property_account_creditor_price_difference:0 +msgid "This account will be used to value price difference between purchase price and cost price." +msgstr "" + diff --git a/addons/account_anglo_saxon/i18n/fr_BE.po b/addons/account_anglo_saxon/i18n/fr_BE.po new file mode 100644 index 00000000000..a2d316633a4 --- /dev/null +++ b/addons/account_anglo_saxon/i18n/fr_BE.po @@ -0,0 +1,54 @@ +# Translation of OpenERP Server. +# This file contains the translation of the following modules: +# * account_anglo_saxon +# +msgid "" +msgstr "" +"Project-Id-Version: OpenERP Server 5.0.6\n" +"Report-Msgid-Bugs-To: support@openerp.com\n" +"POT-Creation-Date: 2009-11-24 12:50:50+0000\n" +"PO-Revision-Date: 2009-11-24 12:50:50+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: account_anglo_saxon +#: view:product.category:0 +msgid " Accounting Property" +msgstr "" + +#. module: account_anglo_saxon +#: model:ir.module.module,description:account_anglo_saxon.module_meta_information +msgid "This module will support the Anglo-Saxons accounting methodology by \n" +" changing the accounting logic with stock transactions. The difference between the Anglo-Saxon accounting countries \n" +" and the Rhine or also called Continental accounting countries is the moment of taking the Cost of Goods Sold versus Cost of Sales. \n" +" Anglo-Saxons accounting does take the cost when sales invoice is created, Continental accounting will take the cost at he moment the goods are shipped.\n" +" This module will add this functionality by using a interim account, to store the value of shipped goods and will contra book this interim account \n" +" when the invoice is created to transfer this amount to the debtor or creditor account." +msgstr "" + +#. module: account_anglo_saxon +#: model:ir.module.module,shortdesc:account_anglo_saxon.module_meta_information +msgid "Stock Account" +msgstr "" + +#. module: account_anglo_saxon +#: constraint:ir.ui.view:0 +msgid "Invalid XML for View Architecture!" +msgstr "" + +#. module: account_anglo_saxon +#: field:product.category,property_account_creditor_price_difference_categ:0 +#: field:product.template,property_account_creditor_price_difference:0 +msgid "Price Difference Account" +msgstr "" + +#. module: account_anglo_saxon +#: help:product.category,property_account_creditor_price_difference_categ:0 +#: help:product.template,property_account_creditor_price_difference:0 +msgid "This account will be used to value price difference between purchase price and cost price." +msgstr "" + diff --git a/addons/account_anglo_saxon/i18n/pl.po b/addons/account_anglo_saxon/i18n/pl.po new file mode 100644 index 00000000000..aab9ded1466 --- /dev/null +++ b/addons/account_anglo_saxon/i18n/pl.po @@ -0,0 +1,66 @@ +# Polish translation for openobject-addons +# Copyright (c) 2010 Rosetta Contributors and Canonical Ltd 2010 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2009-11-24 12:50+0000\n" +"PO-Revision-Date: 2010-02-02 06:30+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Polish \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2010-02-03 04:47+0000\n" +"X-Generator: Launchpad (build Unknown)\n" + +#. module: account_anglo_saxon +#: view:product.category:0 +msgid " Accounting Property" +msgstr "" + +#. module: account_anglo_saxon +#: model:ir.module.module,description:account_anglo_saxon.module_meta_information +msgid "" +"This module will support the Anglo-Saxons accounting methodology by \n" +" changing the accounting logic with stock transactions. The difference " +"between the Anglo-Saxon accounting countries \n" +" and the Rhine or also called Continental accounting countries is the " +"moment of taking the Cost of Goods Sold versus Cost of Sales. \n" +" Anglo-Saxons accounting does take the cost when sales invoice is " +"created, Continental accounting will take the cost at he moment the goods " +"are shipped.\n" +" This module will add this functionality by using a interim account, to " +"store the value of shipped goods and will contra book this interim account \n" +" when the invoice is created to transfer this amount to the debtor or " +"creditor account." +msgstr "" + +#. module: account_anglo_saxon +#: model:ir.module.module,shortdesc:account_anglo_saxon.module_meta_information +msgid "Stock Account" +msgstr "" + +#. module: account_anglo_saxon +#: constraint:ir.ui.view:0 +msgid "Invalid XML for View Architecture!" +msgstr "XML niewłaściwy dla tej architektury wyświetlania!" + +#. module: account_anglo_saxon +#: field:product.category,property_account_creditor_price_difference_categ:0 +#: field:product.template,property_account_creditor_price_difference:0 +msgid "Price Difference Account" +msgstr "Konto różnic cenowych" + +#. module: account_anglo_saxon +#: help:product.category,property_account_creditor_price_difference_categ:0 +#: help:product.template,property_account_creditor_price_difference:0 +msgid "" +"This account will be used to value price difference between purchase price " +"and cost price." +msgstr "" +"To konto będzie stosowane do rejestrowania różnic cenowych między ceną " +"zakupu a kosztem." diff --git a/addons/account_anglo_saxon/invoice.py b/addons/account_anglo_saxon/invoice.py new file mode 100644 index 00000000000..3904cd6b01e --- /dev/null +++ b/addons/account_anglo_saxon/invoice.py @@ -0,0 +1,158 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +from osv import fields, osv + +class account_invoice_line(osv.osv): + _inherit = "account.invoice.line" + + def move_line_get(self, cr, uid, invoice_id, context=None): + res = super(account_invoice_line,self).move_line_get(cr, uid, invoice_id, context) + inv = self.pool.get('account.invoice').browse(cr, uid, invoice_id) + if inv.type in ('out_invoice','out_refund'): + for i_line in inv.invoice_line: + if i_line.product_id: + if inv.type == 'out_invoice': + dacc = i_line.product_id.product_tmpl_id.property_stock_account_output and i_line.product_id.product_tmpl_id.property_stock_account_output.id + if not dacc: + dacc = i_line.product_id.categ_id.property_stock_account_output_categ and i_line.product_id.categ_id.property_stock_account_output_categ.id + else: + dacc = i_line.product_id.product_tmpl_id.property_stock_account_input and i_line.product_id.product_tmpl_id.property_stock_account_input.id + if not dacc: + dacc = i_line.product_id.categ_id.property_stock_account_input_categ and i_line.product_id.categ_id.property_stock_account_input_categ.id + + cacc = i_line.product_id.product_tmpl_id.property_account_expense and i_line.product_id.product_tmpl_id.property_account_expense.id + if not cacc: + cacc = i_line.product_id.categ_id.property_account_expense_categ and i_line.product_id.categ_id.property_account_expense_categ.id + if dacc and cacc: + res.append({ + 'type':'src', + 'name': i_line.name[:64], + 'price_unit':i_line.product_id.product_tmpl_id.standard_price, + 'quantity':i_line.quantity, + 'price':i_line.product_id.product_tmpl_id.standard_price * i_line.quantity, + 'account_id':dacc, + 'product_id':i_line.product_id.id, + 'uos_id':i_line.uos_id.id, + 'account_analytic_id':i_line.account_analytic_id.id, + 'taxes':i_line.invoice_line_tax_id, + }) + + res.append({ + 'type':'src', + 'name': i_line.name[:64], + 'price_unit':i_line.product_id.product_tmpl_id.standard_price, + 'quantity':i_line.quantity, + 'price': -1 * i_line.product_id.product_tmpl_id.standard_price * i_line.quantity, + 'account_id':cacc, + 'product_id':i_line.product_id.id, + 'uos_id':i_line.uos_id.id, + 'account_analytic_id':i_line.account_analytic_id.id, + 'taxes':i_line.invoice_line_tax_id, + }) + elif inv.type in ('in_invoice','in_refund'): + for i_line in inv.invoice_line: + if i_line.product_id: + if i_line.product_id.product_tmpl_id.type != 'service': + acc = i_line.product_id.product_tmpl_id.property_account_creditor_price_difference and i_line.product_id.product_tmpl_id.property_account_creditor_price_difference.id + if not acc: + acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ and i_line.product_id.categ_id.property_account_creditor_price_difference_categ.id + a = None + if inv.type == 'in_invoice': + oa = i_line.product_id.product_tmpl_id.property_stock_account_input and i_line.product_id.product_tmpl_id.property_stock_account_input.id + if not oa: + oa = i_line.product_id.categ_id.property_stock_account_input_categ and i_line.product_id.categ_id.property_stock_account_input_categ.id + else: + oa = i_line.product_id.product_tmpl_id.property_stock_account_output and i_line.product_id.product_tmpl_id.property_stock_account_output.id + if not oa: + oa = i_line.product_id.categ_id.property_stock_account_output_categ and i_line.product_id.categ_id.property_stock_account_output_categ.id + if oa: + fpos = i_line.invoice_id.fiscal_position or False + a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa) + diff_res = [] + for line in res: + if a == line['account_id'] and i_line.product_id.id == line['product_id']: + uom = i_line.product_id.uos_id or i_line.product_id.uom_id + standard_price = self.pool.get('product.uom')._compute_price(cr, uid, uom.id, i_line.product_id.product_tmpl_id.standard_price, i_line.uos_id.id) + if standard_price != i_line.price_unit and line['price'] == i_line.price_unit and acc: + price_diff = i_line.price_unit - standard_price + line.update({'price':standard_price * line['quantity']}) + diff_res.append({ + 'type':'src', + 'name': i_line.name[:64], + 'price_unit':price_diff, + 'quantity':line['quantity'], + 'price': price_diff * line['quantity'], + 'account_id':acc, + 'product_id':line['product_id'], + 'uos_id':line['uos_id'], + 'account_analytic_id':line['account_analytic_id'], + 'taxes':line.get('taxes',[]), + }) + res += diff_res + return res + + def product_id_change(self, cr, uid, ids, product, uom, qty=0, name='', type='out_invoice', partner_id=False, fposition_id=False, price_unit=False, address_invoice_id=False, context=None): + if not product: + return super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, address_invoice_id, context) + else: + res = super(account_invoice_line, self).product_id_change(cr, uid, ids, product, uom, qty, name, type, partner_id, fposition_id, price_unit, address_invoice_id, context) + + if type in ('in_invoice','in_refund'): + product_obj = self.pool.get('product.product').browse(cr, uid, product, context=context) + if type == 'in_invoice': + oa = product_obj.product_tmpl_id.property_stock_account_input and product_obj.product_tmpl_id.property_stock_account_input.id + if not oa: + oa = product_obj.categ_id.property_stock_account_input_categ and product_obj.categ_id.property_stock_account_input_categ.id + else: + oa = product_obj.product_tmpl_id.property_stock_account_output and product_obj.product_tmpl_id.property_stock_account_output.id + if not oa: + oa = product_obj.categ_id.property_stock_account_output_categ and product_obj.categ_id.property_stock_account_output_categ.id + if oa: + fpos = fposition_id and self.pool.get('account.fiscal.position').browse(cr, uid, fposition_id) or False + a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa) + res['value'].update({'account_id':a}) + return res + +account_invoice_line() + +class account_invoice(osv.osv): + _inherit = "account.invoice" + + def _refund_cleanup_lines(self, cr, uid, lines): + for line in lines: + inv_id = line['invoice_id'] + inv_obj = self.browse(cr,uid,inv_id[0]) + if inv_obj.type == 'in_invoice': + if line.get('product_id',False): + product_obj = self.pool.get('product.product').browse(cr,uid,line['product_id'][0]) + oa = product_obj.product_tmpl_id.property_stock_account_output and product_obj.product_tmpl_id.property_stock_account_output.id + if not oa: + oa = product_obj.categ_id.property_stock_account_output_categ and product_obj.categ_id.property_stock_account_output_categ.id + if oa: + fpos = inv_obj.fiscal_position or False + a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa) + account_data = self.pool.get('account.account').read(cr,uid,[a],['name'])[0] + line.update({'account_id': (account_data['id'],account_data['name'])}) + res = super(account_invoice,self)._refund_cleanup_lines(cr, uid, lines) + return res + +account_invoice() +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/account_anglo_saxon/product.py b/addons/account_anglo_saxon/product.py new file mode 100644 index 00000000000..acd6577fbb7 --- /dev/null +++ b/addons/account_anglo_saxon/product.py @@ -0,0 +1,55 @@ +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +from osv import fields, osv + +class product_category(osv.osv): + _inherit = "product.category" + _columns = { + 'property_account_creditor_price_difference_categ': fields.property( + 'account.account', + type='many2one', + relation='account.account', + string="Price Difference Account", + method=True, + view_load=True, + help="This account will be used to value price difference between purchase price and cost price."), + + } +product_category() + +class product_template(osv.osv): + _inherit = "product.template" + _columns = { + 'property_account_creditor_price_difference': fields.property( + 'account.account', + type='many2one', + relation='account.account', + string="Price Difference Account", + method=True, + view_load=True, + help="This account will be used to value price difference between purchase price and cost price."), + + } +product_template() + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/account_anglo_saxon/product_view.xml b/addons/account_anglo_saxon/product_view.xml new file mode 100644 index 00000000000..78e51374128 --- /dev/null +++ b/addons/account_anglo_saxon/product_view.xml @@ -0,0 +1,53 @@ + + + + + product.normal.form.inherit.stock + product.product + form + + + + + + + + + + + + + product.template.product.form.inherit + product.template + form + + + + + + + + + + + + + product.category.property.form.inherit.stock + product.category + form + + +
+ + + + + + +
+
+
+ +
+
+ diff --git a/addons/account_anglo_saxon/purchase.py b/addons/account_anglo_saxon/purchase.py new file mode 100644 index 00000000000..98694a3d5dd --- /dev/null +++ b/addons/account_anglo_saxon/purchase.py @@ -0,0 +1,41 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +from osv import fields, osv + +class purchase_order(osv.osv): + _name = "purchase.order" + _inherit = "purchase.order" + _description = "Purchase order" + + def inv_line_create(self, cr, uid, a, ol): + line = super(purchase_order, self).inv_line_create(cr, uid, a, ol) + if ol.product_id: + oa = ol.product_id.product_tmpl_id.property_stock_account_input and ol.product_id.product_tmpl_id.property_stock_account_input.id + if not oa: + oa = ol.product_id.categ_id.property_stock_account_input_categ and ol.product_id.categ_id.property_stock_account_input_categ.id + if oa: + fpos = ol.order_id.fiscal_position or False + a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa) + line[2].update({'account_id': a}) + return line +purchase_order() +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: \ No newline at end of file diff --git a/addons/account_anglo_saxon/sale.py b/addons/account_anglo_saxon/sale.py new file mode 100644 index 00000000000..e2cfa7bbb3b --- /dev/null +++ b/addons/account_anglo_saxon/sale.py @@ -0,0 +1,43 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +from osv import fields, osv + +#class sale_order_line(osv.osv): +# _name = 'sale.order.line' +# _description = 'Sale Order line' +# _inherit = 'sale.order.line' +# +# def invoice_line_create(self, cr, uid, ids, context={}): +# line_ids = super('sale_order_line',self).invoice_line_create(cr, uid, ids, context) +# invoice_line_obj = self.pool.get('account.invoice.line') +# for line in invoice_line_obj.browse(cr, uid, line_ids): +# if line.product_id: +# a = line.product_id.product_tmpl_id.property_stock_account_output and line.product_id.product_tmpl_id.property_stock_account_output.id +# if not a: +# a = line.product_id.categ_id.property_stock_account_output_categ and line.product_id.categ_id.property_stock_account_output_categ.id +# if a: +# a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, a) +# invoice_line_obj.write(cr, uid, line.id, {'account_id':a}) +# +#sale_order_line() + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account_anglo_saxon/stock.py b/addons/account_anglo_saxon/stock.py new file mode 100644 index 00000000000..ad091364fd8 --- /dev/null +++ b/addons/account_anglo_saxon/stock.py @@ -0,0 +1,51 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +from osv import fields,osv + +#---------------------------------------------------------- +# Stock Picking +#---------------------------------------------------------- +class stock_picking(osv.osv): + _inherit = "stock.picking" + _description = "Packing List" + + def action_invoice_create(self, cr, uid, ids, journal_id=False, + group=False, type='out_invoice', context=None): + '''Return ids of created invoices for the pickings''' + res = super(stock_picking,self).action_invoice_create(cr, uid, ids, journal_id, group, type, context) + if type == 'in_refund': + for inv in self.pool.get('account.invoice').browse(cr, uid, res.values(), context=context): + for ol in inv.invoice_line: + if ol.product_id: + oa = ol.product_id.product_tmpl_id.property_stock_account_output and ol.product_id.product_tmpl_id.property_stock_account_output.id + if not oa: + oa = ol.product_id.categ_id.property_stock_account_output_categ and ol.product_id.categ_id.property_stock_account_output_categ.id + if oa: + fpos = ol.invoice_id.fiscal_position or False + a = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, oa) + self.pool.get('account.invoice.line').write(cr, uid, [ol.id], {'account_id': a}) + return res + +stock_picking() + + +# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/account_followup/i18n/nl.po b/addons/account_followup/i18n/nl.po index 13782599c02..d756121ab3b 100644 --- a/addons/account_followup/i18n/nl.po +++ b/addons/account_followup/i18n/nl.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-12-11 13:00+0000\n" -"Last-Translator: Pieter J. Kersten (EduSense BV) \n" +"PO-Revision-Date: 2010-02-01 15:48+0000\n" +"Last-Translator: Jan Verlaan (Veritos) \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:10+0000\n" +"X-Launchpad-Export-Date: 2010-02-02 04:48+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: account_followup @@ -55,7 +55,7 @@ msgstr "Bedrag in valuta" #. module: account_followup #: rml:account_followup.followup.print:0 msgid "Due" -msgstr "Tot" +msgstr "Bedrag" #. module: account_followup #: view:account.move.line:0 @@ -97,7 +97,7 @@ msgstr "Soort rekening" #. module: account_followup #: rml:account_followup.followup.print:0 msgid "Ref" -msgstr "Ref" +msgstr "Factuur" #. module: account_followup #: wizard_view:account_followup.followup.print.all,next:0 @@ -253,7 +253,7 @@ msgstr "" msgid "" "The Object name must start with x_ and not contain any special character !" msgstr "" -"De objectnaam moet beginnen met x_ en mag geen speciale karakters bevatten !" +"De objectnaam moet beginnen met x_ en mag geen speciale tekens bevatten !" #. module: account_followup #: wizard_button:account_followup.followup.print.all,summary,end:0 diff --git a/addons/account_reporting/i18n/ru.po b/addons/account_reporting/i18n/ru.po index 0395309484f..0fac8e23859 100644 --- a/addons/account_reporting/i18n/ru.po +++ b/addons/account_reporting/i18n/ru.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-11-09 16:28+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-02-02 17:25+0000\n" +"Last-Translator: Nikolay Chesnokov \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:20+0000\n" +"X-Launchpad-Export-Date: 2010-02-03 04:47+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: account_reporting @@ -58,7 +58,7 @@ msgstr "Балансовый отчет" #. module: account_reporting #: constraint:ir.actions.act_window:0 msgid "Invalid model name in the action definition." -msgstr "" +msgstr "Недопустимое имя модели в определении действия." #. module: account_reporting #: selection:account.report.bs,font_style:0 @@ -78,7 +78,7 @@ msgstr "Распечатать балансовый отчет" #. module: account_reporting #: help:account.account.balancesheet.report,init,periods:0 msgid "All periods if empty" -msgstr "" +msgstr "Если не заполнены все периоды" #. module: account_reporting #: field:account.report.bs,color_font:0 @@ -95,7 +95,7 @@ msgstr "Отчет с объектами, счетами и потомками msgid "" "Financial and accounting reporting\n" " Balance Sheet Report" -msgstr "" +msgstr "Финансовые и аналитические отчеты" #. module: account_reporting #: selection:account.report.bs,report_type:0 @@ -126,7 +126,7 @@ msgstr "Отчетность по счету" #. module: account_reporting #: model:ir.ui.menu,name:account_reporting.bs_report_action_form msgid "Balance Sheet Report Form" -msgstr "" +msgstr "Отчет главной книги (Форма)" #. module: account_reporting #: view:account.report.bs:0 @@ -166,7 +166,7 @@ msgstr "Цвета RML" #. module: account_reporting #: model:ir.module.module,shortdesc:account_reporting.module_meta_information msgid "Reporting of Balancesheet for accounting" -msgstr "" +msgstr "Отчет главной книги по счетам" #. module: account_reporting #: field:account.report.bs,code:0 @@ -207,7 +207,7 @@ msgstr "Учётные записи" #. module: account_reporting #: wizard_field:account.account.balancesheet.report,init,periods:0 msgid "Periods" -msgstr "" +msgstr "Периоды" #. module: account_reporting #: field:account.report.bs,color_back:0 diff --git a/addons/auction/i18n/pt_BR.po b/addons/auction/i18n/pt_BR.po index 9cd95c4bc2e..c6a12f8f1aa 100644 --- a/addons/auction/i18n/pt_BR.po +++ b/addons/auction/i18n/pt_BR.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2010-01-23 02:16+0000\n" +"PO-Revision-Date: 2010-02-01 18:21+0000\n" "Last-Translator: Holverat Bortolossi \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2010-01-24 04:50+0000\n" +"X-Launchpad-Export-Date: 2010-02-02 04:48+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: auction @@ -150,74 +150,74 @@ msgstr "Antigo/pequeno produto de cobre ou estanho" #. module: auction #: field:auction.lots,paid_ach:0 msgid "Buyer invoice reconciled" -msgstr "" +msgstr "Fatura do comprador foi reconciliada" #. module: auction #: model:ir.actions.wizard,name:auction.auction_wizard_able_taken msgid "Mark as taken away" -msgstr "" +msgstr "Marcar como retirado" #. module: auction #: xsl:report.auction.seller.list:0 msgid "Inv." -msgstr "" +msgstr "Inv." #. module: auction #: field:auction.deposit.cost,amount:0 msgid "Amount" -msgstr "" +msgstr "Valor" #. module: auction #: model:ir.actions.act_window,name:auction.action_deposit_border #: model:ir.ui.menu,name:auction.menu_auction_deposit_border msgid "Deposit border" -msgstr "" +msgstr "Limite de Depósito" #. module: auction #: xsl:report.auction.deposit:0 msgid "VAT" -msgstr "" +msgstr "Imposto sobre Valor Agregado" #. module: auction #: view:auction.lots:0 msgid "Statements" -msgstr "" +msgstr "Demonstrações" #. module: auction #: xsl:flagey.huissier:0 msgid "Adjudication" -msgstr "" +msgstr "Conceder" #. module: auction #: field:auction.dates,account_analytic_id:0 msgid "Analytic Account" -msgstr "" +msgstr "Conta analítica" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Unclassifieds" -msgstr "" +msgstr "Não classificados" #. module: auction #: model:ir.ui.menu,name:auction.menu_auction_dates_next_by_auction msgid "Objects by Auction" -msgstr "" +msgstr "Objetos em leilão" #. module: auction #: field:auction.lots,lot_num:0 #: field:report.unclassified.objects,lot_num:0 msgid "List Number" -msgstr "" +msgstr "Número de lista" #. module: auction #: field:auction.deposit.cost,name:0 msgid "Cost Name" -msgstr "" +msgstr "Nome do custo" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Antiques" -msgstr "" +msgstr "Antiguidades" #. module: auction #: view:auction.dates:0 @@ -227,17 +227,17 @@ msgstr "" #. module: auction #: field:report.auction.estimation.adj.category,lot_type:0 msgid "Object Type" -msgstr "" +msgstr "Tipo de objeto" #. module: auction #: wizard_field:auction.lots.sms_send,init,text:0 msgid "SMS Message" -msgstr "" +msgstr "Mensagem SMS" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Contemporary Art" -msgstr "" +msgstr "Arte contemporânea" #. module: auction #: view:auction.lots:0 @@ -248,7 +248,7 @@ msgstr "" #. module: auction #: field:report.auction.view,nseller:0 msgid "No of sellers" -msgstr "" +msgstr "Nenhum vendedor" #. module: auction #: selection:auction.lots.send.aie,date_ask,lang:0 @@ -269,7 +269,7 @@ msgstr "" #. module: auction #: field:report.auction.view2,obj_margin_procent:0 msgid "Net margin (%)" -msgstr "" +msgstr "Margem líquida (%)" #. module: auction #: selection:auction.lots,state:0 @@ -277,12 +277,12 @@ msgstr "" #: selection:report.seller.auction,state:0 #: selection:report.unclassified.objects,state:0 msgid "Unsold" -msgstr "" +msgstr "Não vendidos" #. module: auction #: view:auction.dates:0 msgid "Dates" -msgstr "" +msgstr "Datas" #. module: auction #: constraint:product.template:0 @@ -294,13 +294,13 @@ msgstr "" #. module: auction #: rml:auction.total.rml:0 msgid "Items" -msgstr "" +msgstr "Itens" #. module: auction #: model:account.tax,name:auction.auction_tax5 #: field:auction.dates,seller_costs:0 msgid "Seller Costs" -msgstr "" +msgstr "Custos do Vendedor" #. module: auction #: view:auction.bid:0 @@ -310,22 +310,22 @@ msgstr "" #: model:ir.actions.report.xml,name:auction.bid_auction #: model:ir.ui.menu,name:auction.menu_action_bid_open msgid "Bids" -msgstr "" +msgstr "Lances" #. module: auction #: model:ir.ui.menu,name:auction.auction_all_objects_menu msgid "All objects" -msgstr "" +msgstr "Todos Objetos" #. module: auction #: model:ir.actions.act_window,name:auction.action_auction_buyer_reporting_all2 msgid "Buyer's auction for all months" -msgstr "" +msgstr "Leilão do comprador para todos os meses" #. module: auction #: field:auction.bid,contact_tel:0 msgid "Contact" -msgstr "" +msgstr "Contato" #. module: auction #: field:report.auction.view,obj_ret:0 @@ -337,7 +337,7 @@ msgstr "" #. module: auction #: wizard_button:auction.lots.sms_send,init,send:0 msgid "Send SMS" -msgstr "" +msgstr "Enviar SMS" #. module: auction #: selection:auction.lot.category,aie_categ:0 @@ -347,38 +347,38 @@ msgstr "" #. module: auction #: field:auction.lots,sel_inv_id:0 msgid "Seller Invoice" -msgstr "" +msgstr "Fatura do Vendedor" #. module: auction #: view:auction.dates:0 msgid "Commissions" -msgstr "" +msgstr "Comissões" #. module: auction #: wizard_button:auction.lots.numerotate,search,init:0 msgid "Back" -msgstr "" +msgstr "Voltar" #. module: auction #: model:ir.actions.act_window,name:auction.action_all_objects_unsold msgid "Unsold objects" -msgstr "" +msgstr "Objetos não vendidos" #. module: auction #: model:ir.ui.menu,name:auction.menu_all_objects_sold1 msgid "Sold Objects" -msgstr "" +msgstr "Objetos vendidos" #. module: auction #: view:report.buyer.auction2:0 #: view:report.seller.auction2:0 msgid "Sum net margin" -msgstr "" +msgstr "Somar margem líquida" #. module: auction #: view:auction.deposit:0 msgid "Deposit Border Form" -msgstr "" +msgstr "Formuálio de Limite de Depósito" #. module: auction #: field:auction.bid,bid_lines:0 @@ -386,12 +386,12 @@ msgstr "" #: rml:bids.lots:0 #: model:ir.model,name:auction.model_auction_bid_line msgid "Bid" -msgstr "" +msgstr "Lance" #. module: auction #: wizard_field:auction.lots.send.aie,date_ask,lang:0 msgid "Langage" -msgstr "" +msgstr "Idioma" #. module: auction #: field:auction.bid_line,lot_id:0 @@ -399,7 +399,7 @@ msgstr "" #: view:auction.lots:0 #: model:ir.model,name:auction.model_auction_lots msgid "Object" -msgstr "" +msgstr "Objeto" #. module: auction #: model:ir.actions.wizard,name:auction.wizard_map_user @@ -409,7 +409,7 @@ msgstr "" #. module: auction #: wizard_field:auction.lots.numerotate,search,lot_num:0 msgid "Inventory Number" -msgstr "" +msgstr "Número de inventário" #. module: auction #: wizard_button:auction.lots.buyer_map,check,end:0 @@ -417,12 +417,12 @@ msgstr "" #: wizard_button:auction.lots.numerotate,search,end:0 #: wizard_button:auction.lots.numerotate_cont,init,end:0 msgid "Exit" -msgstr "" +msgstr "Sair" #. module: auction #: field:report.buyer.auction2,net_revenue:0 msgid "Net Revenue" -msgstr "" +msgstr "Receita Líquida" #. module: auction #: view:report.buyer.auction:0 @@ -437,17 +437,17 @@ msgstr "" #: field:report.seller.auction,state:0 #: field:report.unclassified.objects,state:0 msgid "Status" -msgstr "" +msgstr "Situação" #. module: auction #: model:ir.actions.wizard,name:auction.wizard_sms msgid "SMS Send" -msgstr "" +msgstr "Enviar SMS" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Antique/Cartoons" -msgstr "" +msgstr "Antigo/Desenhos" #. module: auction #: view:auction.lots:0 @@ -455,42 +455,42 @@ msgstr "" #: selection:report.seller.auction,state:0 #: selection:report.unclassified.objects,state:0 msgid "Sold" -msgstr "" +msgstr "Vendido" #. module: auction #: constraint:hr.employee:0 msgid "Error ! You cannot create recursive Hierarchy of Employees." -msgstr "" +msgstr "Erro! Você não pode criar hierarquia recursiva de funcionários." #. module: auction #: field:auction.bid_line,name:0 msgid "Bid date" -msgstr "" +msgstr "Data do lance" #. module: auction #: field:auction.dates,acc_expense:0 msgid "Expense Account" -msgstr "" +msgstr "Conta de despesas" #. module: auction #: model:ir.ui.menu,name:auction.menu_wizard_emporte msgid "Deliveries Management" -msgstr "" +msgstr "Gerenciamento de entregas" #. module: auction #: field:auction.lots,obj_desc:0 msgid "Object Description" -msgstr "" +msgstr "Descrição do objeto" #. module: auction #: selection:auction.deposit,method:0 msgid "Contact the Seller" -msgstr "" +msgstr "Entre em contato com o vendedor" #. module: auction #: view:auction.lots:0 msgid "Auction Objects" -msgstr "" +msgstr "Leilão de Objetos" #. module: auction #: field:auction.lots,gross_revenue:0 @@ -498,12 +498,12 @@ msgstr "" #: field:report.object.encoded.manager,gross_revenue:0 #: field:report.seller.auction2,gross_revenue:0 msgid "Gross revenue" -msgstr "" +msgstr "Receita bruta" #. module: auction #: view:report.seller.auction2:0 msgid "Auction reporting2 form view" -msgstr "" +msgstr "Relatório2 de formulário de Leilão" #. module: auction #: model:ir.actions.wizard,name:auction.wizard_pay @@ -513,45 +513,45 @@ msgstr "" #. module: auction #: view:report.deposit.border:0 msgid "Depositer's statistics" -msgstr "" +msgstr "Estatísticas dos Depositantes" #. module: auction #: view:auction.lots:0 msgid "Buyer information" -msgstr "" +msgstr "Informações do Comprador" #. module: auction #: model:ir.model,name:auction.model_report_seller_auction msgid "Auction Reporting on seller view" -msgstr "" +msgstr "Relatório Leilão na visão do Vendedor" #. module: auction #: field:auction.lots,name2:0 msgid "Short Description (2)" -msgstr "" +msgstr "Descrição resumida (2)" #. module: auction #: model:ir.model,name:auction.model_auction_dates msgid "auction.dates" -msgstr "" +msgstr "leilão.datas" #. module: auction #: rml:auction.total.rml:0 #: model:ir.ui.menu,name:auction.auction_buyers_menu msgid "Buyers" -msgstr "" +msgstr "Vendedores" #. module: auction #: wizard_field:auction.pay.buy,init,amount:0 #: wizard_field:auction.pay.buy,init,amount2:0 #: wizard_field:auction.pay.buy,init,amount3:0 msgid "Amount paid" -msgstr "" +msgstr "Valor pago" #. module: auction #: model:ir.ui.menu,name:auction.menu_report_latest_doposit_tree msgid "My Latest Deposits" -msgstr "" +msgstr "Meus Últimos Depósitos" #. module: auction #: field:auction.lots,lot_est1:0 @@ -560,32 +560,32 @@ msgstr "" #: field:report.auction.view,min_est:0 #: field:report.unclassified.objects,lot_est1:0 msgid "Minimum Estimation" -msgstr "" +msgstr "Estimativa mínima" #. module: auction #: view:auction.dates:0 msgid "Accounting" -msgstr "" +msgstr "Contabilidade" #. module: auction #: model:ir.ui.menu,name:auction.menu_buyer_allmonth_view2 msgid "Buyer's Revenues" -msgstr "" +msgstr "Receitas do comprador" #. module: auction #: model:ir.model,name:auction.model_report_auction_adjudication msgid "report_auction_adjudication" -msgstr "" +msgstr "relatório_leilão_concedido" #. module: auction #: field:auction.bid_line,price:0 msgid "Maximum Price" -msgstr "" +msgstr "Preço máximo" #. module: auction #: selection:auction.lots.send.aie,date_ask,lang:0 msgid "eng" -msgstr "" +msgstr "Inglês" #. module: auction #: view:report.buyer.auction2:0 @@ -595,17 +595,17 @@ msgstr "" #. module: auction #: wizard_view:auction.lots.sms_send,init:0 msgid "Bulk SMS send" -msgstr "" +msgstr "Enviar SMS em massa" #. module: auction #: wizard_view:auction.taken,init:0 msgid "Select lots which are Sold" -msgstr "" +msgstr "Selecione lotes que são vendidos" #. module: auction #: field:auction.lots,statement_id:0 msgid "Payment" -msgstr "" +msgstr "Pagamento" #. module: auction #: selection:auction.lots.send.aie,date_ask,lang:0 @@ -615,50 +615,50 @@ msgstr "" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Antique/Old weapons and militaria" -msgstr "" +msgstr "Antiguidade/Armas antigas e militares" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Antique/Oriental Arts/Chineese furnitures" -msgstr "" +msgstr "Antiguidade/Arte Orientas/Mobiliário Chinês" #. module: auction #: selection:auction.deposit,method:0 msgid "Keep until sold" -msgstr "" +msgstr "Manter até vender" #. module: auction #: wizard_button:auction.lots.numerotate,init,choice:0 #: wizard_button:auction.lots.send.aie,init,date_ask:0 #: wizard_button:auction.lots.send.aie.results,init,date_ask:0 msgid "Continue" -msgstr "" +msgstr "Continuar" #. module: auction #: field:report.object.encoded,obj_num:0 #: field:report.object.encoded.manager,obj_num:0 msgid "# of Encoded obj." -msgstr "" +msgstr "# do Objeto Codificado" #. module: auction #: xsl:report.auction.vnd_bordereau:0 msgid "List N." -msgstr "" +msgstr "Lista N." #. module: auction #: field:report.deposit.border,moy_est:0 msgid "Avg. Est" -msgstr "" +msgstr "Média Estimada" #. module: auction #: rml:auction.total.rml:0 msgid "# of sellers:" -msgstr "" +msgstr "# dos vendedores:" #. module: auction #: constraint:product.product:0 msgid "Error: Invalid ean code" -msgstr "Error: código EAN invalido" +msgstr "Erro: Código EAN inválido" #. module: auction #: model:ir.module.module,description:auction.module_meta_information @@ -669,26 +669,31 @@ msgid "" " Delivery Management. \n" " " msgstr "" +"Este módulo fornece funcionalidade para \n" +" gerenciar artistas, artigos, vendedores, compradores e leilão.\n" +" Gerenciar ofertas, registro de objetos vendidos, pagos e não pagos.\n" +" A administração das entregas. \n" +" " #. module: auction #: rml:auction.total.rml:0 msgid "# of items taken away:" -msgstr "" +msgstr "# de itens retirados:" #. module: auction #: model:ir.ui.menu,name:auction.menu_auction_allmonth_view1 msgid "Auction's Summary" -msgstr "" +msgstr "Resumo do Leilão" #. module: auction #: rml:buyer.list:0 msgid "%)" -msgstr "" +msgstr "%)" #. module: auction #: wizard_view:auction.lots.able,init:0 msgid "Confirmation set taken away" -msgstr "" +msgstr "Confirmação marcada como retirada" #. module: auction #: wizard_field:auction.lots.make_invoice,init,objects:0 @@ -697,7 +702,7 @@ msgstr "" #: wizard_field:auction.lots.send.aie.results,date_ask,objects:0 #: field:report.deposit.border,nb_obj:0 msgid "# of objects" -msgstr "" +msgstr "# dos Objetos" #. module: auction #: field:auction.lots,lot_est2:0 @@ -706,12 +711,12 @@ msgstr "" #: field:report.auction.view,max_est:0 #: field:report.unclassified.objects,lot_est2:0 msgid "Maximum Estimation" -msgstr "" +msgstr "Estimativa Máxima" #. module: auction #: field:auction.lots,buyer_price:0 msgid "Buyer price" -msgstr "" +msgstr "Preço Comprador" #. module: auction #: model:ir.actions.wizard,name:auction.auction_wizard_payer_sel @@ -721,17 +726,17 @@ msgstr "" #. module: auction #: model:ir.actions.report.xml,name:auction.seller_form_id msgid "Seller List" -msgstr "" +msgstr "Lista de Vendedor" #. module: auction #: field:auction.lots,is_ok:0 msgid "Buyer's payment" -msgstr "" +msgstr "Pagamento do Comprador" #. module: auction #: view:auction.dates:0 msgid "Close + Create all invoices" -msgstr "" +msgstr "Fechar + Criar todas as Faturas" #. module: auction #: model:ir.actions.wizard,name:auction.auction_catalog_flagy @@ -741,48 +746,48 @@ msgstr "" #. module: auction #: field:auction.lot.history,price:0 msgid "Withdrawn price" -msgstr "" +msgstr "Preço de retirada" #. module: auction #: model:account.tax,name:auction.auction_tax4 msgid "Seller Costs1" -msgstr "" +msgstr "Custo1 do Venderor" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Antique/Verreries" -msgstr "" +msgstr "Antiguidade/Cristais" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Antique/Toys" -msgstr "" +msgstr "Antiguidade/Briquedos" #. module: auction #: field:auction.deposit,create_uid:0 #: field:auction.lots,create_uid:0 msgid "Created by" -msgstr "" +msgstr "Criado por" #. module: auction #: xsl:report.auction.ach_bordereau:0 msgid "Buyer form" -msgstr "" +msgstr "Formulário do comprador" #. module: auction #: xsl:report.auction.deposit:0 msgid "Estimate" -msgstr "" +msgstr "Estimativa" #. module: auction #: rml:auction.total.rml:0 msgid "# of buyers:" -msgstr "" +msgstr "# dos compradores:" #. module: auction #: field:auction.lots,costs:0 msgid "Indirect costs" -msgstr "" +msgstr "Custos indiretos" #. module: auction #: xsl:report.auction.ach_bordereau:0 @@ -792,12 +797,12 @@ msgstr "" #. module: auction #: constraint:account.tax.code:0 msgid "Error ! You can not create recursive accounts." -msgstr "" +msgstr "Erro ! Você não pode criar contas repetidas." #. module: auction #: model:ir.ui.menu,name:auction.auction_menu_root msgid "Auction Management" -msgstr "" +msgstr "Gerenciamento de leilão" #. module: auction #: field:auction.dates,journal_seller_id:0 @@ -807,23 +812,23 @@ msgstr "" #. module: auction #: view:report.buyer.auction:0 msgid "Auction buyer reporting tree view" -msgstr "" +msgstr "Relatório de compra do leilão exibição em árvore" #. module: auction #: model:ir.ui.menu,name:auction.menu_auction_artist msgid "Define Artists" -msgstr "" +msgstr "Definir artistas" #. module: auction #: field:auction.lot.category,name:0 msgid "Category Name" -msgstr "" +msgstr "Nome da Categoria" #. module: auction #: model:ir.ui.menu,name:auction.menu_report_auction_estimation_adj_category_manager #: model:ir.ui.menu,name:auction.menu_report_auction_estimation_adj_category_member msgid "Comparison of estimations" -msgstr "" +msgstr "Comparação das estimativas" #. module: auction #: model:ir.actions.report.xml,name:auction.v_report_barcode_lot @@ -833,34 +838,34 @@ msgstr "" #. module: auction #: wizard_button:auction.lots.numerotate,not_exist,init:0 msgid "Retry" -msgstr "" +msgstr "Repetir" #. module: auction #: field:auction.artists,birth_death_dates:0 msgid "Birth / Death dates" -msgstr "" +msgstr "Datas de Nascimento / Morte" #. module: auction #: selection:auction.lot.category,aie_categ:0 msgid "Antique/Jewelry" -msgstr "" +msgstr "Antiguidade/Jóias" #. module: auction #: model:ir.model,name:auction.model_report_unclassified_objects msgid "Unclassified objects " -msgstr "" +msgstr "objetos não classificados " #. module: auction #: wizard_button:auction.lots.numerotate_cont,init,set_number:0 #: wizard_field:auction.lots.send.aie,date_ask,numerotation:0 #: wizard_field:auction.lots.send.aie,init,numerotation:0 msgid "Numerotation" -msgstr "" +msgstr "Numerotation" #. module: auction #: model:ir.actions.act_window,name:auction.act_auction_lot_open_deposit msgid "Bordereau de dépôt" -msgstr "" +msgstr "Comprovante de depósito" #. module: auction #: xsl:report.auction.deposit:0 diff --git a/addons/base_calendar/DAV/BufferingHTTPServer.py b/addons/base_calendar/DAV/BufferingHTTPServer.py new file mode 100644 index 00000000000..37520421432 --- /dev/null +++ b/addons/base_calendar/DAV/BufferingHTTPServer.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python + +""" + Buffering HTTP Server + Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" + + +from utils import VERSION, AUTHOR +__version__ = VERSION +__author__ = AUTHOR + +from BaseHTTPServer import BaseHTTPRequestHandler +import os +class BufferedHTTPRequestHandler(BaseHTTPRequestHandler): + """ + Buffering HTTP Request Handler + + This class is an extension to the BaseHTTPRequestHandler + class which buffers the whole output and sends it at once + after the processing if the request is finished. + + This makes it possible to work together with some clients + which otherwise would break (e.g. cadaver) + + """ + + + def _init_buffer(self): + """initialize the buffer. + + If you override the handle() method remember to call + this (see below) + """ + self.__buffer="" + self.__outfp=os.tmpfile() + + def _append(self,s): + """ append a string to the buffer """ + self.__buffer=self.__buffer+s + + def _flush(self): + """ flush the buffer to wfile """ + self.wfile.write(self.__buffer) + self.__outfp.write(self.__buffer) + self.__outfp.flush() + self.wfile.flush() + self.__buffer="" + + def handle(self): + """ Handle a HTTP request """ + self._init_buffer() + BaseHTTPRequestHandler.handle(self) + self._flush() + + def send_header(self, keyword, value): + """Send a MIME header.""" + if self.request_version != 'HTTP/0.9': + self._append("%s: %s\r\n" % (keyword, value)) + + def end_headers(self): + """Send the blank line ending the MIME headers.""" + if self.request_version != 'HTTP/0.9': + self._append("\r\n") + + def send_response(self, code, message=None): + self.log_request(code) + + if message is None: + if self.responses.has_key(code): + message = self.responses[code][0] + else: + message = '' + + if self.request_version != 'HTTP/0.9': + self._append("%s %s %s\r\n" % + (self.protocol_version, str(code), message)) + + self.send_header('Server', self.version_string()) + self.send_header('Connection', 'close') + self.send_header('Date', self.date_time_string()) + + protocol_version="HTTP/1.1" + diff --git a/addons/base_calendar/DAV/WebDAVServer.py b/addons/base_calendar/DAV/WebDAVServer.py new file mode 100644 index 00000000000..5f409773c18 --- /dev/null +++ b/addons/base_calendar/DAV/WebDAVServer.py @@ -0,0 +1,379 @@ +""" + Python WebDAV Server. + Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +This module builds on AuthServer by implementing the standard DAV +methods. + +Subclass this class and specify an IFACE_CLASS. See example. + +""" + +DEBUG=None + +from utils import VERSION, AUTHOR +__version__ = VERSION +__author__ = AUTHOR + + +import os +import sys +import time +import socket +import string +import posixpath +import base64 +import urlparse +import urllib + +from propfind import PROPFIND +from delete import DELETE +from davcopy import COPY +from davmove import MOVE + +from string import atoi,split +from status import STATUS_CODES +from errors import * + +import BaseHTTPServer + +class DAVRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): + """Simple DAV request handler with + + - GET + - HEAD + - PUT + - OPTIONS + - PROPFIND + - PROPPATCH + - MKCOL + + It uses the resource/collection classes for serving and + storing content. + + """ + + server_version = "DAV/" + __version__ + protocol_version = 'HTTP/1.1' + + ### utility functions + def _log(self, message): + pass + + def _append(self,s): + """ write the string to wfile """ + self.wfile.write(s) + + def send_body(self,DATA,code,msg,desc,ctype='application/octet-stream',headers=None): + """ send a body in one part """ + + if not headers: + headers = {} + self.send_response(code,message=msg) + self.send_header("Connection", "keep-alive") + self.send_header("Accept-Ranges", "bytes") + + for a,v in headers.items(): + self.send_header(a,v) + + if DATA: + self.send_header("Content-Length", str(len(DATA))) + self.send_header("Content-Type", ctype) + else: + self.send_header("Content-Length", "0") + + self.end_headers() + if DATA: + self._append(DATA) + + def send_body_chunks(self,DATA,code,msg,desc,ctype='text/xml; encoding="utf-8"'): + """ send a body in chunks """ + + self.responses[207]=(msg,desc) + self.send_response(code,message=msg) + self.send_header("Content-type", ctype) + self.send_header("Connection", "keep-alive") + self.send_header("Transfer-Encoding", "chunked") + self.end_headers() + self._append(hex(len(DATA))[2:]+"\r\n") + self._append(DATA) + self._append("\r\n") + self._append("0\r\n") + self._append("\r\n") + + ### HTTP METHODS + + def do_OPTIONS(self): + """return the list of capabilities """ + self.send_response(200) + self.send_header("Allow", "GET, HEAD, COPY, MOVE, POST, PUT, PROPFIND, PROPPATCH, OPTIONS, MKCOL, DELETE, TRACE") + self.send_header("Content-Type", "text/plain") + self.send_header("Connection", "keep-alive") + self.send_header("DAV", "1") + self.end_headers() + + def do_PROPFIND(self): + + dc=self.IFACE_CLASS + # read the body + body=None + if self.headers.has_key("Content-Length"): + l=self.headers['Content-Length'] + body=self.rfile.read(atoi(l)) + alt_body = """ + + + + + + + + + + """ + #self.wfile.write(body) + + # which Depth? + if self.headers.has_key('Depth'): + d=self.headers['Depth'] + else: + d="infinity" + + uri=self.geturi() + pf=PROPFIND(uri,dc,d) + + if body: + pf.read_propfind(body) + + try: + DATA=pf.createResponse() + DATA=DATA+"\n" + # print "Data:", DATA + except DAV_NotFound,(ec,dd): + return self.send_notFound(dd, uri) + except DAV_Error, (ec,dd): + return self.send_error(ec,dd) + + self.send_body_chunks(DATA,207,"Multi-Status","Multiple responses") + + def geturi(self): + buri = self.IFACE_CLASS.baseuri + if buri[-1] == '/': + return urllib.unquote(buri[:-1]+self.path) + else: + return urllib.unquote(buri+self.path) + + def do_GET(self): + """Serve a GET request.""" + dc=self.IFACE_CLASS + uri=self.geturi() + + # get the last modified date + try: + lm=dc.get_prop(uri,"DAV:","getlastmodified") + except: + lm="Sun, 01 Dec 2014 00:00:00 GMT" # dummy! + headers={"Last-Modified":lm , "Connection": "keep-alive"} + + # get the content type + try: + ct=dc.get_prop(uri,"DAV:","getcontenttype") + except: + ct="application/octet-stream" + + # get the data + try: + data=dc.get_data(uri) + except DAV_Error, (ec,dd): + self.send_status(ec) + return + + # send the data + self.send_body(data,200,"OK","OK",ct,headers) + + def do_HEAD(self): + """ Send a HEAD response """ + dc=self.IFACE_CLASS + uri=self.geturi() + + # get the last modified date + try: + lm=dc.get_prop(uri,"DAV:","getlastmodified") + except: + lm="Sun, 01 Dec 2014 00:00:00 GMT" # dummy! + + headers={"Last-Modified":lm, "Connection": "keep-alive"} + + # get the content type + try: + ct=dc.get_prop(uri,"DAV:","getcontenttype") + except: + ct="application/octet-stream" + + try: + data=dc.get_data(uri) + headers["Content-Length"]=str(len(data)) + except DAV_NotFound: + self.send_body(None,404,"Not Found","") + return + + self.send_body(None,200,"OK","OK",ct,headers) + + def do_POST(self): + self.send_error(404,"File not found") + + def do_MKCOL(self): + """ create a new collection """ + + dc=self.IFACE_CLASS + uri=self.geturi() + try: + res = dc.mkcol(uri) + if res: + self.send_body(None,201,"Created",'') + else: + self.send_body(None,415,"Cannot create",'') + #self.send_header("Connection", "keep-alive") + # Todo: some content, too + except DAV_Error, (ec,dd): + self.send_body(None,int(ec),dd,dd) + + def do_DELETE(self): + """ delete an resource """ + dc=self.IFACE_CLASS + uri=self.geturi() + dl=DELETE(uri,dc) + if dc.is_collection(uri): + res=dl.delcol() + else: + res=dl.delone() + + if res: + self.send_status(207,body=res) + else: + self.send_status(204) + + def do_PUT(self): + dc=self.IFACE_CLASS + + # read the body + body=None + if self.headers.has_key("Content-Length"): + l=self.headers['Content-Length'] + body=self.rfile.read(atoi(l)) + uri=self.geturi() + + ct=None + if self.headers.has_key("Content-Type"): + ct=self.headers['Content-Type'] + try: + dc.put(uri,body,ct) + except DAV_Error, (ec,dd): + self.send_status(ec) + return + self.send_status(201) + + def do_COPY(self): + """ copy one resource to another """ + try: + self.copymove(COPY) + except DAV_Error, (ec,dd): + self.send_status(ec) + + def do_MOVE(self): + """ move one resource to another """ + try: + self.copymove(MOVE) + except DAV_Error, (ec,dd): + self.send_status(ec) + + def copymove(self,CLASS): + """ common method for copying or moving objects """ + dc=self.IFACE_CLASS + + # get the source URI + source_uri=self.geturi() + + # get the destination URI + dest_uri=self.headers['Destination'] + dest_uri=urllib.unquote(dest_uri) + + # Overwrite? + overwrite=1 + result_code=204 + if self.headers.has_key("Overwrite"): + if self.headers['Overwrite']=="F": + overwrite=None + result_code=201 + + # instanciate ACTION class + cp=CLASS(dc,source_uri,dest_uri,overwrite) + + # Depth? + d="infinity" + if self.headers.has_key("Depth"): + d=self.headers['Depth'] + + if d!="0" and d!="infinity": + self.send_status(400) + return + + if d=="0": + res=cp.single_action() + self.send_status(res) + return + + # now it only can be "infinity" but we nevertheless check for a collection + if dc.is_collection(source_uri): + try: + res=cp.tree_action() + except DAV_Error, (ec,dd): + self.send_status(ec) + return + else: + try: + res=cp.single_action() + except DAV_Error, (ec,dd): + self.send_status(ec) + return + + if res: + self.send_body_chunks(res,207,STATUS_CODES[207],STATUS_CODES[207], + ctype='text/xml; charset="utf-8"') + else: + self.send_status(result_code) + + def get_userinfo(self,user,pw): + """ Dummy method which lets all users in """ + + return 1 + + def send_status(self,code=200,mediatype='text/xml; charset="utf-8"', \ + msg=None,body=None): + + if not msg: msg=STATUS_CODES[code] + self.send_body(body,code,STATUS_CODES[code],msg,mediatype) + + def send_notFound(self,descr,uri): + body = """ + + %s + + %s + + """ + return self.send_status(404,descr, body=body % (uri,descr)) diff --git a/addons/base_calendar/DAV/__init__.py b/addons/base_calendar/DAV/__init__.py new file mode 100644 index 00000000000..10abdb40014 --- /dev/null +++ b/addons/base_calendar/DAV/__init__.py @@ -0,0 +1,20 @@ +""" + python davserver + Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +""" + + diff --git a/addons/base_calendar/DAV/constants.py b/addons/base_calendar/DAV/constants.py new file mode 100644 index 00000000000..e858ac519c2 --- /dev/null +++ b/addons/base_calendar/DAV/constants.py @@ -0,0 +1,18 @@ +""" + +constants definition + + +""" + +# definition for resourcetype +COLLECTION=1 +OBJECT=None +DAV_PROPS=['creationdate', 'displayname', 'getcontentlanguage', 'getcontentlength', 'getcontenttype', 'getetag', 'getlastmodified', 'lockdiscovery', 'resourcetype', 'source', 'supportedlock'] + + +# Request classes in propfind + +RT_ALLPROP=1 +RT_PROPNAME=2 +RT_PROP=3 diff --git a/addons/base_calendar/DAV/davcmd.py b/addons/base_calendar/DAV/davcmd.py new file mode 100644 index 00000000000..5c6be3ff9f7 --- /dev/null +++ b/addons/base_calendar/DAV/davcmd.py @@ -0,0 +1,218 @@ +""" + +davcmd.py +--------- + +containts commands like copy, move, delete for normal +resources and collections + +""" + +from string import split,replace,joinfields +import urlparse + +from utils import create_treelist, is_prefix +from errors import * + +def deltree(dc,uri,exclude={}): + """ delete a tree of resources + + dc -- dataclass to use + uri -- root uri to delete + exclude -- an optional list of uri:error_code pairs which should not + be deleted. + + returns dict of uri:error_code tuples from which + another method can create a multistatus xml element. + + Also note that we only know Depth=infinity thus we don't have + to test for it. + + """ + + tlist=create_treelist(dc,uri) + result={} + + for i in range(len(tlist),0,-1): + problem_uris=result.keys() + element=tlist[i-1] + + # test here, if an element is a prefix of an uri which + # generated an error before. + # note that we walk here from childs to parents, thus + # we cannot delete a parent if a child made a problem. + # (see example in 8.6.2.1) + ok=1 + for p in problem_uris: + if is_prefix(element,p): + ok=None + break + + if not ok: continue + + # here we test for the exclude list which is the other way round! + for p in exclude.keys(): + if is_prefix(p,element): + ok=None + break + + if not ok: continue + + # now delete stuff + try: + delone(dc,element) + except DAV_Error, (ec,dd): + result[element]=ec + + return result + +def delone(dc,uri): + """ delete a single object """ + if dc.is_collection(uri): + dc.rmcol(uri) # should be empty + else: + dc.rm(uri) + +### +### COPY +### + +# helper function + +def copy(dc,src,dst): + """ only copy the element + + This is just a helper method factored out from copy and + copytree. It will not handle the overwrite or depth header. + + """ + + # destination should have been deleted before + if dc.exists(dst): raise DAV_Error, 412 + + # source should exist also + if not dc.exists(src): raise DAV_NotFound + + if dc.is_collection(src): + dc.copycol(src,dst) # an exception will be passed thru + else: + dc.copy(src,dst) # an exception will be passed thru + + +# the main functions + +def copyone(dc,src,dst,overwrite=None): + """ copy one resource to a new destination """ + + if overwrite and dc.exists(dst): + delres=deltree(dc,dst) + else: + delres={} + + # if we cannot delete everything, then do not copy! + if delres: return delres + + try: + copy(dc,src,dst) # pass thru exceptions + except DAV_Error, (ec,dd): + return ec + +def copytree(dc,src,dst,overwrite=None): + """ copy a tree of resources to another location + + dc -- dataclass to use + src -- src uri from where to copy + dst -- dst uri + overwrite -- if 1 then delete dst uri before + + returns dict of uri:error_code tuples from which + another method can create a multistatus xml element. + + """ + + + # first delete the destination resource + if overwrite and dc.exists(dst): + delres=deltree(dc,dst) + else: + delres={} + + # if we cannot delete everything, then do not copy! + if delres: return delres + + # get the tree we have to copy + tlist=create_treelist(dc,src) + result={} + + # prepare destination URIs (get the prefix) + dpath=urlparse.urlparse(dst)[2] + + for element in tlist: + problem_uris=result.keys() + + # now URIs get longer and longer thus we have + # to test if we had a parent URI which we were not + # able to copy in problem_uris which is the prefix + # of the actual element. If it is, then we cannot + # copy this as well but do not generate another error. + ok=1 + for p in problem_uris: + if is_prefix(p,element): + ok=None + break + + if not ok: continue + + # now create the destination URI which corresponds to + # the actual source URI. -> actual_dst + # ("subtract" the base src from the URI and prepend the + # dst prefix to it.) + esrc=replace(element,src,"") + actual_dst=dpath+esrc + + # now copy stuff + try: + copy(dc,element,actual_dst) + except DAV_Error, (ec,dd): + result[element]=ec + + return result + + + +### +### MOVE +### + + +def moveone(dc,src,dst,overwrite=None): + """ move a single resource + + This is done by first copying it and then deleting + the original. + """ + + # first copy it + copyone(dc,src,dst,overwrite) + + # then delete it + dc.rm(src) + +def movetree(dc,src,dst,overwrite=None): + """ move a collection + + This is done by first copying it and then deleting + the original. + + PROBLEM: if something did not copy then we have a problem + when deleting as the original might get deleted! + """ + + # first copy it + res=copytree(dc,src,dst,overwrite) + + # then delete it + res=deltree(dc,src,exclude=res) + + return res + diff --git a/addons/base_calendar/DAV/davcopy.py b/addons/base_calendar/DAV/davcopy.py new file mode 100755 index 00000000000..ec44931b107 --- /dev/null +++ b/addons/base_calendar/DAV/davcopy.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python + +""" + python davserver + Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +""" + + +from xml.dom import ext +from xml.dom.Document import Document + +import sys +import string +import urlparse +import urllib +from StringIO import StringIO + +import utils +from constants import COLLECTION, OBJECT, DAV_PROPS, RT_ALLPROP, RT_PROPNAME, RT_PROP +from errors import * +from utils import create_treelist, quote_uri, gen_estring + +class COPY: + """ copy resources and eventually create multistatus responses + + This module implements the COPY class which is responsible for + copying resources. Usually the normal copy work is done in the + interface class. This class only creates error messages if error + occur. + + """ + + + def __init__(self,dataclass,src_uri,dst_uri,overwrite): + self.__dataclass=dataclass + self.__src=src_uri + self.__dst=dst_uri + self.__overwrite=overwrite + + + def single_action(self): + """ copy a normal resources. + + We try to copy it and return the result code. + This is for Depth==0 + + """ + + dc=self.__dataclass + base=self.__src + + ### some basic tests + # test if dest exists and overwrite is false + if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 + # test if src and dst are the same + # (we assume that both uris are on the same server!) + ps=urlparse.urlparse(self.__src)[2] + pd=urlparse.urlparse(self.__dst)[2] + if ps==pd: raise DAV_Error, 403 + + return dc.copyone(self.__src,self.__dst,self.__overwrite) + + #return copyone(dc,self.__src,self.__dst,self.__overwrite) + + def tree_action(self): + """ copy a tree of resources (a collection) + + Here we return a multistatus xml element. + + """ + dc=self.__dataclass + base=self.__src + + ### some basic tests + # test if dest exists and overwrite is false + if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 + # test if src and dst are the same + # (we assume that both uris are on the same server!) + ps=urlparse.urlparse(self.__src)[2] + pd=urlparse.urlparse(self.__dst)[2] + if ps==pd: raise DAV_Error, 403 + + + result=dc.copytree(self.__src,self.__dst,self.__overwrite) + #result=copytree(dc,self.__src,self.__dst,self.__overwrite) + + if not result: return None + + ### + ### create the multistatus XML element + ### (this is also the same as in delete.py. + ### we might make a common method out of it) + ### + + doc = Document(None) + ms=doc.createElement("D:multistatus") + ms.setAttribute("xmlns:D","DAV:") + doc.appendChild(ms) + + for el,ec in result.items(): + re=doc.createElement("D:response") + hr=doc.createElement("D:href") + st=doc.createElement("D:status") + huri=doc.createTextNode(quote_uri(el)) + t=doc.createTextNode(gen_estring(ec)) + st.appendChild(t) + hr.appendChild(huri) + re.appendChild(hr) + re.appendChild(st) + ms.appendChild(re) + + sfile=StringIO() + ext.PrettyPrint(doc,stream=sfile) + s=sfile.getvalue() + sfile.close() + return s + diff --git a/addons/base_calendar/DAV/davmove.py b/addons/base_calendar/DAV/davmove.py new file mode 100755 index 00000000000..78c1e649389 --- /dev/null +++ b/addons/base_calendar/DAV/davmove.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +""" + python davserver + Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +""" + + +import sys +import string +import urlparse +import urllib +from StringIO import StringIO + +import utils +from constants import COLLECTION, OBJECT, DAV_PROPS +from constants import RT_ALLPROP, RT_PROPNAME, RT_PROP +from errors import * +from utils import create_treelist, quote_uri, gen_estring, make_xmlresponse +from davcmd import moveone, movetree + +class MOVE: + """ move resources and eventually create multistatus responses + + This module implements the MOVE class which is responsible for + moving resources. + + MOVE is implemented by a COPY followed by a DELETE of the old + resource. + + """ + + + def __init__(self,dataclass,src_uri,dst_uri,overwrite): + self.__dataclass=dataclass + self.__src=src_uri + self.__dst=dst_uri + self.__overwrite=overwrite + + + def single_action(self): + """ move a normal resources. + + We try to move it and return the result code. + This is for Depth==0 + + """ + + dc=self.__dataclass + base=self.__src + + ### some basic tests + # test if dest exists and overwrite is false + if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 + # test if src and dst are the same + # (we assume that both uris are on the same server!) + ps=urlparse.urlparse(self.__src)[2] + pd=urlparse.urlparse(self.__dst)[2] + if ps==pd: raise DAV_Error, 403 + + return dc.moveone(self.__src,self.__dst,self.__overwrite) + + def tree_action(self): + """ move a tree of resources (a collection) + + Here we return a multistatus xml element. + + """ + dc=self.__dataclass + base=self.__src + + ### some basic tests + # test if dest exists and overwrite is false + if dc.exists(self.__dst) and not self.__overwrite: raise DAV_Error, 412 + # test if src and dst are the same + # (we assume that both uris are on the same server!) + ps=urlparse.urlparse(self.__src)[2] + pd=urlparse.urlparse(self.__dst)[2] + if ps==pd: raise DAV_Error, 403 + + result=dc.movetree(self.__src,self.__dst,self.__overwrite) + if not result: return None + + # create the multistatus XML element + return make_xmlresponse(result) + diff --git a/addons/base_calendar/DAV/delete.py b/addons/base_calendar/DAV/delete.py new file mode 100755 index 00000000000..ed017e53b6d --- /dev/null +++ b/addons/base_calendar/DAV/delete.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python + +""" + + python davserver + Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +""" +import os +import string +import urllib +from StringIO import StringIO + +from status import STATUS_CODES +from utils import gen_estring, quote_uri, make_xmlresponse +from davcmd import deltree + +class DELETE: + + def __init__(self,uri,dataclass): + self.__dataclass=dataclass + self.__uri=uri + + def delcol(self): + """ delete a collection """ + + dc=self.__dataclass + result=dc.deltree(self.__uri) + + if not len(result.items()): + return None # everything ok + + # create the result element + return make_xmlresponse(result) + + def delone(self): + """ delete a resource """ + + dc=self.__dataclass + result=dc.delone(self.__uri) + + if not result: return None + if not len(result.items()): + return None # everything ok + + # create the result element + return make_xmlresponse(result) + diff --git a/addons/base_calendar/DAV/errors.py b/addons/base_calendar/DAV/errors.py new file mode 100755 index 00000000000..9c0e7667913 --- /dev/null +++ b/addons/base_calendar/DAV/errors.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +""" + + Exceptions for the DAVserver implementation + +""" + +class DAV_Error(Exception): + """ in general we can have the following arguments: + + 1. the error code + 2. the error result element, e.g. a element + """ + + def __init__(self,*args): + if len(args)==1: + self.args=(args[0],"") + else: + self.args=args + +class DAV_Secret(DAV_Error): + """ the user is not allowed to know anything about it + + returning this for a property value means to exclude it + from the response xml element. + """ + + def __init__(self): + DAV_Error.__init__(self,0) + pass + +class DAV_NotFound(DAV_Error): + """ a requested property was not found for a resource """ + + def __init__(self,*args): + if len(args): + if isinstance(args[0],list): + stre = "Path %s not found!"%('/'.join(args[0])) + else: + stre = args[0] + DAV_Error.__init__(self,404,stre) + else: + DAV_Error.__init__(self,404) + + pass + +class DAV_Forbidden(DAV_Error): + """ a method on a resource is not allowed """ + + def __init__(self,*args): + if len(args): + DAV_Error.__init__(self,403,args[0]) + else: + DAV_Error.__init__(self,403) + pass + diff --git a/addons/base_calendar/DAV/iface.py b/addons/base_calendar/DAV/iface.py new file mode 100644 index 00000000000..c8f5fe2594d --- /dev/null +++ b/addons/base_calendar/DAV/iface.py @@ -0,0 +1,263 @@ +""" + +basic interface class + +use this for subclassing when writing your own interface +class. + +""" + +from errors import * + +import time +from string import lower + +class dav_interface: + """ interface class for implementing DAV servers """ + + ### defined properties (modify this but let the DAV stuff there!) + ### the format is namespace: [list of properties] + + PROPS={"DAV:" : ('creationdate', + 'displayname', + 'getcontentlanguage', + 'getcontentlength', + 'getcontenttype', + 'getetag', + 'getlastmodified', + 'lockdiscovery', + 'resourcetype', + 'source', + 'supportedlock'), + "NS2" : ("p1","p2") + } + + # here we define which methods handle which namespace + # the first item is the namespace URI and the second one + # the method prefix + # e.g. for DAV:getcontenttype we call dav_getcontenttype() + M_NS={"DAV:" : "_get_dav", + "NS2" : "ns2" } + + def get_propnames(self,uri): + """ return the property names allowed for the given URI + + In this method we simply return the above defined properties + assuming that they are valid for any resource. + You can override this in order to return a different set + of property names for each resource. + + """ + return self.PROPS + + def get_prop2(self,uri,ns,pname): + """ return the value of a property + """ + if lower(ns)=="dav:": return self.get_dav(uri,pname) + + raise DAV_NotFound + + def get_prop(self,uri,ns,propname): + """ return the value of a given property + + uri -- uri of the object to get the property of + ns -- namespace of the property + pname -- name of the property + """ + if self.M_NS.has_key(ns): + prefix=self.M_NS[ns] + else: + print "No namespace:",ns, "( for prop:", propname,")" + raise DAV_NotFound + mname=prefix+"_"+propname + if not hasattr(self,mname): + raise DAV_NotFound + + try: + m=getattr(self,mname) + r=m(uri) + return r + except AttributeError, e: + print 'Property %s not supported' % propname + print "Exception:", e + raise DAV_NotFound + + ### + ### DATA methods (for GET and PUT) + ### + + def get_data(self,uri): + """ return the content of an object + + return data or raise an exception + + """ + raise DAV_NotFound + + def put(self,uri,data): + """ write an object to the repository + + return a result code or raise an exception + """ + + raise DAV_Forbidden + + ### + ### Methods for DAV properties + ### + + def _get_dav_creationdate(self,uri): + """ return the creationdate of a resource """ + d=self.get_creationdate(uri) + # format it + if isinstance(d, int) or isinstance(d, float): + d = time.localtimetime(d) + return time.strftime("%Y-%m-%dT%H:%M:%S%Z",d) + + def _get_dav_getlastmodified(self,uri): + """ return the last modified date of a resource """ + d=self.get_lastmodified(uri) + if isinstance(d, int) or isinstance(d, float): + d = time.localtime(d) + # format it + return time.asctime(d) + + + ### + ### OVERRIDE THESE! + ### + + def get_creationdate(self,uri): + """ return the creationdate of the resource """ + return time.time() + + def get_lastmodified(self,uri): + """ return the last modification date of the resource """ + return time.time() + + + ### + ### COPY MOVE DELETE + ### + + ### methods for deleting a resource + + def rmcol(self,uri): + """ delete a collection + + This should not delete any children! This is automatically done + before by the DELETE class in DAV/delete.py + + return a success code or raise an exception + + """ + raise DAV_NotFound + + def rm(self,uri): + """ delete a single resource + + return a success code or raise an exception + + """ + raise DAV_NotFound + + """ + + COPY/MOVE HANDLER + + These handler are called when a COPY or MOVE method is invoked by + a client. In the default implementation it works as follows: + + - the davserver receives a COPY/MOVE method + - the davcopy or davmove module will be loaded and the corresponding + class will be initialized + - this class parses the query and decides which method of the interface class + to call: + + copyone for a single resource to copy + copytree for a tree to copy (collection) + (the same goes for move of course). + + - the interface class has now two options: + 1. to handle the action directly (e.g. cp or mv on filesystems) + 2. to let it handle via the copy/move methods in davcmd. + + ad 1) The first approach can be used when we know that no error can + happen inside a tree or when the action can exactly tell which + element made which error. We have to collect these and return + it in a dict of the form {uri: error_code, ...} + + ad 2) The copytree/movetree/... methods of davcmd.py will do the recursion + themselves and call for each resource the copy/move method of the + interface class. Thus method will then only act on a single resource. + (Thus a copycol on a normal unix filesystem actually only needs to do + an mkdir as the content will be copied by the davcmd.py function. + The davcmd.py method will also automatically collect all errors and + return the dictionary described above. + When you use 2) you also have to implement the copy() and copycol() + methods in your interface class. See the example for details. + + To decide which approach is the best you have to decide if your application + is able to generate errors inside a tree. E.g. a function which completely + fails on a tree if one of the tree's childs fail is not what we need. Then + 2) would be your way of doing it. + Actually usually 2) is the better solution and should only be replaced by + 1) if you really need it. + + The remaining question is if we should do the same for the DELETE method. + + """ + + ### MOVE handlers + + def moveone(self,src,dst,overwrite): + """ move one resource with Depth=0 """ + return moveone(self,src,dst,overwrite) + + def movetree(self,src,dst,overwrite): + """ move a collection with Depth=infinity """ + return movetree(self,src,dst,overwrite) + + ### COPY handlers + + def copyone(self,src,dst,overwrite): + """ copy one resource with Depth=0 """ + return copyone(self,src,dst,overwrite) + + def copytree(self,src,dst,overwrite): + """ copy a collection with Depth=infinity """ + return copytree(self,src,dst,overwrite) + + + ### low level copy methods (you only need these for method 2) + def copy(self,src,dst): + """ copy a resource with depth==0 + + You don't need to bother about overwrite or not. + This has been done already. + + return a success code or raise an exception if something fails + """ + return 201 + + + def copycol(self,src,dst): + """ copy a resource with depth==infinity + + You don't need to bother about overwrite or not. + This has been done already. + + return a success code or raise an exception if something fails + """ + return 201 + + ### some utility functions you need to implement + + def exists(self,uri): + """ return 1 or None depending on if a resource exists """ + return None # no + + def is_collection(self,uri): + """ return 1 or None depending on if a resource is a collection """ + return None # no + diff --git a/addons/base_calendar/DAV/propfind.py b/addons/base_calendar/DAV/propfind.py new file mode 100755 index 00000000000..0fb9c229c03 --- /dev/null +++ b/addons/base_calendar/DAV/propfind.py @@ -0,0 +1,372 @@ +#!/usr/bin/env python + +""" + python davserver + Copyright (C) 1999 Christian Scholz (ruebe@aachen.heimat.de) + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library 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 + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public + License along with this library; if not, write to the Free + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +""" + + +from xml.dom import ext +from xml.dom.Document import Document + +import sys +import string +import urlparse +import urllib +from StringIO import StringIO + +import utils +from constants import COLLECTION, OBJECT, DAV_PROPS, RT_ALLPROP, RT_PROPNAME, RT_PROP +from errors import * + +def utf8str(st): + if isinstance(st,unicode): + return st.encode('utf8') + else: + return str(st) + +class PROPFIND: + """ parse a propfind xml element and extract props + + It will set the following instance vars: + + request_class : ALLPROP | PROPNAME | PROP + proplist : list of properties + nsmap : map of namespaces + + The list of properties will contain tuples of the form + (element name, ns_prefix, ns_uri) + + + """ + + + def __init__(self,uri,dataclass,depth): + self.request_type=None + self.nsmap={} + self.proplist={} + self.default_ns=None + self.__dataclass=dataclass + self.__depth=str(depth) + self.__uri=uri + self.use_full_urls=True + self.__has_body=None # did we parse a body? + + def read_propfind(self,xml_doc): + self.request_type,self.proplist,self.namespaces=utils.parse_propfind(xml_doc) + + # a violation of the expected logic: client (korganizer) will ask for DAV:resourcetype + # but we also have to return the http://groupdav.org/:resourcetype property! + if self.proplist.has_key('DAV:') and 'resourcetype' in self.proplist['DAV:']: + if not self.proplist.has_key('http://groupdav.org/'): + self.proplist['http://groupdav.org/'] = [] + self.proplist['http://groupdav.org/'].append('resourcetype') + if 'DAV:' in self.namespaces: #TMP + self.namespaces.append('http://groupdav.org/') + + def createResponse(self): + """ create the multistatus response + + This will be delegated to the specific method + depending on which request (allprop, propname, prop) + was found. + + If we get a PROPNAME then we simply return the list with empty + values which we get from the interface class + + If we get an ALLPROP we first get the list of properties and then + we do the same as with a PROP method. + + If the uri doesn't exist, return an xml response with a 404 status + + """ + + if not self.__dataclass.exists(self.__uri): + raise DAV_NotFound("Path %s doesn't exist" % self.__uri) + + if self.request_type==RT_ALLPROP: + return self.create_allprop() + + if self.request_type==RT_PROPNAME: + return self.create_propname() + + if self.request_type==RT_PROP: + return self.create_prop() + + # no body means ALLPROP! + return self.create_allprop() + + def create_propname(self): + """ create a multistatus response for the prop names """ + + dc=self.__dataclass + # create the document generator + doc = Document(None) + ms=doc.createElement("D:multistatus") + ms.setAttribute("xmlns:D","DAV:") + doc.appendChild(ms) + + if self.__depth=="0": + pnames=dc.get_propnames(self.__uri) + re=self.mk_propname_response(self.__uri,pnames,doc) + ms.appendChild(re) + + elif self.__depth=="1": + pnames=dc.get_propnames(self.__uri) + re=self.mk_propname_response(self.__uri,pnames,doc) + ms.appendChild(re) + + for newuri in dc.get_childs(self.__uri): + pnames=dc.get_propnames(newuri) + re=self.mk_propname_response(newuri,pnames,doc) + ms.appendChild(re) + # *** depth=="infinity" + + sfile=StringIO() + ext.PrettyPrint(doc,stream=sfile) + s=sfile.getvalue() + sfile.close() + return s + + def create_allprop(self): + """ return a list of all properties """ + self.proplist={} + self.namespaces=[] + for ns,plist in self.__dataclass.get_propnames(self.__uri).items(): + self.proplist[ns]=plist + self.namespaces.append(ns) + + return self.create_prop() + + def create_prop(self): + """ handle a request + + This will + + 1. set up the -Framework + + 2. read the property values for each URI + (which is dependant on the Depth header) + This is done by the get_propvalues() method. + + 3. For each URI call the append_result() method + to append the actual -Tag to the result + document. + + We differ between "good" properties, which have been + assigned a value by the interface class and "bad" + properties, which resulted in an error, either 404 + (Not Found) or 403 (Forbidden). + + """ + + + # create the document generator + doc = Document(None) + ms=doc.createElement("D:multistatus") + ms.setAttribute("xmlns:D","DAV:") + doc.appendChild(ms) + + if self.__depth=="0": + gp,bp=self.get_propvalues(self.__uri) + res=self.mk_prop_response(self.__uri,gp,bp,doc) + ms.appendChild(res) + + elif self.__depth=="1": + gp,bp=self.get_propvalues(self.__uri) + res=self.mk_prop_response(self.__uri,gp,bp,doc) + ms.appendChild(res) + + try: + for newuri in self.__dataclass.get_childs(self.__uri): + gp,bp=self.get_propvalues(newuri) + res=self.mk_prop_response(newuri,gp,bp,doc) + ms.appendChild(res) + except DAV_NotFound: + # If no children, never mind. + pass + + sfile=StringIO() + ext.PrettyPrint(doc,stream=sfile) + s=sfile.getvalue() + sfile.close() + return s + + + def mk_propname_response(self,uri,propnames,doc): + """ make a new result element for a PROPNAME request + + This will simply format the propnames list. + propnames should have the format {NS1 : [prop1, prop2, ...], NS2: ...} + + """ + re=doc.createElement("D:response") + + # write href information + href=doc.createElement("D:href") + if self.use_full_urls: + huri=doc.createTextNode(uri) + else: + uparts=urlparse.urlparse(uri) + fileloc=uparts[2] + huri=doc.createTextNode(urllib.quote(fileloc.encode('utf8'))) + href.appendChild(huri) + re.appendChild(href) + + ps=doc.createElement("D:propstat") + nsnum=0 + + for ns,plist in propnames.items(): + # write prop element + pr=doc.createElement("D:prop") + nsp="ns"+str(nsnum) + pr.setAttribute("xmlns:"+nsp,ns) + nsnum=nsnum+1 + + # write propertynames + for p in plist: + pe=doc.createElement(nsp+":"+p) + pr.appendChild(pe) + + ps.appendChild(pr) + + re.appendChild(ps) + + return re + + def mk_prop_response(self,uri,good_props,bad_props,doc): + """ make a new result element + + We differ between the good props and the bad ones for + each generating an extra -Node (for each error + one, that means). + + """ + re=doc.createElement("D:response") + # append namespaces to response + nsnum=0 + for nsname in self.namespaces: + re.setAttribute("xmlns:ns"+str(nsnum),nsname) + nsnum=nsnum+1 + + # write href information + href=doc.createElement("D:href") + if self.use_full_urls: + huri=doc.createTextNode(uri) + else: + uparts=urlparse.urlparse(uri) + fileloc=uparts[2] + huri=doc.createTextNode(urllib.quote(fileloc.encode('utf8'))) + href.appendChild(huri) + re.appendChild(href) + + # write good properties + if good_props and len(good_props.items()): + ps=doc.createElement("D:propstat") + + gp=doc.createElement("D:prop") + for ns in good_props.keys(): + ns_prefix="ns"+str(self.namespaces.index(ns))+":" + for p,v in good_props[ns].items(): + pe=doc.createElement(ns_prefix+str(p)) + if v == None: + pass + elif ns=='DAV:' and p=="resourcetype": + if v == 1: + ve=doc.createElement("D:collection") + pe.appendChild(ve) + elif isinstance(v,tuple) and v[1] == ns: + ve=doc.createElement(ns_prefix+v[0]) + pe.appendChild(ve) + else: + ve=doc.createTextNode(utf8str(v)) + pe.appendChild(ve) + + gp.appendChild(pe) + if gp.hasChildNodes(): + re.appendChild(ps) + ps.appendChild(gp) + s=doc.createElement("D:status") + t=doc.createTextNode("HTTP/1.1 200 OK") + s.appendChild(t) + ps.appendChild(s) + re.appendChild(ps) + + # now write the errors! + if len(bad_props.items()): + + # write a propstat for each error code + for ecode in bad_props.keys(): + ps=doc.createElement("D:propstat") + re.appendChild(ps) + bp=doc.createElement("D:prop") + ps.appendChild(bp) + + for ns in bad_props[ecode].keys(): + ns_prefix="ns"+str(self.namespaces.index(ns))+":" + + for p in bad_props[ecode][ns]: + pe=doc.createElement(ns_prefix+str(p)) + bp.appendChild(pe) + + s=doc.createElement("D:status") + t=doc.createTextNode(utils.gen_estring(ecode)) + s.appendChild(t) + ps.appendChild(s) + re.appendChild(ps) + + # return the new response element + return re + + def get_propvalues(self,uri): + """ create lists of property values for an URI + + We create two lists for an URI: the properties for + which we found a value and the ones for which we + only got an error, either because they haven't been + found or the user is not allowed to read them. + + """ + good_props={} + bad_props={} + + for (ns,plist) in self.proplist.items(): + good_props[ns]={} + bad_props={} + ec = 0 + for prop in plist: + try: + ec = 0 + r=self.__dataclass.get_prop(uri,ns,prop) + good_props[ns][prop]=r + except DAV_Error, error_code: + ec=error_code[0] + + # ignore props with error_code if 0 (invisible) + if ec==0: continue + + if bad_props.has_key(ec): + if bad_props[ec].has_key(ns): + bad_props[ec][ns].append(prop) + else: + bad_props[ec][ns]=[prop] + else: + bad_props[ec]={ns:[prop]} + + return good_props, bad_props + diff --git a/addons/base_calendar/DAV/status.py b/addons/base_calendar/DAV/status.py new file mode 100644 index 00000000000..62cb323c4fb --- /dev/null +++ b/addons/base_calendar/DAV/status.py @@ -0,0 +1,31 @@ +""" + +status codes for DAV services + + +""" + + +STATUS_CODES={ + 102: "Processing", + 200: "Ok", + 201: "Created", + 204: "No Content", + 207: "Multi-Status", + 201: "Created", + 400: "Bad Request", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 409: "Conflict", + 412: "Precondition failed", + 423: "Locked", + 415: "Unsupported Media Type", + 507: "Insufficient Storage", + 422: "Unprocessable Entity", + 423: "Locked", + 424: "Failed Dependency", + 502: "Bad Gateway", + 507: "Insufficient Storage", + 999: "Some error in Create Method please check the data of create method" +} diff --git a/addons/base_calendar/DAV/utils.py b/addons/base_calendar/DAV/utils.py new file mode 100755 index 00000000000..c4bb4d1a4c0 --- /dev/null +++ b/addons/base_calendar/DAV/utils.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python + +""" + +UTILITIES + +- parse a propfind request body into a list of props + +""" + +from xml.dom import ext +from xml.dom.Document import Document +from xml.dom.ext.reader import PyExpat +from xml.dom import Node +from xml.dom import NodeIterator, NodeFilter + +from string import lower, split, atoi, joinfields +import urlparse +from StringIO import StringIO + +from constants import RT_ALLPROP, RT_PROPNAME, RT_PROP +from status import STATUS_CODES + +VERSION = '0.6' +AUTHOR = 'Simon Pamies ' + + +def gen_estring(ecode): + """ generate an error string from the given code """ + ec=atoi(str(ecode)) + if STATUS_CODES.has_key(ec): + return "HTTP/1.1 %s %s" %(ec,STATUS_CODES[ec]) + else: + return "HTTP/1.1 %s" %(ec) + +def parse_propfind(xml_doc): + """ parse an propfind xml file and return a list of props + + returns: + + request_type -- ALLPROP, PROPNAME, PROP + proplist -- list of properties found + namespaces -- list of namespaces found + + """ + doc = PyExpat.Reader().fromString(xml_doc) + snit = doc.createNodeIterator(doc, NodeFilter.NodeFilter.SHOW_ELEMENT, None, None) + + request_type=None + props={} + namespaces=[] + + while 1: + curr_elem = snit.nextNode() + if not curr_elem: break + ename=fname=lower(curr_elem.nodeName) + if ":" in fname: + ename=split(fname,":")[1] + if ename=="prop": request_type=RT_PROP; continue + if ename=="propfind": continue + if ename=="allprop": request_type=RT_ALLPROP; continue + if ename=="propname": request_type=RT_PROPNAME; continue + + # rest should be names of attributes + + ns = curr_elem.namespaceURI + if props.has_key(ns): + props[ns].append(ename) + else: + props[ns]=[ename] + namespaces.append(ns) + + return request_type,props,namespaces + + +def create_treelist(dataclass,uri): + """ create a list of resources out of a tree + + This function is used for the COPY, MOVE and DELETE methods + + uri - the root of the subtree to flatten + + It will return the flattened tree as list + + """ + queue=[uri] + list=[uri] + while len(queue): + element=queue[-1] + if dataclass.is_collection(element): + childs=dataclass.get_childs(element) + else: + childs=[] + if len(childs): + list=list+childs + # update queue + del queue[-1] + if len(childs): + queue=queue+childs + return list + +def is_prefix(uri1,uri2): + """ returns 1 of uri1 is a prefix of uri2 """ + if uri2[:len(uri1)]==uri1: + return 1 + else: + return None + +def quote_uri(uri): + """ quote an URL but not the protocol part """ + import urlparse + import urllib + + up=urlparse.urlparse(uri) + np=urllib.quote(up[2]) + return urlparse.urlunparse((up[0],up[1],np,up[3],up[4],up[5])) + +def get_uriparentpath(uri): + """ extract the uri path and remove the last element """ + up=urlparse.urlparse(uri) + return joinfields(split(up[2],"/")[:-1],"/") + +def get_urifilename(uri): + """ extract the uri path and return the last element """ + up=urlparse.urlparse(uri) + return split(up[2],"/")[-1] + +def get_parenturi(uri): + """ return the parent of the given resource""" + up=urlparse.urlparse(uri) + np=joinfields(split(up[2],"/")[:-1],"/") + return urlparse.urlunparse((up[0],up[1],np,up[3],up[4],up[5])) + +### XML utilities + +def make_xmlresponse(result): + """ construct a response from a dict of uri:error_code elements """ + doc = Document(None) + ms=doc.createElement("D:multistatus") + ms.setAttribute("xmlns:D","DAV:") + doc.appendChild(ms) + + for el,ec in result.items(): + re=doc.createElement("D:response") + hr=doc.createElement("D:href") + st=doc.createElement("D:status") + huri=doc.createTextNode(quote_uri(el)) + t=doc.createTextNode(gen_estring(ec)) + st.appendChild(t) + hr.appendChild(huri) + re.appendChild(hr) + re.appendChild(st) + ms.appendChild(re) + + sfile=StringIO() + ext.PrettyPrint(doc,stream=sfile) + s=sfile.getvalue() + sfile.close() + return s + diff --git a/addons/base_calendar/__init__.py b/addons/base_calendar/__init__.py index c127cc7c124..017a1748449 100644 --- a/addons/base_calendar/__init__.py +++ b/addons/base_calendar/__init__.py @@ -21,5 +21,6 @@ import base_calendar import wizard - +import base_calendar_node +import webdav_server # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/base_calendar/base_calendar.py b/addons/base_calendar/base_calendar.py index 3fb557a86b6..31b2d7f7cc8 100644 --- a/addons/base_calendar/base_calendar.py +++ b/addons/base_calendar/base_calendar.py @@ -71,7 +71,10 @@ def get_attribute_mapping(cr, uid, calname, context={}): pool = pooler.get_pool(cr.dbname) field_obj = pool.get('basic.calendar.fields') type_obj = pool.get('basic.calendar.lines') - type_id = type_obj.search(cr, uid, [('object_id.model', '=', context.get('model'))]) + domain = [('object_id.model', '=', context.get('model'))] + if context.get('calendar_id'): + domain.append(('calendar_id', '=', context.get('calendar_id'))) + type_id = type_obj.search(cr, uid, domain) fids = field_obj.search(cr, uid, [('type_id', '=', type_id[0])]) res = {} for field in field_obj.browse(cr, uid, fids): @@ -194,6 +197,8 @@ class CalDAV(object): self.ical_set(cal_data.name.lower(), vals, 'value') continue if cal_data.name.lower() in self.__attribute__: + if cal_data.params.get('X-VOBJ-ORIGINAL-TZID'): + self.ical_set('vtimezone', cal_data.params.get('X-VOBJ-ORIGINAL-TZID'), 'value') self.ical_set(cal_data.name.lower(), cal_data.value, 'value') vals = map_data(cr, uid, self) return vals @@ -255,7 +260,7 @@ class CalDAV(object): dtfield = vevent.add(field) dtfield.value = parser.parse(data[map_field]) if tzval: - dtfield.params['TZID'] = [tzval] + dtfield.params['TZID'] = [tzval.title()] elif map_type == "timedelta": vevent.add(field).value = timedelta(hours=data[map_field]) elif map_type == "many2one": @@ -312,9 +317,6 @@ class CalDAV(object): for child in parsedCal.getChildren(): if child.name.lower() in ('vevent', 'vtodo'): vals = self.parse_ics(cr, uid, child, context=context) - elif child.name.lower() == 'vtimezone': - tz_obj = self.pool.get('basic.calendar.timezone') - tz_obj.import_cal(cr, uid, child, context=context) else: vals = {} continue @@ -361,7 +363,9 @@ class Calendar(CalDAV, osv.osv): mod_obj = self.pool.get(line.object_id.model) data_ids = mod_obj.search(cr, uid, eval(line.domain), context=context) datas = mod_obj.read(cr, uid, data_ids, context=context) - context.update({'model': line.object_id.model}) + context.update({'model': line.object_id.model, + 'calendar_id': cal.id + }) self.__attribute__ = get_attribute_mapping(cr, uid, line.name, context) self.create_ics(cr, uid, datas, line.name, ical, context=context) return ical.serialize() @@ -380,7 +384,9 @@ class Calendar(CalDAV, osv.osv): cal_children[line.name] = line.object_id.model for child in parsedCal.getChildren(): if child.name.lower() in cal_children: - context.update({'model': cal_children[child.name.lower()]}) + context.update({'model': cal_children[child.name.lower()], + 'calendar_id': cal.id + }) self.__attribute__ = get_attribute_mapping(cr, uid, child.name.lower(), context=context) val = self.parse_ics(cr, uid, child, cal_children=cal_children, context=context) obj = self.pool.get(cal_children[child.name.lower()]) @@ -616,7 +622,7 @@ class Timezone(CalDAV, osv.osv_memory): } def get_name_offset(self, cr, uid, tzid, context={}): - mytz = pytz.timezone(tzid) + mytz = pytz.timezone(tzid.title()) mydt = datetime.now(tz=mytz) offset = mydt.utcoffset() val = offset.days * 24 + float(offset.seconds) / 3600 @@ -629,7 +635,7 @@ class Timezone(CalDAV, osv.osv_memory): ctx = context.copy() ctx.update({'model': model}) cal_tz = ical.add('vtimezone') - cal_tz.add('TZID').value = tzid + cal_tz.add('TZID').value = tzid.title() tz_std = cal_tz.add('STANDARD') tzname, offset = self.get_name_offset(cr, uid, tzid) tz_std.add("TZOFFSETFROM").value = offset @@ -769,14 +775,15 @@ class Attendee(CalDAV, osv.osv_memory): cn_val = '' for a_key, a_val in self.__attribute__.items(): if attendee[a_val['field']] and a_val['field'] != 'cn': - if a_val['type'] == 'text': + if a_val['type'] in ('text', 'char', 'selection'): attendee_add.params[a_key] = [str(attendee[a_val['field']])] elif a_val['type'] == 'boolean': attendee_add.params[a_key] = [str(attendee[a_val['field']])] if a_val['field'] == 'cn' and attendee[a_val['field']]: cn_val = [str(attendee[a_val['field']])] - if cn_val: - attendee_add.params['CN'] = cn_val + if cn_val: + attendee_add.params['CN'] = cn_val + attendee_add.value = 'MAILTO:' + attendee['email'] return vevent Attendee() diff --git a/addons/base_calendar/base_calendar_node.py b/addons/base_calendar/base_calendar_node.py new file mode 100644 index 00000000000..1f5a2352b5f --- /dev/null +++ b/addons/base_calendar/base_calendar_node.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +from osv import osv, fields +from tools.translate import _ +import pooler +import tools +import time +import base64 + +class node_calendar(object): + def __init__(self, path, context, calendar): + self.path = path + self.context = context + self.calendar_id = calendar.id + self.mimetype = 'ics' + self.create_date = calendar.create_date + self.write_date = calendar.write_date or calendar.create_date + self.content_length = 0 + self.displayname = calendar.name + + + def get_data(self, cr, uid): + calendar_obj = pooler.get_pool(cr.dbname).get('basic.calendar') + return calendar_obj.export_cal(cr, uid, [self.calendar_id]) + + def get_data_len(self, cr): + return self.content_length + + def set_data(self, cr, uid, data): + calendar_obj = pooler.get_pool(cr.dbname).get('basic.calendar') + return calendar_obj.import_cal(cr, uid, base64.encodestring(data), self.calendar_id) + + def get_etag(self,cr): + """ Get a tag, unique per object + modification. + + see. http://tools.ietf.org/html/rfc2616#section-13.3.3 """ + return self._get_ttag(cr) + ':' + self._get_wtag(cr) + + def _get_wtag(self,cr): + """ Return the modification time as a unique, compact string """ + if self.write_date: + wtime = time.mktime(time.strptime(self.write_date,'%Y-%m-%d %H:%M:%S')) + else: wtime = time.time() + return str(wtime) + + def _get_ttag(self,cr): + return 'calendar-%d' % self.calendar_id + +class Calendar(osv.osv): + _inherit = 'basic.calendar' + + def get_calendar_object(self, cr, uid, uri, context=None): + if not uri: + return None + if len(uri) > 1: + return None + name, file_type = tuple(uri[0].split('.')) + res = self.name_search(cr, uid, name) + if not res: + return None + calendar_id, calendar_name = res[0] + calendar = self.browse(cr, uid, calendar_id) + return node_calendar(uri, context, calendar) +Calendar() diff --git a/addons/base_calendar/cache.py b/addons/base_calendar/cache.py new file mode 100644 index 00000000000..7f03aa6da13 --- /dev/null +++ b/addons/base_calendar/cache.py @@ -0,0 +1,39 @@ +import time +import heapq + +def memoize(maxsize): + """decorator to 'memoize' a function - caching its results""" + def decorating_function(f): + cache = {} # map from key to value + heap = [] # list of keys, in LRU heap + cursize = 0 # because len() is slow + def wrapper(*args): + key = repr(args) + # performance crap + _cache=cache + _heap=heap + _heappop = heapq.heappop + _heappush = heapq.heappush + _time = time.time + _cursize = cursize + _maxsize = maxsize + if not _cache.has_key(key): + if _cursize == _maxsize: + # pop oldest element + (_,oldkey) = _heappop(_heap) + _cache.pop(oldkey) + else: + _cursize += 1 + # insert this element + _cache[key] = f(*args) + _heappush(_heap,(_time(),key)) + wrapper.misses += 1 + else: + wrapper.hits += 1 + return cache[key] + wrapper.__doc__ = f.__doc__ + wrapper.__name__ = f.__name__ + wrapper.hits = wrapper.misses = 0 + return wrapper + return decorating_function + diff --git a/addons/base_calendar/dav_fs.py b/addons/base_calendar/dav_fs.py new file mode 100644 index 00000000000..e44a41a8e07 --- /dev/null +++ b/addons/base_calendar/dav_fs.py @@ -0,0 +1,335 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# Copyright (C) 2004-2009 Tiny SPRL (). +# +# 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 . +# +############################################################################## + +import pooler + +import base64 +import sys +import os +import time +from string import joinfields, split, lower + +from service import security + +import netsvc +import urlparse + +from DAV.constants import COLLECTION, OBJECT +from DAV.errors import * +from DAV.iface import * +import urllib + +from DAV.davcmd import copyone, copytree, moveone, movetree, delone, deltree +from document.nodes import node_res_dir, node_res_obj +from cache import memoize +from tools import misc +CACHE_SIZE=20000 + + +class tinydav_handler(dav_interface): + """ + This class models a Tiny ERP interface for the DAV server + """ + PROPS={'DAV:': dav_interface.PROPS['DAV:'], } + + M_NS={ "DAV:" : dav_interface.M_NS['DAV:'], } + + def __init__(self, parent, verbose=False): + self.db_name = False + self.parent = parent + self.baseuri = parent.baseuri + + def get_propnames(self, uri): + props = self.PROPS + self.parent.log_message('get propnames: %s' % uri) + if uri[-1]=='/':uri=uri[:-1] + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + if not dbname: + cr.close() + return props + node = self.uri2object(cr,uid,pool, uri2) + if node: + props.update(node.get_dav_props(cr)) + cr.close() + return props + + def get_prop(self,uri,ns,propname): + """ return the value of a given property + + uri -- uri of the object to get the property of + ns -- namespace of the property + pname -- name of the property + """ + if self.M_NS.has_key(ns): + return dav_interface.get_prop(self,uri,ns,propname) + + if uri[-1]=='/':uri=uri[:-1] + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + if not dbname: + cr.close() + raise DAV_NotFound + node = self.uri2object(cr,uid,pool, uri2) + if not node: + cr.close() + raise DAV_NotFound + res = node.get_dav_eprop(cr,ns,propname) + cr.close() + return res + + def urijoin(self,*ajoin): + """ Return the base URI of this request, or even join it with the + ajoin path elements + """ + return self.baseuri+ '/'.join(ajoin) + + def uri2local(self, uri): + uparts=urlparse.urlparse(uri) + reluri=uparts[2] + if reluri and reluri[-1]=="/": + reluri=reluri[:-1] + return reluri + + # + # pos: -1 to get the parent of the uri + # + def get_cr(self, uri): + pdb = self.parent.auth_proxy.last_auth + reluri = self.uri2local(uri) + try: + dbname = reluri.split('/')[2] + except: + dbname = False + if not dbname: + return None, None, None, False, None + if not pdb and dbname: + # if dbname was in our uri, we should have authenticated + # against that. + raise Exception("Programming error") + assert pdb == dbname, " %s != %s" %(pdb, dbname) + user, passwd, dbn2, uid = self.parent.auth_proxy.auth_creds[pdb] + db,pool = pooler.get_db_and_pool(dbname) + cr = db.cursor() + uri2 = reluri.split('/')[3:] + return cr, uid, pool, dbname, uri2 + + def uri2object(self, cr, uid, pool, uri): + if not uid: + return None + return pool.get('basic.calendar').get_calendar_object(cr, uid, uri) + + def get_data(self,uri): + self.parent.log_message('GET: %s' % uri) + if uri[-1]=='/':uri=uri[:-1] + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + try: + if not dbname: + raise DAV_Error, 409 + node = self.uri2object(cr,uid,pool, uri2) + if not node: + raise DAV_NotFound(uri2) + try: + datas = node.get_data(cr, uid) + except TypeError,e: + import traceback + self.parent.log_error("GET typeError: %s", str(e)) + self.parent.log_message("Exc: %s",traceback.format_exc()) + raise DAV_Forbidden + except IndexError,e : + self.parent.log_error("GET IndexError: %s", str(e)) + raise DAV_NotFound(uri2) + except Exception,e: + import traceback + self.parent.log_error("GET exception: %s",str(e)) + self.parent.log_message("Exc: %s", traceback.format_exc()) + raise DAV_Error, 409 + return datas + finally: + cr.close() + + @memoize(CACHE_SIZE) + def _get_dav_resourcetype(self,uri): + """ return type of object """ + self.parent.log_message('get RT: %s' % uri) + if uri[-1]=='/':uri=uri[:-1] + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + try: + if not dbname: + return COLLECTION + node = self.uri2object(cr,uid,pool, uri2) + if not node: + raise DAV_NotFound(uri2) + return OBJECT + finally: + cr.close() + + def _get_dav_displayname(self,uri): + self.parent.log_message('get DN: %s' % uri) + if uri[-1]=='/':uri=uri[:-1] + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + if not dbname: + cr.close() + return COLLECTION + node = self.uri2object(cr,uid,pool, uri2) + if not node: + cr.close() + raise DAV_NotFound(uri2) + cr.close() + return node.displayname + + @memoize(CACHE_SIZE) + def _get_dav_getcontentlength(self,uri): + """ return the content length of an object """ + self.parent.log_message('get length: %s' % uri) + if uri[-1]=='/':uri=uri[:-1] + result = 0 + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + if not dbname: + cr.close() + return '0' + node = self.uri2object(cr, uid, pool, uri2) + if not node: + cr.close() + raise DAV_NotFound(uri2) + result = node.content_length or 0 + cr.close() + return str(result) + + @memoize(CACHE_SIZE) + def _get_dav_getetag(self,uri): + """ return the ETag of an object """ + self.parent.log_message('get etag: %s' % uri) + if uri[-1]=='/':uri=uri[:-1] + result = 0 + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + if not dbname: + cr.close() + return '0' + node = self.uri2object(cr, uid, pool, uri2) + if not node: + cr.close() + raise DAV_NotFound(uri2) + result = node.get_etag(cr) + cr.close() + return str(result) + + @memoize(CACHE_SIZE) + def get_lastmodified(self,uri): + """ return the last modified date of the object """ + if uri[-1]=='/':uri=uri[:-1] + today = time.time() + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + try: + if not dbname: + return today + node = self.uri2object(cr,uid,pool, uri2) + if not node: + raise DAV_NotFound(uri2) + if node.write_date: + return time.mktime(time.strptime(node.write_date,'%Y-%m-%d %H:%M:%S')) + else: + return today + finally: + cr.close() + + @memoize(CACHE_SIZE) + def get_creationdate(self,uri): + """ return the last modified date of the object """ + + if uri[-1]=='/':uri=uri[:-1] + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + try: + if not dbname: + raise DAV_Error, 409 + node = self.uri2object(cr,uid,pool, uri2) + if not node: + raise DAV_NotFound(uri2) + if node.create_date: + result = time.strptime(node.create_date,'%Y-%m-%d %H:%M:%S') + else: + result = time.gmtime() + return result + finally: + cr.close() + + @memoize(CACHE_SIZE) + def _get_dav_getcontenttype(self,uri): + self.parent.log_message('get contenttype: %s' % uri) + if uri[-1]=='/':uri=uri[:-1] + cr, uid, pool, dbname, uri2 = self.get_cr(uri) + try: + if not dbname: + return 'httpd/unix-directory' + node = self.uri2object(cr,uid,pool, uri2) + if not node: + raise DAV_NotFound(uri2) + result = node.mimetype + return result + #raise DAV_NotFound, 'Could not find %s' % path + finally: + cr.close() + + + + def put(self, uri, data, content_type=None): + """ put the object into the filesystem """ + self.parent.log_message('Putting %s (%d), %s'%( misc.ustr(uri), len(data), content_type)) + parent='/'.join(uri.split('/')[:-1]) + cr, uid, pool,dbname, uri2 = self.get_cr(uri) + if not dbname: + raise DAV_Forbidden + try: + node = self.uri2object(cr,uid,pool, uri2[:]) + except: + node = False + + if not node: + raise DAV_Forbidden + else: + try: + node.set_data(cr, uid, data) + except Exception,e: + import traceback + self.parent.log_error("Cannot save :%s", str(e)) + self.parent.log_message("Exc: %s",traceback.format_exc()) + raise DAV_Forbidden + + cr.commit() + cr.close() + return 201 + + + + + def exists(self,uri): + """ test if a resource exists """ + result = False + cr, uid, pool,dbname, uri2 = self.get_cr(uri) + if not dbname: + cr.close() + return True + try: + node = self.uri2object(cr,uid,pool, uri2) + if node: + result = True + except: + pass + cr.close() + return result diff --git a/addons/base_calendar/webdav_server.py b/addons/base_calendar/webdav_server.py new file mode 100644 index 00000000000..9fd6d8bf009 --- /dev/null +++ b/addons/base_calendar/webdav_server.py @@ -0,0 +1,79 @@ +# -*- encoding: utf-8 -*- + +# +# Copyright P. Christeas 2008,2009 +# +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsability of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# garantees and support are strongly adviced to contract a Free Software +# Service Company +# +# This program is Free Software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +############################################################################### + + +import netsvc +from dav_fs import tinydav_handler +from tools.config import config +from DAV.WebDAVServer import DAVRequestHandler +from service.websrv_lib import HTTPDir,FixSendError + +class DAVHandler(FixSendError,DAVRequestHandler): + verbose = False + + def get_userinfo(self,user,pw): + return False + + def _log(self, message): + netsvc.Logger().notifyChannel("webdav",netsvc.LOG_DEBUG,message) + + def handle(self): + pass + + def finish(self): + pass + + def setup(self): + davpath = '/calendar/' + self.baseuri = "http://%s:%d%s"% (self.server.server_name,self.server.server_port,davpath) + self.IFACE_CLASS = tinydav_handler(self) + pass + + def log_message(self, format, *args): + netsvc.Logger().notifyChannel('webdav',netsvc.LOG_DEBUG_RPC,format % args) + + def log_error(self, format, *args): + netsvc.Logger().notifyChannel('webdav',netsvc.LOG_WARNING,format % args) + + +try: + from service.http_server import reg_http_service,OpenERPAuthProvider + davpath = '/calendar/' + handler = DAVHandler + handler.verbose = config.get_misc('webdav','verbose',True) + handler.debug = config.get_misc('webdav','debug',True) + reg_http_service(HTTPDir(davpath,DAVHandler,OpenERPAuthProvider())) + netsvc.Logger().notifyChannel('webdav',netsvc.LOG_INFO,"WebDAV service registered at path: %s/ "% davpath) +except Exception, e: + logger = netsvc.Logger() + logger.notifyChannel('webdav', netsvc.LOG_ERROR, 'Cannot launch webdav: %s' % e) + +#eof + + + diff --git a/addons/base_contact/i18n/nl.po b/addons/base_contact/i18n/nl.po index 31eb515e773..21dacd2804b 100644 --- a/addons/base_contact/i18n/nl.po +++ b/addons/base_contact/i18n/nl.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-12-11 22:28+0000\n" -"Last-Translator: Pieter J. Kersten (EduSense BV) \n" +"PO-Revision-Date: 2010-02-01 15:47+0000\n" +"Last-Translator: Jan Verlaan (Veritos) \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:08+0000\n" +"X-Launchpad-Export-Date: 2010-02-02 04:48+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base_contact @@ -36,7 +36,7 @@ msgstr "res.partner.contact" msgid "" "The Object name must start with x_ and not contain any special character !" msgstr "" -"De objectnaam moet beginnen met x_ en mag geen speciale karakters bevatten !" +"De objectnaam moet beginnen met x_ en mag geen speciale tekens bevatten !" #. module: base_contact #: field:res.partner.job,function_id:0 diff --git a/addons/base_module_quality/i18n/ru.po b/addons/base_module_quality/i18n/ru.po index eb6a90f89b5..bd6fee8b068 100644 --- a/addons/base_module_quality/i18n/ru.po +++ b/addons/base_module_quality/i18n/ru.po @@ -7,19 +7,19 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0_rc3\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-11-17 08:55+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-02-02 17:45+0000\n" +"Last-Translator: Nikolay Chesnokov \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:26+0000\n" +"X-Launchpad-Export-Date: 2010-02-03 04:47+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base_module_quality #: field:module.quality.check,final_score:0 msgid "Final Score (%)" -msgstr "" +msgstr "% выполнения" #. module: base_module_quality #: constraint:ir.model:0 @@ -32,12 +32,12 @@ msgstr "" #. module: base_module_quality #: model:ir.module.module,shortdesc:base_module_quality.module_meta_information msgid "Base module quality" -msgstr "" +msgstr "Базисный модуль контроля качества" #. module: base_module_quality #: field:module.quality.check,name:0 msgid "Rated Module" -msgstr "" +msgstr "Оценка модуля" #. module: base_module_quality #: view:module.quality.detail:0 @@ -47,17 +47,17 @@ msgstr "Подробности" #. module: base_module_quality #: field:module.quality.detail,note:0 msgid "Note" -msgstr "" +msgstr "Заметка" #. module: base_module_quality #: field:module.quality.detail,state:0 msgid "State" -msgstr "" +msgstr "Область/штат" #. module: base_module_quality #: field:module.quality.detail,detail:0 msgid "Details" -msgstr "" +msgstr "Подробно" #. module: base_module_quality #: field:module.quality.detail,ponderation:0 @@ -75,12 +75,12 @@ msgstr "" #: view:module.quality.check:0 #: field:module.quality.check,check_detail_ids:0 msgid "Tests" -msgstr "" +msgstr "Тесты" #. module: base_module_quality #: selection:module.quality.detail,state:0 msgid "Skipped" -msgstr "" +msgstr "Пропущено" #. module: base_module_quality #: help:module.quality.detail,state:0 @@ -88,6 +88,7 @@ msgid "" "The test will be completed only if the module is installed or if the test " "may be processed on uninstalled module." msgstr "" +"Тест проводится только для установленных или помеченных для деинсталяции." #. module: base_module_quality #: constraint:ir.ui.view:0 @@ -102,27 +103,27 @@ msgstr "" #. module: base_module_quality #: field:module.quality.detail,name:0 msgid "Name" -msgstr "" +msgstr "Имя" #. module: base_module_quality #: wizard_field:quality_detail_save,init,module_file:0 msgid "Save report" -msgstr "" +msgstr "Сохранить отчет" #. module: base_module_quality #: wizard_field:quality_detail_save,init,name:0 msgid "File name" -msgstr "" +msgstr "Имя файла" #. module: base_module_quality #: field:module.quality.detail,score:0 msgid "Score (%)" -msgstr "" +msgstr "% выполнения" #. module: base_module_quality #: help:quality_detail_save,init,name:0 msgid "Save report as .html format" -msgstr "" +msgstr "Сохранить отчет в формате HTML" #. module: base_module_quality #: view:module.quality.detail:0 @@ -133,22 +134,22 @@ msgstr "Аннотация" #. module: base_module_quality #: model:ir.actions.wizard,name:base_module_quality.quality_detail_save msgid "Report Save" -msgstr "" +msgstr "Сохранить отчет" #. module: base_module_quality #: wizard_view:quality_detail_save,init:0 msgid "Standard entries" -msgstr "" +msgstr "Стандартные точки входа" #. module: base_module_quality #: view:module.quality.detail:0 msgid "Save Report" -msgstr "" +msgstr "Сохранить отчет" #. module: base_module_quality #: model:ir.actions.wizard,name:base_module_quality.create_quality_check_id msgid "Quality Check" -msgstr "" +msgstr "Проверка качества" #. module: base_module_quality #: model:ir.model,name:base_module_quality.model_module_quality_detail @@ -158,23 +159,23 @@ msgstr "" #. module: base_module_quality #: selection:module.quality.detail,state:0 msgid "Done" -msgstr "" +msgstr "Завершено" #. module: base_module_quality #: view:module.quality.check:0 #: view:module.quality.detail:0 msgid "Result" -msgstr "" +msgstr "Результат" #. module: base_module_quality #: wizard_button:quality_detail_save,init,end:0 msgid "Cancel" -msgstr "" +msgstr "Отмена" #. module: base_module_quality #: field:module.quality.detail,message:0 msgid "Message" -msgstr "" +msgstr "Сообщение" #. module: base_module_quality #: field:module.quality.detail,quality_check_id:0 diff --git a/addons/base_report_creator/i18n/zh_CN.po b/addons/base_report_creator/i18n/zh_CN.po index 69e7da8c2f5..6b64eeb3280 100644 --- a/addons/base_report_creator/i18n/zh_CN.po +++ b/addons/base_report_creator/i18n/zh_CN.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-12-01 15:37+0000\n" -"Last-Translator: Wei \"oldrev\" Li \n" +"PO-Revision-Date: 2010-02-01 23:39+0000\n" +"Last-Translator: mrshelly \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:21+0000\n" +"X-Launchpad-Export-Date: 2010-02-03 04:47+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base_report_creator @@ -22,7 +22,7 @@ msgid "" "Provide an expression for the field based on which you want to filter the " "records.\n" " e.g. res_partner.id=3" -msgstr "" +msgstr "可以使用基于字段的Python条件表达式做为过滤条件.(如 res_partner.id=3)" #. module: base_report_creator #: field:base_report_creator.report,view_graph_type:0 @@ -406,6 +406,8 @@ msgid "" "After installing the module, it adds a menu to define custom report in\n" "the \"Dashboard\" menu.\n" msgstr "" +"本模块可以让您使用SQL查询器新建很灵活的对象统计报表.\n" +"安装此模块后, \"Dashboard(系统面板)\" 上会添加对应的菜单入口.\n" #. module: base_report_creator #: selection:base_report_creator.report.fields,calendar_mode:0 diff --git a/addons/base_setup/i18n/ru.po b/addons/base_setup/i18n/ru.po index 9000fd7722e..853d609b31b 100644 --- a/addons/base_setup/i18n/ru.po +++ b/addons/base_setup/i18n/ru.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-11-17 08:51+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-02-02 17:28+0000\n" +"Last-Translator: Nikolay Chesnokov \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:10+0000\n" +"X-Launchpad-Export-Date: 2010-02-03 04:47+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base_setup @@ -134,7 +134,7 @@ msgstr "Улица (2-я строка)" #. module: base_setup #: wizard_view:base_setup.base_setup,company:0 msgid "Report Information" -msgstr "" +msgstr "Информация об отчетах" #. module: base_setup #: wizard_field:base_setup.base_setup,company,phone:0 @@ -194,6 +194,9 @@ msgid "" "We suggest you to write legal sentences here:\n" "Web: http://openerp.com - Fax: +32.81.73.35.01 - Fortis Bank: 126-2013269-07" msgstr "" +"Это предложение появится внизу ваших отчетов.\n" +"Рекомендуем поместить здесь юридическую информацию:\n" +"Web: http://openerp.com - Fax: +32.81.73.35.01 - Fortis Bank: 126-2013269-07" #. module: base_setup #: wizard_view:base_setup.base_setup,update:0 @@ -219,6 +222,9 @@ msgid "" "We suggest you to put a slogan here:\n" "\"Open Source Business Solutions\"." msgstr "" +"Это предложение появится в правом верхнем углу ваших отчетов.\n" +"Рекомендуем поместить здесь слоган:\n" +"\"Решения для бизнеса с открытым исходным кодом\"." #. module: base_setup #: wizard_field:base_setup.base_setup,company,rml_header1:0 diff --git a/addons/base_vat/i18n/ru.po b/addons/base_vat/i18n/ru.po index 3d8e316e279..fc16d1b23f8 100644 --- a/addons/base_vat/i18n/ru.po +++ b/addons/base_vat/i18n/ru.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.4\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-11-17 08:49+0000\n" -"Last-Translator: Sergei Kostigoff \n" +"PO-Revision-Date: 2010-02-02 17:36+0000\n" +"Last-Translator: Nikolay Chesnokov \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:09+0000\n" +"X-Launchpad-Export-Date: 2010-02-03 04:47+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: base_vat @@ -21,18 +21,20 @@ msgstr "" msgid "" "Enable the VAT Number for the partner. Check the validity of that VAT Number." msgstr "" +"Разрешить использование кода налога для партнера. Проверьте правильность " +"кода налога." #. module: base_vat #: constraint:ir.ui.view:0 msgid "Invalid XML for View Architecture!" -msgstr "Неправильный XML для просмотра архитектуры!" +msgstr "Некорректный формат XML структуры для просмотра." #. module: base_vat #: field:res.partner,vat_subjected:0 msgid "VAT Legal Statement" -msgstr "Официальное положение по НДС" +msgstr "Действующий код налога" #. module: base_vat #: model:ir.module.module,shortdesc:base_vat.module_meta_information msgid "VAT" -msgstr "НДС" +msgstr "Код налога" diff --git a/addons/board_crm_configuration/i18n/it.po b/addons/board_crm_configuration/i18n/it.po index c662003bda4..dad58efb8b1 100644 --- a/addons/board_crm_configuration/i18n/it.po +++ b/addons/board_crm_configuration/i18n/it.po @@ -7,13 +7,13 @@ msgstr "" "Project-Id-Version: OpenERP Server 5.0.0\n" "Report-Msgid-Bugs-To: support@openerp.com\n" "POT-Creation-Date: 2009-08-28 16:01+0000\n" -"PO-Revision-Date: 2009-11-17 07:58+0000\n" -"Last-Translator: Fabien (Open ERP) \n" +"PO-Revision-Date: 2010-02-02 09:39+0000\n" +"Last-Translator: Carlo Vettore \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Launchpad-Export-Date: 2009-12-16 05:10+0000\n" +"X-Launchpad-Export-Date: 2010-02-03 04:47+0000\n" "X-Generator: Launchpad (build Unknown)\n" #. module: board_crm_configuration @@ -86,7 +86,7 @@ msgstr "Richieste di Lavoro del Mese per Personale Assunto" #. module: board_crm_configuration #: constraint:ir.actions.act_window:0 msgid "Invalid model name in the action definition." -msgstr "" +msgstr "Nome del modulo non valido nella definizione dell'azione." #. module: board_crm_configuration #: view:board.board:0 diff --git a/addons/document_caldav/__init__.py b/addons/board_hr/__init__.py similarity index 97% rename from addons/document_caldav/__init__.py rename to addons/board_hr/__init__.py index d764dfca72d..e68279cad8d 100644 --- a/addons/document_caldav/__init__.py +++ b/addons/board_hr/__init__.py @@ -19,5 +19,6 @@ # ############################################################################## -import document_caldav + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + diff --git a/addons/document_caldav/__terp__.py b/addons/board_hr/__terp__.py similarity index 72% rename from addons/document_caldav/__terp__.py rename to addons/board_hr/__terp__.py index d7480c734bd..80b00961dbe 100644 --- a/addons/document_caldav/__terp__.py +++ b/addons/board_hr/__terp__.py @@ -21,17 +21,22 @@ { - 'name': 'Support for caldav based on Document Management System', + 'name': 'Board for Human Reasources Management', 'version': '1.0', - 'category': 'Generic Modules/Others', - 'description': """Allows to synchronise calendars with others applications.""", + 'category': '', + 'description': """ +This module implements a dashboard for human resource management that includes: + * My Current Timesheet + * My Leaves + * My Expenses + """, 'author': 'Tiny', - 'website': 'http://www.openerp.com', - 'depends': ['caldav','document_webdav', 'crm'], - 'init_xml': ['document_data.xml'], - 'update_xml': ['document_view.xml'], - 'demo_xml': ['document_demo.xml'], + 'depends': [ 'crm_project','board','hr_expense','hr_timesheet_sheet','hr_holidays' + ], + 'update_xml': ['board_hr_view.xml'], + 'demo_xml': [], 'installable': True, 'active': False, + 'certificate': '', } # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: diff --git a/addons/board_hr/board_hr_view.xml b/addons/board_hr/board_hr_view.xml new file mode 100644 index 00000000000..2e84eff0749 --- /dev/null +++ b/addons/board_hr/board_hr_view.xml @@ -0,0 +1,72 @@ + + + + + + + + My Expenses + hr.expense.expense + form + [('state','=','draft'),('user_id','=',uid)] + + + + + Timesheets + ir.actions.act_window + hr_timesheet_sheet.sheet + form + + [('date_to','>=',time.strftime('%Y-%m-01')),('date_from','<=',time.strftime('%Y-%m-01'))] + + + + + My Leaves + ir.actions.act_window + hr.holidays.per.user + form + tree,form + + + + + board.hr.form + board.board + form + +
+ + + + + + + + + +
+
+
+ + + Human Resources Dashboard + board.board + form + form + menu + + + + +
+
diff --git a/addons/board_project/__terp__.py b/addons/board_project/__terp__.py index e32ae9a789b..558b633d36e 100644 --- a/addons/board_project/__terp__.py +++ b/addons/board_project/__terp__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ############################################################################## -# +# # OpenERP, Open Source Management Solution # Copyright (C) 2004-2010 Tiny SPRL (). # @@ -15,7 +15,7 @@ # 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 . +# along with this program. If not, see . # ############################################################################## @@ -27,10 +27,9 @@ 'description': """ This module implements a dashboard for project member that includes: * List of my open tasks - * List of my next deadlines - * List of public notes - * Graph of my timesheet - * Graph of my work analysis + * List of messages + * Members list of project + * Issues """, 'author': 'Tiny', 'depends': [ @@ -40,7 +39,9 @@ This module implements a dashboard for project member that includes: 'report_analytic_planning', 'report_analytic_line', 'report_task', - 'hr_timesheet_sheet' + 'hr_timesheet_sheet', + 'crm_project', + 'resource' ], 'update_xml': ['board_project_view.xml', 'board_project_manager_view.xml'], 'demo_xml': ['board_project_demo.xml'], diff --git a/addons/board_project/board_project_manager_view.xml b/addons/board_project/board_project_manager_view.xml index a45857af033..3f805115fd4 100644 --- a/addons/board_project/board_project_manager_view.xml +++ b/addons/board_project/board_project_manager_view.xml @@ -1,18 +1,18 @@ - + Project managers - + - + - + Pipeline of tasks report.task.user.pipeline.open @@ -20,7 +20,7 @@ graph,tree [('user_id','in',department_users_get())] - + hr.timesheet.sheet.tree.simplified.board hr_timesheet_sheet.sheet @@ -43,8 +43,8 @@ form - - + + board.project.manager.form board.board @@ -53,27 +53,27 @@
- - + + - + - + -