bzr revid: rvalyi@gmail.com-20100720184615-gqa02owht89y9d4z
This commit is contained in:
Raphaël Valyi
2010-07-20 15:46:15 -03:00
130 changed files with 3501 additions and 1871 deletions
+2
View File
@@ -60,6 +60,8 @@
<field name="account_payment"/>
<field name="account_followup"/>
<field name="account_asset"/>
<field name="account_voucher"/>
<field name="account_voucher_payment"/>
</group>
</group>
</group>
+14 -15
View File
@@ -44,7 +44,7 @@
<field name="model">account.fiscalyear</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Fiscalyear">
<tree colors="blue:state in ('draft');gray:state in ('done') " string="Fiscalyear">
<field name="code"/>
<field name="name"/>
<field name="state"/>
@@ -113,7 +113,7 @@
<field name="model">account.period</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Period">
<tree colors="blue:state in ('draft');gray:state in ('done') " string="Period">
<field name="code"/>
<field name="name"/>
<field name="date_start"/>
@@ -203,10 +203,9 @@
<field name="type">tree</field>
<field name="field_parent">child_id</field>
<field name="arch" type="xml">
<tree string="Chart of accounts" toolbar="1" colors="blue:type in ('view');black:type not in ('view')">
<tree colors="blue:type in ('view');black:type in ('other','receivable','payable','consolidation');gray:type in ('closed')" string="Chart of accounts" toolbar="1" >
<field name="code"/>
<field name="name"/>
<field name="parent_id" invisible="1"/>
<field name="user_type" invisible="1"/>
<field name="debit"/>
<field name="credit"/>
@@ -234,7 +233,7 @@
<field name="type">tree</field>
<field name="field_parent">child_id</field>
<field name="arch" type="xml">
<tree string="Chart of accounts" toolbar="1" colors="blue:type in ('view');black:type not in ('view')">
<tree colors="blue:type in ('view');black:type in ('other','receivable','payable','consolidation');gray:type in ('closed')" string="Chart of accounts" toolbar="1" >
<field name="code"/>
<field name="name"/>
<field name="debit"/>
@@ -484,7 +483,7 @@
<group col="6" colspan="4">
<field name="name" select="1"/>
<field name="date" select="1"/>
<field name="journal_id" on_change="onchange_journal_id(journal_id)" select="1"/>
<field name="journal_id" domain="[('type', '=', 'bank')]" on_change="onchange_journal_id(journal_id)" select="1"/>
<field name="period_id"/>
<field name="balance_start"/>
<field name="balance_end_real"/>
@@ -657,7 +656,7 @@
<field name="model">account.move</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Accounting Entries">
<tree colors="blue:state in ('draft');black:state in ('posted')" string="Accounting Entries">
<field name="name"/>
<field name="date"/>
<field name="ref"/>
@@ -869,7 +868,7 @@
<field name="type">tree</field>
<field eval="4" name="priority"/>
<field name="arch" type="xml">
<tree string="Account Entry Line" editable="top" on_write="on_create_write">
<tree colors="blue:state in ('draft');black:state in ('valid')" string="Account Entry Line" editable="top" on_write="on_create_write">
<field name="date"/>
<field name="period_id"/>
<field name="move_id"/>
@@ -1048,7 +1047,7 @@
<field name="model">account.move</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Account Entry">
<tree colors="blue:state in ('draft');black:state in ('posted')" string="Account Entry">
<field name="name"/>
<field name="date"/>
<field name="ref"/>
@@ -1105,7 +1104,7 @@
<field name="statement_id"/>
<field name="state"/>
</form>
<tree editable="top" string="Account Entry Line">
<tree colors="blue:state in ('draft');black:state in ('posted')" editable="top" string="Account Entry Line">
<field name="ref"/>
<field name="invoice"/>
<field name="name"/>
@@ -1420,7 +1419,7 @@
<field name="model">account.journal.period</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Journals">
<tree colors="blue:state in ('draft');gray:state in ('done');black:state in ('printed')" string="Journals">
<field icon="icon" name="fiscalyear_id"/>
<field name="period_id"/>
<field name="journal_id"/>
@@ -1634,7 +1633,7 @@
<field name="model">account.subscription</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Entry Subscription">
<tree colors="blue:state in ('draft');gray:state in ('done');black:state in ('running')" string="Entry Subscription">
<field name="ref"/>
<field name="name"/>
<field name="date_start"/>
@@ -1736,7 +1735,7 @@
<field name="type">tree</field>
<field eval="4" name="priority"/>
<field name="arch" type="xml">
<tree string="Account Entry Line">
<tree colors="blue:state in ('draft');black:state in ('valid')" string="Account Entry Line">
<field name="date"/>
<field name="move_id"/>
<field name="statement_id" string="St."/>
@@ -2290,7 +2289,7 @@
<field name="model">account.bank.statement</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree colors="red:balance_end_real!=balance_end;blue:state=='draft' and (balance_end_real==balance_end);black:state in ('open');blue:state in ('draft')" string="Statement">
<tree colors="red:balance_end_real!=balance_end;blue:state=='draft' and (balance_end_real==balance_end);black:state in ('open')" string="Statement">
<field name="date"/>
<field name="name"/>
<field name="journal_id"/>
@@ -2298,7 +2297,7 @@
<field name="balance_start"/>
<field name="balance_end_real"/>
<field name="balance_end"/>
<field name="user_id" select="1"/>
<field name="user_id" select="1"/>
<field name="state"/>
<button type="object" string="Open" name="button_open" states="draft" icon="terp-camera_test"/>
<button type="object" string="Confirm" name="button_confirm_cash" states="open" icon="terp-gtk-go-back-rtl"/>
+6
View File
@@ -67,6 +67,12 @@ class account_installer(osv.osv_memory):
'account_asset':fields.boolean('Assets Management',
help="Enables asset management in the accounting application, "
"including asset categories and usage periods."),
'account_voucher':fields.boolean('Voucher Management',
help="Account Voucher module includes all the basic requirements of "
"Voucher Entries for Bank, Cash, Sales, Purchase, Expanse, Contra, etc... "),
'account_voucher_payment':fields.boolean('Voucher and Reconcile Management',
help="Extension Account Voucher module includes allows to link payment / receipt "
"entries with voucher, also automatically reconcile during the payment and receipt entries."),
'date_start': fields.date('Start Date', required=True),
'date_stop': fields.date('End Date', required=True),
'period':fields.selection([('month','Monthly'), ('3months','3 Monthly')],
+19 -12
View File
@@ -220,7 +220,7 @@ class account_invoice(osv.osv):
_name = "account.invoice"
_description = 'Invoice'
_order = "number"
_log_create = True
_columns = {
'name': fields.char('Description', size=64, select=True, readonly=True, states={'draft':[('readonly',False)]}),
'origin': fields.char('Source Document', size=64, help="Reference of the document that produced this invoice.", readonly=True, states={'draft':[('readonly',False)]}),
@@ -242,13 +242,13 @@ class account_invoice(osv.osv):
('proforma','Pro-forma'),
('proforma2','Pro-forma'),
('open','Open'),
('paid','Done'),
('paid','Paid'),
('cancel','Cancelled')
],'State', select=True, readonly=True,
help=' * The \'Draft\' state is used when a user is encoding a new and unconfirmed Invoice. \
\n* The \'Pro-forma\' when invoice is in Pro-forma state,invoice does not have an invoice number. \
\n* The \'Open\' state is used when user create invoice,a invoice number is generated.Its in open state till user does not pay invoice. \
\n* The \'Done\' state is set automatically when invoice is paid.\
\n* The \'Paid\' state is set automatically when invoice is paid.\
\n* The \'Cancelled\' state is used when user cancel invoice.'),
'date_invoice': fields.date('Date Invoiced', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]}, help="Keep empty to use the current date"),
'date_due': fields.date('Due Date', states={'paid':[('readonly',True)], 'open':[('readonly',True)], 'close':[('readonly',True)]},
@@ -344,6 +344,9 @@ class account_invoice(osv.osv):
def create(self, cr, uid, vals, context=None):
try:
res = super(account_invoice, self).create(cr, uid, vals, context)
for inv_id, name in self.name_get(cr, uid, [res], context=context):
message = _('Invoice ') + " '" + name + "' "+ _("is waiting for validation.")
self.log(cr, uid, inv_id, message)
return res
except Exception, e:
if '"journal_id" viol' in e.args[0]:
@@ -354,9 +357,6 @@ class account_invoice(osv.osv):
def confirm_paid(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'paid'}, context=context)
for (id, name) in self.name_get(cr, uid, ids):
message = _('Document ') + " '" + name + "' "+ _("has been paid.")
self.log(cr, uid, id, message)
return True
def unlink(self, cr, uid, ids, context=None):
@@ -374,7 +374,7 @@ class account_invoice(osv.osv):
# res = self.pool.get('res.partner').address_get(cr, uid, [part], ['invoice'])
# return [{}]
def onchange_partner_id(self, cr, uid, ids, type, partner_id,\
date_invoice=False, payment_term=False, partner_bank=False, company_id=False):
date_invoice=False, payment_term=False, partner_bank_id=False, company_id=False):
invoice_addr_id = False
contact_addr_id = False
partner_payment_term = False
@@ -439,7 +439,7 @@ class account_invoice(osv.osv):
else:
result['value']['date_due'] = False
if partner_bank != bank_id:
if partner_bank_id != bank_id:
to_update = self.onchange_partner_bank(cr, uid, ids, bank_id)
result['value'].update(to_update['value'])
return result
@@ -474,7 +474,7 @@ class account_invoice(osv.osv):
def onchange_invoice_line(self, cr, uid, ids, lines):
return {}
def onchange_partner_bank(self, cursor, user, ids, partner_bank):
def onchange_partner_bank(self, cursor, user, ids, partner_bank_id=False):
return {'value': {}}
def onchange_company_id(self, cr, uid, ids, company_id, part_id, type, invoice_line, currency_id):
@@ -922,7 +922,7 @@ class account_invoice(osv.osv):
invtype = obj_inv.type
number = obj_inv.number
move_id = obj_inv.move_id and obj_inv.move_id.id or False
reference = obj_inv.reference
reference = obj_inv.reference or ''
if not number:
tmp_context = {
'fiscalyear_id': obj_inv.period_id.fiscalyear_id.id
@@ -955,8 +955,9 @@ class account_invoice(osv.osv):
'WHERE account_move_line.move_id = %s ' \
'AND account_analytic_line.move_id = account_move_line.id',
(ref, move_id))
message = _('Invoice ') + " '" + number + "' "+ _("is confirm")
self.log(cr, uid, id, message)
for inv_id, name in self.name_get(cr, uid, [id]):
message = _('Invoice ') + " '" + name + "' "+ _("is validated.")
self.log(cr, uid, inv_id, message)
return True
def action_cancel(self, cr, uid, ids, *args):
@@ -1175,9 +1176,15 @@ class account_invoice(osv.osv):
if l.account_id.id==src_account_id:
line_ids.append(l.id)
total += (l.debit or 0.0) - (l.credit or 0.0)
inv_id, name = self.name_get(cr, uid, [invoice.id], context=context)[0]
if (not round(total,self.pool.get('decimal.precision').precision_get(cr, uid, 'Account'))) or writeoff_acc_id:
self.log(cr, uid, inv_id, _('Invoice ') + " '" + name + "' "+ _("is totally paid."))
self.pool.get('account.move.line').reconcile(cr, uid, line_ids, 'manual', writeoff_acc_id, writeoff_period_id, writeoff_journal_id, context)
else:
code = invoice.currency_id.code
amt = str(pay_amount) + code + ' on ' + str(invoice.amount_total) + code + ' (' + str(total) + code + ' remaining)'
self.log(cr, uid, inv_id, _('Invoice ') + " '" + name + "' "+ _("is paid partially: ") + amt)
self.pool.get('account.move.line').reconcile_partial(cr, uid, line_ids, 'manual', context)
# Update the stored value (fields.function), so we write to trigger recompute
@@ -6,7 +6,7 @@
<field name="model">account.entries.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Entries Analysis">
<tree colors="blue:state in ('draft');black:state in ('posted')" string="Entries Analysis">
<field name="date" invisible="1"/>
<field name="date_created" invisible="1"/>
<field name="date_maturity" invisible="1"/>
@@ -6,7 +6,7 @@
<field name="model">account.invoice.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Invoices Analysis">
<tree colors="blue:state in ('draft');gray:state in ('cancel','paid');black:state in ('proforma','proforma2')" string="Invoices Analysis">
<field name="date" invisible="1"/>
<field name="user_id" invisible="1"/>
<field name="year" invisible="1"/>
@@ -89,7 +89,7 @@
<field name="model">report.invoice.created</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree colors="blue:state in ('draft');black:state not in ('draft') " string="Invoices">
<tree colors="blue:state in ('draft');black:state in ('proforma','proforma2','open');gray:state in('paid','cancel') " string="Invoices">
<field name="create_date" select="1"/>
<field name="name" select="1"/>
<field name="type"/>
@@ -50,7 +50,7 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Use Model">
<group colspan="4" col="6">
<group colspan="4" col="6" width="300" height="70">
<label string = "Entry Lines Created." colspan="2"/>
<newline/>
<button icon="gtk-ok" special="cancel" string="Ok"/>
@@ -66,7 +66,7 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Use Model">
<group colspan="4" col="6">
<group colspan="4" col="6" width="300" height="70">
<label string = "Are you sure you want to create entries?" colspan="2"/>
<newline/>
<button icon="terp-gtk-stop" special="cancel" string="Cancel"/>
@@ -163,7 +163,7 @@
<field name="model">crossovered.budget</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Budget">
<tree colors="blue:state in ('draft');gray:state in ('done','cancel');black:state in ('confirm','validate')" string="Budget">
<field name="name" colspan="1" select="1"/>
<field name="code" colspan="1" select="1" />
<field name="state"/>
@@ -95,7 +95,7 @@
<field name="type">tree</field>
<field eval="32" name="priority"/>
<field name="arch" type="xml">
<tree editable="bottom" string="Partner entries">
<tree colors="blue:state in ('draft');black:state in ('validate')" editable="bottom" string="Partner entries">
<field name="date"/>
<field name="move_id"/>
<field name="ref"/>
@@ -177,7 +177,7 @@
<field name="type">tree</field>
<field eval="4" name="priority"/>
<field name="arch" type="xml">
<tree string="Payment order">
<tree colors="blue:state in ('draft');gray:state in ('cancel','done');black:state in ('open')" editable="bottom" string="Payment order">
<field name="reference"/>
<field name="mode"/>
<field name="date_planned"/>
+1 -1
View File
@@ -6,7 +6,7 @@
<field name="model">account.voucher</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Accounting Vouchers">
<tree colors="blue:state in ('draft');gray:state in ('cancel','done');black:state in ('open','proforma','posted','recheck','audit')" string="Accounting Vouchers">
<field name="date"/>
<field name="number"/>
<field name="name"/>
+3 -3
View File
@@ -231,7 +231,7 @@
<field name="type">tree</field>
<field name="priority" eval="1"/>
<field name="arch" type="xml">
<tree string="Catalog">
<tree colors="blue:state in ('unsold','draft');black:state in ('sold','taken_away');gray:state in ('paid') " string="Catalog">
<field name="obj_num" string="Ref" select="1"/>
<field name="name" select="1"/>
<field name="ach_login"/>
@@ -1366,7 +1366,7 @@ Auction adjudication
<field name="model">report.auction.adjudication</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Total adjudication">
<tree colors="blue:state in ('draft');black:state in ('close')" string="Total adjudication">
<field name="name" select="1"/>
<field name="adj_total" select="1"/>
<field name="state" select="1"/>
@@ -1540,7 +1540,7 @@ Object encoded
<field name="model">report.unclassified.objects</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Unclassified objects">
<tree colors="blue:state in ('draft','unsold');black:state in ('sold');gray:state in ('paid')"string="Unclassified objects">
<field name="obj_num" string="Ref" select="1"/>
<field name="name" select="1"/>
<field name="ach_login"/>
+2 -2
View File
@@ -37,7 +37,7 @@
<field name="model">audittrail.rule</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="AuditTrail Rules">
<tree colors="blue:state in ('draft');black:state in ('subscribed')" string="AuditTrail Rules">
<field name="name" />
<field name="object_id"/>
<field name="user_id" />
@@ -157,7 +157,7 @@
<field name="view_type">form</field>
</record>
<menuitem name="Logs" id="menu_action_audittrail_log_tree"
action="action_audittrail_log_tree" parent="menu_action_audittrail" />
action="action_audittrail_log_tree" parent="menu_action_audittrail" />
</data>
</openerp>
+11 -11
View File
@@ -23,18 +23,18 @@
<attribute name="string">Install Generic Modules</attribute>
</separator>
<group colspan="8">
<field name="crm" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/> <field name="sale" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="project" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/> <field name="knowledge" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="stock" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/> <field name="mrp" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="account" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/> <field name="purchase" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="hr" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/> <field name="point_of_sale" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="marketing" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/> <field name="misc_tools" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="report_designer" groups="base.group_extended" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="crm" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/> <field name="sale" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="project" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/> <field name="knowledge" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="stock" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/> <field name="mrp" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="account" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/> <field name="purchase" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="hr" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/> <field name="point_of_sale" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="marketing" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/> <field name="misc_tools" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="report_designer" groups="base.group_extended" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<separator string="Install Specific Business Modules" colspan="4"/>
<field name="association" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="profile_auction" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="profile_bookstore" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="product_expiry" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,profile_association,profile_auction,profile_bookstore)"/>
<field name="association" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="profile_auction" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="profile_bookstore" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
<field name="product_expiry" on_change="onchange_moduleselection(crm,sale,project,knowledge,stock,mrp,account,purchase,hr,point_of_sale,marketing,misc_tools,report_designer,association,profile_auction,profile_bookstore)"/>
</group>
</data>
</field>
+6 -3
View File
@@ -198,7 +198,8 @@ class crm_lead(osv.osv, crm_case):
self.write(cr, uid, ids, value)
for (id, name) in self.name_get(cr, uid, ids):
message = _('The Lead') + " '" + name + "' "+ _("has been written as Open.")
type = self.browse(cr, uid, id).type
message = (_('The ') + type.title() or 'Lead') + " '" + name + "' "+ _("has been written as Open.")
self.log(cr, uid, id, message)
return res
@@ -213,8 +214,10 @@ class crm_lead(osv.osv, crm_case):
res = super(crm_lead, self).case_close(cr, uid, ids, args)
self.write(cr, uid, ids, {'date_closed': time.strftime('%Y-%m-%d %H:%M:%S')})
for (id, name) in self.name_get(cr, uid, ids):
message = _('The Lead') + " '" + name + "' "+ _("has been written as Closed.")
self.log(cr, uid, id, message)
lead = self.browse(cr, uid, id)
if lead.type == 'lead':
message = _('The Lead') + " '" + name + "' "+ _("has been written as Closed.")
self.log(cr, uid, id, message)
return res
def convert_opportunity(self, cr, uid, ids, context=None):
+4 -2
View File
@@ -93,8 +93,10 @@ class crm_opportunity(osv.osv):
res = self.write(cr, uid, ids, value)
for (id, name) in self.name_get(cr, uid, ids):
message = _('The Opportunity') + " '" + name + "' "+ _("has been written as Lost.")
self.log(cr, uid, id, message)
opp = self.browse(cr, uid, id)
if opp.type == 'opportunity':
message = _('The Opportunity') + " '" + name + "' "+ _("has been written as Lost.")
self.log(cr, uid, id, message)
return res
def case_cancel(self, cr, uid, ids, *args):
+1 -1
View File
@@ -38,7 +38,7 @@
<field name="model">crm.phonecall</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Phone Calls" colors="grey:state in ('cancel','done');blue:state in ('pending',)">
<tree colors="gray:state in ('cancel','done');blue:state in ('pending',)" string="Phone Calls">
<field name="date" string="Date"/>
<field name="name" string="Call Summary"/>
<field name="partner_id" string="Partner"/>
+2 -2
View File
@@ -9,7 +9,7 @@
<field name="model">crm.lead.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Leads Analysis">
<tree colors="blue:state in ('draft');black:state in ('open','pending','done');gray:state in ('cancel') " string="Leads Analysis">
<field name="name" invisible="1"/>
<field name="state" invisible="1"/>
<field name="stage_id" invisible="1"/>
@@ -154,7 +154,7 @@
<field name="model">crm.lead.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Opportunities Analysis">
<tree colors="blue:state in ('draft');black:state in ('open','pending','done');gray:state in ('cancel') " string="Opportunities Analysis">
<field name="name" invisible="1"/>
<field name="month" invisible="1"/>
<field name="section_id" invisible="1" groups="base.group_extended"/>
@@ -9,7 +9,7 @@
<field name="model">crm.phonecall.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Phone calls">
<tree colors="blue:state in ('draft');black:state in ('open','pending','done');gray:state in ('cancel') " string="Phone calls">
<field name="name" invisible="1"/>
<field name="month" invisible="1"/>
<field name="section_id" invisible="1"/>
+2 -2
View File
@@ -12,7 +12,7 @@
<field name="arch" type="xml">
<page string="History" position="inside">
<field name="opportunity_ids" colspan="4" nolabel="1">
<tree string="Opportunities" colors="blue:state=='pending';grey:state=='cancel')">
<tree string="Opportunities" colors="blue:state=='pending';gray:state=='cancel')">
<field name="create_date"/>
<field name="name"/>
<field name="type"/>
@@ -45,7 +45,7 @@
</tree>
</field>
<field name="opportunity_assigned_ids" colspan="4" nolabel="1">
<tree string="Assigned Opportunities" colors="blue:state=='pending';grey:state=='cancel')">
<tree string="Assigned Opportunities" colors="blue:state=='pending';gray:state=='cancel')">
<field name="create_date"/>
<field name="name"/>
<field name="type"/>
+3 -1
View File
@@ -187,12 +187,14 @@ class crm_lead_forward_to_partner(osv.osv_memory):
email_re = r'([^ ,<@]+@[^> ,]+)'
email_cc = re.findall(email_re, case.email_cc or '')
new_cc = []
if case.email_cc:
new_cc.append(case.email_cc)
for to in this.email_to.split(','):
email_to = re.findall(email_re, to)
email_to = email_to and email_to[0] or ''
if email_to not in email_cc:
new_cc.append(to)
to_write.update({'email_cc' : case.email_cc or '' + ','.join(new_cc)})
to_write.update({'email_cc' : ', '.join(new_cc) })
case_pool.write(cr, uid, case.id, to_write, context=context)
return {}
+1 -1
View File
@@ -57,7 +57,7 @@
<field name="model">crm.claim</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Claims" colors="blue:state=='pending';black:state=='open';grey:state in ('close', 'cancel')">
<tree string="Claims" colors="blue:state=='pending';black:state=='open';gray:state in ('close', 'cancel')">
<field name="name"/>
<field name="partner_id"/>
<field name="user_id" />
@@ -9,7 +9,7 @@
<field name="model">crm.fundraising.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Fundraising">
<tree colors="blue:state in ('draft');black:state in ('open','pending');gray:state in ('done','cancel') " string="Fundraising">
<field name="name" invisible="1"/>
<field name="month" invisible="1"/>
<field name="section_id" invisible="1"/>
+1 -1
View File
@@ -168,7 +168,7 @@
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Helpdesk Support Tree"
colors="black:state=='open';blue:state=='pending';grey:state in ('cancel','close')">
colors="black:state=='open';blue:state=='pending';gray:state in ('cancel','close')">
<field name="name" string="Query" />
<field name="partner_id" string="Partner"/>
<field name="date" string="Date"/>
@@ -9,7 +9,7 @@
<field name="model">crm.helpdesk.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Helpdesk">
<tree colors="blue:state in ('draft');black:state in ('open','pending');gray:state in ('done','cancel') " string="Helpdesk">
<field name="name" />
<field name="month"/>
<field name="section_id" />
+1 -1
View File
@@ -53,7 +53,7 @@
],
'demo_xml': [ 'document_demo.xml','board_document_demo.xml'],
'test': [
'test/document_test.yml',
'test/document_test2.yml',
],
'installable': True,
'active': False,
+5 -5
View File
@@ -53,7 +53,7 @@
<menuitem id="menu_reporting" name="Reporting" sequence="2" parent="knowledge.menu_document"/>
<menuitem
name="Dashboard"
id="menu_board_document"
id="menu_reports_document"
parent="menu_reporting"
sequence="0"
icon="terp-graph"
@@ -61,10 +61,10 @@
<menuitem
parent="menu_board_document"
parent="menu_reports_document"
action="open_board_document_manager"
sequence="1"
id="menu_board_document_manager"
id="menu_reports_document_manager"
icon="terp-graph"/>
<record model="ir.ui.view" id="board_document_manager_form1">
@@ -107,10 +107,10 @@
</record>
<menuitem
parent="menu_board_document"
parent="menu_reports_document"
action="open_board_document_manager1"
sequence="1"
id="menu_board_document_manager1"
id="menu_reports_document_manager1"
icon="terp-graph"/>
</data>
</openerp>
+4 -1
View File
@@ -153,7 +153,10 @@ class contentIndex(object):
if realfname :
fname = realfname
else:
bname,ext = os.path.splitext(filename)
try:
bname,ext = os.path.splitext(filename or 'test.tmp')
except Exception:
bname, ext = filename, 'tmp'
fd, fname = tempfile.mkstemp(suffix=ext)
os.write(fd, content)
os.close(fd)
+17 -4
View File
@@ -58,7 +58,9 @@ class document_directory_content(osv.osv):
'suffix': fields.char('Suffix', size=16),
'report_id': fields.many2one('ir.actions.report.xml', 'Report'),
'extension': fields.selection(_extension_get, 'Document Type', required=True, size=4),
'include_name': fields.boolean('Include Record Name', help="Check this field if you want that the name of the file start by the record name."),
'include_name': fields.boolean('Include Record Name',
help="Check this field if you want that the name of the file to contain the record name." \
"\nIf set, the directory will have to be a resource one."),
'directory_id': fields.many2one('document.directory', 'Directory'),
}
_defaults = {
@@ -88,12 +90,19 @@ class document_directory_content(osv.osv):
tname = (content.prefix or '') + (content.suffix or '') + (content.extension or '')
if tname.find('/'):
tname=tname.replace('/', '_')
act_id = False
if 'dctx_res_id' in node.dctx:
act_id = node.dctx['dctx_res_id']
elif hasattr(node, 'res_id'):
act_id = node.res_id
else:
act_id = node.context.context.get('res_id',False)
if not nodename:
n = nodes.node_content(tname, node, node.context,content)
n = nodes.node_content(tname, node, node.context,content, act_id=act_id)
res2.append( n)
else:
if nodename == tname:
n = nodes.node_content(tname, node, node.context,content)
n = nodes.node_content(tname, node, node.context,content, act_id=act_id)
n.fill_fields(cr)
res2.append(n)
return res2
@@ -108,6 +117,10 @@ class document_directory_content(osv.osv):
raise Exception("Invalid content: %s" % node.extension)
report = self.pool.get('ir.actions.report.xml').browse(cr, uid, node.report_id)
srv = netsvc.Service._services['report.'+report.report_name]
pdf,pdftype = srv.create(cr, uid, [node.context.context['res_id']], {}, {})
ctx = node.context.context.copy()
ctx.update(node.dctx)
pdf,pdftype = srv.create(cr, uid, [node.act_id,], {}, context=ctx)
return pdf
document_directory_content()
#eof
+91 -50
View File
@@ -28,6 +28,7 @@ import os
import pooler
import netsvc
from osv.orm import except_orm
#import StringIO
from psycopg2 import Binary
@@ -46,7 +47,10 @@ class document_file(osv.osv):
def _data_get(self, cr, uid, ids, name, arg, context):
fbrl = self.browse(cr, uid, ids, context=context)
nctx = nodes.get_node_context(cr, uid, context)
nctx = nodes.get_node_context(cr, uid, context={})
# nctx will /not/ inherit the caller's context. Most of
# it would be useless, anyway (like active_id, active_model,
# bin_size etc.)
result = {}
bin_size = context.get('bin_size', False)
for fbro in fbrl:
@@ -66,39 +70,38 @@ class document_file(osv.osv):
if not value:
return True
fbro = self.browse(cr, uid, id, context=context)
nctx = nodes.get_node_context(cr, uid, context)
nctx = nodes.get_node_context(cr, uid, context={})
fnode = nodes.node_file(None, None, nctx, fbro)
res = fnode.set_data(cr, base64.decodestring(value), fbro)
return res
_columns = {
'user_id': fields.many2one('res.users', 'Owner', select=1),
'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
# the directory id now is mandatory. It can still be computed automatically.
'parent_id': fields.many2one('document.directory', 'Directory', select=1),
'file_size': fields.integer('File Size', required=True),
'file_type': fields.char('Content Type', size=128),
# Columns from ir.attachment:
'create_date': fields.datetime('Date Created', readonly=True),
'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
'write_date': fields.datetime('Date Modified', readonly=True),
'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
'res_model': fields.char('Attached Model', size=64, readonly=True),
'res_id': fields.integer('Attached ID', readonly=True),
# If ir.attachment contained any data before document is installed, preserve
# the data, don't drop the column!
'db_datas': fields.binary('Data', oldname='datas'),
'index_content': fields.text('Indexed Content'),
'write_date': fields.datetime('Date Modified', readonly=True),
'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
'create_date': fields.datetime('Date Created', readonly=True),
'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
'store_method': fields.selection([('db', 'Database'), ('fs', 'Filesystem'), ('link', 'Link')], "Storing Method"),
'datas': fields.function(_data_get, method=True, fnct_inv=_data_set, string='File Content', type="binary", nodrop=True),
'url': fields.char('File URL',size=64),
'store_fname': fields.char('Stored Filename', size=200),
'res_model': fields.char('Attached Model', size=64), #res_model
'res_id': fields.integer('Attached ID'), #res_id
'partner_id':fields.many2one('res.partner', 'Partner', select=1),
'type':fields.selection([
('url','URL'),
('binary','Binary'),
],'Type', help="Type is used to separate URL and binary File"),
# Fields of document:
'user_id': fields.many2one('res.users', 'Owner', select=1),
# 'group_ids': fields.many2many('res.groups', 'document_group_rel', 'item_id', 'group_id', 'Groups'),
# the directory id now is mandatory. It can still be computed automatically.
'parent_id': fields.many2one('document.directory', 'Directory', select=1, required=True),
'index_content': fields.text('Indexed Content'),
'partner_id':fields.many2one('res.partner', 'Partner', select=1),
'company_id': fields.many2one('res.company', 'Company'),
'file_size': fields.integer('File Size', required=True),
'file_type': fields.char('Content Type', size=128),
# fields used for file storage
'store_fname': fields.char('Stored Filename', size=200),
}
def __get_def_directory(self, cr, uid, context=None):
@@ -109,8 +112,6 @@ class document_file(osv.osv):
'company_id': lambda s,cr,uid,c: s.pool.get('res.company')._company_default_get(cr, uid, 'ir.attachment', context=c),
'user_id': lambda self, cr, uid, ctx:uid,
'file_size': lambda self, cr, uid, ctx:0,
'store_method': lambda *args: 'db',
'type': 'binary',
'parent_id': __get_def_directory
}
_sql_constraints = [
@@ -122,7 +123,7 @@ class document_file(osv.osv):
res_model = vals.get('res_model', False)
res_id = vals.get('res_id', 0)
if op == 'write':
for file in self.browse(cr, uid, ids):
for file in self.browse(cr, uid, ids): # FIXME fields_only
if not name:
name = file.name
if not parent_id:
@@ -156,8 +157,44 @@ class document_file(osv.osv):
return False
if not self._check_duplication(cr, uid, vals, ids, 'write'):
raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
result = super(document_file, self).write(cr, uid, ids, vals, context=context)
cr.commit()
# if nodes call this write(), they must skip the code below
from_node = context and context.get('__from_node', False)
if (('parent_id' in vals) or ('name' in vals)) and not from_node:
# perhaps this file is renaming or changing directory
nctx = nodes.get_node_context(cr,uid,context={})
dirobj = self.pool.get('document.directory')
if 'parent_id' in vals:
dbro = dirobj.browse(cr, uid, vals['parent_id'], context=context)
dnode = nctx.get_dir_node(cr, dbro)
else:
dbro = None
dnode = None
ids2 = []
result = False
for fbro in self.browse(cr, uid, ids, context=context):
if ('parent_id' not in vals or fbro.parent_id.id == vals['parent_id']) \
and ('name' not in vals or fbro.name == vals['name']) :
ids2.append(fbro.id)
continue
fnode = nctx.get_file_node(cr, fbro)
res = fnode.move_to(cr, dnode or fnode.parent, vals.get('name', fbro.name), fbro, dbro, True)
if isinstance(res, dict):
vals2 = vals.copy()
vals2.update(res)
wid = res.get('id', fbro.id)
result = super(document_file,self).write(cr,uid,wid,vals2,context=context)
# TODO: how to handle/merge several results?
elif res == True:
ids2.append(fbro.id)
elif res == False:
pass
ids = ids2
if 'file_size' in vals: # only write that field using direct SQL calls
del vals['file_size']
if len(ids) and len(vals):
result = super(document_file,self).write(cr, uid, ids, vals, context=context)
cr.commit() # ?
return result
def create(self, cr, uid, vals, context=None):
@@ -170,26 +207,10 @@ class document_file(osv.osv):
vals['res_id'] = context.get('default_res_id', False)
if not vals.get('res_model', False) and context.get('default_res_model', False):
vals['res_model'] = context.get('default_res_model', False)
if vals.get('res_id', False) and vals.get('res_model', False):
obj_model = self.pool.get(vals['res_model'])
result = obj_model.read(cr, uid, [vals['res_id']], ['name', 'partner_id', 'address_id'], context=context)
if len(result):
obj = result[0]
if obj_model._name == 'res.partner':
vals['partner_id'] = obj['id']
elif obj.get('address_id', False):
if isinstance(obj['address_id'], tuple) or isinstance(obj['address_id'], list):
address_id = obj['address_id'][0]
else:
address_id = obj['address_id']
address = self.pool.get('res.partner.address').read(cr, uid, [address_id], context=context)
if len(address):
vals['partner_id'] = address[0]['partner_id'][0] or False
elif obj.get('partner_id', False):
if isinstance(obj['partner_id'], tuple) or isinstance(obj['partner_id'], list):
vals['partner_id'] = obj['partner_id'][0]
else:
vals['partner_id'] = obj['partner_id']
if vals.get('res_id', False) and vals.get('res_model', False) \
and not vals.get('partner_id', False):
vals['partner_id'] = self.__get_partner_id(cr, uid, \
vals['res_model'], vals['res_id'], context)
datas = None
if vals.get('link', False) :
@@ -198,13 +219,33 @@ class document_file(osv.osv):
else:
datas = vals.get('datas', False)
vals['file_size'] = datas and len(datas) or 0
if datas:
vals['file_size'] = len(datas)
else:
if vals.get('file_size'):
del vals['file_size']
if not self._check_duplication(cr, uid, vals):
raise osv.except_osv(_('ValidateError'), _('File name must be unique!'))
result = super(document_file, self).create(cr, uid, vals, context)
cr.commit()
cr.commit() # ?
return result
def __get_partner_id(self, cr, uid, res_model, res_id, context):
""" A helper to retrieve the associated partner from any res_model+id
It is a hack that will try to discover if the mentioned record is
clearly associated with a partner record.
"""
obj_model = self.pool.get(res_model)
if obj_model._name == 'res.partner':
return res_id
elif 'partner_id' in obj_model._columns and obj_model._columns['partner_id']._obj == 'res.partner':
bro = obj_model.browse(cr, uid, res_id, context=context)
return bro.partner_id.id
elif 'address_id' in obj_model._columns and obj_model._columns['address_id']._obj == 'res.partner.address':
bro = obj_model.browse(cr, uid, res_id, context=context)
return bro.address_id.partner_id.id
return False
def unlink(self, cr, uid, ids, context={}):
stor = self.pool.get('document.storage')
unres = []
+2 -5
View File
@@ -9,7 +9,7 @@
</record>
<record model="document.storage" id="storage_default">
<field name="name">Default DB storage</field>
<field name="name">Database storage</field>
<field name="type">db</field>
<field name="user_id" ref="base.user_admin"/>
</record>
@@ -17,7 +17,7 @@
<record model="document.directory" id="dir_root">
<field name="name">Documents</field>
<field name="user_id" ref="base.user_admin"/>
<field name="storage_id" ref="storage_default"/>
<field name="storage_id" ref="storage_default"/>
<field name="ressource_id">0</field>
</record>
@@ -25,7 +25,6 @@
<field name="name">My Folder</field>
<field name="parent_id" ref="dir_root"/>
<field name="user_id" ref="base.user_admin"/>
<field name="storage_id" ref="storage_default"/>
<field name="ressource_id">0</field>
</record>
@@ -35,7 +34,6 @@
<field name="parent_id" ref="dir_root"/>
<field name="type">ressource</field>
<field name="ressource_tree">1</field>
<field name="storage_id" ref="storage_default"/>
<field name="ressource_id">0</field>
<field name="ressource_type_id" search="[('model','=','res.partner.category')]" />
@@ -57,7 +55,6 @@
<field name="name">Personal Folders</field>
<field name="parent_id" ref="dir_root"/>
<field name="type">ressource</field>
<field name="storage_id" ref="storage_default"/>
<field name="ressource_type_id" ref="base.model_res_users" />
<field name="ressource_id">0</field>
+36 -18
View File
@@ -39,20 +39,27 @@ class document_directory(osv.osv):
'write_uid': fields.many2one('res.users', 'Last Modification User', readonly=True),
'create_date': fields.datetime('Date Created', readonly=True),
'create_uid': fields.many2one('res.users', 'Creator', readonly=True),
'file_type': fields.char('Content Type', size=32),
'domain': fields.char('Domain', size=128, help="Use a domain if you want to apply an automatic filter on visible resources."),
'user_id': fields.many2one('res.users', 'Owner'),
'storage_id': fields.many2one('document.storage', 'Storage'),
'group_ids': fields.many2many('res.groups', 'document_directory_group_rel', 'item_id', 'group_id', 'Groups'),
'parent_id': fields.many2one('document.directory', 'Parent Item'),
'parent_id': fields.many2one('document.directory', 'Parent Directory', select=1),
'child_ids': fields.one2many('document.directory', 'parent_id', 'Children'),
'file_ids': fields.one2many('ir.attachment', 'parent_id', 'Files'),
'content_ids': fields.one2many('document.directory.content', 'directory_id', 'Virtual Files'),
'type': fields.selection([('directory','Static Directory'),('ressource','Other Resources')], 'Type', required=True),
'ressource_type_id': fields.many2one('ir.model', 'Directories Mapped to Objects',
help="Select an object here and Open ERP will create a mapping for each of these " \
"objects, using the given domain, when browsing through FTP."),
'type': fields.selection([
('directory','Static Directory'),
('ressource','Folders per resource'),
],
'Type', required=True, select=1,
help="Defines directory's behaviour."),
'ressource_type_id': fields.many2one('ir.model', 'Resource model',
help="Select an object here and there will be one folder per record of that resource."),
'resource_field': fields.many2one('ir.model.fields', 'Name field', help='Field to be used as name on resource directories. If empty, the "name" will be used.'),
'resource_find_all': fields.boolean('Find all resources', required=True,
help="If true, all attachments that match this resource will " \
" be located. If false, only ones that have this as parent." ),
'ressource_parent_type_id': fields.many2one('ir.model', 'Parent Model',
help="If you put an object here, this directory template will appear bellow all of these objects. " \
"Don't put a parent directory if you select a parent model."),
@@ -60,7 +67,7 @@ class document_directory(osv.osv):
'ressource_tree': fields.boolean('Tree Structure',
help="Check this if you want to use the same tree structure as the object selected in the system."),
'dctx_ids': fields.one2many('document.directory.dctx', 'dir_id', 'Context fields'),
'company_id': fields.many2one('res.company', 'Company'),
'company_id': fields.many2one('res.company', 'Company'),
}
@@ -99,10 +106,12 @@ class document_directory(osv.osv):
'type': lambda *args: 'directory',
'ressource_id': lambda *a: 0,
'storage_id': _get_def_storage,
'resource_find_all': True,
}
_sql_constraints = [
('dirname_uniq', 'unique (name,parent_id,ressource_id,ressource_parent_type_id)', 'The directory name must be unique !'),
('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!')
('no_selfparent', 'check(parent_id <> id)', 'Directory cannot be parent of itself!'),
('dir_parented', 'check(parent_id IS NOT NULL OR storage_id IS NOT NULL)', 'Directory must have a parent or a storage')
]
def name_get(self, cr, uid, ids, context={}):
res = []
@@ -190,18 +199,25 @@ class document_directory(osv.osv):
"""
if not context:
context = {}
lang = context.get('lang',False)
if not lang:
user = self.pool.get('res.users').browse(cr, uid, uid)
lang = user.context_lang
context['lang'] = lang
try: #just instrumentation
return nodes.get_node_context(cr, uid, context).get_uri(cr, uri)
except Exception,e:
print "exception: ",e
raise
return nodes.get_node_context(cr, uid, context).get_uri(cr, uri)
def get_dir_permissions(self, cr, uid, ids ):
"""Check what permission user 'uid' has on directory 'id'
"""
assert len(ids) == 1
id = ids[0]
cr.execute( "SELECT count(dg.item_id) AS needs, count(ug.uid) AS has " \
" FROM document_directory_group_rel dg " \
" LEFT OUTER JOIN res_groups_users_rel ug " \
" ON (dg.group_id = ug.gid AND ug.uid = %s) " \
" WHERE dg.item_id = %s ", (uid, id))
needs, has = cr.fetchone()
if needs and not has:
return 1 # still allow to descend into.
else:
return 15
def _locate_child(self, cr, uid, root_id, uri,nparent, ncontext):
""" try to locate the node in uri,
@@ -274,6 +290,8 @@ class document_directory(osv.osv):
raise osv.except_osv(_('ValidateError'), _('Directory name contains special characters!'))
return super(document_directory,self).create(cr, uid, vals, context)
# TODO def unlink(...
document_directory()
class document_directory_dctx(osv.osv):
+466 -39
View File
@@ -24,7 +24,10 @@ from osv import osv, fields
import os
import tools
import base64
import errno
import logging
from StringIO import StringIO
import psycopg2
from tools.misc import ustr
from tools.translate import _
@@ -33,7 +36,9 @@ from osv.orm import except_orm
import random
import string
import pooler
import netsvc
import nodes
from content_index import cntIndex
DMS_ROOT_PATH = tools.config.get('document_path', os.path.join(tools.config.get('root_path'), 'filestore'))
@@ -84,6 +89,222 @@ def create_directory(path):
os.makedirs(path)
return dir_name
class nodefd_file(nodes.node_descriptor):
""" A descriptor to a real file
Inheriting directly from file doesn't work, since file exports
some read-only attributes (like 'name') that we don't like.
"""
def __init__(self, parent, path, mode):
nodes.node_descriptor.__init__(self, parent)
self.__file = open(path, mode)
if mode.endswith('b'):
mode = mode[:-1]
self.mode = mode
for attr in ('closed', 'read', 'write', 'seek', 'tell'):
setattr(self,attr, getattr(self.__file, attr))
def close(self):
# TODO: locking in init, close()
fname = self.__file.name
self.__file.close()
if self.mode in ('w', 'w+', 'r+'):
par = self._get_parent()
cr = pooler.get_db(par.context.dbname).cursor()
icont = ''
mime = ''
filename = par.path
if isinstance(filename, (tuple, list)):
filename = '/'.join(filename)
try:
mime, icont = cntIndex.doIndex(None, filename=filename,
content_type=None, realfname=fname)
except Exception:
logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
pass
try:
icont_u = ustr(icont)
except UnicodeError:
icont_u = ''
try:
fsize = os.stat(fname).st_size
cr.execute("UPDATE ir_attachment " \
" SET index_content = %s, file_type = %s, " \
" file_size = %s " \
" WHERE id = %s",
(icont_u, mime, fsize, par.file_id))
par.content_length = fsize
par.content_type = mime
cr.commit()
cr.close()
except Exception:
logging.getLogger('document.storage').warning('Cannot save file indexed content:', exc_info=True)
elif self.mode in ('a', 'a+' ):
try:
par = self._get_parent()
cr = pooler.get_db(par.context.dbname).cursor()
fsize = os.stat(fname).st_size
cr.execute("UPDATE ir_attachment SET file_size = %s " \
" WHERE id = %s",
(fsize, par.file_id))
par.content_length = fsize
cr.commit()
cr.close()
except Exception:
logging.getLogger('document.storage').warning('Cannot save file appended content:', exc_info=True)
class nodefd_db(StringIO, nodes.node_descriptor):
""" A descriptor to db data
"""
def __init__(self, parent, ira_browse, mode):
nodes.node_descriptor.__init__(self, parent)
if mode.endswith('b'):
mode = mode[:-1]
if mode in ('r', 'r+'):
cr = ira_browse._cr # reuse the cursor of the browse object, just now
cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s',(ira_browse.id,))
data = cr.fetchone()[0]
StringIO.__init__(self, data)
elif mode in ('w', 'w+'):
StringIO.__init__(self, None)
# at write, we start at 0 (= overwrite), but have the original
# data available, in case of a seek()
elif mode == 'a':
StringIO.__init__(self, None)
else:
logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
raise IOError(errno.EINVAL, "Invalid file mode")
self.mode = mode
def close(self):
# we now open a *separate* cursor, to update the data.
# FIXME: this may be improved, for concurrency handling
par = self._get_parent()
uid = par.context.uid
cr = pooler.get_db(par.context.dbname).cursor()
try:
if self.mode in ('w', 'w+', 'r+'):
data = self.getvalue()
icont = ''
mime = ''
filename = par.path
if isinstance(filename, (tuple, list)):
filename = '/'.join(filename)
try:
mime, icont = cntIndex.doIndex(data, filename=filename,
content_type=None, realfname=None)
except Exception:
logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
pass
try:
icont_u = ustr(icont)
except UnicodeError:
icont_u = ''
out = psycopg2.Binary(data)
cr.execute("UPDATE ir_attachment " \
"SET db_datas = %s, file_size=%s, " \
" index_content= %s, file_type=%s " \
" WHERE id = %s",
(out, len(data), icont_u, mime, par.file_id))
elif self.mode == 'a':
data = self.getvalue()
out = psycopg2.Binary(data)
cr.execute("UPDATE ir_attachment " \
"SET db_datas = COALESCE(db_datas,'') || %s, " \
" file_size = COALESCE(file_size, 0) + %s " \
" WHERE id = %s",
(out, len(data), par.file_id))
cr.commit()
except Exception, e:
logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
raise
finally:
cr.close()
StringIO.close(self)
class nodefd_db64(StringIO, nodes.node_descriptor):
""" A descriptor to db data, base64 (the old way)
It stores the data in base64 encoding at the db. Not optimal, but
the transparent compression of Postgres will save the day.
"""
def __init__(self, parent, ira_browse, mode):
nodes.node_descriptor.__init__(self, parent)
if mode.endswith('b'):
mode = mode[:-1]
if mode in ('r', 'r+'):
StringIO.__init__(self, base64.decodestring(ira_browse.db_datas))
elif mode in ('w', 'w+'):
StringIO.__init__(self, None)
# at write, we start at 0 (= overwrite), but have the original
# data available, in case of a seek()
elif mode == 'a':
StringIO.__init__(self, None)
else:
logging.getLogger('document.storage').error("Incorrect mode %s specified", mode)
raise IOError(errno.EINVAL, "Invalid file mode")
self.mode = mode
def close(self):
# we now open a *separate* cursor, to update the data.
# FIXME: this may be improved, for concurrency handling
par = self._get_parent()
uid = par.context.uid
cr = pooler.get_db(par.context.dbname).cursor()
try:
if self.mode in ('w', 'w+', 'r+'):
data = self.getvalue()
icont = ''
mime = ''
filename = par.path
if isinstance(filename, (tuple, list)):
filename = '/'.join(filename)
try:
mime, icont = cntIndex.doIndex(data, filename=filename,
content_type=None, realfname=None)
except Exception:
logging.getLogger('document.storage').debug('Cannot index file:', exc_info=True)
pass
try:
icont_u = ustr(icont)
except UnicodeError:
icont_u = ''
cr.execute('UPDATE ir_attachment SET db_datas = %s::bytea, file_size=%s, ' \
'index_content = %s, file_type = %s ' \
'WHERE id = %s',
(base64.encodestring(out), len(out), icont_u, mime, par.file_id))
elif self.mode == 'a':
out = self.getvalue()
# Yes, we're obviously using the wrong representation for storing our
# data as base64-in-bytea
cr.execute("UPDATE ir_attachment " \
"SET db_datas = encode( (COALESCE(decode(encode(db_datas,'escape'),'base64'),'') || decode(%s, 'base64')),'base64')::bytea , " \
" file_size = COALESCE(file_size, 0) + %s " \
" WHERE id = %s",
(base64.encodestring(out), len(out), par.file_id))
cr.commit()
except Exception, e:
logging.getLogger('document.storage').exception('Cannot update db file #%d for close:', par.file_id)
raise
finally:
cr.close()
StringIO.close(self)
class document_storage(osv.osv):
""" The primary object for data storage.
@@ -110,7 +331,7 @@ class document_storage(osv.osv):
'group_ids': fields.many2many('res.groups', 'document_storage_group_rel', 'item_id', 'group_id', 'Groups'),
'dir_ids': fields.one2many('document.directory', 'parent_id', 'Directories'),
'type': fields.selection([('db', 'Database'), ('filestore', 'Internal File storage'),
('realstore', 'External file storage'), ('virtual', 'Virtual storage')], 'Type', required=True),
('realstore','External file storage'),], 'Type', required=True),
'path': fields.char('Path', size=250, select=1, help="For file storage, the root path of the storage"),
'online': fields.boolean('Online', help="If not checked, media is currently offline and its contents not available", required=True),
'readonly': fields.boolean('Read Only', help="If set, media is for reading only"),
@@ -133,6 +354,49 @@ class document_storage(osv.osv):
('path_uniq', 'UNIQUE(type,path)', "The storage path must be unique!")
]
def __get_random_fname(self, path):
flag = None
# This can be improved
if os.path.isdir(path):
for dirs in os.listdir(path):
if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000:
flag = dirs
break
flag = flag or create_directory(path)
filename = random_name()
return os.path.join(flag, filename)
def __prepare_realpath(self, cr, file_node, ira, store_path, do_create=True):
""" Cleanup path for realstore, create dirs if needed
@param file_node the node
@param ira ir.attachment browse of the file_node
@param store_path the path of the parent storage object, list
@param do_create create the directories, if needed
@return tuple(path "/var/filestore/real/dir/", npath ['dir','fname.ext'] )
"""
file_node.fix_ppath(cr, ira)
npath = file_node.full_path() or []
# npath may contain empty elements, for root directory etc.
npath = filter(lambda x: x is not None, npath)
# if self._debug:
# self._doclog.debug('Npath: %s', npath)
for n in npath:
if n == '..':
raise ValueError("Invalid '..' element in path")
for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?',):
if ch in n:
raise ValueError("Invalid char %s in path %s" %(ch, n))
dpath = [store_path,]
dpath += npath[:-1]
path = os.path.join(*dpath)
if not os.path.isdir(path):
self._doclog.debug("Create dirs: %s", path)
os.makedirs(path)
return path, npath
def get_data(self, cr, uid, id, file_node, context=None, fil_obj=None):
""" retrieve the contents of some file_node having storage_id = id
optionally, fil_obj could point to the browse object of the file
@@ -141,15 +405,71 @@ class document_storage(osv.osv):
if not context:
context = {}
boo = self.browse(cr, uid, id, context)
if not boo.online:
raise IOError(errno.EREMOTE, 'medium offline')
if fil_obj:
ira = fil_obj
else:
ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
return self.__get_data_3(cr, uid, boo, ira, context)
def __get_data_3(self, cr, uid, boo, ira, context):
def get_file(self, cr, uid, id, file_node, mode, context=None):
""" Return a file-like object for the contents of some node
"""
if context is None:
context = {}
boo = self.browse(cr, uid, id, context)
if not boo.online:
raise RuntimeError('media offline')
raise IOError(errno.EREMOTE, 'medium offline')
if boo.readonly and mode not in ('r', 'rb'):
raise IOError(errno.EPERM, "Readonly medium")
ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
if boo.type == 'filestore':
if not ira.store_fname:
# On a migrated db, some files may have the wrong storage type
# try to fix their directory.
if mode in ('r','r+'):
if ira.file_size:
self._doclog.warning( "ir.attachment #%d does not have a filename, but is at filestore, fix it!" % ira.id)
raise IOError(errno.ENOENT, 'No file can be located')
else:
store_fname = self.__get_random_fname(boo.path)
cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
(store_fname, ira.id))
fpath = os.path.join(boo.path, store_fname)
else:
fpath = os.path.join(boo.path, ira.store_fname)
return nodefd_file(file_node, path=fpath, mode=mode)
elif boo.type == 'db':
# TODO: we need a better api for large files
return nodefd_db(file_node, ira_browse=ira, mode=mode)
elif boo.type == 'db64':
return nodefd_db64(file_node, ira_browse=ira, mode=mode)
elif boo.type == 'realstore':
path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path,
do_create = (mode[1] in ('w','a')) )
fpath = os.path.join(path, npath[-1])
if (not os.path.exists(fpath)) and mode[1] == 'r':
raise IOError("File not found: %s" % fpath)
elif mode[1] in ('w', 'a') and not ira.store_fname:
store_fname = os.path.join(*npath)
cr.execute('UPDATE ir_attachment SET store_fname = %s WHERE id = %s',
(store_fname, ira.id))
return nodefd_file(file_node, path=fpath, mode=mode)
elif boo.type == 'virtual':
raise ValueError('Virtual storage does not support static files')
else:
raise TypeError("No %s storage" % boo.type)
def __get_data_3(self, cr, uid, boo, ira, context):
if boo.type == 'filestore':
if not ira.store_fname:
# On a migrated db, some files may have the wrong storage type
@@ -159,13 +479,21 @@ class document_storage(osv.osv):
return None
fpath = os.path.join(boo.path, ira.store_fname)
return file(fpath, 'rb').read()
elif boo.type == 'db':
elif boo.type == 'db64':
# TODO: we need a better api for large files
if ira.db_datas:
out = base64.decodestring(ira.db_datas)
else:
out = ''
return out
elif boo.type == 'db':
# We do an explicit query, to avoid type transformations.
cr.execute('SELECT db_datas FROM ir_attachment WHERE id = %s', (ira.id,))
res = cr.fetchone()
if res:
return res[0]
else:
return ''
elif boo.type == 'realstore':
if not ira.store_fname:
# On a migrated db, some files may have the wrong storage type
@@ -179,7 +507,11 @@ class document_storage(osv.osv):
elif not ira.store_fname:
return None
else:
raise IOError("File not found: %s" % fpath)
raise IOError(errno.ENOENT, "File not found: %s" % fpath)
elif boo.type == 'virtual':
raise ValueError('Virtual storage does not support static files')
else:
raise TypeError("No %s storage" % boo.type)
@@ -197,30 +529,25 @@ class document_storage(osv.osv):
ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
if not boo.online:
raise RuntimeError('media offline')
raise IOError(errno.EREMOTE, 'medium offline')
if boo.readonly:
raise IOError(errno.EPERM, "Readonly medium")
self._doclog.debug( "Store data for ir.attachment #%d" % ira.id)
store_fname = None
fname = None
if boo.type == 'filestore':
path = boo.path
try:
flag = None
# This can be improved
if os.path.isdir(path):
for dirs in os.listdir(path):
if os.path.isdir(os.path.join(path, dirs)) and len(os.listdir(os.path.join(path, dirs))) < 4000:
flag = dirs
break
flag = flag or create_directory(path)
filename = random_name()
fname = os.path.join(path, flag, filename)
store_fname = self.__get_random_fname(path)
fname = os.path.join(path, store_fname)
fp = file(fname, 'wb')
fp.write(data)
fp.close()
self._doclog.debug( "Saved data to %s" % fname)
filesize = len(data) # os.stat(fname).st_size
store_fname = os.path.join(flag, filename)
# TODO Here, an old file would be left hanging.
except Exception, e:
@@ -228,27 +555,19 @@ class document_storage(osv.osv):
raise except_orm(_('Error!'), str(e))
elif boo.type == 'db':
filesize = len(data)
# will that work for huge data? TODO
# will that work for huge data?
out = psycopg2.Binary(data)
cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
(out, file_node.file_id))
elif boo.type == 'db64':
filesize = len(data)
# will that work for huge data?
out = base64.encodestring(data)
cr.execute('UPDATE ir_attachment SET db_datas = %s WHERE id = %s',
(out, file_node.file_id))
elif boo.type == 'realstore':
try:
file_node.fix_ppath(cr, ira)
npath = file_node.full_path() or []
# npath may contain empty elements, for root directory etc.
for i, n in enumerate(npath):
if n == None:
del npath[i]
for n in npath:
for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'):
if ch in n:
raise ValueError("Invalid char %s in path %s" %(ch, n))
dpath = [boo.path,]
dpath += npath[:-1]
path = os.path.join(*dpath)
if not os.path.isdir(path):
os.makedirs(path)
path, npath = self.__prepare_realpath(cr, file_node, ira, boo.path, do_create=True)
fname = os.path.join(path, npath[-1])
fp = file(fname,'wb')
fp.write(data)
@@ -260,6 +579,10 @@ class document_storage(osv.osv):
except Exception,e :
self._doclog.warning("Couldn't save data:", exc_info=True)
raise except_orm(_('Error!'), str(e))
elif boo.type == 'virtual':
raise ValueError('Virtual storage does not support static files')
else:
raise TypeError("No %s storage" % boo.type)
@@ -276,16 +599,21 @@ class document_storage(osv.osv):
self._doclog.debug('Cannot index file:', exc_info=True)
pass
try:
icont_u = ustr(icont)
except UnicodeError:
icont_u = ''
# a hack: /assume/ that the calling write operation will not try
# to write the fname and size, and update them in the db concurrently.
# We cannot use a write() here, because we are already in one.
cr.execute('UPDATE ir_attachment SET store_fname = %s, file_size = %s, index_content = %s, file_type = %s WHERE id = %s',
(store_fname, filesize, ustr(icont), mime, file_node.file_id))
(store_fname, filesize, icont_u, mime, file_node.file_id))
file_node.content_length = filesize
file_node.content_type = mime
return True
except Exception, e :
self._doclog.warning( "Couldn't save data:", exc_info=True)
self._doclog.warning("Couldn't save data:", exc_info=True)
# should we really rollback once we have written the actual data?
# at the db case (only), that rollback would be safe
raise except_orm(_('Error at doc write!'), str(e))
@@ -295,7 +623,10 @@ class document_storage(osv.osv):
files that have to be removed, too. """
if not storage_bo.online:
raise RuntimeError('media offline')
raise IOError(errno.EREMOTE, 'medium offline')
if storage_bo.readonly:
raise IOError(errno.EPERM, "Readonly medium")
if storage_bo.type == 'filestore':
fname = fil_bo.store_fname
@@ -303,7 +634,7 @@ class document_storage(osv.osv):
return None
path = storage_bo.path
return (storage_bo.id, 'file', os.path.join(path, fname))
elif storage_bo.type == 'db':
elif storage_bo.type in ('db', 'db64'):
return None
elif storage_bo.type == 'realstore':
fname = fil_bo.store_fname
@@ -312,7 +643,7 @@ class document_storage(osv.osv):
path = storage_bo.path
return ( storage_bo.id, 'file', os.path.join(path, fname))
else:
raise TypeError("No %s storage" % boo.type)
raise TypeError("No %s storage" % storage_bo.type)
def do_unlink(self, cr, uid, unres):
for id, ktype, fname in unres:
@@ -326,6 +657,102 @@ class document_storage(osv.osv):
return True
def simple_rename(self, cr, uid, file_node, new_name, context=None):
""" A preparation for a file rename.
It will not affect the database, but merely check and perhaps
rename the realstore file.
@return the dict of values that can safely be be stored in the db.
"""
sbro = self.browse(cr, uid, file_node.storage_id, context=context)
assert sbro, "The file #%d didn't provide storage" % file_node.file_id
if not sbro.online:
raise IOError(errno.EREMOTE, 'medium offline')
if sbro.readonly:
raise IOError(errno.EPERM, "Readonly medium")
if sbro.type in ('filestore', 'db', 'db64'):
# nothing to do for a rename, allow to change the db field
return { 'name': new_name, 'datas_fname': new_name }
elif sbro.type == 'realstore':
ira = self.pool.get('ir.attachment').browse(cr, uid, file_node.file_id, context=context)
path, npath = self.__prepare_realpath(cr, file_node, ira, sbro.path, do_create=False)
fname = ira.store_fname
if not fname:
self._doclog.warning("Trying to rename a non-stored file")
if fname != os.path.join(*npath):
self._doclog.warning("inconsistency in realstore: %s != %s" , fname, repr(npath))
oldpath = os.path.join(path, npath[-1])
newpath = os.path.join(path, new_name)
os.rename(oldpath, newpath)
store_path = npath[:-1]
store_path.append(new_name)
store_fname = os.path.join(*store_path)
return { 'name': new_name, 'datas_fname': new_name, 'store_fname': store_fname }
else:
raise TypeError("No %s storage" % boo.type)
def simple_move(self, cr, uid, file_node, ndir_bro, context=None):
""" A preparation for a file move.
It will not affect the database, but merely check and perhaps
move the realstore file.
@param ndir_bro a browse object of document.directory, where this
file should move to.
@return the dict of values that can safely be be stored in the db.
"""
sbro = self.browse(cr, uid, file_node.storage_id, context=context)
assert sbro, "The file #%d didn't provide storage" % file_node.file_id
if not sbro.online:
raise IOError(errno.EREMOTE, 'medium offline')
if sbro.readonly:
raise IOError(errno.EPERM, "Readonly medium")
par = ndir_bro
psto = None
while par:
if par.storage_id:
psto = par.storage_id.id
break
par = par.parent_id
if file_node.storage_id != psto:
self._doclog.debug('Cannot move file %r from %r to %r', file_node, file_node.parent, ndir_bro.name)
raise NotImplementedError('Cannot move files between storage media')
if sbro.type in ('filestore', 'db', 'db64'):
# nothing to do for a rename, allow to change the db field
return { 'parent_id': ndir_bro.id }
elif sbro.type == 'realstore':
raise NotImplementedError("Cannot move in realstore, yet") # TODO
fname = fil_bo.store_fname
if not fname:
return ValueError("Tried to rename a non-stored file")
path = storage_bo.path
oldpath = os.path.join(path, fname)
for ch in ('*', '|', "\\", '/', ':', '"', '<', '>', '?', '..'):
if ch in new_name:
raise ValueError("Invalid char %s in name %s" %(ch, new_name))
file_node.fix_ppath(cr, ira)
npath = file_node.full_path() or []
dpath = [path,]
dpath.extend(npath[:-1])
dpath.append(new_name)
newpath = os.path.join(*dpath)
# print "old, new paths:", oldpath, newpath
os.rename(oldpath, newpath)
return { 'name': new_name, 'datas_fname': new_name, 'store_fname': new_name }
else:
raise TypeError("No %s storage" % boo.type)
document_storage()
+59 -42
View File
@@ -16,7 +16,7 @@
<field name="online"/>
<field name="readonly"/>
</group>
<group colspan="2" attrs="{'invisible':[('type','=','db')]}">
<group colspan="2" attrs="{'invisible':[('type','in',['db', 'db64'])]}">
<field name="path"/>
</group>
</form>
@@ -61,6 +61,7 @@
name="Storage Media"
action="action_document_storage_form"
id="menu_document_storage_media"
groups="base.group_extended"
parent="menu_document_management_configuration"/>
<record model="ir.ui.view" id="view_document_directory_form">
@@ -68,27 +69,35 @@
<field name="model">document.directory</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Directories">
<form string="Directories" col="6">
<field name="name" select="1" colspan="4"/>
<field name="user_id"/>
<field name="parent_id"/>
<field name="storage_id" />
<field name="storage_id" widget="selection" />
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
<notebook colspan="4">
<page string="Definition">
<separator string="Directory Type" colspan="4"/>
<field name="type"/>
<field name="ressource_type_id" on_change="onchange_content_id(ressource_type_id)" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/>
<newline/>
<field name="domain" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/>
<field name="ressource_tree"/>
<field name="resource_field" domain="[('model_id','=',ressource_type_id), ('ttype', 'in', ('char', 'selection', 'date', 'datetime'))]"/>
<field name="ressource_id" select="2" readonly="1"/>
<field name="ressource_parent_type_id"/>
<group colspan="4" col="4" attrs="{'invisible': [('type','!=','ressource')]}">
<field name="ressource_type_id" on_change="onchange_content_id(ressource_type_id)"
attrs="{'required': [('type','=','ressource')] }"/>
<field name="resource_find_all" groups="base.group_extended" />
<newline/>
<field name="resource_field" domain="[('model_id','=',ressource_type_id), ('ttype', 'in', ('char', 'selection', 'date', 'datetime'))]"/>
<field name="ressource_tree"/>
<newline/>
<field name="domain" attrs="{'required': [('type','=','ressource')], 'readonly': [('type','=','static')]}"/>
</group>
<group colspan="4" col="4">
<field name="ressource_parent_type_id"/>
<field name="ressource_id" select="2" readonly="1"/>
</group>
</page>
<page string="Generated Files">
<page string="Generated Files" groups="base.group_extended">
<label colspan="4" string="For each entry here, virtual files will appear in this folder." />
<field name="content_ids" nolabel="1" colspan="4" attrs="{'readonly': [('ressource_type_id','=',False)]}">
<field name="content_ids" nolabel="1" colspan="4" >
<form string="Contents">
<field name="name"/>
<field name="sequence"/>
@@ -108,6 +117,7 @@
</field>
</page>
<page string="Dynamic context" groups="base.group_extended">
<label colspan="4" string="Define words in the context, for all child directories and files" />
<field name="dctx_ids" nolabel="1" colspan="4">
<tree string="Fields" editable="bottom">
<field name="field"/>
@@ -120,6 +130,8 @@
</field>
</page>
<page string="Security">
<label colspan="4" string="Only members of these groups will have access to this directory and its files." />
<label colspan="4" string="These groups, however, do NOT apply to children directories, which must define their own groups." />
<field name="group_ids" colspan="4" nolabel="1"/>
</page>
</notebook>
@@ -213,44 +225,46 @@
<field name="arch" type="xml">
<form string="Documents">
<group colspan="4" col="6">
<field name="name" select="1" />
<field name="type"/>
<field name="name" select="1" colspan="4" />
<field name="parent_id"/>
<newline/>
<field name="user_id"/>
<field name="company_id" groups="base.group_multi_company" widget="selection"/>
</group>
<notebook colspan="4">
<page string="Attachment">
<group col="2" colspan="2">
<separator string="Data" colspan="2"/>
<field name="type"/>
<newline />
<group col="2" colspan="2" attrs="{'invisible':[('type','=','url')]}">
<field name="datas" filename="datas_fname"/>
<field name="datas_fname" select="1"/>
<field name="datas" filename="datas_fname"/>
<field name="datas_fname" select="1"/>
</group>
<group col="2" colspan="2" attrs="{'invisible':[('type','=','binary')]}">
<field name="url" widget="url"/>
<field name="url" widget="url"/>
</group>
</group>
<group col="2" colspan="2">
<separator string="Relation" colspan="2"/>
<field name="res_name" readonly="1"/>
<field name="partner_id"/>
<field name="user_id"/>
</group>
<group col="4" colspan="4">
<separator string="History" colspan="4"/>
<group col="4" colspan="4">
<field name="create_uid"/>
<field name="create_date"/>
</group>
<group col="4" colspan="4">
<field name="write_uid"/>
<field name="write_date"/>
<field name="res_id" invisible="True"/>
<group col="2" colspan="2" attrs="{'invisible': [('res_id','=',0)]}">
<separator string="Attached To" colspan="2"/>
<field name="res_model" readonly="True" invisible="True"/>
<field name="res_name" readonly="1"/>
</group>
<separator string="Related to" colspan="2"/>
<field name="partner_id"/>
</group>
<group col="2" colspan="2" groups="base.group_extended">
<separator string="Created" colspan="2"/>
<field name="create_uid" readonly="1"/>
<field name="create_date" readonly="1"/>
</group>
<group col="2" colspan="2" groups="base.group_extended">
<separator string="Modified" colspan="2"/>
<field name="write_uid" readonly="1"/>
<field name="write_date" readonly="1"/>
</group>
</page>
<page string="Security">
<field name="group_ids" colspan="4" nolabel="1"/>
</page>
<page string="Indexed Content" groups="base.group_extended">
<field name="index_content" colspan="4" nolabel="1"/>
@@ -269,11 +283,13 @@
<field name="type">search</field>
<field name="arch" type="xml">
<search string="IR Attachment">
<filter icon="terp-go-month" string="Recent"
help="less 1 month modified/created attachments"
<filter icon="terp-go-month" string="Recent Month"
help="Attachment modified/created last 30 days"
domain="[('create_date','&lt;=', time.strftime('%%Y-%%m-%%d')),('create_date','&gt;',(datetime.date.today()-datetime.timedelta(days=30)).strftime('%%Y-%%m-%%d'))]"
/>
<separator orientation="vertical"/>
<field name="name"/>
<field name="parent_id" />
<field name="user_id">
<filter icon="terp-personal"
domain="[('user_id','=', False)]"
@@ -284,9 +300,9 @@
<newline/>
<group expand="0" string="Group By...">
<filter string="Partner" icon="terp-personal" domain="[]"
context="{'group_by':'partner_id'}" />
context="{'group_by':'partner_id'}" groups="base.group_extended"/>
<filter string="Directory" icon="terp-folder-green" domain="[]" context="{'group_by':'parent_id'}"/>
<filter string="Type" domain="[]" context="{'group_by':'type'}"/>
<filter string="Type" domain="[]" context="{'group_by':'type'}" groups="base.group_extended"/>
<filter string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'user_id'}"/>
<filter string="Company" icon="terp-personal" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
</group>
@@ -301,11 +317,12 @@
<field name="arch" type="xml">
<tree colors="blue:type in ('url',)">
<field name="name"/>
<field name="type"/>
<field name="datas_fname"/>
<field name="parent_id" />
<field name="user_id"/>
<field name="create_date"/>
<field name="write_date"/>
<field name="partner_id" groups="base.group_extended" />
<field name="type" groups="base.group_extended"/>
</tree>
</field>
+682 -176
View File
File diff suppressed because it is too large Load Diff
+11 -11
View File
@@ -33,22 +33,22 @@
</field>
</record>
<record id="view_report_document_user_search" model="ir.ui.view">
<record id="view_report_document_user_search" model="ir.ui.view">
<field name="name">report.document.user.search</field>
<field name="model">report.document.user</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="All users files">
<group col="12" colspan="4">
<group col="12" colspan="4">
<separator orientation="vertical"/>
<filter icon="terp-go-year" string="This Year" domain="[('name','=',time.localtime()[0])]" help="All Months Files"/>
<filter icon="terp-go-month" string="This Month" domain="[('month','=',time.localtime()[1])]" help="This Months Files"/>
<separator orientation="vertical"/>
<field name="name" select="1"/>
<field name="month" select="1"/>
<field name="user" select="1"/>
<field name="directory" select="1"/>
</group>
<filter icon="terp-go-year" string="This Year" domain="[('name','=',time.localtime()[0])]" help="All Months Files"/>
<filter icon="terp-go-month" string="This Month" domain="[('month','=',time.localtime()[1])]" help="This Months Files"/>
<separator orientation="vertical"/>
<field name="name" select="1"/>
<field name="month" select="1"/>
<field name="user" select="1"/>
<field name="directory" select="1"/>
</group>
</search>
</field>
</record>
@@ -61,7 +61,7 @@
<field name="res_model">report.document.user</field>
<field name="view_type">form</field>
<field name="view_mode">tree</field>
<field name="context">{"search_default_user":uid}</field>
<field name="context">{'search_default_user': 'user_id'}</field>
<field name="search_view_id" ref="view_report_document_user_search"/>
</record>
+72
View File
@@ -0,0 +1,72 @@
-
In order to test the document management
-
I make sure the default installation has some storage and root directory.
-
!assert {model: document.storage, id: storage_default }:
- id != False
-
!assert {model: document.directory, id: dir_root}:
- storage_id != False
-
I create a "Testing" folder where all the test data will go.
-
!record {model: document.directory, id: dir_tests }:
name: 'Testing'
parent_id: dir_root
-
I create an attachment into the root folder (w. empty fields, test that
defaults work)
-
!record {model: ir.attachment, id: file_test1 }:
name: Test file
-
I delete the attachment from the root folder
-
!delete {model: ir.attachment, id: file_test1, search: }
-
I create an attachment into the Testing folder.
-
!record {model: ir.attachment, id: file_test2 }:
name: Test file 2
parent_id: dir_tests
-
I update the attachment with data, namely "abcd"
-
!record {model: ir.attachment, id: file_test2 }:
datas: "abcd"
-
I test that the datas of the attachment are correct
-
!assert {model: ir.attachment, id: file_test2 }:
- datas == "abcd\n"
- file_size == 5
- file_type == 'text/plain'
-
I rename the attachment.
-
!record {model: ir.attachment, id: file_test2 }:
name: Test renamed 2
-
I search the testing folder for attachments.
-
!python {model: ir.attachment}: |
ids = self.search(cr, uid, [('parent_id.name','=', 'Testing'), ('name','=','Test renamed 2')])
assert ids == [ ref("file_test2") ], ids
-
I create an attachment to a 3rd resource, eg. a res.country
-
!record {model: ir.attachment, id: attach_3rd }:
name: 'Res country attachment.txt'
parent_id: dir_tests
datas: 'defg'
res_model: res.country
res_id: !eval ref("base.za")
-
I search for the res.country attachment
-
!python {model: ir.attachment}: |
ids = self.search(cr, uid, [('res_model', '=', 'res.country'), ('res_id', '=', ref("base.za"))])
assert ids == [ ref("attach_3rd")], ids
-
!delete {model: ir.attachment, id: attach_3rd, search: }
+7 -4
View File
@@ -26,20 +26,23 @@
'category': 'Generic Modules/Others',
'description': """This is a support FTP Interface with document management system.
With this module you would not only be able to access documents through open erp
but you would also be able to connect with them through the file system using the FTP protocol.
but you would also be able to connect with them through the file system using the
a FTP client.
""",
'author': 'Tiny',
'website': 'http://www.openerp.com',
'depends': ['base', 'document'],
'init_xml': [],
'update_xml': [
'wizard/ftp_configuration_view.xml',
'wizard/ftp_browse_view.xml',
'wizard/ftp_configuration_view.xml',
'wizard/ftp_browse_view.xml',
'security/ir.model.access.csv'
],
'demo_xml': [],
'test': [
'test/document_ftp_test.yml',
'test/document_ftp_test2.yml',
# 'test/document_ftp_test2.yml',
'test/document_ftp_test4.yml',
],
'installable': True,
'active': False,
File diff suppressed because it is too large Load Diff
+249 -248
View File
@@ -221,6 +221,33 @@ def _strerror(err):
else:
return err.strerror
def _to_unicode(s):
try:
return s.decode('utf-8')
except UnicodeError:
pass
try:
return s.decode('latin')
except UnicodeError:
pass
try:
return s.encode('ascii')
except UnicodeError:
return s
def _to_decode(s):
try:
return s.encode('utf-8')
except UnicodeError:
pass
try:
return s.encode('latin')
except UnicodeError:
pass
try:
return s.decode('ascii')
except UnicodeError:
return s
# --- library defined exceptions
@@ -793,7 +820,7 @@ class DTPHandler(asyncore.dispatcher):
if self.transfer_finished:
self.cmd_channel.respond("226 Transfer complete.")
if self.file_obj:
fname = self.cmd_channel.fs.fs2ftp(self.file_obj.name)
fname = self.file_obj.name
self.cmd_channel.log('"%s" %s.' %(fname, action))
else:
tot_bytes = self.get_transmitted_bytes()
@@ -1138,19 +1165,11 @@ class AbstractedFS:
# note: the following operations are no more blocking
def get_list_dir(self, path):
def get_list_dir(self, datacr):
""""Return an iterator object that yields a directory listing
in a form suitable for LIST command.
"""
if self.isdir(path):
listing = self.listdir(path)
listing.sort()
return self.format_list(path, listing)
# if path is a file or a symlink we return information about it
else:
basedir, filename = os.path.split(path)
self.lstat(path) # raise exc in case of problems
return self.format_list(basedir, [filename])
raise DeprecationWarning()
def get_stat_dir(self, rawline):
"""Return an iterator object that yields a list of files
@@ -1335,6 +1354,11 @@ class AbstractedFS:
# --- FTP
class FTPExceptionSent(Exception):
"""An FTP exception that FTPHandler has processed
"""
pass
class FTPHandler(asynchat.async_chat):
"""Implements the FTP server Protocol Interpreter (see RFC-959),
handling commands received from the client on the control channel.
@@ -1434,6 +1458,14 @@ class FTPHandler(asynchat.async_chat):
self._epsvall = False
self.__in_dtp_queue = None
self.__out_dtp_queue = None
self.__errno_responses = {
errno.EPERM: 553,
errno.EINVAL: 504,
errno.ENOENT: 550,
errno.EREMOTE: 450,
errno.EEXIST: 521,
}
# mlsx facts attributes
self.current_facts = ['type', 'perm', 'size', 'modify']
@@ -1591,26 +1623,8 @@ class FTPHandler(asynchat.async_chat):
def __check_path(self, cmd, line):
"""Check whether a path is valid."""
# For the following commands we have to make sure that the real
# path destination belongs to the user's root directory.
# If provided path is a symlink we follow its final destination
# to do so.
if cmd in ('APPE','CWD','DELE','MDTM','NLST','MLSD','MLST','RETR',
'RMD','SIZE','STOR','XCWD','XRMD'):
datacr = None
datacr = self.fs.get_cr(line)
try:
if not self.fs.validpath(self.fs.ftp2fs(line, datacr)):
line = self.fs.ftpnorm(line)
err = '"%s" points to a path which is outside ' \
"the user's root directory" %line
self.respond("550 %s." %err)
self.log('FAIL %s "%s". %s.' %(cmd, line, err))
self.fs.close_cr(datacr)
return False
except:
pass
self.fs.close_cr(datacr)
# Always true, we will only check later, once we have a cursor
return True
def __check_perm(self, cmd, line, datacr):
@@ -1623,6 +1637,7 @@ class FTPHandler(asynchat.async_chat):
'RNFR':'f',
'MKD':'m', 'XMKD':'m',
'STOR':'w'}
raise NotImplementedError
if cmd in map:
if cmd == 'STAT' and not line:
return True
@@ -1813,6 +1828,58 @@ class FTPHandler(asynchat.async_chat):
# --- connection
def try_as_current_user(self, function, args=None, kwargs=None, line=None, errno_resp=None):
"""run function as current user, auto-respond in exceptions
@param args,kwargs the arguments, in list and dict respectively
@param errno_resp a dictionary of responses to IOError, OSError
"""
if errno_resp:
eresp = self.__errno_responses.copy()
eresp.update(errno_resp)
else:
eresp = self.__errno_responses
uline = ''
if line:
uline = ' "%s"' % _to_unicode(line)
try:
if args is None:
args = ()
if kwargs is None:
kwargs = {}
return self.run_as_current_user(function, *args, **kwargs)
except NotImplementedError, err:
cmdname = function.__name__
why = err.args[0] or 'Not implemented'
self.log('FAIL %s() not implemented: %s.' %(cmdname, why))
self.respond('502 %s.' %why)
raise FTPExceptionSent(why)
except EnvironmentError, err:
cmdname = function.__name__
try:
logline(traceback.format_exc())
except Exception:
pass
ret_code = eresp.get(err.errno, '451')
why = (err.strerror) or 'Error in command'
self.log('FAIL %s() %s errno=%s: %s.' %(cmdname, uline, err.errno, why))
self.respond('%s %s.' % (str(ret_code), why))
raise FTPExceptionSent(why)
except Exception, e:
cmdname = function.__name__
try:
logerror(traceback.format_exc())
except Exception:
pass
why = (err.args and err.args[0]) or 'Exception'
self.log('FAIL %s() %s Exception: %s.' %(cmdname, uline, why))
self.respond('451 %s.' % why)
raise FTPExceptionSent(why)
def get_crdata2(self, *args, **kwargs):
return self.try_as_current_user(self.fs.get_crdata, args, kwargs, line=args[0])
def _make_eport(self, ip, port):
"""Establish an active data channel with remote client which
issued a PORT or EPRT command.
@@ -2032,56 +2099,58 @@ class FTPHandler(asynchat.async_chat):
# - Some older FTP clients erroneously issue /bin/ls-like LIST
# formats in which case we fall back on cwd as default.
if not line or line.lower() in ('-a', '-l', '-al', '-la'):
line = self.fs.cwd
line = ''
datacr = None
try:
data = None
data = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, data)
line = self.fs.ftpnorm(line)
iterator = self.run_as_current_user(self.fs.get_list_dir, path)
except OSError, err:
self.fs.close_cr(data)
why = _strerror(err)
self.log('FAIL LIST "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
self.fs.close_cr(data)
self.log('OK LIST "%s". Transfer starting.' %line)
datacr = self.get_crdata2(line, mode='list')
iterator = self.try_as_current_user(self.fs.get_list_dir, (datacr,))
except FTPExceptionSent:
self.fs.close_cr(datacr)
return
try:
self.log('OK LIST "%s". Transfer starting.' % line)
producer = BufferedIteratorProducer(iterator)
self.push_dtp_data(producer, isproducer=True)
finally:
self.fs.close_cr(datacr)
def ftp_NLST(self, line):
"""Return a list of files in the specified directory in a
compact form to the client.
"""
if not line:
line = self.fs.cwd
line = ''
datacr = None
try:
data = None
data = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, data)
line = self.fs.ftpnorm(line)
if self.fs.isdir(path):
listing = self.run_as_current_user(self.fs.listdir, path)
listing = map(lambda x:os.path.split(x.path)[1], listing)
datacr = self.get_crdata2(line, mode='list')
if not datacr:
datacr = ( None, None, None )
if self.fs.isdir(datacr[1]):
nodelist = self.try_as_current_user(self.fs.listdir, (datacr,))
else:
# if path is a file we just list its name
self.fs.lstat(path) # raise exc in case of problems
basedir, filename = os.path.split(line)
listing = [filename]
except OSError, err:
self.fs.close_cr(data)
why = _strerror(err)
self.log('FAIL NLST "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
self.fs.close_cr(data)
data = ''
if listing:
listing.sort()
data = '\r\n'.join(listing) + '\r\n'
self.log('OK NLST "%s". Transfer starting.' %line)
self.push_dtp_data(data)
nodelist = [datacr[1],]
listing = []
for nl in nodelist:
if isinstance(nl.path, (list, tuple)):
listing.append(nl.path[-1])
else:
listing.append(nl.path) # assume string
except FTPExceptionSent:
self.fs.close_cr(datacr)
return
self.fs.close_cr(datacr)
data = ''
if listing:
listing.sort()
data = ''.join([ _to_decode(x) + '\r\n' for x in listing ])
self.log('OK NLST "%s". Transfer starting.' %line)
self.push_dtp_data(data)
# --- MLST and MLSD commands
@@ -2096,22 +2165,17 @@ class FTPHandler(asynchat.async_chat):
"""
# if no argument, fall back on cwd as default
if not line:
line = self.fs.cwd
line = ''
datacr = None
try:
datacr = None
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
basedir, basename = os.path.split(path)
datacr = self.get_crdata2(line, mode='list')
perms = self.authorizer.get_perms(self.username)
iterator = self.run_as_current_user(self.fs.format_mlsx, basedir,
[basename], perms, self.current_facts, ignore_err=False)
iterator = self.try_as_current_user(self.fs.format_mlsx, (datacr[0], datacr[1].parent,
[datacr[1],], perms, self.current_facts), {'ignore_err':False})
data = ''.join(iterator)
except OSError, err:
except FTPExceptionSent:
self.fs.close_cr(datacr)
why = _strerror(err)
self.log('FAIL MLST "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
return
else:
self.fs.close_cr(datacr)
# since TVFS is supported (see RFC-3659 chapter 6), a fully
@@ -2129,28 +2193,25 @@ class FTPHandler(asynchat.async_chat):
"""
# if no argument, fall back on cwd as default
if not line:
line = self.fs.cwd
line = ''
datacr = None
try:
datacr = None
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
datacr = self.get_crdata2(line, mode='list')
# RFC-3659 requires 501 response code if path is not a directory
if not self.fs.isdir(path):
if not self.fs.isdir(datacr[1]):
err = 'No such directory'
self.log('FAIL MLSD "%s". %s.' %(line, err))
self.respond("501 %s." %err)
return
listing = self.run_as_current_user(self.fs.listdir, path)
except OSError, err:
listing = self.try_as_current_user(self.fs.listdir, (datacr,))
except FTPExceptionSent:
self.fs.close_cr(datacr)
why = _strerror(err)
self.log('FAIL MLSD "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
return
else:
self.fs.close_cr(datacr)
perms = self.authorizer.get_perms(self.username)
iterator = self.fs.format_mlsx(path, listing, perms,
iterator = self.fs.format_mlsx(datacr[0], datacr[1], listing, perms,
self.current_facts)
producer = BufferedIteratorProducer(iterator)
self.log('OK MLSD "%s". Transfer starting.' %line)
@@ -2160,23 +2221,12 @@ class FTPHandler(asynchat.async_chat):
"""Retrieve the specified file (transfer from the server to the
client)
"""
datacr = None
try:
datacr = None
datacr = self.fs.get_cr(line)
file = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
fd = self.run_as_current_user(self.fs.open, file, 'rb')
except OSError, err:
datacr = self.get_crdata2(line, mode='file')
fd = self.try_as_current_user(self.fs.open, (datacr, 'rb'))
except FTPExceptionSent:
self.fs.close_cr(datacr)
why = _strerror(err)
self.log('FAIL RETR "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
return
except IOError, err:
self.fs.close_cr(datacr)
why = _strerror(err)
self.log('FAIL RETR "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
return
if self.restart_position:
@@ -2187,7 +2237,7 @@ class FTPHandler(asynchat.async_chat):
# the REST.
ok = 0
try:
assert not self.restart_position > self.fs.getsize(file)
assert not self.restart_position > self.fs.getsize(datacr)
fd.seek(self.restart_position)
ok = 1
except AssertionError:
@@ -2217,30 +2267,14 @@ class FTPHandler(asynchat.async_chat):
else:
cmd = 'STOR'
line = self.fs.ftpnorm(line)
basedir,basename = os.path.split(line)
datacr = None
try:
datacr = self.fs.get_cr(line)
file = self.fs.ftp2fs(basedir, datacr)
except OSError, err:
datacr = self.get_crdata2(line,mode='create')
if self.restart_position:
mode = 'r+'
fd = self.try_as_current_user(self.fs.create, (datacr, datacr[2], mode + 'b'))
except FTPExceptionSent:
self.fs.close_cr(datacr)
why = _strerror(err)
self.log('FAIL %s "%s". %s.' %(cmd, line, why))
self.respond('550 %s.' %why)
return
if self.restart_position:
mode = 'r+'
try:
fd = self.run_as_current_user(self.fs.create, file, basename, mode + 'b')
except IOError, err:
self.fs.close_cr(datacr)
why = _strerror(err)
self.log('FAIL %s "%s". %s.' %(cmd, line, why))
self.respond('550 %s.' %why)
return
if self.restart_position:
@@ -2251,7 +2285,7 @@ class FTPHandler(asynchat.async_chat):
# specified in the REST.
ok = 0
try:
assert not self.restart_position > self.fs.getsize(self.fs.ftp2fs(line, datacr))
assert not self.restart_position > self.fs.getsize(datacr)
fd.seek(self.restart_position)
ok = 1
except AssertionError:
@@ -2293,21 +2327,21 @@ class FTPHandler(asynchat.async_chat):
self.respond("450 Can't STOU while REST request is pending.")
return
datacr = None
datacr = self.fs.get_cr(line)
if line:
line = self.fs.ftpnorm(line)
basedir,prefix = os.path.split(line)
basedir = self.fs.ftp2fs(basedir, datacr)
#prefix = prefix + '.'
datacr = self.get_crdata2(line, mode='create')
# TODO
else:
# TODO
basedir = self.fs.ftp2fs(self.fs.cwd, datacr)
prefix = 'ftpd.'
try:
fd = self.run_as_current_user(self.fs.mkstemp, prefix=prefix,
dir=basedir)
except IOError, err:
fd = self.try_as_current_user(self.fs.mkstemp, kwargs={'prefix':prefix,
'dir': basedir}, line=line )
except FTPExceptionSent:
self.fs.close_cr(datacr)
return
except IOError, err: # TODO
# hitted the max number of tries to find out file with
# unique name
if err.errno == errno.EEXIST:
@@ -2500,33 +2534,25 @@ class FTPHandler(asynchat.async_chat):
def ftp_PWD(self, line):
"""Return the name of the current working directory to the client."""
self.respond('257 "%s" is the current directory.' %self.fs.cwd)
cwd = self.fs.get_cwd()
self.respond('257 "%s" is the current directory.' % cwd)
def ftp_CWD(self, line):
"""Change the current working directory."""
# TODO: a lot of FTP servers go back to root directory if no
# check: a lot of FTP servers go back to root directory if no
# arg is provided but this is not specified in RFC-959.
# Search for official references about this behaviour.
if not line:
line = '/'
datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
self.run_as_current_user(self.fs.chdir, path)
except OSError, err:
if err.errno==2:
why = 'Authentication Required or Failed'
self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
self.respond('530 %s.' %why)
else:
why = _strerror(err)
self.log('FAIL CWD "%s". %s.' %(self.fs.ftpnorm(line), why))
self.respond('550 %s.' %why)
else:
self.log('OK CWD "%s".' %self.fs.cwd)
self.respond('250 "%s" is the current directory.' %self.fs.cwd)
self.fs.close_cr(datacr)
datacr = self.get_crdata2(line,'cwd')
self.try_as_current_user(self.fs.chdir, (datacr,), line=line, errno_resp={2: 530})
cwd = self.fs.get_cwd()
self.log('OK CWD "%s".' % cwd)
self.respond('250 "%s" is the current directory.' % cwd)
except FTPExceptionSent:
return
finally:
self.fs.close_cr(datacr)
def ftp_CDUP(self, line):
"""Change into the parent directory."""
@@ -2554,20 +2580,17 @@ class FTPHandler(asynchat.async_chat):
"""
datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
if self.fs.isdir(path):
why = "%s is not retrievable" %line
self.log('FAIL SIZE "%s". %s.' %(line, why))
self.respond("550 %s." %why)
self.fs.close_cr(datacr)
return
size = self.run_as_current_user(self.fs.getsize, path)
except OSError, err:
why = _strerror(err)
self.log('FAIL SIZE "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
datacr = self.get_crdata2(line, mode='file')
#if self.fs.isdir(datacr[1]):
# why = "%s is not retrievable" %line
# self.log('FAIL SIZE "%s". %s.' %(line, why))
# self.respond("550 %s." %why)
# self.fs.close_cr(datacr)
# return
size = self.try_as_current_user(self.fs.getsize,(datacr,), line=line)
except FTPExceptionSent:
self.fs.close_cr(datacr)
return
else:
self.respond("213 %s" %size)
self.log('OK SIZE "%s".' %line)
@@ -2578,40 +2601,35 @@ class FTPHandler(asynchat.async_chat):
3307 style timestamp (YYYYMMDDHHMMSS) as defined in RFC-3659.
"""
datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
if not self.fs.isfile(self.fs.realpath(path)):
why = "%s is not retrievable" %line
self.log('FAIL MDTM "%s". %s.' %(line, why))
self.respond("550 %s." %why)
self.fs.close_cr(datacr)
return
lmt = self.run_as_current_user(self.fs.getmtime, path)
except OSError, err:
why = _strerror(err)
self.log('FAIL MDTM "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
if line.find('/', 1) < 0:
# root or db, just return local
lmt = None
else:
datacr = self.get_crdata2(line)
if not datacr:
raise IOError(errno.ENOENT, "%s is not retrievable" %line)
#if not self.fs.isfile(datacr[1]):
# raise IOError(errno.EPERM, "%s is not a regular file" % line)
lmt = self.try_as_current_user(self.fs.getmtime, (datacr,), line=line)
lmt = time.strftime("%Y%m%d%H%M%S", time.localtime(lmt))
self.respond("213 %s" %lmt)
self.log('OK MDTM "%s".' %line)
self.fs.close_cr(datacr)
except FTPExceptionSent:
return
finally:
self.fs.close_cr(datacr)
def ftp_MKD(self, line):
"""Create the specified directory."""
datacr = None
line = self.fs.ftpnorm(line)
basedir,basename = os.path.split(line)
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(basedir, datacr)
self.run_as_current_user(self.fs.mkdir, path, basename)
except OSError, err:
why = _strerror(err)
self.log('FAIL MKD "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
datacr = self.get_crdata2(line, mode='create')
self.try_as_current_user(self.fs.mkdir, (datacr, datacr[2]), line=line)
except FTPExceptionSent:
self.fs.close_cr(datacr)
return
else:
self.log('OK MKD "%s".' %line)
self.respond("257 Directory created.")
@@ -2621,40 +2639,30 @@ class FTPHandler(asynchat.async_chat):
"""Remove the specified directory."""
datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
if self.fs.realpath(path) == self.fs.realpath(self.fs.root):
datacr = self.get_crdata2(line, mode='delete')
if not datacr[1]:
msg = "Can't remove root directory."
self.respond("550 %s" %msg)
self.respond("553 %s" %msg)
self.log('FAIL MKD "/". %s' %msg)
self.fs.close_cr(datacr)
return
self.run_as_current_user(self.fs.rmdir, path)
except OSError, err:
why = _strerror(err)
self.log('FAIL RMD "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
self.try_as_current_user(self.fs.rmdir, (datacr,), line=line)
self.log('OK RMD "%s".' %line)
self.respond("250 Directory removed.")
except FTPExceptionSent:
pass
self.fs.close_cr(datacr)
def ftp_DELE(self, line):
"""Delete the specified file."""
datacr = None
try:
datacr = self.fs.get_cr(line)
path = self.fs.ftp2fs(line, datacr)
line = self.fs.ftpnorm(line)
self.run_as_current_user(self.fs.remove, path)
except OSError, err:
why = _strerror(err)
self.log('FAIL DELE "%s". %s.' %(line, why))
self.respond('550 %s.' %why)
else:
datacr = self.get_crdata2(line, mode='delete')
self.try_as_current_user(self.fs.remove, (datacr,), line=line)
self.log('OK DELE "%s".' %line)
self.respond("250 File removed.")
except FTPExceptionSent:
pass
self.fs.close_cr(datacr)
def ftp_RNFR(self, line):
@@ -2662,18 +2670,16 @@ class FTPHandler(asynchat.async_chat):
here, see RNTO command)"""
datacr = None
try:
datacr = self.fs.get_cr(line)
line = self.fs.ftpnorm(line)
path = self.fs.ftp2fs(line, datacr)
if not self.fs.lexists(path):
datacr = self.get_crdata2(line, mode='rfnr')
if not datacr[1]:
self.respond("550 No such file or directory.")
elif self.fs.realpath(path) == self.fs.realpath(self.fs.root):
self.respond("550 Can't rename the home directory.")
elif not datacr[1]:
self.respond("553 Can't rename the home directory.")
else:
self.fs.rnfr = line
self.fs.rnfr = datacr[1]
self.respond("350 Ready for destination name.")
except:
self.respond("550 Can't find the file or directory.")
except FTPExceptionSent:
pass
self.fs.close_cr(datacr)
def ftp_RNTO(self, line):
@@ -2685,22 +2691,17 @@ class FTPHandler(asynchat.async_chat):
return
datacr = None
try:
try:
datacr = self.fs.get_cr(line)
src = self.fs.ftp2fs(self.fs.rnfr, datacr)
line = self.fs.ftpnorm(line)
basedir,basename = os.path.split(line)
dst = self.fs.ftp2fs(basedir, datacr)
self.run_as_current_user(self.fs.rename, src, dst,basename)
except OSError, err:
why = _strerror(err)
self.log('FAIL RNFR/RNTO "%s ==> %s". %s.' \
%(self.fs.ftpnorm(self.fs.rnfr), line, why))
self.respond('550 %s.' %why)
else:
self.log('OK RNFR/RNTO "%s ==> %s".' \
%(self.fs.ftpnorm(self.fs.rnfr), line))
self.respond("250 Renaming ok.")
datacr = self.get_crdata2(line,'create')
oldname = self.fs.rnfr.path
if isinstance(oldname, (list, tuple)):
oldname = '/'.join(oldname)
self.try_as_current_user(self.fs.rename, (self.fs.rnfr, datacr), line=line)
self.fs.rnfr = None
self.log('OK RNFR/RNTO "%s ==> %s".' % \
(_to_unicode(oldname), _to_unicode(line)))
self.respond("250 Renaming ok.")
except FTPExceptionSent:
pass
finally:
self.fs.rnfr = None
self.fs.close_cr(datacr)
@@ -2789,9 +2790,9 @@ class FTPHandler(asynchat.async_chat):
datacr = None
try:
datacr = self.fs.get_cr(line)
iterator = self.run_as_current_user(self.fs.get_stat_dir, line, datacr)
except OSError, err:
self.respond('550 %s.' %_strerror(err))
iterator = self.try_as_current_user(self.fs.get_stat_dir, (line, datacr), line=line)
except FTPExceptionSent:
pass
else:
self.push('213-Status of "%s":\r\n' %self.fs.ftpnorm(line))
self.push_with_producer(BufferedIteratorProducer(iterator))
@@ -0,0 +1,220 @@
-
In order to test the document_ftp functionality
-
I open the 8021 port and see for ftp presence there
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_plain_ftp(timeout=2.0)
assert ftp.sock and (ftp.lastresp == '220'), ftp.lastresp
-
I read the list of databases at port 8021 and confirm our db is
there
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_login(cr, uid, self)
assert cr.dbname in ftp.nlst("/")
-
I try to locate the default "Documents" folder in the db.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_login(cr, uid, self)
ftp.cwd('Documents')
-
I create a "test.txt" file at the server (directly). The file
should have the "abcd" content
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test.txt', fdata)
-
I look for the "test.txt" file at the server
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
assert ftp.nlst("test.txt") == ['test.txt']
-
I check that the content of "test.txt" is "abcd"
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
assert te.get_ftp_fulldata(ftp, "test.txt") == 'abcd'
-
I append the string 'defgh' into "test.txt"
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('defgh')
ftp.storbinary('APPE test.txt', fdata)
-
I check that the content of "text.txt" is 'abcddefgh'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
assert te.get_ftp_fulldata(ftp, "test.txt") == 'abcddefgh'
-
I try to cd into an non-existing folder 'Not-This'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
import ftplib
ftp = te.get_ftp_login(cr, uid, self)
try:
ftp.cwd('/Not-This')
assert False, "We should't be able to change here"
except ftplib.error_perm:
pass
except OSError, err:
assert err.errno == 2, err.errno
-
I create a "test2.txt" file through FTP.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test2.txt', fdata)
-
I look for the "test2.txt" file at the server
-
!python {model: ir.attachment }: |
ids = self.search(cr, uid, [('name', '=', 'test2.txt')])
assert ids
-
I delete the "test2.txt" file using FTP.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.delete('test2.txt')
-
I check at the server that test2.txt is deleted
-
!python {model: ir.attachment }: |
ids = self.search(cr, uid, [('name', '=', 'test2.txt')])
assert not ids
-
I create a test2.txt file again.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test2.txt', fdata)
-
I delete the test2.txt from the server (RPC).
-
!delete { model: ir.attachment, id:, search: "[('name','=','test2.txt')]" }
-
I check through FTP that test2.txt does not appear.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
assert ftp.nlst("test2.txt") == []
-
I create a "test-name.txt" file
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
fdata = StringIO('abcd')
ftp.storbinary('STOR test-name.txt', fdata)
-
I rename the "test-name.txt" file through ftp.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.rename("test-name.txt", "test-renamed.txt")
-
I check that test-name.txt has been renamed.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from ftplib import error_perm
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
try:
res = ftp.nlst("test-name.txt")
assert res == []
except error_perm, e:
pass
assert ftp.nlst("test-renamed.txt") == ['test-renamed.txt']
-
I create a new folder 'Test-Folder2' through FTP
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.mkd("Test-Folder2")
-
I create a file 'test3.txt' at the 'Test-Folder2'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2')
fdata = StringIO('abcd')
ftp.storbinary('STOR test3.txt', fdata)
-
I try to retrieve test3.txt
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2')
assert ftp.nlst("test3.txt") == ['test3.txt']
-
I create a new folder, 'Test-Folder3', through FTP
I try to move test3.txt to 'Test-Folder3'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents')
ftp.mkd("Test-Folder2")
# TODO move
-
I remove the 'Test-Folder3'
-
I check that test3.txt is removed.
-
I create 200 files through FTP
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2')
fdata = StringIO('abcd')
# TODO speed
for i in range(0, 200):
fdata.seek(0)
ftp.storbinary('STOR test-name%s.txt' %i, fdata)
-
I list the 200 files, check speed
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Test-Folder2')
# TODO speed
assert len(ftp.nlst()) > 200
-
I read the 200 files, check speed
# TODO
-
I move the 200 files to 'Test-Folder2'
# TODO
@@ -0,0 +1,82 @@
-
In order to check international character functionality
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_plain_ftp(timeout=1.0)
-
I create in the server a folder called 'Äïêéìáóôéêüò ÖÜêåëëïò'
-
!record {model: document.directory, id: dir_itests }:
name: 'Äïêéìáóôéêüò ÖÜêåëëïò'
parent_id: document.dir_root
-
And then I create another folder, under it, through FTP
-
!python {model: ir.attachment}: |
cr.commit()
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò')
ftp.mkd("ÖÜêåëëïò áðü êÜôù")
-
I check that this folder exists at the server
-
!assert {model: document.directory, id: , search: "[('name','=','ÖÜêåëëïò áðü êÜôù')]" }:
- parent_id != False
-
I login with FTP and check that 'Äïêéìáóôéêüò ÖÜêåëëïò' is there
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù')
-
I create a file named 'ÄïêéìÞ' into that folder
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù')
fdata = StringIO('êåßìåíï ìå utf-8')
ftp.storbinary('STOR ÄïêéìÞ.txt', fdata)
-
I remove the 'ÄïêéìÞ.txt' file
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/ÖÜêåëëïò áðü êÜôù')
ftp.delete('ÄïêéìÞ.txt')
-
I rename 'ÖÜêåëëïò áðü êÜôù' into 'Üëëïò'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò')
ftp.rename("ÖÜêåëëïò áðü êÜôù", "Üëëïò")
-
I place a file 'file Ö3' in 'Üëëïò'
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò')
fdata = StringIO('êé Üëëï êåßìåíï')
ftp.storbinary('STOR file Ö3.txt', fdata)
-
I rename the file into file+range(1..200) (large filename)
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò')
vuvuzela = 'b'+''.join('z' * 200)+'!'
ftp.rename("file Ö3.txt", vuvuzela)
-
I delete the file with the large name
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Äïêéìáóôéêüò ÖÜêåëëïò/Üëëïò')
vuvuzela = 'b'+''.join('z' * 200)+'!'
ftp.delete(vuvuzela)
@@ -0,0 +1,120 @@
-
In order to check dynamic folder functionality of document + FTP
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_plain_ftp(timeout=1.0)
- |
I create two partners 'Partner1' and 'Partner2'.
I create three partner categories: 'none', 'pat1' and 'all'
I attach Partner1 to pat1, Partner1+Partner2 to 'all'
-
!record {model: res.partner.category, id: tpat_categ_none }:
name: 'No partners'
-
!record {model: res.partner.category, id: tpat_categ_pat1 }:
name: 'Pat 1'
-
!record {model: res.partner.category, id: tpat_categ_all }:
name: 'All Partner1+2'
-
!record {model: res.partner, id: tpartner1 }:
name: Partner 1
category_id:
- tpat_categ_pat1
- tpat_categ_all
-
!record {model: res.partner, id: tpartner_2 }:
name: 'Partner 2'
category_id:
- tpat_categ_all
-
I create a resource folder of partners, by the (none, pat1, all)
categories.
-
!record {model: document.directory, id: dir_tests2 }:
name: Partners Testing
parent_id: document.dir_root
type: ressource
ressource_type_id: base.model_res_partner_category
domain: [] # TODO
-
I commit (because FTP operations are on different transaction)
-
!python {model: document.directory, id: }: |
cr.commit()
-
I browse through ftp in the resource folder, checking that three
categories are there.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Partners Testing')
dirs = ftp.nlst()
for dir in [ 'All Partner1+2', 'No partners', 'Pat 1' ]:
assert dir in dirs, "Dir %s not in folder" % dir
-
I create a 'partners' folder by the first resource one.
-
!record {model: document.directory, id: dir_respart1 }:
name: Partners of Test
parent_id: dir_tests2
type: ressource
ressource_type_id: base.model_res_partner
domain: "[('category_id','in',[active_id])]"
ressource_parent_type_id : base.model_res_partner_category
-
!python {model: document.directory, id: }: |
cr.commit()
-
I check through FTP that the correct partners are listed at each
'partners' folder.
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Partners Testing')
correct = { 'All Partner1+2': [ 'Partner 1', 'Partner 2' ],
'No partners': [],
'Pat 1': ['Partner 1',] }
for dir in correct:
res = ftp.nlst(dir+'/Partners of Test')
assert res == correct[dir], "Dir %s falsely contains %s" %(dir, res)
-
I create an ir.attachment, attached (not related) to Partner1
-
!record {model: ir.attachment, id: file_test1 }:
name: File of pat1
res_model: res.partner
res_id: !eval ref("tpartner1")
-
I check that pat1/Partner1 folder has the file.
I check that all/Partner1 folder has the file
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Partners Testing')
dirs = [ 'All Partner1+2', 'Pat 1' ]
for dir in dirs:
res = ftp.nlst(dir+'/Partners of Test/Partner 1')
assert 'File of pat1' in res, "Dir %s contains only %s" %(dir, res)
-
I place a file at the 'pat1'/Partner1 folder, through FTP
-
!python {model: ir.attachment}: |
from document_ftp import test_easyftp as te
from cStringIO import StringIO
ftp = te.get_ftp_folder(cr, uid, self, 'Documents/Partners Testing/Pat 1/Partners of Test/Partner 1')
fdata = StringIO('abcd')
ftp.storbinary('STOR pat1-dynamic.txt', fdata)
-
I check at the server that the file is attached to Partner1
-
!assert {model: ir.attachment, id: , search: "[('name','=','pat1-dynamic.txt')]" }:
- parent_id.name == 'Partners of Test'
- res_model == 'res.partner'
- res_id != False
-
I check that all/Partner1 also has the file
- |
Bonus Piste:
I create a 'Partner3' under 'all'
+66
View File
@@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
""" This is a testing module, which exports some functions for the YAML tests.
Instead of repeating the same FTP code all over, we prefer to have
it in this file
"""
from ftplib import FTP
from tools.misc import detect_ip_addr
from tools import config
def get_plain_ftp(timeout=10.0):
ftp = FTP()
host = config.get('ftp_server_host', '127.0.0.1')
port = config.get('ftp_server_port','8021')
ftp.connect(host,port, timeout=timeout)
return ftp
def get_ftp_login(cr, uid, ormobj):
ftp = get_plain_ftp()
user = ormobj.pool.get('res.users').read(cr, uid, uid)
ftp.login(user.get('login',''),user.get('password',''))
ftp.cwd("/" + cr.dbname)
return ftp
def get_ftp_anonymous(cr):
ftp = get_plain_ftp()
ftp.login('anonymous', 'the-test')
ftp.cwd("/")
return ftp
def get_ftp_folder(cr, uid, ormobj, foldername):
ftp = get_ftp_login(cr, uid, ormobj)
ftp.cwd("/" + cr.dbname+"/"+foldername)
return ftp
def get_ftp_fulldata(ftp, fname, limit=8192):
from functools import partial
data = []
def ffp(data, ndata):
if len(data)+ len(ndata) > limit:
raise IndexError('Data over the limit')
data.append(ndata)
ftp.retrbinary('RETR %s' % fname, partial(ffp,data))
return ''.join(data)
#eof
+78 -40
View File
@@ -46,6 +46,15 @@ CACHE_SIZE=20000
urlparse.uses_netloc.append('webdav')
urlparse.uses_netloc.append('webdavs')
class DAV_NotFound2(DAV_NotFound):
"""404 exception, that accepts our list uris
"""
def __init__(self, *args):
if len(args) and isinstance(args[0], (tuple, list)):
path = ''.join([ '/' + x for x in args[0]])
args = (path, )
DAV_NotFound.__init__(self, *args)
class openerp_dav_handler(dav_interface):
"""
This class models a OpenERP interface for the DAV server
@@ -74,6 +83,38 @@ class openerp_dav_handler(dav_interface):
cr.close()
return props
def _try_function(self, funct, args, opname='run function', cr=None,
default_exc=DAV_Forbidden):
""" Try to run a function, and properly convert exceptions to DAV ones.
@objname the name of the operation being performed
@param cr if given, the cursor to close at exceptions
"""
try:
funct(*args)
except DAV_Error:
if cr: cr.close()
raise
except NotImplementedError, e:
if cr: cr.close()
import traceback
self.parent.log_error("Cannot %s: %s", opname, str(e))
self.parent.log_message("Exc: %s",traceback.format_exc())
# see par 9.3.1 of rfc
raise DAV_Error(403, str(e) or 'Not supported at this path')
except EnvironmentError, err:
if cr: cr.close()
import traceback
self.parent.log_error("Cannot %s: %s", opname, err.strerror)
self.parent.log_message("Exc: %s",traceback.format_exc())
raise default_exc(err.strerror)
except Exception,e:
import traceback
self.parent.log_error("Cannot create %s: %s", opname, str(e))
self.parent.log_message("Exc: %s",traceback.format_exc())
raise default_exc("Operation failed")
def _get_dav_lockdiscovery(self, uri):
raise DAV_NotFound
@@ -192,7 +233,7 @@ class openerp_dav_handler(dav_interface):
if not node:
if cr: cr.close()
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
else:
fp = node.full_path()
if fp and len(fp):
@@ -255,26 +296,25 @@ class openerp_dav_handler(dav_interface):
raise DAV_Error, 409
node = self.uri2object(cr, uid, pool, uri2)
if not node:
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
try:
if rrange:
self.parent.log_error("Doc get_data cannot use range")
raise DAV_Error(409)
datas = node.get_data(cr)
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
# for the collections that return this error, the DAV standard
# says we'd better just return 200 OK with empty data
return ''
except IndexError,e :
self.parent.log_error("GET IndexError: %s", str(e))
raise DAV_NotFound(uri2)
raise DAV_NotFound2(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
return str(datas) # FIXME!
finally:
if cr: cr.close()
@@ -288,7 +328,7 @@ class openerp_dav_handler(dav_interface):
return COLLECTION
node = self.uri2object(cr,uid,pool, uri2)
if not node:
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
if node.type in ('collection','database'):
return COLLECTION
return OBJECT
@@ -304,7 +344,7 @@ class openerp_dav_handler(dav_interface):
node = self.uri2object(cr, uid, pool, uri2)
if not node:
cr.close()
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
cr.close()
return node.displayname
@@ -320,7 +360,7 @@ class openerp_dav_handler(dav_interface):
node = self.uri2object(cr, uid, pool, uri2)
if not node:
cr.close()
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
result = node.content_length or 0
cr.close()
return str(result)
@@ -337,7 +377,7 @@ class openerp_dav_handler(dav_interface):
node = self.uri2object(cr, uid, pool, uri2)
if not node:
cr.close()
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
result = node.get_etag(cr)
cr.close()
return str(result)
@@ -352,7 +392,7 @@ class openerp_dav_handler(dav_interface):
try:
node = self.uri2object(cr, uid, pool, uri2)
if not node:
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
if node.write_date:
return time.mktime(time.strptime(node.write_date,'%Y-%m-%d %H:%M:%S'))
else:
@@ -369,7 +409,7 @@ class openerp_dav_handler(dav_interface):
try:
node = self.uri2object(cr, uid, pool, uri2)
if not node:
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
if node.create_date:
result = time.mktime(time.strptime(node.create_date,'%Y-%m-%d %H:%M:%S'))
else:
@@ -387,7 +427,7 @@ class openerp_dav_handler(dav_interface):
try:
node = self.uri2object(cr, uid, pool, uri2)
if not node:
raise DAV_NotFound(uri2)
raise DAV_NotFound2(uri2)
result = str(node.mimetype)
return result
#raise DAV_NotFound, 'Could not find %s' % path
@@ -395,20 +435,28 @@ class openerp_dav_handler(dav_interface):
if cr: cr.close()
def mkcol(self,uri):
""" create a new collection """
""" create a new collection
see par. 9.3 of rfc4918
"""
self.parent.log_message('MKCOL: %s' % uri)
uri = self.uri2local(uri)[1:]
if uri[-1]=='/':uri=uri[:-1]
parent = '/'.join(uri.split('/')[:-1])
parent = self.baseuri + parent
uri = self.baseuri + uri
cr, uid, pool,dbname, uri2 = self.get_cr(uri)
cr, uid, pool, dbname, uri2 = self.get_cr(uri)
if not uri2[-1]:
cr.close()
raise DAV_Error(409, "Cannot create nameless collection")
if not dbname:
cr.close()
raise DAV_Error, 409
node = self.uri2object(cr,uid,pool, uri2[:-1])
if node:
node.create_child_collection(cr, uri2[-1])
cr.commit()
if not node:
cr.close()
raise DAV_Error(409, "Parent path %s does not exist" % uri2[:-1])
nc = node.child(cr, uri2[-1])
if nc:
cr.close()
raise DAV_Error(405, "Path already exists")
self._try_function(node.create_child_collection, (cr, uri2[-1]),
"create col %s" % uri2[-1], cr=cr)
cr.commit()
cr.close()
return True
@@ -430,22 +478,13 @@ class openerp_dav_handler(dav_interface):
if not node:
dir_node = self.uri2object(cr, uid, pool, uri2[:-1])
if not dir_node:
cr.close()
raise DAV_NotFound('Parent folder not found')
try:
dir_node.create_child(cr, objname, data)
except Exception,e:
import traceback
self.parent.log_error("Cannot create %s: %s", objname, str(e))
self.parent.log_message("Exc: %s",traceback.format_exc())
raise DAV_Forbidden
self._try_function(dir_node.create_child, (cr, objname, data),
"create %s" % objname, cr=cr)
else:
try:
node.set_data(cr, data)
except Exception,e:
import traceback
self.parent.log_error("Cannot save %s: %s", objname, str(e))
self.parent.log_message("Exc: %s",traceback.format_exc())
raise DAV_Forbidden
self._try_function(node.set_data, (cr, data), "save %s" % objname, cr=cr)
cr.commit()
cr.close()
@@ -618,7 +657,6 @@ class openerp_dav_handler(dav_interface):
advanced systems we might also have to copy properties from
the source to the destination.
"""
print " copy a collection."
return self.mkcol(dst)
+4
View File
@@ -48,6 +48,8 @@ def mk_prop_response(self, uri, good_props, bad_props, doc):
# write href information
uparts=urlparse.urlparse(uri)
fileloc=uparts[2]
if isinstance(fileloc, unicode):
fileloc = fileloc.encode('utf-8')
href=doc.createElement("D:href")
davpath = self._dataclass.parent.get_davpath()
hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc))
@@ -126,6 +128,8 @@ def mk_propname_response(self,uri,propnames,doc):
# write href information
uparts=urlparse.urlparse(uri)
fileloc=uparts[2]
if isinstance(fileloc, unicode):
fileloc = fileloc.encode('utf-8')
href=doc.createElement("D:href")
davpath = self._dataclass.parent.get_davpath()
hurl = '%s://%s%s%s' % (uparts[0], uparts[1], davpath, urllib.quote(fileloc))
+17
View File
@@ -78,6 +78,18 @@ class DAVHandler(FixSendError,DAVRequestHandler):
self.baseuri = "http://%s:%d/"% (self.server.server_name, self.server.server_port)
self.IFACE_CLASS = openerp_dav_handler(self, self.verbose)
def copymove(self, CLASS):
""" Our uri scheme removes the /webdav/ component from there, so we
need to mangle the header, too.
"""
dest = self.headers['Destination']
up = urlparse.urlparse(urllib.unquote(self.headers['Destination']))
if up.path.startswith(self.davpath):
self.headers['Destination'] = up.path[len(self.davpath):]
else:
raise DAV_Forbidden("Not allowed to copy/move outside webdav path")
DAVRequestHandler.copymove(self, CLASS)
def get_davpath(self):
return self.davpath
@@ -175,6 +187,11 @@ class DAVHandler(FixSendError,DAVRequestHandler):
self.send_body(None, '201', 'Created', '', headers=headers)
def do_DELETE(self):
try:
DAVRequestHandler.do_DELETE(self)
except DAV_Error, (ec, dd):
return self.send_status(ec)
from service.http_server import reg_http_service,OpenERPAuthProvider
@@ -54,7 +54,7 @@
<field name="model">email_template.account</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="SMTP Server">
<tree colors="blue:state in ('draft');black:state in ('suspended','approved')" string="SMTP Server">
<field name="name" select="2" />
<field name="email_id" select="2" />
<field name="smtpuname" select="2" />
+1 -1
View File
@@ -84,7 +84,7 @@
<field name="view_mode">form</field>
<field name="view_id" ref="board_associations_manager_form"/>
</record>
<menuitem id="board_associations" icon="terp-graph" name="Dashboard" parent="menu_report_event" sequence="0"/>
<menuitem id="board_associations" icon="terp-graph" name="Dashboard" parent="base.menu_report_association" sequence="0"/>
<menuitem
name="Associations" parent="board_associations"
action="open_board_associations_manager"
+33 -11
View File
@@ -226,7 +226,7 @@ class event_event(osv.osv):
'note': fields.text('Notes', help="Description or Summary of Event", readonly=False, states={'done': [('readonly', True)]}),
'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', readonly=True, states={'draft': [('readonly', False)]}, help="Pricelist version for current event."),
'unit_price': fields.related('product_id', 'list_price', type='float', string='Registration Cost', readonly=True, states={'draft':[('readonly',False)]}, help="This will be the default price used as registration cost when invoicing this event. Note that you can specify for each registration a specific amount if you want to"),
'main_speaker_id': fields.many2one('res.partner','Main Speaker', readonly=False, states={'done': [('readonly', True)]}),
'main_speaker_id': fields.many2one('res.partner','Main Speaker', readonly=False, states={'done': [('readonly', True)]}, help="Speaker who are giving speech on event."),
'speaker_ids':fields.many2many('res.partner', 'event_speaker_rel', 'speaker_id', 'partner_id', 'Other Speakers', readonly=False, states={'done': [('readonly', True)]}),
'address_id': fields.many2one('res.partner.address','Location Address', readonly=False, states={'done': [('readonly', True)]}),
'speaker_confirmed': fields.boolean('Speaker Confirmed', readonly=False, states={'done': [('readonly', True)]}),
@@ -285,7 +285,7 @@ class event_registration(osv.osv):
"""Event Registration"""
_name= 'event.registration'
_description = __doc__
_inherit = 'crm.meeting'
_inherit = 'mailgate.thread'
def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
cur_obj = self.pool.get('res.currency')
@@ -303,6 +303,7 @@ class event_registration(osv.osv):
'email_cc': fields.text('CC', size=252 , readonly=False, states={'done': [('readonly', True)]}, help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
'nb_register': fields.integer('Quantity', required=True, readonly=True, states={'draft': [('readonly', False)]}, help="Number of Registrations or Tickets"),
'event_id': fields.many2one('event.event', 'Event Related', required=True, readonly=True, states={'draft': [('readonly', False)]}),
'partner_id': fields.many2one('res.partner', 'Partner', states={'done': [('readonly', True)]}),
"partner_invoice_id": fields.many2one('res.partner', 'Partner Invoiced', readonly=True, states={'draft': [('readonly', False)]}),
"contact_id": fields.many2one('res.partner.contact', 'Partner Contact', readonly=False, states={'done': [('readonly', True)]}), #TODO: filter only the contacts that have a function into the selected partner_id
"unit_price": fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Event Price'), readonly=True, states={'draft': [('readonly', False)]}),
@@ -314,16 +315,30 @@ class event_registration(osv.osv):
'date_closed': fields.datetime('Closed', readonly=True),
'ref': fields.reference('Reference', selection=crm._links_get, size=128),
'ref2': fields.reference('Reference 2', selection=crm._links_get, size=128),
'email_from': fields.char('Email', size=128, states={'done': [('readonly', True)]}, help="These people will receive email."),
'create_date': fields.datetime('Creation Date' , readonly=True),
'write_date': fields.datetime('Write Date' , readonly=True),
'description': fields.text('Description', states={'done': [('readonly', True)]}),
'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
'log_ids': fields.one2many('mailgate.message', 'res_id', 'Logs', domain=[('history', '=', False),('model','=',_name)]),
'date_deadline': fields.related('event_id','date_end', type='datetime', string="End Date", readonly=True, store=True),
'date': fields.related('event_id', 'date_begin', type='datetime', string="Start Date", readonly=True, store=True),
'section_id': fields.related('event_id', 'section_id', type='many2one', relation='crm.case.section', string='Sale Team', store=True, readonly=True, states={'draft':[('readonly',False)]}),
'date_deadline': fields.related('event_id','date_end', type='datetime', string="End Date", readonly=True),
'date': fields.related('event_id', 'date_begin', type='datetime', string="Start Date", readonly=True),
'user_id': fields.many2one('res.users', 'Responsible', states={'done': [('readonly', True)]}),
'active': fields.boolean('Active'),
'section_id': fields.related('event_id', 'section_id', type='many2one', relation='crm.case.section', string='Sale Team', store=True, readonly=True),
'company_id': fields.related('event_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True, states={'draft':[('readonly',False)]}),
'state': fields.selection([('open', 'Confirmed'),
('draft', 'Unconfirmed'),
('cancel', 'Cancelled'),
('done', 'Done')], 'State', \
size=16, readonly=True)
}
_defaults = {
'nb_register': 1,
'tobe_invoiced': True,
'state': lambda *a: 'draft',
'active': lambda *a: 1,
'user_id': lambda self, cr, uid, ctx: uid,
}
def _make_invoice(self, cr, uid, reg, lines, context=None):
@@ -353,7 +368,7 @@ class event_registration(osv.osv):
})
inv_id = inv_pool.create(cr, uid, val_invoice['value'])
inv_pool.button_compute(cr, uid, [inv_id])
self._history(cr, uid, [reg], _('Invoiced'))
self.history(cr, uid, [reg], _('Invoiced'))
return inv_id
def action_invoice_create(self, cr, uid, ids, grouped=False, date_inv = False, context=None):
@@ -422,7 +437,7 @@ class event_registration(osv.osv):
"""
res = self.write(cr, uid, ids, {'state': 'open'}, context=context)
self.mail_user(cr, uid, ids)
self._history(cr, uid, ids, _('Open'))
self.history(cr, uid, ids, _('Open'))
return res
def do_close(self, cr, uid, ids, context=None):
@@ -436,7 +451,7 @@ class event_registration(osv.osv):
if invoice_id:
values['invoice_id'] = invoice_id
res = self.write(cr, uid, ids, values)
self._history(cr, uid, ids, msg)
self.history(cr, uid, ids, msg)
return res
def check_confirm(self, cr, uid, ids, context=None):
@@ -509,7 +524,7 @@ class event_registration(osv.osv):
"""This Function Cancel Event Registration.
"""
registrations = self.browse(cr, uid, ids)
self._history(cr, uid, registrations, _('Cancel'))
self.history(cr, uid, registrations, _('Cancel'))
self.write(cr, uid, ids, {'state': 'cancel'})
return True
@@ -606,7 +621,9 @@ class event_registration(osv.osv):
data_event = event_obj.browse(cr, uid, event_id)
res = {'value': {'unit_price': False, 'event_product': False, 'user_id': False,
'date': data_event.date_begin, 'date_deadline': data_event.date_end, 'description': data_event.note, 'name': data_event.name}}
'date': data_event.date_begin, 'date_deadline': data_event.date_end, 'description': data_event.note, 'name': data_event.name,
'section_id': data_event.section_id and data_event.section_id.id or False,
}}
if data_event.user_id.id:
res['value'].update({'user_id':data_event.user_id.id})
if data_event.product_id:
@@ -615,6 +632,8 @@ class event_registration(osv.osv):
partner = res_obj.browse(cr, uid, partner_invoice_id, context=context)
pricelist_id = pricelist_id or partner.property_product_pricelist.id
unit_price = prod_obj._product_price(cr, uid, [data_event.product_id.id], False, False, {'pricelist': pricelist_id})[data_event.product_id.id]
if not unit_price:
unit_price = data_event.unit_price
res['value'].update({'unit_price': unit_price, 'event_product': data_event.product_id.name})
return res
@@ -675,7 +694,10 @@ class event_registration(osv.osv):
if partner_invoice_id:
partner = res_obj.browse(cr, uid, partner_invoice_id, context=context)
pricelist_id = pricelist_id or partner.property_product_pricelist.id
data['unit_price'] = prod_obj._product_price(cr, uid, [data_event.product_id.id], False, False, {'pricelist': pricelist_id})[data_event.product_id.id]
unit_price = prod_obj._product_price(cr, uid, [data_event.product_id.id], False, False, {'pricelist': pricelist_id})[data_event.product_id.id]
if not unit_price:
unit_price = data_event.unit_price
data['unit_price'] = unit_price
return {'value': data}
event_registration()
+20 -19
View File
@@ -131,21 +131,23 @@
<page string="Mailing">
<field name="reply_to" />
<newline/>
<group col="3" colspan="2" expand="1">
<separator string="Auto Registration Email" colspan="2"/>
<newline/>
<field name="mail_auto_registr"/>
<separator string="Registration Email Body" colspan="2"/>
<newline/>
<field name="mail_registr" colspan="2" nolabel="1" attrs="{'readonly':[('mail_auto_registr','=',False)]}"/>
</group>
<group col="2" colspan="2" >
<separator string="Auto Confirmation Email" colspan="2"/>
<field name="mail_auto_confirm"/>
<newline/>
<separator string="Confirmation Email Body" colspan="2"/>
<field name="mail_confirm" colspan="2" nolabel="1" attrs="{'readonly':[('mail_auto_confirm','=',False)]}"/>
<group col="4" colspan="4">
<group col="2" colspan="2">
<separator string="Auto Registration Email" colspan="4"/>
<field name="mail_auto_registr" colspan="4"/>
<group colspan="4" attrs="{'readonly':[('mail_auto_registr','=',False)]}">
<separator string="Registration Email Body" colspan="4"/>
<field name="mail_registr" colspan="4" nolabel="1" />
</group>
</group>
<group col="2" colspan="2">
<separator string="Auto Confirmation Email" colspan="4"/>
<field name="mail_auto_confirm" colspan="4"/>
<group colspan="4" attrs="{'readonly':[('mail_auto_confirm','=',False)]}">
<separator string="Confirmation Email Body" colspan="4"/>
<field name="mail_confirm" nolabel="1" colspan="4"/>
</group>
</group>
</group>
</page>
</notebook>
@@ -330,9 +332,8 @@
</group>
<separator string="Description" colspan="4"/>
<field name="description" colspan="4" nolabel="1"/>
<group col="8" colspan="4">
<separator string="" colspan="4"/>
<newline/>
<separator string="" colspan="4"/>
<group col="8" colspan="4">
<field name="state" select="1" colspan="2"/>
<button name="button_reg_close" string="Close Registration" states="open" type="object" icon="gtk-close"/>
<button name="check_confirm" string="Confirm Registration" states="draft" type="object" icon="gtk-apply"/>
@@ -488,6 +489,6 @@
name="Registrations"
id="menu_action_registration" parent="menu_event_main"
action="action_registration"/>
<menuitem name="Reporting" id="menu_report_event" parent="base.menu_association" sequence="20"/>
<menuitem name="Reporting" id="base.menu_report_association" parent="base.menu_association" sequence="20"/>
</data>
</openerp>
@@ -51,6 +51,7 @@ class report_event_registration(osv.osv):
initialize the sql view for the event registration
cr -- the cursor
"""
tools.drop_view_if_exists(cr, 'report_event_registration')
cr.execute("""
create or replace view report_event_registration as (
select
@@ -7,7 +7,7 @@
<field name="model">report.event.registration</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Event on Registration">
<tree colors="blue:state in ('draft');black:state in ('confirm','approved');gray:state in('done','cancel')" string="Event on Registration">
<field name="date" invisible="1"/>
<field name="user_id" invisible="1"/>
<field name="speaker_id" invisible="1"/>
@@ -124,7 +124,7 @@
</record>
<!--<menuitem parent="menu_report_event" action="action_event_registration" id="menu_report_event_registration"/>-->
<menuitem parent="menu_report_event" action="action_report_event_registration" id="menu_report_event_registration" />
<menuitem parent="base.menu_report_association" action="action_report_event_registration" id="menu_report_event_registration" />
<!-- end... -->
</data>
+2 -2
View File
@@ -16,7 +16,7 @@
<field name="project_id"/>
<button string="Create Retro-Planning" name="%(action_event_project)d" type="action" icon="gtk-execute"/>
<field name="task_ids" colspan="4" nolabel="1" widget="one2many_list" >
<tree string="All tasks" colors="red:date_deadline&lt;current_date and state=='draft';blue:date_deadline==current_date and state=='draft';grey:state in ('cancel','done');black:state not in ('cancel','done')">
<tree string="All tasks" colors="red:date_deadline&lt;current_date and state=='draft';blue:date_deadline==current_date and state=='draft';gray:state in ('cancel','done')">
<field name="sequence"/>
<field name="name"/>
<field name="user_id" />
@@ -37,7 +37,7 @@
src_model="event.event"
view_mode="tree,form,calendar,graph"
domain="[('project_id', '=', project_id)]"
view_type="form"/>
view_type="form"/>
</data>
</openerp>
+36 -12
View File
@@ -27,7 +27,8 @@
from osv import fields, osv
from tools.translate import _
import time
from datetime import date, datetime
class event_project(osv.osv_memory):
"""
Event Project
@@ -36,23 +37,46 @@ class event_project(osv.osv_memory):
_description = "Event Project"
_columns = {
'project_id': fields.many2one('project.project', 'Template of Project', domain = [('active', '<>', False), ('state', '=', 'template')], required =True, help="This is Template Project. Project of event is a duplicate of this Template. After click on 'Create Retro-planning', New Project will be duplicated from this template project.")
'project_id': fields.many2one('project.project', 'Template of Project',
domain = [('active', '<>', False), ('state', '=', 'template')],
required =True,
help="This is Template Project. Project of event is a duplicate of this Template. After click on 'Create Retro-planning', New Project will be duplicated from this template project."),
'date_start': fields.date('Date Start'),
'date': fields.date('Date End'),
}
def default_get(self, cr, uid, fields, context=None):
"""
This function gets default values
@param fields: List of fields for default value
@param context: A standard dictionary for contextual values
@return : default values of fields.
"""
event_obj=self.pool.get('event.event')
project_obj = self.pool.get('project.project')
event = event_obj.browse(cr, uid, context.get('active_id', False))
res = super(event_project, self).default_get(cr, uid, fields, context=context)
if 'date_start' in fields:
res.update({'date_start': time.strftime('%Y-%m-%d')})
if 'date' in fields:
res.update({'date': datetime.strptime(event.date_end, "%Y-%m-%d %H:%M:%S").strftime("%Y-%m-%d")})
return res
def create_duplicate(self, cr, uid, ids, context):
event_obj=self.pool.get('event.event')
project_obj = self.pool.get('project.project')
event = event_obj.browse(cr, uid, context.get('active_id', False))
for current in self.browse(cr, uid, ids):
duplicate_project_id = project_obj.copy(cr, uid, current.project_id.id, {'active': True})
duplicate_project = project_obj.browse(cr, uid, duplicate_project_id, context)
project_obj.write(cr, uid, [duplicate_project_id], {
'name': duplicate_project.name ,
'date_start':time.strftime('%Y-%m-%d'),
'date': event.date_begin[0:10] })
for current in self.browse(cr, uid, ids):
duplicate_project_id = project_obj.copy(cr, uid, current.project_id.id, {
'active': True,
'date_start':current.date_start,
'date': current.date,
})
event_obj.write(cr, uid, [event.id], {'project_id': duplicate_project_id })
return {}
event_project()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
@@ -8,7 +8,9 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Retro-Planning">
<field name="project_id" />
<field name="project_id" colspan="4"/>
<field name="date_start"/>
<field name="date"/>
<separator string="" colspan="4"/>
<group colspan="4" col="6">
<button icon="gtk-close" special="cancel" string="Close"/>
+1 -1
View File
@@ -7,7 +7,7 @@
<field name="model">email.server</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="POP/IMAP Servers">
<tree colors="blue:state in ('draft');black:state in ('wating');gray:state in('done')" string="POP/IMAP Servers">
<field name="name" select="1"/>
<field name="type" select="1"/>
<field name="user" select="1"/>
+1
View File
@@ -72,6 +72,7 @@
<field name="res_model">hr.department</field>
<field name="view_type">form</field>
<field name="search_view_id" ref="view_department_filter"/>
<field name="help">Your Company's Departments Structure is used to manage all documents related to employees by departments: expenses and timesheet validation, leaves management, recruitements, etc.</field>
</record>
<menuitem action="open_module_tree_department" id="menu_department_def" parent="hr.menu_department_tree"/>
+2
View File
@@ -29,6 +29,8 @@
<field name="hr_contract"/>
<field name="hr_evaluation"/>
<field name="hr_attendance"/>
<field name="hr_payroll"/>
<field name="hr_payroll_account"/>
</group>
<xpath expr="//button[@string='Install Modules']" position="attributes">
<attribute name="string">Configure</attribute>
+2
View File
@@ -147,6 +147,7 @@
<field name="domain">[]</field>
<field name="view_id" ref="view_employee_tree"/>
<field name="search_view_id" ref="view_employee_filter"/>
<field name="help">The employee directory contains all data related to your employees: from their photo up to their hourly estimated costs for the timesheets. Employees are managed by departments and can be linked to users to manage their access rights.</field>
</record>
<menuitem action="open_view_employee_list_my" id="menu_open_view_employee_list_my" sequence="3" parent="menu_hr_main"/>
@@ -386,6 +387,7 @@
<field name="res_model">hr.job</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="help">Job Positions are used to define the jobs, the requirements. You can attach a survey to a job position. This survey will be used in the recruitement process to evaluate the applicants for this job position.</field>
</record>
<menuitem name="Recruitment" id="base.menu_crm_case_job_req_main" parent="menu_hr_root"/>
+4
View File
@@ -46,6 +46,10 @@ class hr_installer(osv.osv_memory):
"performance review of employees."),
'hr_attendance': fields.boolean('Attendances (Sign In/Out)',
help="Simplifies the management of employee attendances."),
'hr_payroll': fields.boolean('Payroll' ,
help="Generic Payroll system"),
'hr_payroll_account': fields.boolean('Payroll with Accounting',
help="Generic Payroll system Integrated with Accountings."),
}
_defaults = {
'hr_holidays': True,
@@ -71,6 +71,7 @@
<field name="view_mode">tree,form</field>
<field name="context">{"search_default_employee":1}</field>
<field name="search_view_id" ref="view_hr_attendance_filter" />
<field name="help">Time Tracking functionality aims to manage employee's attendances on the basis of the actions (Sign in/Sign out) performed by them. You can also link this to an attndance machine using OpenERP's webservices features.</field>
</record>
<!--<menuitem id="menu_hr_attendance" name="Attendances" parent="hr.menu_hr_root"
@@ -43,6 +43,7 @@
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_hr_attendance_sigh_in_out"/>
<field name="target">new</field>
<field name="help">Sign in / Sign out. In some companies, staff have to sign in when they arrive at work and sign out again at the end of the day. If each employee has been linked to a system user, then they can encode their time with this action button.</field>
</record>
<menuitem action="action_hr_attendance_sigh_in_out" id="menu_hr_attendance_sigh_in_out"
+5 -1
View File
@@ -200,7 +200,7 @@
<field name="model">hr_evaluation.evaluation</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Evaluation">
<tree colors="blue:state in ('draft');black:state in ('wait','progress');gray:state in('done','cancel')" string="Evaluation">
<!--field name="create_date"/-->
<field name="employee_id"/>
<field name="plan_id"/>
@@ -256,10 +256,12 @@
</record>
<record model="ir.actions.act_window" id="open_view_hr_evaluation_tree">
<field name="name">Evaluation</field>
<field name="res_model">hr_evaluation.evaluation</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form,graph</field>
<field name="search_view_id" ref="hr_evaluation.evaluation_search"/>
<field name="help">Each employee can be assigned to an evaluation plan. These plans define the frequency and the way you manage your periodic personnal evalautions. You will be able to define steps and attach interview forms to each step. OpenERP manages all kind of evaluations: bottom-up, top-down, self evaluation and final evaluation by the manager.</field>
</record>
<menuitem name="Evaluation" parent="menu_eval_hr" id="menu_open_view_hr_evaluation_tree"
@@ -339,12 +341,14 @@
</record>
<record model="ir.actions.act_window" id="action_hr_evaluation_interview_tree">
<field name="name">Interview Requests</field>
<field name="res_model">hr.evaluation.interview</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('is_evaluation' ,'=', True)]</field>
<field name="context">{"search_default_todo":1,"search_default_user_id":uid}</field>
<field name="search_view_id" ref="view_hr_evaluation_interview_search"/>
<field name="help">Interview Requests are generated automatically by OpenERP according to employees evaluation plan. Each user receives automatic emails and requests to perform evaluation of their collegues periodically.</field>
</record>
<menuitem name="Interview Requests" parent="menu_eval_hr" id="menu_open_hr_evaluation_interview_requests"
@@ -7,7 +7,8 @@
<field name="model">hr.evaluation.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Evaluations Analysis">
<tree colors="blue:state in ('draft');black:state in ('wait','progress');gray:state in('done','cancel')"
string="Evaluations Analysis">
<field name="create_date" invisible="1"/>
<field name="employee_id" invisible="1"/>
<field name="deadline" invisible="1"/>
+2 -1
View File
@@ -45,7 +45,7 @@
<field name="model">hr.expense.expense</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Expenses" editable="top">
<tree colors="blue:state in ('draft');black:state in ('confirm','accepted','invoiced','paid');gray:state in('cancelled')" string="Expenses" editable="top">
<field name="date"/>
<field name="employee_id"/>
<field name="department_id" groups="base.group_extended"/>
@@ -158,6 +158,7 @@
<field name="view_type">form</field>
<field name="context">{"search_default_user_id":uid}</field>
<field name="search_view_id" ref="view_hr_expense_filter"/>
<field name="help">The OpenERP expenses management module allows you to track the full flow. Every month, the employees record their expenses. At the end of the month, their managers validates the expenses sheets which creates costs on projects/analytic accounts. The accountant validates the proposed entries and the employee can be reimbursed. You can also reinvoice the customer at the end of the flow.</field>
</record>
<menuitem id="next_id_49" name="Expenses" sequence="4" parent="hr.menu_hr_root"/>
@@ -7,7 +7,7 @@
<field name="model">hr.expense.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Expenses Analysis">
<tree colors="blue:state in ('draft');black:state in ('confirm','accepted','invoiced','paid');gray:state in('cancelled')" string="Expenses Analysis">
<field name="date" invisible="1"/>
<field name="employee_id" invisible="1"/>
<field name="year" invisible="1"/>
+4 -3
View File
@@ -130,7 +130,7 @@
<field name="model">hr.holidays</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Leaves" colors="red:state == 'refuse';grey:state == 'cancel'">
<tree colors="red:state in ('refuse');blue:state in (' draft');black:state in ('confirm','validate','validate1');gray:state in ('cancel') " string="Leaves" >
<field name="employee_id"/>
<field name="department_id"/>
<field name="number_of_days" string="Number of Days" sum='Remaining Days'/>
@@ -185,7 +185,7 @@
<field name="type">tree</field>
<field name="priority">20</field>
<field name="arch" type="xml">
<tree string="Leaves Summary">
<tree colors="red:state in ('refuse');blue:state in (' draft');black:state in ('confirm','validate','validate1');gray:state in ('cancel')" string="Leaves Summary">
<field name="employee_id"/>
<field name="department_id" invisible="1"/>
<field name="number_of_days" string="Number of Days" sum='Remaining Days'/>
@@ -206,7 +206,7 @@
<field name="model">hr.holidays</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Leaves" colors="red:state == 'refuse';grey:state == 'cancel'">
<tree colors="red:state in ('refuse');blue:state in (' draft');black:state in ('confirm','validate','validate1');gray:state in ('cancel')">
<field name="employee_id"/>
<field name="department_id"/>
<field name="number_of_days" string="Number of Days" sum='Remaining Days'/>
@@ -253,6 +253,7 @@
<field name="domain">[('type','=','remove')]</field>
<field name="context">{'search_default_user_id':uid}</field>
<field name="search_view_id" ref="view_hr_holidays_filter"/>
<field name="help">Leaves requests can be recorded by employees and validated by their managers. Once a leave request is validated it appears automatically in the agenda of the employee. You can define several allowance types (paid holidays, sickness, etc) and manage allowances per type.</field>
</record>
<record model="ir.actions.act_window.view" id="action_open_ask_holidays_tree">
@@ -7,7 +7,7 @@
<field name="model">hr.holidays.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Leaves Statistics">
<tree colors="blue:state in ('draft');black:state in ('confirm','refuse','validate');gray:state in('cancel')" string="Leaves Statistics">
<field name="date" invisible="1"/>
<field name="employee_id" invisible="1"/>
<field name="category_id" invisible="1"/>
+3 -3
View File
@@ -424,7 +424,7 @@
<field name="model">hr.payslip</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Payslip">
<tree colors="blue:state in ('draft');black:state in ('confirm','new','hr_check','accont_check');gray:state in('done','cancel')" string="Payslip">
<field name="number"/>
<field name="employee_id"/>
<field name="name"/>
@@ -851,7 +851,7 @@
<field name="model">hr.payroll.register</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Payroll Register">
<tree colors="blue:state in ('draft');black:state in ('confirm','new','hr_check','accont_check');gray:state in('done','cancel')" string="Payroll Register">
<field name="number"/>
<field name="name"/>
<field name="date"/>
@@ -878,7 +878,7 @@
<notebook colspan="4">
<page string="Payslips">
<field name="line_ids" colspan="4" nolabel="1">
<tree string="Payslip">
<tree colors="blue:state in ('draft');black:state in ('confirm','new','hr_check','accont_check');gray:state in('done','cancel')" string="Payslip">
<field name="number"/>
<field name="employee_id"/>
<field name="name"/>
+8 -18
View File
@@ -45,12 +45,13 @@ AVAILABLE_PRIORITIES = [
class hr_recruitment_stage(osv.osv):
""" Stage of HR Recruitment """
_name = "hr.recruitment.stage"
_description = "Stage of Recruitment"
_order = 'sequence'
_columns = {
'name': fields.char('Name', size=64, required=True, translate=True),
'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of stages."),
'department_id':fields.many2one('hr.department', 'Department'),
'requirements': fields.text('Requirements')
}
_defaults = {
@@ -68,9 +69,6 @@ class hr_applicant(osv.osv, crm.crm_case):
'message_ids': fields.one2many('mailgate.message', 'res_id', 'Messages', domain=[('model','=',_name)]),
'active': fields.boolean('Active', help="If the active field is set to false, it will allow you to hide the case without removing it."),
'description': fields.text('Description'),
'section_id': fields.many2one('crm.case.section', 'Sales Team', \
select=True, help='Sales team to which Case belongs to.\
Define Responsible user and Email account for mail gateway.'),
'email_from': fields.char('Email', size=128, help="These people will receive email."),
'email_cc': fields.text('Watchers Emails', size=252 , help="These email addresses will be added to the CC field of all inbound and outbound emails for this record before being sent. Separate multiple email addresses with a comma"),
'probability': fields.float('Probability'),
@@ -79,12 +77,7 @@ class hr_applicant(osv.osv, crm.crm_case):
domain="[('partner_id','=',partner_id)]"),
'create_date': fields.datetime('Creation Date' , readonly=True),
'write_date': fields.datetime('Update Date' , readonly=True),
# 'stage_id': fields.many2one ('crm.case.stage', 'Stage', \
# domain="[('section_id','=',section_id),\
# ('object_id.model', '=', 'crm.opportunity')]"),
'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage', \
domain="[('section_id','=',section_id),\
('object_id.model', '=', 'crm.opportunity')]"),
'stage_id': fields.many2one ('hr.recruitment.stage', 'Stage'),
'state': fields.selection(AVAILABLE_STATES, 'State', size=16, readonly=True,
help='The state is set to \'Draft\', when a case is created.\
\nIf the case is in progress the state is set to \'Open\'.\
@@ -103,7 +96,7 @@ class hr_applicant(osv.osv, crm.crm_case):
'partner_name': fields.char("Applicant's Name", size=64),
'partner_phone': fields.char('Phone', size=32),
'partner_mobile': fields.char('Mobile', size=32),
'type_id': fields.many2one('crm.case.resource.type', 'Degree', domain="[('section_id','=',section_id),('object_id.model', '=', 'hr.applicant')]"),
'type_id': fields.many2one('crm.case.resource.type', 'Degree', domain="[('object_id.model', '=', 'hr.applicant')]"),
'department_id':fields.many2one('hr.department', 'Department'),
'state': fields.selection(AVAILABLE_STATES, 'State', size=16, readonly=True),
'survey' : fields.related('job_id', 'survey_id', type='many2one', relation='survey', string='Survey'),
@@ -124,7 +117,6 @@ class hr_applicant(osv.osv, crm.crm_case):
# 'user_id': crm.crm_case._get_default_user,
'email_from': crm.crm_case. _get_default_email,
'state': lambda *a: 'draft',
'section_id': crm.crm_case. _get_section,
'company_id': lambda s, cr, uid, c: s.pool.get('res.company')._company_default_get(cr, uid, 'crm.helpdesk', context=c),
'priority': lambda *a: crm.AVAILABLE_PRIORITIES[2][0],
}
@@ -148,9 +140,9 @@ class hr_applicant(osv.osv, crm.crm_case):
if not context:
context = {}
for case in self.browse(cr, uid, ids, context):
section = (case.section_id.id or False)
department = (case.department_id.id or False)
st = case.stage_id.id or False
stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, [])
stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)])
if st and stage_ids.index(st):
self.write(cr, uid, [case.id], {'stage_id': stage_ids[stage_ids.index(st)-1]})
return True
@@ -166,9 +158,9 @@ class hr_applicant(osv.osv, crm.crm_case):
if not context:
context = {}
for case in self.browse(cr, uid, ids, context):
section = (case.section_id.id or False)
department = (case.department_id.id or False)
st = case.stage_id.id or False
stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, [])
stage_ids = self.pool.get('hr.recruitment.stage').search(cr, uid, ['|',('department_id','=',department),('department_id','=',False)])
if st and len(stage_ids) != stage_ids.index(st)+1:
self.write(cr, uid, [case.id], {'stage_id': stage_ids[stage_ids.index(st)+1]})
return True
@@ -204,7 +196,6 @@ class hr_applicant(osv.osv, crm.crm_case):
context = {
'default_opportunity_id': opp.id,
'default_partner_id': opp.partner_id and opp.partner_id.id or False,
'default_section_id': opp.section_id and opp.section_id.id or False,
'default_email_from': opp.email_from,
'default_state': 'open',
'default_name': opp.name
@@ -374,7 +365,6 @@ class hr_job(osv.osv):
_columns = {
'survey_id': fields.many2one('survey', 'Survey'),
}
hr_job()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+23 -20
View File
@@ -1,16 +1,16 @@
<?xml version="1.0"?>
<openerp>
<data>
<menuitem
id="menu_hr_recruitment_recruitment"
name="Recruitment"
parent="hr.menu_hr_configuration"
sequence="2"/>
<menuitem
id="menu_hr_config_applicant"
name="Applicant"
parent="menu_hr_recruitment_recruitment"
sequence="2"/>
<menuitem
id="menu_hr_recruitment_recruitment"
name="Recruitment"
parent="hr.menu_hr_configuration"
sequence="2"/>
<menuitem
id="menu_hr_config_applicant"
name="Applicant"
parent="menu_hr_recruitment_recruitment"
sequence="2"/>
# ------------------------------------------------------
# Job Categories
@@ -85,16 +85,15 @@
<button string="Schedule a Phone Call" name="%(action_hr_recruitment_phonecall)d" icon="gtk-redo" type="action"/>
<button name="action_makeMeeting" type="object" string="Schedule Meeting" icon="terp-hr"/>
<newline/>
<field name="department_id" widget="selection"/>
<group colspan="2" col="4">
<field name="stage_id"/>
<field name="stage_id" domain="[('department_id','=',department_id)]"/>
<button name="stage_previous" string="" type="object" icon="gtk-go-back"/>
<button icon="gtk-go-forward" string="" name="stage_next" type="object"/>
</group>
<field name="user_id"/>
<field name="department_id" widget="selection"/>
<button name="action_makeMeeting" type="object" string="Schedule Meeting" icon="terp-hr"/>
<button name="action_print_survey" string="Print Interview" type="object" icon="gtk-print" attrs="{'readonly':[('survey','=',False)]}"/>
<button name="%(survey.action_view_survey_question_message)d" string="Interview Question" type="action" icon="gtk-execute" context="{'survey_id': survey, 'response_id': [response], 'response_no':0 ,'active' : response, 'object' : 'hr.applicant', 'cur_id' :active_id}" attrs="{'readonly':[('survey','=',False)]}" groups="base.group_extended"/>
<field name="user_id"/>
<button name="action_print_survey" string="Print Interview" type="object" icon="gtk-print" attrs="{'readonly':[('survey','=',False)]}"/>
<button name="%(survey.action_view_survey_question_message)d" string="Interview Question" type="action" icon="gtk-execute" context="{'survey_id': survey, 'response_id': [response], 'response_no':0 ,'active' : response, 'object' : 'hr.applicant', 'cur_id' :active_id}" attrs="{'readonly':[('survey','=',False)]}" groups="base.group_extended"/>
</group>
<notebook colspan="4">
<page string="Job Info">
@@ -279,8 +278,9 @@
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Stages">
<field name="sequence"/>
<field name="sequence" invisible="1"/>
<field name="name"/>
<field name="department_id"/>
</tree>
</field>
</record>
@@ -293,9 +293,12 @@
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Stage">
<separator string="Stage Definition" colspan="4"/>
<field name="name" select="1"/>
<field name="sequence"/>
<group colspan="4" col="6">
<separator string="Stage Definition" colspan="6"/>
<field name="name" select="1"/>
<field name="department_id"/>
<field name="sequence"/>
</group>
<separator string="Requirements" colspan="4"/>
<field name="requirements" nolabel="1" colspan="4"/>
</form>
@@ -46,8 +46,9 @@
<field name="res_model">hr.sign.in.project</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_hr_timesheet_sign_in_message"/>
<field name="target">new</field>
<field name="view_id" ref="view_hr_timesheet_sign_in_message"/>
<field name="target">new</field>
<field name="help">Employees can encode their time spent on the different projects. A project is an analytic account and the time spent on a project generate costs on the analytic account. This feature allows to record at the same time the attendance and the timesheet.</field>
</record>
<menuitem action="action_hr_timesheet_sign_in"
@@ -36,7 +36,7 @@
<field name="model">hr_timesheet_sheet.sheet</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Timesheets">
<tree colors="blue:state in ('draft');black:state in ('confirm','new');gray:state in('done')" string="Timesheets">
<field name="date_from"/>
<field name="user_id"/>
<field name="name"/>
@@ -7,7 +7,7 @@
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Timesheets"
colors="blue:state == 'draft';grey:state == 'done'">
colors="blue:state == 'draft';gray:state == 'done'">
<field name="user_id"/>
<field name="date_from"/>
<field name="date_to"/>
@@ -334,7 +334,7 @@
<field name="type">tree</field>
<field eval="20" name="priority"/>
<field name="arch" type="xml">
<tree string="Timesheets">
<tree colors="blue:state in ('draft');black:state in ('confirm','new');gray:state in('done')" string="Timesheets">
<field name="date_from"/>
<field name="user_id"/>
<field name="name"/>
@@ -18,7 +18,7 @@
<field name="model">timesheet.report</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Timesheet">
<tree colors="blue:state in ('draft');black:state in ('confirm','new');gray:state in('cancel')" string="Timesheet">
<field name="date_current" invisible="1"/>
<field name="name" invisible="1"/>
<field name="user_id" invisible="1"/>
@@ -121,7 +121,7 @@
<field name="context">{'search_default_month':1,'search_default_User_id':1,'group_by_no_leaf':1,'group_by':[]}</field>
<field name="search_view_id" ref="view_timesheet_report_search"/>
</record>
<menuitem action="action_timesheet_report_stat_all" id="menu_timesheet_report_all"
<menuitem action="action_timesheet_report_stat_all" id="menu_timesheet_report_all"
parent="hr_timesheet.menu_hr_reporting_timesheet" sequence="1"/>
</data>
</openerp>
+1 -1
View File
@@ -260,7 +260,7 @@
<field name="model">idea.idea</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Ideas">
<tree colors="blue:state in ('draft');black:state in ('open','close');gray:state in('cancel')" string="Ideas">
<field name="title"/>
<field name="category_id" />
<field name="created_date"/>
+1 -1
View File
@@ -45,7 +45,7 @@
<field name="model">lunch.order</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Order">
<tree colors="blue:state in ('draft');black:state in ('confirmed')" string="Order">
<field name="user_id"/>
<field name="product"/>
<field name="date"/>
@@ -40,7 +40,7 @@
<field name="model">marketing.campaign</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Campaigns">
<tree colors="blue:state in ('draft');black:state in ('running');gray:state in ('done','cancelled')" string="Campaigns">
<field name="name" select="1"/>
<field name="object_id" select="1"/>
<field name="mode"/>
@@ -155,7 +155,7 @@
<field name="model">marketing.campaign.segment</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Segments">
<tree colors="blue:state in ('draft');black:state in ('running');gray:state in ('done','cancelled')" string="Segments">
<field name="name"/>
<field name="campaign_id"/>
<field name="date_run"/>
@@ -224,8 +224,8 @@
<field name="signal"/>
<field name="start"/>
<field name="variable_cost" select="1"/>
<newline/>
<newline/>
<separator string="Action" colspan="4"/>
<group colspan='4' col='4'>
<group colspan="4" col="6">
@@ -272,7 +272,7 @@
</form>
</field>
</record>
<record model="ir.ui.view" id="view_marketing_campaign_activity_tree">
<field name="name">marketing.campaign.activity.tree</field>
<field name="model">marketing.campaign.activity</field>
@@ -299,7 +299,7 @@
<field name="model">marketing.campaign.workitem</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Marketing Campaign Activities">
<tree colors="red:state in ('exception');black:state in ('todo');gray:state in ('cancelled')" string="Marketing Campaign Activities">
<field name="segment_id"/>
<field name="activity_id" />
<field name="res_name" />
@@ -18,7 +18,7 @@
<group colspan="2" attrs="{'invisible':[('type','!=','mailchimp')]}">
<field name="mailchimp_account_id" attrs="{'required':[('type','=','mailchimp')]}" domain="[('state','=','approved')]" on_change="onchange_mailchimp(mailchimp_account_id)"/>
<field name="mailchimp_list" attrs="{'required':[('type','=','mailchimp')]}" on_change="onchange_mailchimp_list(mailchimp_account_id,mailchimp_list)"/>
</group>
</group>
</xpath>
</field>
</record>
@@ -38,7 +38,7 @@
<field name="model">mailchimp.account</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="All Accounts">
<tree colors="blue:state in ('draft');black:state in ('approved');gray:state in ('cancelled')" string="All Accounts">
<field name="name"/>
<field name="username" />
<field name="data_center"/>
+72 -98
View File
@@ -22,7 +22,7 @@
from osv import fields, osv
from tools import config
import time
from tools.translate import _
import decimal_precision as dp
@@ -46,96 +46,6 @@ STATE_PRIOR = {
'paid' : 7
}
#~ REQUETE = '''SELECT partner, state FROM (
#~ SELECT members.partner AS partner,
#~ CASE WHEN MAX(members.state) = 0 THEN 'none'
#~ ELSE CASE WHEN MAX(members.state) = 1 THEN 'canceled'
#~ ELSE CASE WHEN MAX(members.state) = 2 THEN 'old'
#~ ELSE CASE WHEN MAX(members.state) = 3 THEN 'waiting'
#~ ELSE CASE WHEN MAX(members.state) = 4 THEN 'invoiced'
#~ ELSE CASE WHEN MAX(members.state) = 6 THEN 'free'
#~ ELSE CASE WHEN MAX(members.state) = 7 THEN 'paid'
#~ END END END END END END END END
#~ AS state FROM (
#~ SELECT partner,
#~ CASE WHEN MAX(inv_digit.state) = 4 THEN 7
#~ ELSE CASE WHEN MAX(inv_digit.state) = 3 THEN 4
#~ ELSE CASE WHEN MAX(inv_digit.state) = 2 THEN 3
#~ ELSE CASE WHEN MAX(inv_digit.state) = 1 THEN 1
#~ END END END END
#~ AS state
#~ FROM (
#~ SELECT p.id as partner,
#~ CASE WHEN ai.state = 'paid' THEN 4
#~ ELSE CASE WHEN ai.state = 'open' THEN 3
#~ ELSE CASE WHEN ai.state = 'proforma' THEN 2
#~ ELSE CASE WHEN ai.state = 'draft' THEN 2
#~ ELSE CASE WHEN ai.state = 'cancel' THEN 1
#~ END END END END END
#~ AS state
#~ FROM res_partner p
#~ JOIN account_invoice ai ON (
#~ p.id = ai.partner_id
#~ )
#~ JOIN account_invoice_line ail ON (
#~ ail.invoice_id = ai.id
#~ )
#~ JOIN membership_membership_line ml ON (
#~ ml.account_invoice_line = ail.id
#~ )
#~ WHERE ml.date_from <= '%s'
#~ AND ml.date_to >= '%s'
#~ GROUP BY
#~ p.id,
#~ ai.state
#~ )
#~ AS inv_digit
#~ GROUP by partner
#~ UNION
#~ SELECT p.id AS partner,
#~ CASE WHEN p.free_member THEN 6
#~ ELSE CASE WHEN p.associate_member IN (
#~ SELECT ai.partner_id FROM account_invoice ai JOIN
#~ account_invoice_line ail ON (ail.invoice_id = ai.id AND ai.state = 'paid')
#~ JOIN membership_membership_line ml ON (ml.account_invoice_line = ail.id)
#~ WHERE ml.date_from <= '%s'
#~ AND ml.date_to >= '%s'
#~ )
#~ THEN 5
#~ END END
#~ AS state
#~ FROM res_partner p
#~ WHERE p.free_member
#~ OR p.associate_member > 0
#~ UNION
#~ SELECT p.id as partner,
#~ MAX(CASE WHEN ai.state = 'paid' THEN 2
#~ ELSE 0
#~ END)
#~ AS state
#~ FROM res_partner p
#~ JOIN account_invoice ai ON (
#~ p.id = ai.partner_id
#~ )
#~ JOIN account_invoice_line ail ON (
#~ ail.invoice_id = ai.id
#~ )
#~ JOIN membership_membership_line ml ON (
#~ ml.account_invoice_line = ail.id
#~ )
#~ WHERE ml.date_from < '%s'
#~ AND ml.date_to < '%s'
#~ AND ml.date_from <= ml.date_to
#~ GROUP BY
#~ p.id
#~ )
#~ AS members
#~ GROUP BY members.partner
#~ )
#~ AS final
#~ %s
#~ '''
class membership_line(osv.osv):
'''Member line'''
@@ -215,12 +125,16 @@ class membership_line(osv.osv):
_name = 'membership.membership_line'
_columns = {
'partner': fields.many2one('res.partner', 'Partner', ondelete='cascade', select=1),
'membership_id': fields.many2one('product.product', string="Membership", required=True),
'date_from': fields.date('From', readonly=True),
'date_to': fields.date('To', readonly=True),
'date_cancel' : fields.date('Cancel date'),
'date': fields.date('Join Date'),
'member_price':fields.float('Member Price', digits_compute= dp.get_precision('Sale Price'), required=True),
'account_invoice_line': fields.many2one('account.invoice.line', 'Account Invoice line', readonly=True),
'account_invoice_id': fields.related('account_invoice_line', 'invoice_id', type='many2one', relation='account.invoice', string='Invoice', readonly=True),
'state': fields.function(_state, method=True, string='State', type='selection', selection=STATE),
}
}
_rec_name = 'partner'
_order = 'id desc'
_constraints = [
@@ -381,14 +295,13 @@ class Partner(osv.osv):
_columns = {
'associate_member': fields.many2one('res.partner', 'Associate member'),
'member_lines': fields.one2many('membership.membership_line', 'partner', 'Membership'),
'member': fields.boolean('Member'),
'free_member': fields.boolean('Free member'),
'membership_amount': fields.float(
'Membership amount', digits=(16, 2),
help='The price negociated by the partner'),
'membership_state': fields.function(
__get_membership_state, method = True,
string = 'Current membership state', type = 'selection',
string = 'Current Membership State', type = 'selection',
selection = STATE ,store = {
'account.invoice':(_get_invoice_partner,['state'], 10),
'membership.membership_line':(_get_partner_id,['state'], 10),
@@ -453,6 +366,61 @@ class Partner(osv.osv):
default = default.copy()
default['member_lines'] = []
return super(Partner, self).copy(cr, uid, id, default, context)
def create_membership_invoice(self, cr, uid, ids, product_id=None, datas=None, context=None):
""" Create Customer Invoice of Membership for partners.
@param datas: datas has dictionary value which consist Id of Membership product and Cost Amount of Membership.
datas = {'membership_product_id': None, 'amount':None}
"""
invoice_obj = self.pool.get('account.invoice')
product_obj = self.pool.get('product.product')
invoice_line_obj = self.pool.get('account.invoice.line')
invoice_tax_obj = self.pool.get('account.invoice.tax')
product_id = product_id or datas.get('membership_product_id',False)
amount = datas.get('amount', 0.0)
if not context:
context={}
invoice_list = []
if type(ids) in (int,long,):
ids = [ids]
for partner in self.browse(cr, uid, ids, context=context):
account_id = partner.property_account_receivable and partner.property_account_receivable.id or False
fpos_id = partner.property_account_position and partner.property_account_position.id or False
addr = self.address_get(cr, uid, [partner.id], ['invoice'])
if partner.free_member:
raise osv.except_osv(_('Error !'),
_("Partner is a free Member."))
if not addr.get('invoice', False):
raise osv.except_osv(_('Error !'),
_("Partner doesn't have an address to make the invoice."))
quantity = 1
line_value = {
'product_id' : product_id,
}
line_dict = invoice_line_obj.product_id_change(cr, uid, {},
product_id, False, quantity, '', 'out_invoice', partner.id, fpos_id, price_unit=amount, context=context)
line_value.update(line_dict['value'])
if line_value.get('invoice_line_tax_id', False):
tax_tab = [(6, 0, line_value['invoice_line_tax_id'])]
line_value['invoice_line_tax_id'] = tax_tab
invoice_id = invoice_obj.create(cr, uid, {
'partner_id' : partner.id,
'address_invoice_id': addr.get('invoice', False),
'account_id': account_id,
'fiscal_position': fpos_id or False
}
)
line_value['invoice_id'] = invoice_id
invoice_line_id = invoice_line_obj.create(cr, uid, line_value, context=context)
invoice_obj.write(cr, uid, invoice_id, {'invoice_line':[(6,0,[invoice_line_id])]}, context=context)
invoice_list.append(invoice_id)
if line_value['invoice_line_tax_id']:
tax_value = invoice_tax_obj.compute(cr, uid, invoice_id).values()
for tax in tax_value:
invoice_tax_obj.create(cr, uid, tax, context=context)
return invoice_list
Partner()
@@ -460,7 +428,7 @@ class product_template(osv.osv):
_inherit = 'product.template'
_columns = {
'member_price':fields.float('Member Price', digits_compute= dp.get_precision('Sale Price')),
}
}
product_template()
class Product(osv.osv):
@@ -484,8 +452,8 @@ class Product(osv.osv):
_inherit = 'product.product'
_columns = {
'membership': fields.boolean('Membership', help='Specify if this product is a membership product'),
'membership_date_from': fields.date('Date from'),
'membership_date_to': fields.date('Date to'),
'membership_date_from': fields.date('Date from', help='Active Membership since this date'),
'membership_date_to': fields.date('Date to', help='Expired date of Membership'),
# 'member_price':fields.float('Member Price'),
}
@@ -535,6 +503,9 @@ class account_invoice_line(osv.osv):
date_from = line.invoice_id.date_invoice
line_id = member_line_obj.create(cr, uid, {
'partner': line.invoice_id.partner_id.id,
'membership_id': line.product_id.id,
'member_price': line.price_unit,
'date': time.strftime('%Y-%m-%d'),
'date_from': date_from,
'date_to': date_to,
'account_invoice_line': line.id,
@@ -572,6 +543,9 @@ class account_invoice_line(osv.osv):
date_from = line.invoice_id.date_invoice
line_id = member_line_obj.create(cr, uid, {
'partner': line.invoice_id.partner_id and line.invoice_id.partner_id.id or False,
'membership_id': line.product_id.id,
'member_price': line.price_unit,
'date': time.strftime('%Y-%m-%d'),
'date_from': date_from,
'date_to': date_to,
'account_invoice_line': line.id,
@@ -579,4 +553,4 @@ class account_invoice_line(osv.osv):
return result
account_invoice_line()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
+21 -153
View File
@@ -2,170 +2,38 @@
<openerp>
<data>
<!-- Demo Data for Membership Product -->
<record id="member_product_0" model="product.product">
<!-- Demo Data for Memberships -->
<record id="membership_0" model="product.product">
<field name="membership">True</field>
<field name="membership_date_from">2009-01-01</field>
<field name="membership_date_to">2010-06-01</field>
<field name="name">Membership Product - 1</field>
<field name="list_price">80</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-06-01')" name="membership_date_to"/>
<field name="name">Golden Membership</field>
<field name="list_price">180</field>
<field name="categ_id" ref="product.cat1"/>
<field name="type">service</field>
</record>
<record id="member_product_1" model="product.product">
<record id="membership_1" model="product.product">
<field name="membership">True</field>
<field name="membership_date_from">2009-01-01</field>
<field name="membership_date_to">2010-08-01</field>
<field name="name">Membership Product - 2</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-08-01')" name="membership_date_to"/>
<field name="name">Silver Membership</field>
<field name="categ_id" ref="product.cat1"/>
<field name="list_price">80</field>
<field name="type">service</field>
</record>
<!-- Demo data for Accounts -->
<record id="account_account_membershipparent" model="account.account">
<field name="name">membership</field>
<field name="code">Membership-0</field>
<field name="company_id" ref="base.main_company"/>
<!--<field eval="1" name="sign"/>-->
<field eval="1" name="active"/>
<field name="type">view</field>
<field eval="0" name="reconcile"/>
<field name="user_type" ref="account.account_type_root"/>
</record>
<record id="account_account_membershipaccount0" model="account.account">
<field name="name">membership account</field>
<field name="code">Membership-1254</field>
<field name="company_id" ref="base.main_company"/>
<!--<field eval="1" name="sign"/>-->
<field eval="1" name="active"/>
<field name="type">other</field>
<field eval="1" name="reconcile"/>
<field name="user_type" ref="account.account_type_expense"/>
<field name="parent_id" ref="account_account_membershipparent"/>
</record>
<!-- Demo data for Invoices -->
<record id="account_invoice_0" model="account.invoice">
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="company_id" ref="base.main_company"/>
<field name="currency_id" ref="base.EUR"/>
<field name="address_invoice_id" ref="base.res_partner_address_8"/>
<field name="partner_id" ref="base.res_partner_agrolait"/>
<field name="date_invoice">2008-05-09</field>
<field name="journal_id" ref="account.sales_journal"/>
<field name="state">paid</field>
<field name="type">out_invoice</field>
</record>
<record id="account_invoice_1" model="account.invoice">
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="company_id" ref="base.main_company"/>
<field name="currency_id" ref="base.EUR"/>
<field name="address_invoice_id" ref="base.res_partner_address_9"/>
<field name="partner_id" ref="base.res_partner_2"/>
<field name="date_invoice">2008-05-09</field>
<field name="journal_id" ref="account.sales_journal"/>
<field name="state">open</field>
<field name="type">out_invoice</field>
</record>
<record id="account_invoice_2" model="account.invoice">
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="company_id" ref="base.main_company"/>
<field name="currency_id" ref="base.EUR"/>
<field name="address_invoice_id" ref="base.res_partner_address_zen"/>
<field name="partner_id" ref="base.res_partner_3"/>
<field name="date_invoice">2008-05-09</field>
<field name="journal_id" ref="account.sales_journal"/>
<field name="state">cancel</field>
<field name="type">out_invoice</field>
</record>
<record id="account_invoice_3" model="account.invoice">
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="company_id" ref="base.main_company"/>
<field name="currency_id" ref="base.EUR"/>
<field name="address_invoice_id" ref="base.res_partner_address_14"/>
<field name="partner_id" ref="base.res_partner_15"/>
<field name="date_invoice">2008-05-09</field>
<field name="journal_id" ref="account.sales_journal"/>
<field name="state">draft</field>
<field name="type">out_invoice</field>
</record>
<!-- Demo data for invoice line -->
<record id="account_invoice_line_membershipproduct0" model="account.invoice.line">
<field name="name">Membership Product - 1</field>
<field name="invoice_id" ref="account_invoice_0"/>
<field eval="1.0" name="price_unit"/>
<field eval="1.0" name="quantity"/>
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="product_id" ref="membership.member_product_0"/>
<field name="account_analytic_id" ref="account.analytic_project_2"/>
</record>
<record id="account_invoice_line_membershipproduct1" model="account.invoice.line">
<field name="name">Membership Product - 2</field>
<field name="invoice_id" ref="account_invoice_1"/>
<field eval="1.0" name="price_unit"/>
<field eval="1.0" name="quantity"/>
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="product_id" ref="membership.member_product_1"/>
<field name="account_analytic_id" ref="account.analytic_project_2"/>
</record>
<record id="account_invoice_line_membershipproduct2" model="account.invoice.line">
<field name="name">Membership Product - 2</field>
<field name="invoice_id" ref="account_invoice_2"/>
<field eval="1.0" name="price_unit"/>
<field eval="1.0" name="quantity"/>
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="product_id" ref="membership.member_product_1"/>
<field name="account_analytic_id" ref="account.analytic_project_2"/>
</record>
<record id="account_invoice_line_membershipproduct3" model="account.invoice.line">
<field name="name">Membership Product - 2</field>
<field name="invoice_id" ref="account_invoice_3"/>
<field eval="1.0" name="price_unit"/>
<field eval="1.0" name="quantity"/>
<field name="account_id" ref="account_account_membershipaccount0"/>
<field name="product_id" ref="membership.member_product_1"/>
<field name="account_analytic_id" ref="account.analytic_project_2"/>
</record>
<!-- Demo data for membership line -->
<record id="member_line_1" model="membership.membership_line">
<field name="date_from">2008-01-01</field>
<field name="date_to">2009-08-01</field>
<field name="account_invoice_line" ref="account_invoice_line_membershipproduct0" />
<field name="state">paid</field>
<field name="partner" ref="base.res_partner_agrolait"/>
</record>
<record id="member_line_2" model="membership.membership_line">
<field name="date_from">2008-01-01</field>
<field name="date_to">2009-08-01</field>
<field name="account_invoice_line" ref="account_invoice_line_membershipproduct1" />
<field name="state">invoiced</field>
<field name="partner" ref="base.res_partner_2"/>
</record>
<record id="member_line_3" model="membership.membership_line">
<field name="date_from">2008-01-01</field>
<field name="date_to">2009-08-01</field>
<field name="date_cancel">2008-08-05</field>
<field name="account_invoice_line" ref="account_invoice_line_membershipproduct2" />
<field name="state">canceled</field>
<field name="partner" ref="base.res_partner_3"/>
</record>
<record id="member_line_3" model="membership.membership_line">
<field name="date_from">2008-01-01</field>
<field name="date_to">2009-08-01</field>
<field name="account_invoice_line" ref="account_invoice_line_membershipproduct3" />
<field name="state">waiting</field>
<field name="partner" ref="base.res_partner_15"/>
</record>
<!-- Demo data for cancel member -->
<record id="base.res_partner_3" model="res.partner">
<field name="membership_cancel">2009-01-01</field>
<record id="membership_2" model="product.product">
<field name="membership">True</field>
<field eval="time.strftime('%Y-01-01')" name="membership_date_from"/>
<field eval="time.strftime('%Y-08-01')" name="membership_date_to"/>
<field name="name">Basic Membership</field>
<field name="categ_id" ref="product.cat1"/>
<field name="list_price">40</field>
<field name="type">service</field>
</record>
<!-- Join Memberships -->
<function model="res.partner" name="create_membership_invoice" eval="(ref('base.res_partner_agrolait'), ref('membership_0'), {'amount':180})"/>
<!-- Demo data for free member -->
<record id="base.res_partner_desertic_hispafuentes" model="res.partner">
<field name="free_member">True</field>
+49 -58
View File
@@ -18,29 +18,26 @@
<field name="name"/>
<field name="membership_date_from"/>
<field name="membership_date_to"/>
<field name="lst_price"/>
<field name="list_price" string="Membership Fee"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="membership_products_form">
<field name="name">Membership products</field>
<field name="name">Memberships</field>
<field name="model">product.product</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Membership products">
<field name="default_code"/>
<form string="Membership products">
<field name="name"/>
<field name="membership_date_from" required="1"/>
<field name="membership_date_to" required="1"/>
<field name="list_price"/>
<field name="default_code"/>
<field name="type"/>
<field name="categ_id"/>
<field name="membership" colspan="1"/>
<field name="active" colspan="1"/>
<newline/>
<field name="membership_date_from" required="1"/>
<field name="membership_date_to" required="1"/>
<field name="list_price" string="Membership Fee"/>
<separator colspan="4" string="Customer Taxes:"/>
<field name="taxes_id" colspan="4" nolabel="1"/>
<field name="taxes_id" colspan="4" nolabel="1"/>
</form>
</field>
</record>
@@ -62,7 +59,7 @@
</record>
<record model="ir.actions.act_window" id="action_membership_products">
<field name="name">Membership Products</field>
<field name="name">Memberships</field>
<field name="res_model">product.product</field>
<field name="domain">[('membership','=',True), ('type', '=', 'service')]</field>
<field name="context">{'membership':True, 'type':'service'}</field>
@@ -82,16 +79,14 @@
<field name="act_window_id" ref="action_membership_products"/>
</record>
<menuitem name="Membership" id="menu_membership" icon="terp-crm" sequence="25"/>
<menuitem name="Members" parent="menu_membership"
id="menu_member"
sequence="1"/>
<menuitem name="Configuration" parent="menu_membership"
id="menu_conf"
sequence="3"/>
<menuitem name="Membership products" parent="menu_conf"
<menuitem name="Association" id="base.menu_association" icon="terp-calendar" sequence="9"/>
<menuitem name="Membership" id="menu_membership" icon="terp-crm" sequence="0" parent="base.menu_association"/>
<menuitem name="Memberships" parent="menu_membership"
id="menu_membership_products"
action="action_membership_products"/>
action="action_membership_products" sequence="1"/>
@@ -129,7 +124,7 @@
</field>
</field>
</record>
<record id="view_res_partner_member_filter2" model="ir.ui.view">
<field name="name">res.partner.select2</field>
<field name="model">res.partner</field>
@@ -152,7 +147,6 @@
<record model="ir.actions.act_window" id="action_membership_members">
<field name="name">Members</field>
<field name="res_model">res.partner</field>
<field name="domain">[('membership_state','&lt;&gt;','none'),('membership_state','&lt;&gt;','canceled'),('membership_state','&lt;&gt;','old'),('membership_state','&lt;&gt;','waiting')]</field>
<field name="search_view_id" ref="view_res_partner_member_filter"/>
<field name="context">{"search_default_old": 1}</field>
</record>
@@ -169,8 +163,8 @@
<field name="view_mode">form</field>
<field name="act_window_id" ref="action_membership_members"/>
</record>
<menuitem name="Members" parent="menu_member" id="menu_members" sequence="2" action="action_membership_members"/>
<menuitem name="Members" parent="menu_membership" id="menu_members" sequence="2" action="action_membership_members"/>
<!-- PARTNERS -->
<!--
@@ -182,17 +176,7 @@
/>
-->
<record id="view_res_partner_member_form2" model="ir.ui.view">
<field name="name">res.partner.form2</field>
<field name="model">res.partner</field>
<field name="type">form</field>
<field name="inherit_id" ref="base.view_partner_form"/>
<field name="arch" type="xml">
<field name="supplier" position="after">
<field name="member"/>
</field>
</field>
</record>
<record model="ir.ui.view" id="view_partner_tree">
<field name="name">res.partner.tree.form.inherit</field>
@@ -213,37 +197,44 @@
<field name="arch" type="xml">
<notebook position="inside">
<page string="Membership">
<field name="membership_state"/>
<field name="membership_amount"/>
<field name="associate_member"/>
<field name="free_member"/>
<field name="membership_start"/>
<field name="membership_stop"/>
<field name="membership_cancel"/>
<field name="member_lines" nolabel="1" colspan="4">
<tree string="Membership">
<field name="date_from"/>
<field name="date_to"/>
<group colspan="2" col="2">
<group colspan="4" col="3">
<field name="free_member"/>
<button name="%(action_membership_invoice_view)d" type="action" string="Join Membership" icon="gtk-apply"/>
<field colspan="4" name="associate_member" attrs="{'invisible':[('membership_state', '!=', 'none')], 'readonly':[('free_member','=',True)]}"/>
<field colspan="4" name="membership_state"/>
</group>
</group>
<group colspan="2" col="2">
<field name="membership_start"/>
<field name="membership_stop"/>
<field name="membership_cancel"/>
</group>
<field name="member_lines" nolabel="1" colspan="4" readonly="1">
<tree string="Memberships">
<field name="date"/>
<field name="membership_id"/>
<field name="member_price"/>
<field name="account_invoice_id"/>
<field name="state"/>
</tree>
<form string="Membership">
<field name="date_from"/>
<field name="date_to"/>
<field name="account_invoice_line"/>
<field name="state"/>
<form string="Memberships">
<field name="date"/>
<field name="member_price"/>
<field name="membership_id"/>
<field name="account_invoice_id"/>
<field name="state" colspan="4"/>
</form>
</field>
</field>
</page>
</notebook>
</field>
</record>
<!-- MEMBERSHIP/REPORTING -->
<menuitem name="Reporting" parent="menu_membership"
sequence="2"
id="menu_reporting"/>
<menuitem name="Reporting" id="base.menu_report_association" parent="base.menu_association" sequence="20"/>
<!-- View for product.product object inherited from product module................ -->
<record model="ir.ui.view" id="view_product_form_inherit1">
<field name="name">product.normal.form</field>
@@ -55,13 +55,15 @@ class report_membership(osv.osv):
('done', 'Old Member'),
('open', 'Invoiced Member'),
('free', 'Free Member'), ('paid', 'Paid Member')], 'State'),
'partner_id': fields.many2one('res.partner', 'Members', readonly=True, select=3)
'partner_id': fields.many2one('res.partner', 'Members', readonly=True, select=3),
'membership_id': fields.many2one('product.product', 'Membership', readonly=True, select=3)
}
def init(self, cr):
'''Create the view'''
tools.drop_view_if_exists(cr, 'report_membership')
cr.execute("""
CREATE OR REPLACE VIEW report_membership AS (
SELECT
@@ -78,6 +80,7 @@ class report_membership(osv.osv):
month,
create_date,
partner_id,
membership_id,
state,
currency
@@ -105,7 +108,8 @@ class report_membership(osv.osv):
ai.partner_id AS partner_id,
ai.currency_id AS currency,
ai.state as state,
MIN(ml.id) AS id
MIN(ml.id) AS id,
ml.membership_id AS membership_id
FROM membership_membership_line ml
JOIN (account_invoice_line ail
LEFT JOIN account_invoice ai
@@ -114,10 +118,10 @@ class report_membership(osv.osv):
JOIN res_partner p
ON (ml.partner = p.id)
GROUP BY TO_CHAR(ml.date_from, 'YYYY'), TO_CHAR(ml.date_from, 'MM'), TO_CHAR(ml.date_from, 'YYYY-MM-DD'), ai.state, ai.partner_id,
ai.currency_id, ml.id) AS foo
GROUP BY year, month, create_date, currency, partner_id, state)
ai.currency_id, ml.id, ml.membership_id) AS foo
GROUP BY year, month, create_date, currency, partner_id, membership_id, state)
""")
report_membership()
#
#
@@ -9,9 +9,10 @@
<field name="model">report.membership</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Membership">
<tree colors="blue:state in ('draft');black:state in ('open','free');gray:state in ('done','cancel') " string="Membership">
<field name="state"/>
<field name="partner_id"/>
<field name="membership_id"/>
<field name="canceled_number"/>
<field name="waiting_number"/>
<field name="invoiced_number"/>
@@ -47,12 +48,14 @@
<separator orientation="vertical"/>
<field name="partner_id"/>
<field name="membership_id"/>
</group>
<newline/>
<group expand="0" string="Group By..." colspan="4" col="12">
<filter string="Members" icon="terp-personal" name="member"
domain="[]" context="{'group_by':'partner_id'}"/>
<filter string="Memberships" icon="terp-personal" name="membership"
domain="[]" context="{'group_by':'membership_id'}"/>
<filter string="state" icon="terp-stock_effects-object-colorize"
domain="[]" context="{'group_by':'state'}"/>
@@ -104,9 +107,9 @@
<field name="act_window_id" ref="action_report_membership_tree"/>
</record>
<menuitem name="Membership" parent="menu_reporting"
<menuitem name="Membership" parent="base.menu_report_association"
action="action_report_membership_tree"
id="menu_report_membership"/>
</data>
</openerp>
</openerp>
+23 -46
View File
@@ -23,6 +23,7 @@ import time
from osv import fields, osv
from tools.translate import _
import tools
import decimal_precision as dp
class membership_invoice(osv.osv_memory):
"""Membership Invoice"""
@@ -30,56 +31,32 @@ class membership_invoice(osv.osv_memory):
_name = "membership.invoice"
_description = "Membership Invoice"
_columns = {
'product_id': fields.many2one('product.product','Membership Product', required=True),
'product_id': fields.many2one('product.product','Membership', required=True),
'member_price':fields.float('Member Price', digits_compute= dp.get_precision('Sale Price'), required=True),
}
def onchange_product(self, cr, uid, ids, product_id):
"""This function returns value of product's member price based on product id.
"""
if not product_id:
return {'value': {'unit_price': False}}
else:
unit_price=self.pool.get('product.product').price_get(cr, uid, [product_id])[product_id]
return {'value': {'member_price': unit_price}}
def membership_invoice(self, cr, uid, ids, context=None):
invoice_obj = self.pool.get('account.invoice')
partner_obj = self.pool.get('res.partner')
product_obj = self.pool.get('product.product')
invoice_line_obj = self.pool.get('account.invoice.line')
invoice_tax_obj = self.pool.get('account.invoice.tax')
datas = {}
if not context:
context={}
partner_ids = context.get('active_ids', [])
invoice_list = []
for partner in partner_obj.browse(cr, uid, partner_ids, context=context):
account_id = partner.property_account_receivable and partner.property_account_receivable.id or False
fpos_id = partner.property_account_position and partner.property_account_position.id or False
addr = partner_obj.address_get(cr, uid, [partner.id], ['invoice'])
if not addr.get('invoice', False):
continue
for data in self.browse(cr, uid, ids, context=context):
product_id = data.product_id and data.product_id.id or False
product_uom_id = data.product_id and data.product_id.uom_id.id
quantity = 1
line_value = {
'product_id' : product_id,
}
line_dict = invoice_line_obj.product_id_change(cr, uid, {},
product_id, product_uom_id, quantity, '', 'out_invoice', partner.id, fpos_id, context=context)
line_value.update(line_dict['value'])
if line_value.get('invoice_line_tax_id', False):
tax_tab = [(6, 0, line_value['invoice_line_tax_id'])]
line_value['invoice_line_tax_id'] = tax_tab
invoice_id = invoice_obj.create(cr, uid, {
'partner_id' : partner.id,
'address_invoice_id': addr.get('invoice', False),
'account_id': account_id,
'fiscal_position': fpos_id or False
}
)
line_value['invoice_id'] = invoice_id
invoice_line_id = invoice_line_obj.create(cr, uid, line_value, context=context)
invoice_obj.write(cr, uid, invoice_id, {'invoice_line':[(6,0,[invoice_line_id])]}, context=context)
invoice_list.append(invoice_id)
if line_value['invoice_line_tax_id']:
tax_value = invoice_tax_obj.compute(cr, uid, invoice_id).values()
for tax in tax_value:
invoice_tax_obj.create(cr, uid, tax, context=context)
context = {}
data = self.browse(cr, uid, ids)
if data:
data = data[0]
datas = {
'membership_product_id': data.product_id.id,
'amount': data.member_price
}
invoice_ids = context.get('active_ids', [])
invoice_list = partner_obj.create_membership_invoice(cr, uid, invoice_ids, datas=datas, context=context)
return {
'domain': [('id', 'in', invoice_list)],
@@ -88,7 +65,7 @@ class membership_invoice(osv.osv_memory):
'view_mode': 'tree,form',
'res_model': 'account.invoice',
'type': 'ir.actions.act_window',
}
}
membership_invoice()
@@ -9,34 +9,23 @@
<field name="arch" type="xml">
<form string="Membership Invoice">
<group colspan="4" >
<field name="product_id" context="{'product':membership_product}" domain="[('membership','=',True)]" widget="selection"/>
<field name="product_id" colspan="4" domain="[('membership','=',True)]" widget="selection" on_change="onchange_product(product_id)"/>
<field name="member_price" colspan="4" readonly="1"/>
</group>
<group colspan="4" col="6">
<button icon="gtk-close" special="cancel" string="Close"/>
<button icon="gtk-apply" string="Confirm" name="membership_invoice" type="object"/>
<button icon="gtk-apply" string="Join" name="membership_invoice" type="object"/>
</group>
</form>
</field>
</record>
<record id="action_membership_invoice_view" model="ir.actions.act_window">
<field name="name">Membership Invoice</field>
<field name="name">Join Membership</field>
<field name="res_model">membership.invoice</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_membership_invoice_view"/>
<field name="target">new</field>
</record>
<record id="action_idea_post_vote_values" model="ir.values">
<field name="model_id" ref="base.model_res_partner" />
<field name="object" eval="1" />
<field name="name">vote</field>
<field name="key2">client_action_multi</field>
<field name="value" eval="'ir.actions.act_window,' + str(ref('action_membership_invoice_view'))"/>
<field name="key">action</field>
<field name="model">res.partner</field>
</record>
</record>
</data>
</openerp>
@@ -26,8 +26,8 @@ class member_unpaid_invoice(osv.osv_memory):
_name = "membership.unpaid.invoice"
_description = "List of Unpaid Partner"
_columns ={
'product': fields.many2one('product.product','Membership product', size=64,required=True, help='Select Membership product'),
}
'product': fields.many2one('product.product','Membership', size=64,required=True, help='Select Membership.'),
}
def invoice_membership(self, cr, uid, ids, context):
model_obj = self.pool.get('ir.model.data')
@@ -45,7 +45,7 @@ class member_unpaid_invoice(osv.osv_memory):
return {
'domain': [('id', 'in', partners)],
'name': 'Unpaid Partners',
'name': 'Unpaid Members',
'view_type': 'form',
'view_mode': 'tree,form',
'res_model': 'res.partner',
@@ -7,20 +7,20 @@
<field name="model">membership.unpaid.invoice</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="List Unpaid Partners">
<form string="List Unpaid Members">
<group colspan="4" >
<field name="product" context="{'product':membership_product}" domain="[('membership','=',True)]"/>
<field name="product" domain="[('membership','=',True)]" widget="selection"/>
</group>
<group colspan="4" col="6">
<button icon="gtk-close" special="cancel" string="Close"/>
<button icon="gtk-ok" string="Unpaid Partners" name="invoice_membership" type="object"/>
<button icon="gtk-ok" string="Unpaid" name="invoice_membership" type="object"/>
</group>
</form>
</field>
</record>
<record id="action_unpaid_invoice_view" model="ir.actions.act_window">
<field name="name">List Unpaid Partners</field>
<field name="name">List Unpaid Members</field>
<field name="res_model">membership.unpaid.invoice</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
@@ -28,9 +28,9 @@
<field name="target">new</field>
</record>
<menuitem name="List Unpaid Partners" id="menu_unpaid_invoice"
<menuitem name="List Unpaid Members" id="menu_unpaid_invoice"
action="action_unpaid_invoice_view"
parent="menu_member" />
parent="menu_membership" />
</data>
</openerp>
+4 -4
View File
@@ -496,7 +496,7 @@
<field name="model">mrp.production</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree colors="blue:state in ('confirmed');red:date_planned&lt;current_date and state not in ('done','cancel');black:date_planned&gt;=current_date;gray:state in ('done','cancel') " string="Manufacturing Orders">
<tree colors="blue:state in ('draft','confirmed');red:date_planned&lt;current_date and state not in ('done','cancel');black:date_planned&gt;=current_date;gray:state in ('done','cancel') " string="Manufacturing Orders">
<field name="name"/>
<field name="product_id"/>
<field name="product_qty" sum="Total Qty"/>
@@ -590,7 +590,7 @@
<field colspan="2" name="move_lines" nolabel="1" widget="one2many_list"
mode="tree,form" height="275" domain="[('state','&lt;&gt;', ('done', 'cancel'))]">
<tree string="Products to Consume">
<tree colors="blue:state in ('draft');black:state in ('picking_except','confirmed','ready','in_production');gray:state in ('cancel','done') " string="Products to Consume">
<field name="product_id" />
<field name="product_qty" string="Qty"/>
<field name="product_uom" string="UOM"/>
@@ -609,7 +609,7 @@
<field colspan="2" name="move_lines2" nolabel="1" domain="[('state','in', ('done', 'cancel'))]"
widget="one2many_list" mode="tree,form" height="275">
<tree string="Consumed Products" editable="bottom" colors="red:scraped==True">
<tree colors="red:scraped==True;blue:state in('draft');black:state in('picking_except','confirmed','ready','in_production');gray:state in('cancel','done') " string="Consumed Products" editable="bottom">
<field name="product_id" readonly="1"/>
<field name="product_qty" readonly="1" string="Qty"/>
<field name="product_uom" readonly="1" string="UOM"/>
@@ -659,7 +659,7 @@
<field colspan="2" name="move_created_ids2" nolabel="1" domain="[('state','in', ('done', 'cancel'))]"
widget="one2many_list" mode="tree,form" height="275">
<tree string="Finished Products" editable="bottom" colors="red:scraped==True">
<tree colors="red:scraped==True;blue:state in('draft');black:state in('picking_except','confirmed','ready','in_production');gray:state in('cancel','done') " string="Finished Products" editable="bottom">
<field name="product_id" readonly="1"/>
<field name="product_qty" readonly="1" string="Qty"/>
<field name="product_uom" readonly="1" string="UOM"/>
@@ -10,7 +10,7 @@
<field name="model">mrp.production.order</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Production">
<tree colors="blue:state in ('draft');black:state in ('picking_except','confirmed','ready','in_production');gray:state in ('done','cancel') " string="Production">
<field name="date_planned"/>
<field name="bom_id" invisible="1"/>
<field name="product_id" invisible="1"/>
-1
View File
@@ -100,7 +100,6 @@ class StockMove(osv.osv):
'location_id': move.location_dest_id.id,
'auto_validate': True,
'picking_id': False,
'location_id': dest,
'state': 'waiting'
})
for m in procurement_obj.search(cr, uid, [('move_id','=',move.id)], context):
+1 -1
View File
@@ -7,7 +7,7 @@ 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-09-08 16:00+0000\n"
"PO-Revision-Date: 2010-07-19 19:07:52+0000\n"
"Last-Translator: Genie-soft <rawan@genie-soft.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
+1 -1
View File
@@ -7,7 +7,7 @@ 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: 2010-03-29 11:28+0000\n"
"PO-Revision-Date: 2010-07-19 19:07:52+0000\n"
"Last-Translator: Boris <boris.t.ivanov@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
+1 -1
View File
@@ -7,7 +7,7 @@ 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 10:41+0000\n"
"PO-Revision-Date: 2010-07-19 19:07:52+0000\n"
"Last-Translator: Miro Glavić <glavicmiro@gmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
+1 -1
View File
@@ -7,7 +7,7 @@ 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-20 17:36+0000\n"
"PO-Revision-Date: 2010-07-19 19:07:52+0000\n"
"Last-Translator: Jordi Esteve (www.zikzakmedia.com) "
"<jesteve@zikzakmedia.com>\n"
"Language-Team: \n"
+1 -1
View File
@@ -7,7 +7,7 @@ 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-02-04 06:31+0000\n"
"PO-Revision-Date: 2010-07-19 19:07:52+0000\n"
"Last-Translator: Fabien (Open ERP) <fp@tinyerp.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
+1 -1
View File
@@ -7,7 +7,7 @@ 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-22 19:33+0000\n"
"PO-Revision-Date: 2010-07-19 19:07:52+0000\n"
"Last-Translator: Ferdinand @ ChriCar <Unknown>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
+1 -1
View File
@@ -7,7 +7,7 @@ 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-20 17:46+0000\n"
"PO-Revision-Date: 2010-07-19 19:07:52+0000\n"
"Last-Translator: Jordi Esteve (www.zikzakmedia.com) "
"<jesteve@zikzakmedia.com>\n"
"Language-Team: \n"

Some files were not shown because too many files have changed in this diff Show More