bzr revid: hmo@tinyerp.com-20120914084146-1tednotth2ozxcjk
This commit is contained in:
Harry (OpenERP) 2012-09-14 14:11:46 +05:30
commit 840a0c9590
56 changed files with 1176 additions and 450 deletions

View File

@ -2338,6 +2338,16 @@ class account_model(osv.osv):
return move_ids
def onchange_journal_id(self, cr, uid, ids, journal_id, context=None):
company_id = False
if journal_id:
journal = self.pool.get('account.journal').browse(cr, uid, journal_id, context=context)
if journal.company_id.id:
company_id = journal.company_id.id
return {'value': {'company_id': company_id}}
account_model()
class account_model_line(osv.osv):
@ -3013,9 +3023,9 @@ class wizard_multi_charts_accounts(osv.osv_memory):
'purchase_tax_rate': fields.float('Purchase Tax(%)'),
'complete_tax_set': fields.boolean('Complete Set of Taxes', help='This boolean helps you to choose if you want to propose to the user to encode the sales and purchase rates or use the usual m2o fields. This last choice assumes that the set of tax defined for the chosen template is complete'),
}
def onchange_company_id(self, cr, uid, ids, company_id, context=None):
currency_id = False
currency_id = False
if company_id:
currency_id = self.pool.get('res.company').browse(cr, uid, company_id, context=context).currency_id.id
return {'value': {'currency_id': currency_id}}

View File

@ -43,11 +43,15 @@ class bank(osv.osv):
"Return the name to use when creating a bank journal"
return (bank.bank_name or '') + ' ' + bank.acc_number
def _prepare_name_get(self, cr, uid, bank_type_obj, bank_obj, context=None):
"""Add ability to have %(currency_name)s in the format_layout of
res.partner.bank.type"""
bank_obj._data[bank_obj.id]['currency_name'] = bank_obj.currency_id and bank_obj.currency_id.name or ''
return super(bank, self)._prepare_name_get(cr, uid, bank_type_obj, bank_obj, context=context)
def _prepare_name_get(self, cr, uid, bank_dicts, context=None):
"""Add ability to have %(currency_name)s in the format_layout of res.partner.bank.type"""
currency_ids = list(set(data['currency_id'][0] for data in bank_dicts if data['currency_id']))
currencies = self.pool.get('res.currency').browse(cr, uid, currency_ids, context=context)
currency_name = dict((currency.id, currency.name) for currency in currencies)
for data in bank_dicts:
data['currency_name'] = data['currency_id'] and currency_name[data['currency_id'][0]] or ''
return super(bank, self)._prepare_name_get(cr, uid, bank_dicts, context=context)
def post_write(self, cr, uid, ids, context={}):
if isinstance(ids, (int, long)):

View File

@ -112,7 +112,7 @@
<field name="fiscalyear_id" widget="selection"/>
<label for="date_start" string="Duration"/>
<div>
<field name="date_start" class="oe_inline" nolabel="1"/> -
<field name="date_start" class="oe_inline" nolabel="1"/> -
<field name="date_stop" nolabel="1" class="oe_inline"/>
</div>
</group>
@ -181,7 +181,7 @@
<form string="Account" version="7.0">
<label for="code" class="oe_edit_only" string="Account Code and Name"/>
<h1>
<field name="code" class="oe_inline" placeholder="Account code" style="width: 6em"/> -
<field name="code" class="oe_inline" placeholder="Account code" style="width: 6em"/> -
<field name="name" class="oe_inline" placeholder="Account name"/>
</h1>
<group>
@ -1082,7 +1082,7 @@
<field eval="2" name="priority"/>
<field name="arch" type="xml">
<form string="Journal Item" version="7.0">
<sheet>
<sheet>
<group>
<group>
<field name="name"/>
@ -1349,7 +1349,7 @@
<field name="date"/>
<field name="to_check"/>
<field name="amount" invisible="1"/>
</group>
</group>
</group>
<notebook>
<page string="Journal Items">
@ -1651,8 +1651,8 @@
<form string="Journal Entry Model" version="7.0">
<group col="4">
<field name="name"/>
<field name="journal_id"/>
<field name="company_id" widget='selection' groups="base.group_multi_company"/>
<field name="journal_id" on_change="onchange_journal_id(journal_id)"/>
<field name="company_id" widget="selection" groups="base.group_multi_company"/>
</group>
<field name="lines_id" widget="one2many_list"/>

View File

@ -51,10 +51,10 @@ class account_config_settings(osv.osv_memory):
'code_digits': fields.integer('# of Digits', help="No. of digits to use for account code"),
'tax_calculation_rounding_method': fields.related('company_id',
'tax_calculation_rounding_method', type='selection', selection=[
('round_per_line', 'Round per Line'),
('round_globally', 'Round Globally'),
('round_per_line', 'Round per line'),
('round_globally', 'Round globally'),
], string='Tax calculation rounding method',
help="If you select 'Round per Line' : for each tax, the tax amount will first be computed and rounded for each PO/SO/invoice line and then these rounded amounts will be summed, leading to the total amount for that tax. If you select 'Round Globally': for each tax, the tax amount will be computed for each PO/SO/invoice line, then these amounts will be summed and eventually this total tax amount will be rounded. If you sell with tax included, you should choose 'Round per line' because you certainly want the sum of your tax-included line subtotals to be equal to the total amount with taxes."),
help="If you select 'Round per line' : for each tax, the tax amount will first be computed and rounded for each PO/SO/invoice line and then these rounded amounts will be summed, leading to the total amount for that tax. If you select 'Round globally': for each tax, the tax amount will be computed for each PO/SO/invoice line, then these amounts will be summed and eventually this total tax amount will be rounded. If you sell with tax included, you should choose 'Round per line' because you certainly want the sum of your tax-included line subtotals to be equal to the total amount with taxes."),
'sale_tax': fields.many2one("account.tax.template", "Default sale tax"),
'purchase_tax': fields.many2one("account.tax.template", "Default purchase tax"),
'sale_tax_rate': fields.float('Sales tax (%)'),
@ -72,40 +72,40 @@ class account_config_settings(osv.osv_memory):
'sale_refund_journal_id': fields.many2one('account.journal', 'Sale refund journal'),
'sale_refund_sequence_prefix': fields.related('sale_refund_journal_id', 'sequence_id', 'prefix', type='char', string='Credit note sequence'),
'sale_refund_sequence_next': fields.related('sale_refund_journal_id', 'sequence_id', 'number_next', type='integer', string='Next credit note number'),
'purchase_journal_id': fields.many2one('account.journal', 'Purchase Journal'),
'purchase_journal_id': fields.many2one('account.journal', 'Purchase journal'),
'purchase_sequence_prefix': fields.related('purchase_journal_id', 'sequence_id', 'prefix', type='char', string='Supplier invoice sequence'),
'purchase_sequence_next': fields.related('purchase_journal_id', 'sequence_id', 'number_next', type='integer', string='Next supplier invoice number'),
'purchase_refund_journal_id': fields.many2one('account.journal', 'Purchase refund journal'),
'purchase_refund_sequence_prefix': fields.related('purchase_refund_journal_id', 'sequence_id', 'prefix', type='char', string='Supplier credit note sequence'),
'purchase_refund_sequence_next': fields.related('purchase_refund_journal_id', 'sequence_id', 'number_next', type='integer', string='Next supplier credit note number'),
'module_account_check_writing': fields.boolean('pay your suppliers by check',
'module_account_check_writing': fields.boolean('Pay your suppliers by check',
help="""This allows you to check writing and printing.
This installs the module account_check_writing."""),
'module_account_accountant': fields.boolean('full accounting features: journals, legal statements, chart of accounts, etc.',
'module_account_accountant': fields.boolean('Full accounting features: journals, legal statements, chart of accounts, etc.',
help="""If you do not check this box, you will be able to do invoicing & payments, but not accounting (Journal Items, Chart of Accounts, ...)"""),
'module_account_asset': fields.boolean('assets management',
'module_account_asset': fields.boolean('Assets management',
help="""This allows you to manage the assets owned by a company or a person.
It keeps track of the depreciation occurred on those assets, and creates account move for those depreciation lines.
This installs the module account_asset. If you do not check this box, you will be able to do invoicing & payments,
but not accounting (Journal Items, Chart of Accounts, ...)"""),
'module_account_budget': fields.boolean('budget management',
'module_account_budget': fields.boolean('Budget management',
help="""This allows accountants to manage analytic and crossovered budgets.
Once the master budgets and the budgets are defined,
the project managers can set the planned amount on each analytic account.
This installs the module account_budget."""),
'module_account_payment': fields.boolean('manage payment orders',
'module_account_payment': fields.boolean('Manage payment orders',
help="""This allows you to create and manage your payment orders, with purposes to
* serve as base for an easy plug-in of various automated payment mechanisms, and
* provide a more efficient way to manage invoice payments.
This installs the module account_payment."""),
'module_account_voucher': fields.boolean('manage customer payments',
'module_account_voucher': fields.boolean('Manage customer payments',
help="""This includes all the basic requirements of voucher entries for bank, cash, sales, purchase, expense, contra, etc.
This installs the module account_voucher."""),
'module_account_followup': fields.boolean('manage customer payment follow-ups',
'module_account_followup': fields.boolean('Manage customer payment follow-ups',
help="""This allows to automate letters for unpaid invoices, with multi-level recalls.
This installs the module account_followup."""),
'group_proforma_invoices': fields.boolean('allow pro-forma invoices',
'group_proforma_invoices': fields.boolean('Allow pro-forma invoices',
implied_group='account.group_proforma_invoices',
help="Allows you to put invoices in pro-forma state."),
'default_sale_tax': fields.many2one('account.tax', 'Default sale tax',
@ -114,7 +114,7 @@ class account_config_settings(osv.osv_memory):
help="This purchase tax will be assigned by default on new products."),
'decimal_precision': fields.integer('Decimal precision on journal entries',
help="""As an example, a decimal precision of 2 will allow journal entries like: 9.99 EUR, whereas a decimal precision of 4 will allow journal entries like: 0.0231 EUR."""),
'group_multi_currency': fields.boolean('allow multi currencies',
'group_multi_currency': fields.boolean('Allow multi currencies',
implied_group='base.group_multi_currency',
help="Allows you multi currency environment"),
}
@ -223,6 +223,16 @@ class account_config_settings(osv.osv_memory):
return {'value': {'date_stop': end_date.strftime('%Y-%m-%d')}}
return {}
def open_company_form(self, cr, uid, ids, context=None):
config = self.browse(cr, uid, ids[0], context)
return {
'type': 'ir.actions.act_window',
'name': 'Configure your Company',
'res_model': 'res.company',
'res_id': config.company_id.id,
'view_mode': 'form',
}
def set_default_taxes(self, cr, uid, ids, context=None):
""" set default sale and purchase taxes for products """
ir_values = self.pool.get('ir.values')

View File

@ -224,10 +224,8 @@
<div>
<div>
<label for="company_footer"/>
<button name="%(action_bank_tree)d"
string="Configure your bank accounts"
icon="gtk-go-forward"
type="action"
<button name="open_company_form" type="object"
string="Configure your company bank accounts" icon="gtk-go-forward"
class="oe_inline oe_link"/>
<field name="company_footer"/>
</div>

View File

@ -25,7 +25,7 @@ class base_config_settings(osv.TransientModel):
_inherit = 'base.config.settings'
_columns = {
'auth_signup_uninvited': fields.boolean('allow public users to sign up', help="If unchecked only invited users may sign up"),
'auth_signup_uninvited': fields.boolean('Allow public users to sign up', help="If unchecked only invited users may sign up"),
'auth_signup_template_user_id': fields.many2one('res.users', 'Template user for new users created through signup'),
}

View File

@ -25,16 +25,16 @@ class base_config_settings(osv.osv_memory):
_name = 'base.config.settings'
_inherit = 'res.config.settings'
_columns = {
'module_multi_company': fields.boolean('manage multiple companies',
'module_multi_company': fields.boolean('Manage multiple companies',
help="""Work in multi-company environments, with appropriate security access between companies.
This installs the module multi_company."""),
'module_share': fields.boolean('allow documents sharing',
'module_share': fields.boolean('Allow documents sharing',
help="""Share or embbed any screen of openerp."""),
'module_portal': fields.boolean('activate the customer/supplier portal',
'module_portal': fields.boolean('Activate the customer/supplier portal',
help="""Give access your customers and suppliers to their documents."""),
'module_auth_anonymous': fields.boolean('activate the public portal',
'module_auth_anonymous': fields.boolean('Activate the public portal',
help="""Enable the public part of openerp, openerp becomes a public website."""),
'module_auth_oauth': fields.boolean('use external authentication providers, sign in with google, facebook, ...'),
'module_auth_oauth': fields.boolean('Use external authentication providers, sign in with google, facebook, ...'),
'module_base_import': fields.boolean("Allow users to import data from CSV files"),
}
@ -57,17 +57,17 @@ class sale_config_settings(osv.osv_memory):
_name = 'sale.config.settings'
_inherit = 'res.config.settings'
_columns = {
'module_web_linkedin': fields.boolean('get contacts automatically from LinkedIn',
'module_web_linkedin': fields.boolean('Get contacts automatically from linkedIn',
help="""When you create a new contact (person or company), you will be able to load all the data from LinkedIn (photos, address, etc)."""),
'module_crm': fields.boolean('CRM'),
'module_plugin_thunderbird': fields.boolean('enable Thunderbird plugin',
'module_plugin_thunderbird': fields.boolean('Enable Thunderbird plugin',
help="""The plugin allows you archive email and its attachments to the selected
OpenERP objects. You can select a partner, or a lead and
attach the selected mail as a .eml file in
the attachment of a selected record. You can create documents for CRM Lead,
Partner from the selected emails.
This installs the module plugin_thunderbird."""),
'module_plugin_outlook': fields.boolean('enable Outlook plugin',
'module_plugin_outlook': fields.boolean('Enable Outlook plugin',
help="""The Outlook plugin allows you to select an object that you would like to add
to your email and its attachments from MS Outlook. You can select a partner,
or a lead object and archive a selected

View File

@ -16,7 +16,7 @@
<div>
<p>
<label string="You will find more options in your company details: address for the header and footer, overdue payments texts, etc."/>
<button type="object" name="open_company" string="Configure Your Company Data" icon="gtk-execute" class="oe_inline oe_link"/>
<button type="object" name="open_company" string="Configure your company data" icon="gtk-execute" class="oe_inline oe_link"/>
</p>
</div>
<group>

View File

@ -27,13 +27,15 @@ import time
import tools
from tools.translate import _
from base.res.res_partner import format_address
CRM_LEAD_PENDING_STATES = (
crm.AVAILABLE_STATES[2][0], # Cancelled
crm.AVAILABLE_STATES[3][0], # Done
crm.AVAILABLE_STATES[4][0], # Pending
)
class crm_lead(base_stage, osv.osv):
class crm_lead(base_stage, format_address, osv.osv):
""" CRM Lead Case """
_name = "crm.lead"
_description = "Lead/Opportunity"
@ -105,6 +107,12 @@ class crm_lead(base_stage, osv.osv):
return result, fold
def fields_view_get(self, cr, user, view_id=None, view_type='form', context=None, toolbar=False, submenu=False):
res = super(crm_lead,self).fields_view_get(cr, user, view_id, view_type, context, toolbar=toolbar, submenu=submenu)
if view_type == 'form':
res['arch'] = self.fields_view_get_address(cr, user, res['arch'], context=context)
return res
_group_by_full = {
'stage_id': _read_group_stage_ids
}

View File

@ -26,19 +26,19 @@ class crm_configuration(osv.osv_memory):
_inherit = ['sale.config.settings', 'fetchmail.config.settings']
_columns = {
'fetchmail_lead': fields.boolean("create leads from incoming mails",
'fetchmail_lead': fields.boolean("Create leads from incoming mails",
fetchmail_model='crm.lead', fetchmail_name='Incoming Leads',
help="""Allows you to configure your incoming mail server, and create leads from incoming emails."""),
'module_crm_caldav': fields.boolean("applications with Caldav protocol",
'module_crm_caldav': fields.boolean("Applications with Caldav protocol",
help="""Use protocol caldav to synchronize meetings with other calendar applications (like Sunbird).
This installs the module crm_caldav."""),
'module_import_sugarcrm': fields.boolean("SugarCRM",
help="""Import SugarCRM leads, opportunities, users, accounts, contacts, employees, meetings, phonecalls, emails, project and project tasks data.
This installs the module import_sugarcrm."""),
'module_import_google': fields.boolean("Google (Contacts and Calendar)",
'module_import_google': fields.boolean("Google (contacts and calendar)",
help="""Import google contact in partner address and add google calendar events details in Meeting.
This installs the module import_google."""),
'module_google_map': fields.boolean("add google maps on customers",
'module_google_map': fields.boolean("Add google maps on customers",
help="""Locate customers on Google Map.
This installs the module google_map."""),
'group_fund_raising': fields.boolean("Manage Fund Raising",

View File

@ -26,7 +26,7 @@ class crm_claim_settings(osv.osv_memory):
_inherit = ['sale.config.settings', 'fetchmail.config.settings']
_columns = {
'fetchmail_claim': fields.boolean("create claims from incoming mails",
'fetchmail_claim': fields.boolean("Create claims from incoming mails",
fetchmail_model='crm.claim', fetchmail_name='Incoming Claims',
help="""Allows you to configure your incoming mail server, and create claims from incoming emails."""),
}

View File

@ -20,17 +20,10 @@
##############################################################################
import base64
from openerp.tests import common
from openerp.addons.mail.tests import test_mail
class test_message_compose(common.TransactionCase):
def _mock_smtp_gateway(self, *args, **kwargs):
return True
def _mock_build_email(self, *args, **kwargs):
self._build_email_args = args
self._build_email_kwargs = kwargs
return self.build_email_real(*args, **kwargs)
class test_message_compose(test_mail.TestMailMockups):
def setUp(self):
super(test_message_compose, self).setUp()
@ -40,11 +33,6 @@ class test_message_compose(common.TransactionCase):
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
# Install mock SMTP gateway
self.build_email_real = self.registry('ir.mail_server').build_email
self.registry('ir.mail_server').build_email = self._mock_build_email
self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
# create a 'pigs' and 'bird' groups that will be used through the various tests
self.group_pigs_id = self.mail_group.create(self.cr, self.uid,
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})

View File

@ -26,23 +26,23 @@ class hr_config_settings(osv.osv_memory):
_inherit = 'res.config.settings'
_columns = {
'module_hr_timesheet_sheet': fields.boolean('allow timesheets validation by managers',
'module_hr_timesheet_sheet': fields.boolean('Allow timesheets validation by managers',
help ="""This installs the module hr_timesheet_sheet."""),
'module_hr_attendance': fields.boolean('track attendances',
'module_hr_attendance': fields.boolean('Track attendances',
help ="""This installs the module hr_attendance."""),
'module_hr_timesheet': fields.boolean('manage timesheets',
'module_hr_timesheet': fields.boolean('Manage timesheets',
help ="""This installs the module hr_timesheet."""),
'module_hr_holidays': fields.boolean('manage holidays, leaves and allocation requests',
'module_hr_holidays': fields.boolean('Manage holidays, leaves and allocation requests',
help ="""This installs the module hr_holidays."""),
'module_hr_expense': fields.boolean('manage employees expenses',
'module_hr_expense': fields.boolean('Manage employees expenses',
help ="""This installs the module hr_expense."""),
'module_hr_recruitment': fields.boolean('manage the recruitment process',
'module_hr_recruitment': fields.boolean('Manage the recruitment process',
help ="""This installs the module hr_recruitment."""),
'module_hr_contract': fields.boolean('record contracts per employee',
'module_hr_contract': fields.boolean('Record contracts per employee',
help ="""This installs the module hr_contract."""),
'module_hr_evaluation': fields.boolean('organize employees periodic evaluation',
'module_hr_evaluation': fields.boolean('Organize employees periodic evaluation',
help ="""This installs the module hr_evaluation."""),
'module_hr_payroll': fields.boolean('manage payroll',
'module_hr_payroll': fields.boolean('Manage payroll',
help ="""This installs the module hr_payroll."""),
}

View File

@ -24,6 +24,6 @@ from osv import osv, fields
class human_resources_configuration(osv.osv_memory):
_inherit = 'hr.config.settings'
_columns = {
'module_hr_payroll_account': fields.boolean('link your payroll to accounting system',
help ="""Create Journal Entries from Payslips"""),
'module_hr_payroll_account': fields.boolean('Link your payroll to accounting system',
help ="""Create journal entries from payslips"""),
}

View File

@ -26,10 +26,10 @@ class hr_applicant_settings(osv.osv_memory):
_inherit = ['hr.config.settings', 'fetchmail.config.settings']
_columns = {
'module_document_ftp': fields.boolean('allow the automatic indexation of resumes',
'module_document_ftp': fields.boolean('Allow the automatic indexation of resumes',
help="""Manage your CV's and motivation letter related to all applicants.
This installs the module document_ftp. This will install the knowledge management module in order to allow you to search using specific keywords through the content of all documents (PDF, .DOCx...)"""),
'fetchmail_applicants': fields.boolean('create applicants from an incoming email account',
'fetchmail_applicants': fields.boolean('Create applicants from an incoming email account',
fetchmail_model='hr.applicant', fetchmail_name='Incoming HR Applications',
help ="""Allow applicants to send their job application to an email address (jobs@mycompany.com),
and create automatically application documents in the system."""),

View File

@ -25,16 +25,16 @@ class knowledge_config_settings(osv.osv_memory):
_name = 'knowledge.config.settings'
_inherit = 'res.config.settings'
_columns = {
'module_document_page': fields.boolean('create static web pages',
'module_document_page': fields.boolean('Create static web pages',
help="""This installs the module document_page."""),
'module_document': fields.boolean('manage documents',
'module_document': fields.boolean('Manage documents',
help="""This is a complete document management system, with: user authentication,
full document search (but pptx and docx are not supported), and a document dashboard.
This installs the module document."""),
'module_document_ftp': fields.boolean('share repositories (FTP)',
'module_document_ftp': fields.boolean('Share repositories (FTP)',
help="""Access your documents in OpenERP through an FTP interface.
This installs the module document_ftp."""),
'module_document_webdav': fields.boolean('share repositories (WebDAV)',
'module_document_webdav': fields.boolean('Share repositories (WebDAV)',
help="""Access your documents in OpenERP through WebDAV.
This installs the module document_webdav."""),
}

View File

@ -61,6 +61,7 @@ The main features of the module are:
'website': 'http://www.openerp.com',
'depends': ['base', 'base_tools', 'base_setup'],
'data': [
'wizard/invite_view.xml',
'wizard/mail_compose_message_view.xml',
'res_config_view.xml',
'mail_message_view.xml',

View File

@ -86,7 +86,7 @@ class mail_alias(osv.Model):
help="Optional ID of a thread (record) to which all incoming "
"messages will be attached, even if they did not reply to it. "
"If set, this will disable the creation of new records completely."),
'alias_domain': fields.function(_get_alias_domain, string="Alias Domain", type='char', size=None),
'alias_domain': fields.function(_get_alias_domain, string="Alias domain", type='char', size=None),
}
_defaults = {

View File

@ -23,6 +23,7 @@ from osv import osv
from osv import fields
import tools
class mail_followers(osv.Model):
""" mail_followers holds the data related to the follow mechanism inside
OpenERP. Partners can choose to follow documents (records) of any kind
@ -84,16 +85,44 @@ class mail_notification(osv.Model):
notif_ids = self.search(cr, uid, [('partner_id', '=', partner_id), ('message_id', '=', msg_id)], context=context)
return self.write(cr, uid, notif_ids, {'read': True}, context=context)
def get_partners_to_notify(self, cr, uid, partner_ids, message, context=None):
""" Return the list of partners to notify, based on their preferences.
:param browse_record message: mail.message to notify
"""
notify_pids = []
for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
# Do not send an email to the writer
if partner.user_ids and partner.user_ids[0].id == uid:
continue
# Do not send to partners without email address defined
if not partner.email:
continue
# Partner does not want to receive any emails
if partner.notification_email_send == 'none':
continue
# Partner wants to receive only emails and comments
if partner.notification_email_send == 'comment' and message.type not in ('email', 'comment'):
continue
# Partner wants to receive only emails
if partner.notification_email_send == 'email' and message.type != 'email':
continue
notify_pids.append(partner.id)
return notify_pids
def notify(self, cr, uid, partner_ids, msg_id, context=None):
""" Send by email the notification depending on the user preferences """
context = context or {}
# mail_noemail (do not send email) or no partner_ids: do not send, return
if context.get('mail_noemail') or not partner_ids:
return True
mail_mail = self.pool.get('mail.mail')
msg = self.pool.get('mail.message').browse(cr, uid, msg_id, context=context)
notify_partner_ids = self.get_partners_to_notify(cr, uid, partner_ids, msg, context=context)
if not notify_partner_ids:
return True
mail_mail = self.pool.get('mail.mail')
# add signature
body_html = msg.body
signature = msg.author_id and msg.author_id.user_ids[0].signature or ''
@ -107,27 +136,6 @@ class mail_notification(osv.Model):
'body_html': body_html,
'state': 'outgoing',
}
for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
# Do not send an email to the writer
if partner.user_ids and partner.user_ids[0].id == uid:
continue
# Do not send to partners without email address defined
if not partner.email:
continue
# Partner does not want to receive any emails
if partner.notification_email_send == 'none':
continue
# Partner wants to receive only emails and comments
if partner.notification_email_send == 'comment' and msg.type not in ('email', 'comment'):
continue
# Partner wants to receive only emails
if partner.notification_email_send == 'email' and msg.type != 'email':
continue
if partner.email not in mail_values['email_to']:
mail_values['email_to'].append(partner.email)
if mail_values['email_to']:
mail_values['email_to'] = ', '.join(mail_values['email_to'])
email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
mail_mail.send(cr, uid, [email_notif_id], context=context)
return True
mail_values['email_to'] = ', '.join(mail_values['email_to'])
email_notif_id = mail_mail.create(cr, uid, mail_values, context=context)
return mail_mail.send(cr, uid, [email_notif_id], recipient_ids=notify_partner_ids, context=context)

View File

@ -19,13 +19,11 @@
#
##############################################################################
import datetime as DT
import openerp
import openerp.tools as tools
from operator import itemgetter
from osv import osv
from osv import fields
from tools.translate import _
class mail_group(osv.Model):
""" A mail_group is a collection of users sharing messages in a discussion
@ -47,7 +45,7 @@ class mail_group(osv.Model):
_columns = {
'description': fields.text('Description'),
'menu_id': fields.many2one('ir.ui.menu', string='Related Menu', required=True, ondelete="cascade"),
'public': fields.selection([('public','Public'),('private','Private'),('groups','Selected Group Only')], 'Privacy', required=True,
'public': fields.selection([('public', 'Public'), ('private', 'Private'), ('groups', 'Selected Group Only')], 'Privacy', required=True,
help='This group is visible by non members. \
Invisible groups can add members through the invite button.'),
'group_public_id': fields.many2one('res.groups', string='Authorized Group'),
@ -126,14 +124,14 @@ class mail_group(osv.Model):
search_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'mail', 'view_message_search')
params = {
'search_view_id': search_ref and search_ref[1] or False,
'domain': [('model','=','mail.group'), ('res_id','=',mail_group_id)],
'domain': [('model', '=', 'mail.group'), ('res_id', '=', mail_group_id)],
'context': {'default_model': 'mail.group', 'default_res_id': mail_group_id},
'res_model': 'mail.message',
'thread_level': 1,
}
cobj = self.pool.get('ir.actions.client')
newref = cobj.copy(cr, uid, ref[1], default={'params': str(params), 'name': vals['name']}, context=context)
self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,'+str(newref), 'mail_group_id': mail_group_id}, context=context)
self.write(cr, uid, [mail_group_id], {'action': 'ir.actions.client,' + str(newref), 'mail_group_id': mail_group_id}, context=context)
mail_alias.write(cr, uid, [vals['alias_id']], {"alias_force_thread_id": mail_group_id}, context)
@ -142,9 +140,13 @@ class mail_group(osv.Model):
return mail_group_id
def unlink(self, cr, uid, ids, context=None):
groups = self.browse(cr, uid, ids, context=context)
# Cascade-delete mail aliases as well, as they should not exist without the mail group.
mail_alias = self.pool.get('mail.alias')
alias_ids = [group.alias_id.id for group in self.browse(cr, uid, ids, context=context) if group.alias_id]
alias_ids = [group.alias_id.id for group in groups if group.alias_id]
# Cascade-delete menu entries as well
self.pool.get('ir.ui.menu').unlink(cr, uid, [group.menu_id.id for group in groups if group.menu_id], context=context)
# Delete mail_group
res = super(mail_group, self).unlink(cr, uid, ids, context=context)
mail_alias.unlink(cr, uid, alias_ids, context=context)
return res

View File

@ -30,6 +30,7 @@ from tools.translate import _
_logger = logging.getLogger(__name__)
class mail_mail(osv.Model):
""" Model holding RFC2822 email messages to send. This model also provides
facilities to queue and send new email messages. """
@ -58,8 +59,8 @@ class mail_mail(osv.Model):
'body_html': fields.text('Rich-text Contents', help="Rich-text/HTML message"),
# Auto-detected based on create() - if 'mail_message_id' was passed then this mail is a notification
# and during unlink() we will cascade delete the parent and its attachments
'notification': fields.boolean('Is Notification')
# and during unlink() we will not cascade delete the parent and its attachments
'notification': fields.boolean('Is Notification')
}
def _get_default_from(self, cr, uid, context=None):
@ -76,21 +77,21 @@ class mail_mail(osv.Model):
def create(self, cr, uid, values, context=None):
if 'notification' not in values and values.get('mail_message_id'):
values['notification'] = True
return super(mail_mail,self).create(cr, uid, values, context=context)
return super(mail_mail, self).create(cr, uid, values, context=context)
def unlink(self, cr, uid, ids, context=None):
# cascade-delete the parent message for all mails that are not created for a notification
ids_to_cascade = self.search(cr, uid, [('notification','=',False),('id','in',ids)])
ids_to_cascade = self.search(cr, uid, [('notification', '=', False), ('id', 'in', ids)])
parent_msg_ids = [m.mail_message_id.id for m in self.browse(cr, uid, ids_to_cascade, context=context)]
res = super(mail_mail,self).unlink(cr, uid, ids, context=context)
res = super(mail_mail, self).unlink(cr, uid, ids, context=context)
self.pool.get('mail.message').unlink(cr, uid, parent_msg_ids, context=context)
return res
def mark_outgoing(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state':'outgoing'}, context=context)
return self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
def cancel(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state':'cancel'}, context=context)
return self.write(cr, uid, ids, {'state': 'cancel'}, context=context)
def process_email_queue(self, cr, uid, ids=None, context=None):
"""Send immediately queued messages, committing after each
@ -127,7 +128,7 @@ class mail_mail(osv.Model):
"""Perform any post-processing necessary after sending ``mail``
successfully, including deleting it completely along with its
attachment if the ``auto_delete`` flag of the mail was set.
Overridden by subclasses for extra post-processing behaviors.
Overridden by subclasses for extra post-processing behaviors.
:param browse_record mail: the mail that was just sent
:return: True
@ -136,14 +137,46 @@ class mail_mail(osv.Model):
mail.unlink()
return True
def _send_get_mail_subject(self, cr, uid, mail, force=False, context=None):
""" if void and related document: '<Author> posted on <Resource>'
:param mail: mail.mail browse_record """
def send_get_mail_subject(self, cr, uid, mail, force=False, partner=None, context=None):
""" If subject is void and record_name defined: '<Author> posted on <Resource>'
:param boolean force: force the subject replacement
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
if force or (not mail.subject and mail.model and mail.res_id):
return '%s posted on %s' % (mail.author_id.name, mail.record_name)
return mail.subject
def send(self, cr, uid, ids, auto_commit=False, context=None):
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
""" Return a specific ir_email body. The main purpose of this method
is to be inherited by Portal, to add a link for signing in, in
each notification email a partner receives.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
return mail.body_html
def send_get_email_dict(self, cr, uid, mail, partner=None, context=None):
""" Return a dictionary for specific email values, depending on a
partner, or generic to the whole recipients given by mail.email_to.
:param browse_record mail: mail.mail browse_record
:param browse_record partner: specific recipient partner
"""
body = self.send_get_mail_body(cr, uid, mail, partner=partner, context=context)
subject = self.send_get_mail_subject(cr, uid, mail, partner=partner, context=context)
body_alternative = tools.html2plaintext(body)
email_to = [partner.email] if partner else tools.email_split(mail.email_to)
return {
'body': body,
'body_alternative': body_alternative,
'subject': subject,
'email_to': email_to,
}
def send(self, cr, uid, ids, auto_commit=False, recipient_ids=None, context=None):
""" Sends the selected emails immediately, ignoring their current
state (mails that have already been sent should not be passed
unless they should actually be re-sent).
@ -154,50 +187,55 @@ class mail_mail(osv.Model):
:param bool auto_commit: whether to force a commit of the mail status
after sending each mail (meant only for scheduler processing);
should never be True during normal transactions (default: False)
:param list recipient_ids: specific list of res.partner recipients.
If set, one email is sent to each partner. Its is possible to
tune the sent email through ``send_get_mail_body`` and ``send_get_mail_subject``.
If not specified, one email is sent to mail_mail.email_to.
:return: True
"""
ir_mail_server = self.pool.get('ir.mail_server')
for mail in self.browse(cr, uid, ids, context=context):
try:
body = mail.body_html
subject = self._send_get_mail_subject(cr, uid, mail, context=context)
# handle attachments
attachments = []
for attach in mail.attachment_ids:
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
# use only sanitized html and set its plaintexted version as alternative
body_alternative = tools.html2plaintext(body)
content_subtype_alternative = 'plain'
# specific behavior to customize the send email for notified partners
email_list = []
if recipient_ids:
for partner in self.pool.get('res.partner').browse(cr, uid, recipient_ids, context=context):
email_list.append(self.send_get_email_dict(cr, uid, mail, partner=partner, context=context))
else:
email_list.append(self.send_get_email_dict(cr, uid, mail, context=context))
# build an RFC2822 email.message.Message object and send it without queuing
msg = ir_mail_server.build_email(
email_from = mail.email_from,
email_to = tools.email_split(mail.email_to),
subject = subject,
body = body,
body_alternative = body_alternative,
email_cc = tools.email_split(mail.email_cc),
reply_to = mail.reply_to,
attachments = attachments,
message_id = mail.message_id,
references = mail.references,
object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype = 'html',
subtype_alternative = content_subtype_alternative)
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=mail.mail_server_id.id, context=context)
for email in email_list:
msg = ir_mail_server.build_email(
email_from = mail.email_from,
email_to = email.get('email_to'),
subject = email.get('subject'),
body = email.get('body'),
body_alternative = email.get('body_alternative'),
email_cc = tools.email_split(mail.email_cc),
reply_to = mail.reply_to,
attachments = attachments,
message_id = mail.message_id,
references = mail.references,
object_id = mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
subtype = 'html',
subtype_alternative = 'plain')
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=mail.mail_server_id.id, context=context)
if res:
mail.write({'state':'sent', 'message_id': res})
mail.write({'state': 'sent', 'message_id': res})
else:
mail.write({'state':'exception'})
mail.write({'state': 'exception'})
mail.refresh()
if mail.state == 'sent':
self._postprocess_sent_message(cr, uid, mail, context=context)
except Exception:
_logger.exception('failed sending mail.mail %s', mail.id)
mail.write({'state':'exception'})
mail.write({'state': 'exception'})
if auto_commit == True:
cr.commit()

View File

@ -20,11 +20,13 @@
##############################################################################
import logging
import openerp
import tools
from email.header import decode_header
from operator import itemgetter
from osv import osv, fields
from tools.translate import _
_logger = logging.getLogger(__name__)
@ -35,6 +37,7 @@ def decode(text):
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
class mail_message(osv.Model):
""" Messages model: system notification (replacing res.log notifications),
comments (OpenChatter discussion) and incoming emails. """
@ -57,7 +60,10 @@ class mail_message(osv.Model):
for message in self.browse(cr, uid, ids, context=context):
if not message.model or not message.res_id:
continue
result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
try:
result[message.id] = self._shorten_name(self.pool.get(message.model).name_get(cr, uid, [message.res_id], context=context)[0][1])
except openerp.exceptions.AccessDenied, e:
pass
return result
def _get_unread(self, cr, uid, ids, name, arg, context=None):
@ -341,3 +347,25 @@ class mail_message(osv.Model):
default = {}
default.update(message_id=False, headers=False)
return super(mail_message, self).copy(cr, uid, id, default=default, context=context)
#------------------------------------------------------
# Tools
#------------------------------------------------------
def check_partners_email(self, cr, uid, partner_ids, context=None):
""" Verify that selected partner_ids have an email_address defined.
Otherwise throw a warning. """
partner_wo_email_lst = []
for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
if not partner.email:
partner_wo_email_lst.append(partner)
if not partner_wo_email_lst:
return {}
warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
for partner in partner_wo_email_lst:
warning_msg += '\n- %s' % (partner.name)
return {'warning': {
'title': _('Partners email addresses not found'),
'message': warning_msg,
}
}

View File

@ -623,14 +623,8 @@ class mail_thread(osv.AbstractModel):
return self.message_subscribe(cr, uid, ids, partner_ids, context=context)
def message_subscribe(self, cr, uid, ids, partner_ids, context=None):
""" Add partners to the records followers.
:param partner_ids: a list of partner_ids to subscribe
:param return: new value of followers if read_back key in context
"""
self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
if context and context.get('read_back'):
return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
return []
""" Add partners to the records followers. """
return self.write(cr, uid, ids, {'message_follower_ids': [(4, pid) for pid in partner_ids]}, context=context)
def message_unsubscribe_users(self, cr, uid, ids, user_ids=None, context=None):
""" Wrapper on message_subscribe, using users. If user_ids is not
@ -640,14 +634,8 @@ class mail_thread(osv.AbstractModel):
return self.message_unsubscribe(cr, uid, ids, partner_ids, context=context)
def message_unsubscribe(self, cr, uid, ids, partner_ids, context=None):
""" Remove partners from the records followers.
:param partner_ids: a list of partner_ids to unsubscribe
:param return: new value of followers if read_back key in context
"""
self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
if context and context.get('read_back'):
return [follower.id for thread in self.browse(cr, uid, ids, context=context) for follower in thread.message_follower_ids]
return []
""" Remove partners from the records followers. """
return self.write(cr, uid, ids, {'message_follower_ids': [(3, pid) for pid in partner_ids]}, context=context)
#------------------------------------------------------
# Thread state

View File

@ -72,6 +72,9 @@ class res_users(osv.Model):
def create(self, cr, uid, data, context=None):
# create default alias same as the login
if not data.get('login', False):
raise osv.except_osv(_('Invalid Action!'), _('You may not create a user.'))
mail_alias = self.pool.get('mail.alias')
alias_id = mail_alias.create_unique_alias(cr, uid, {'alias_name': data['login']}, model_name=self._name, context=context)
data['alias_id'] = alias_id

View File

@ -96,10 +96,6 @@
width: 120px;
}
.openerp button.oe_mail_button_followers {
display: inline;
}
.openerp button.oe_mail_button_mouseout {
color: white;
background-color: #8a89ba;

View File

@ -31,33 +31,52 @@ openerp_mail_followers = function(session, mail) {
},
start: function() {
var self = this;
// NB: all the widget should be modified to check the actual_mode property on view, not use
// any other method to know if the view is in create mode anymore
// use actual_mode property on view to know if the view is in create mode anymore
this.view.on("change:actual_mode", this, this._check_visibility);
this._check_visibility();
this.$el.find('button.oe_mail_button_follow').click(function () { self.do_follow(); })
.mouseover(function () { $(this).html('Follow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); })
.mouseleave(function () { $(this).html('Not following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); });
this.$el.find('button.oe_mail_button_unfollow').click(function () { self.do_unfollow(); })
.mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); })
.mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); });
this.reinit();
this.bind_events();
},
_check_visibility: function() {
this.$el.toggle(this.view.get("actual_mode") !== "create");
},
destroy: function () {
this._super.apply(this, arguments);
},
reinit: function() {
this.$el.find('button.oe_mail_button_follow').hide();
this.$el.find('button.oe_mail_button_unfollow').hide();
},
bind_events: function() {
var self = this;
this.$('button.oe_mail_button_unfollow').on('click', function () { self.do_unfollow(); })
.mouseover(function () { $(this).html('Unfollow').removeClass('oe_mail_button_mouseout').addClass('oe_mail_button_mouseover'); })
.mouseleave(function () { $(this).html('Following').removeClass('oe_mail_button_mouseover').addClass('oe_mail_button_mouseout'); });
this.$el.on('click', 'button.oe_mail_button_follow', function () { self.do_follow(); });
this.$el.on('click', 'button.oe_mail_button_invite', function(event) {
action = {
type: 'ir.actions.act_window',
res_model: 'mail.wizard.invite',
view_mode: 'form',
view_type: 'form',
views: [[false, 'form']],
target: 'new',
context: {
'default_res_model': self.view.dataset.model,
'default_res_id': self.view.datarecord.id
},
}
self.do_action(action, function() { self.read_value(); });
});
},
read_value: function() {
var self = this;
return this.ds_model.read_ids([this.view.datarecord.id], ['message_follower_ids']).pipe(function (results) {
return results[0].message_follower_ids;
}).pipe(this.proxy('set_value'));
},
set_value: function(value_) {
this.reinit();
if (! this.view.datarecord.id ||
@ -65,11 +84,11 @@ openerp_mail_followers = function(session, mail) {
this.$el.find('div.oe_mail_recthread_aside').hide();
return;
}
return this.fetch_followers(value_);
return this.fetch_followers(value_ || this.get_value());
},
fetch_followers: function (value_) {
return this.ds_follow.call('read', [value_ || this.get_value(), ['name', 'user_ids']]).then(this.proxy('display_followers'));
return this.ds_follow.call('read', [value_, ['name', 'user_ids']]).pipe(this.proxy('display_followers'));
},
/** Display the followers, evaluate is_follower directly */
@ -91,13 +110,13 @@ openerp_mail_followers = function(session, mail) {
},
do_follow: function () {
var context = new session.web.CompoundContext(this.build_context(), {'read_back': true});
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('set_value'));
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_subscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value'));
},
do_unfollow: function () {
var context = new session.web.CompoundContext(this.build_context(), {'read_back': true});
return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('set_value'));
var context = new session.web.CompoundContext(this.build_context(), {});
return this.ds_model.call('message_unsubscribe_users', [[this.view.datarecord.id], undefined, context]).pipe(this.proxy('read_value'));
},
});
};

View File

@ -114,7 +114,7 @@
<!-- dropdown menu with message options and actions -->
<span class="oe_dropdown_toggle oe_dropdown_arrow">
<ul class="oe_dropdown_menu">
<li t-if="record.is_author &amp; options.show_dd_delete"><a class="oe_mail_msg_delete" t-attf-data-id='{record.id}'>Delete</a></li>
<li t-if="record.is_author and options.show_dd_delete"><a class="oe_mail_msg_delete" t-attf-data-id='{record.id}'>Delete</a></li>
<li t-if="options.show_dd_hide"><a class="oe_mail_msg_hide" t-attf-data-id='{record.id}'>Remove notification</a></li>
<!-- Uncomment when adding subtype hiding
<li t-if="display['show_hide']">
@ -130,14 +130,14 @@
<t t-raw="record.subject"/>
</h1>
<div class="oe_mail_msg_body">
<t t-if="options.show_record_name &amp; (!record.subject) &amp; (options.thread_level > 0)">
<t t-if="options.show_record_name and record.record_name and (!record.subject) and (options.thread_level > 0)">
<a t-attf-href="#model=#{record.model}&amp;id=#{record.res_id}"><t t-raw="record.record_name"/></a>
</t>
<t t-raw="record.body"/>
</div>
<div class="oe_clear"/>
<ul class="oe_mail_msg_footer">
<li t-if="options.show_record_name &amp; record.subject &amp; options.thread_level > 0">
<li t-if="options.show_record_name and record.record_name and record.subject and options.thread_level > 0">
<a t-attf-href="#model=#{record.model}&amp;id=#{record.res_id}"><t t-raw="record.record_name"/></a>
</li>
<li><a t-attf-href="#model=res.partner&amp;id=#{record.author_id[0]}"><t t-raw="record.author_id[1]"/></a></li>

View File

@ -7,7 +7,8 @@
-->
<div t-name="mail.followers" class="oe_mail_recthread_aside oe_semantic_html_override">
<div class="oe_mail_recthread_actions">
<button type="button" class="oe_mail_button_follow oe_mail_button_mouseout">Not following</button>
<button type="button" class="oe_mail_button_invite">Invite</button>
<button type="button" class="oe_mail_button_follow">Follow</button>
<button type="button" class="oe_mail_button_unfollow oe_mail_button_mouseout">Following</button>
</div>
<div class="oe_mail_recthread_followers">

View File

@ -19,6 +19,8 @@
#
##############################################################################
import tools
from openerp.tests import common
from openerp.tools.html_sanitize import html_sanitize
@ -82,15 +84,42 @@ Sylvie
"""
class test_mail(common.TransactionCase):
class TestMailMockups(common.TransactionCase):
def _mock_smtp_gateway(self, *args, **kwargs):
return True
def _init_mock_build_email(self):
self._build_email_args_list = []
self._build_email_kwargs_list = []
def _mock_build_email(self, *args, **kwargs):
self._build_email_args = args
self._build_email_kwargs = kwargs
return self.build_email_real(*args, **kwargs)
self._build_email_args_list.append(args)
self._build_email_kwargs_list.append(kwargs)
return self._build_email(*args, **kwargs)
def setUp(self):
super(TestMailMockups, self).setUp()
# Install mock SMTP gateway
self._init_mock_build_email()
self._build_email = self.registry('ir.mail_server').build_email
self.registry('ir.mail_server').build_email = self._mock_build_email
self._send_email = self.registry('ir.mail_server').send_email
self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
def tearDown(self):
# Remove mocks
self.registry('ir.mail_server').build_email = self._build_email
self.registry('ir.mail_server').send_email = self._send_email
super(TestMailMockups, self).tearDown()
class test_mail(TestMailMockups):
def _mock_send_get_mail_body(self, *args, **kwargs):
# def _send_get_mail_body(self, cr, uid, mail, partner=None, context=None)
body = tools.append_content_to_html(args[2].body_html, kwargs.get('partner').name if kwargs.get('partner') else 'No specific partner')
return body
def setUp(self):
super(test_mail, self).setUp()
@ -105,10 +134,9 @@ class test_mail(common.TransactionCase):
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
# Install mock SMTP gateway
self.build_email_real = self.registry('ir.mail_server').build_email
self.registry('ir.mail_server').build_email = self._mock_build_email
self.registry('ir.mail_server').send_email = self._mock_smtp_gateway
# Mock send_get_mail_body to test its functionality without other addons override
self._send_get_mail_body = self.registry('mail.mail').send_get_mail_body
self.registry('mail.mail').send_get_mail_body = self._mock_send_get_mail_body
# groups@.. will cause the creation of new mail groups
self.mail_group_model_id = self.ir_model.search(self.cr, self.uid, [('model', '=', 'mail.group')])[0]
@ -118,6 +146,11 @@ class test_mail(common.TransactionCase):
self.group_pigs_id = self.mail_group.create(self.cr, self.uid,
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
def tearDown(self):
# Remove mocks
self.registry('mail.mail').send_get_mail_body = self._send_get_mail_body
super(test_mail, self).tearDown()
def test_00_message_process(self):
cr, uid = self.cr, self.uid
# Incoming mail creates a new mail_group "frogs"
@ -274,18 +307,20 @@ class test_mail(common.TransactionCase):
_attachments = [('First', 'My first attachment'), ('Second', 'My second attachment')]
# CASE1: post comment, body and subject specified
self._init_mock_build_email()
msg_id = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body1, subject=_subject, type='comment')
message = self.mail_message.browse(cr, uid, msg_id)
sent_email = self._build_email_kwargs
sent_emails = self._build_email_kwargs_list
# Test: notifications have been deleted
self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id)]), 'mail.mail notifications should have been auto-deleted!')
# Test: mail_message: subject is _subject, body is _body1 (no formatting done)
self.assertEqual(message.subject, _subject, 'mail.message subject incorrect')
self.assertEqual(message.body, _body1, 'mail.message body incorrect')
# Test: sent_email: email send by server: correct subject, body; body_alternative
self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
self.assertEqual(sent_email['body'], _mail_body1, 'sent_email body incorrect')
self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1, 'sent_email body_alternative is incorrect')
# Test: sent_email: email send by server: correct subject, body, body_alternative
for sent_email in sent_emails:
self.assertEqual(sent_email['subject'], _subject, 'sent_email subject incorrect')
self.assertEqual(sent_email['body'], _mail_body1 + '\n<pre>Bert Tartopoils</pre>\n', 'sent_email body incorrect')
self.assertEqual(sent_email['body_alternative'], _mail_bodyalt1 + '\nBert Tartopoils', 'sent_email body_alternative is incorrect')
# Test: mail_message: partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids])
test_pids = set([p_a_id, p_b_id, p_c_id])
@ -295,14 +330,16 @@ class test_mail(common.TransactionCase):
notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
# Test: sent_email: email_to should contain b@b, not c@c (pref email), not a@a (writer)
self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
for sent_email in sent_emails:
self.assertEqual(sent_email['email_to'], ['b@b'], 'sent_email email_to is incorrect')
# CASE2: post an email with attachments, parent_id, partner_ids
# TESTS: automatic subject, signature in body_html, attachments propagation
self._init_mock_build_email()
msg_id2 = self.mail_group.message_post(cr, uid, self.group_pigs_id, body=_body2, type='email',
partner_ids=[(6, 0, [p_d_id])], parent_id=msg_id, attachments=_attachments)
message = self.mail_message.browse(cr, uid, msg_id2)
sent_email = self._build_email_kwargs
sent_emails = self._build_email_kwargs_list
self.assertFalse(self.mail_mail.search(cr, uid, [('mail_message_id', '=', msg_id2)]), 'mail.mail notifications should have been auto-deleted!')
# Test: mail_message: subject is False, body is _body2 (no formatting done), parent_id is msg_id
@ -310,9 +347,11 @@ class test_mail(common.TransactionCase):
self.assertEqual(message.body, html_sanitize(_body2), 'mail.message body incorrect')
self.assertEqual(message.parent_id.id, msg_id, 'mail.message parent_id incorrect')
# Test: sent_email: email send by server: correct subject, body, body_alternative
self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
self.assertEqual(sent_email['body'], _mail_body2, 'sent_email body incorrect')
self.assertEqual(sent_email['body_alternative'], _mail_bodyalt2, 'sent_email body_alternative incorrect')
self.assertEqual(len(sent_emails), 2, 'sent_email number of sent emails incorrect')
for sent_email in sent_emails:
self.assertEqual(sent_email['subject'], _mail_subject, 'sent_email subject incorrect')
self.assertIn(_mail_body2, sent_email['body'], 'sent_email body incorrect')
self.assertIn(_mail_bodyalt2, sent_email['body_alternative'], 'sent_email body_alternative incorrect')
# Test: mail_message: partner_ids = group followers
message_pids = set([partner.id for partner in message.partner_ids])
test_pids = set([p_a_id, p_b_id, p_c_id, p_d_id])
@ -322,7 +361,8 @@ class test_mail(common.TransactionCase):
notif_pids = set([notif.partner_id.id for notif in self.mail_notification.browse(cr, uid, notif_ids)])
self.assertEqual(notif_pids, test_pids, 'mail.message notification partners incorrect')
# Test: sent_email: email_to should contain b@b, c@c, not a@a (writer)
self.assertEqual(set(sent_email['email_to']), set(['b@b', 'c@c']), 'sent_email email_to incorrect')
for sent_email in sent_emails:
self.assertTrue(set(sent_email['email_to']).issubset(set(['b@b', 'c@c'])), 'sent_email email_to incorrect')
# Test: attachments
for attach in message.attachment_ids:
self.assertEqual(attach.res_model, 'mail.group', 'mail.message attachment res_model incorrect')

View File

@ -19,6 +19,7 @@
#
##############################################################################
import invite
import mail_compose_message
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,82 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2012-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from osv import osv
from osv import fields
from tools.translate import _
class invite_wizard(osv.osv_memory):
""" Wizard to invite partners and make them followers. """
_name = 'mail.wizard.invite'
_description = 'Invite wizard'
def default_get(self, cr, uid, fields, context=None):
result = super(invite_wizard, self).default_get(cr, uid, fields, context=context)
if 'message' in fields and result.get('res_model') and result.get('res_id'):
document_name = self.pool.get(result.get('res_model')).name_get(cr, uid, [result.get('res_id')], context=context)[0][1]
message = _('<div>You have been invited to follow %s.</div>' % document_name)
result['message'] = message
elif 'message' in fields:
result['message'] = _('<div>You have been invited to follow a new document.</div>')
return result
_columns = {
'res_model': fields.char('Related Document Model', size=128,
required=True, select=1,
help='Model of the followed resource'),
'res_id': fields.integer('Related Document ID', select=1,
help='Id of the followed resource'),
'partner_ids': fields.many2many('res.partner', string='Partners'),
'message': fields.html('Message'),
}
def onchange_partner_ids(self, cr, uid, ids, value, context=None):
""" onchange_partner_ids (value format: [[6, 0, [3, 4]]]). The
basic purpose of this method is to check that destination partners
effectively have email addresses. Otherwise a warning is thrown.
"""
res = {'value': {}}
if not value or not value[0] or not value[0][0] == 6:
return
res.update(self.pool.get('mail.message').check_partners_email(cr, uid, value[0][2], context=context))
return res
def add_followers(self, cr, uid, ids, context=None):
for wizard in self.browse(cr, uid, ids, context=context):
model_obj = self.pool.get(wizard.res_model)
document = model_obj.browse(cr, uid, wizard.res_id, context=context)
# filter partner_ids to get the new followers, to avoid sending email to already following partners
new_follower_ids = [p.id for p in wizard.partner_ids if p.id not in document.message_follower_ids]
model_obj.message_subscribe(cr, uid, [wizard.res_id], new_follower_ids, context=context)
# send an email
if wizard.message:
for follower_id in new_follower_ids:
mail_mail = self.pool.get('mail.mail')
mail_id = mail_mail.create(cr, uid, {
'subject': 'Invitation to follow %s' % document.name_get()[0][1],
'body_html': '%s' % wizard.message,
'auto_delete': True,
}, context=context)
mail_mail.send(cr, uid, [mail_id], recipient_ids=[follower_id], context=context)
return {'type': 'ir.actions.act_window_close'}

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<!-- wizard view -->
<record model="ir.ui.view" id="mail_wizard_invite_form">
<field name="name">Add Followers</field>
<field name="model">mail.wizard.invite</field>
<field name="arch" type="xml">
<form string="Add Followers" version="7.0">
<group>
<field name="res_model" invisible="1"/>
<field name="res_id" invisible="1"/>
<field name="partner_ids" widget="many2many_tags"
on_change="onchange_partner_ids(partner_ids)" />
<field name="message"/>
</group>
<footer>
<button string="Add Followers"
name="add_followers" type="object" class="oe_highlight" />
or
<button string="Cancel" class="oe_link" special="cancel" />
</footer>
</form>
</field>
</record>
</data>
</openerp>

View File

@ -193,24 +193,6 @@ class mail_compose_message(osv.TransientModel):
"""
return {'value': {'content_subtype': value}}
def _verify_partner_email(self, cr, uid, partner_ids, context=None):
""" Verify that selected partner_ids have an email_address defined.
Otherwise throw a warning. """
partner_wo_email_lst = []
for partner in self.pool.get('res.partner').browse(cr, uid, partner_ids, context=context):
if not partner.email:
partner_wo_email_lst.append(partner)
if not partner_wo_email_lst:
return {}
warning_msg = _('The following partners chosen as recipients for the email have no email address linked :')
for partner in partner_wo_email_lst:
warning_msg += '\n- %s' % (partner.name)
return {'warning': {
'title': _('Partners email addresses not found'),
'message': warning_msg,
}
}
def onchange_partner_ids(self, cr, uid, ids, value, context=None):
""" The basic purpose of this method is to check that destination partners
effectively have email addresses. Otherwise a warning is thrown.
@ -219,7 +201,7 @@ class mail_compose_message(osv.TransientModel):
res = {'value': {}}
if not value or not value[0] or not value[0][0] == 6:
return
res.update(self._verify_partner_email(cr, uid, value[0][2], context=context))
res.update(self.check_partners_email(cr, uid, value[0][2], context=context))
return res
def dummy(self, cr, uid, ids, context=None):

View File

@ -25,14 +25,14 @@ class marketing_config_settings(osv.osv_memory):
_name = 'marketing.config.settings'
_inherit = 'res.config.settings'
_columns = {
'module_marketing_campaign': fields.boolean('Marketing Campaigns',
'module_marketing_campaign': fields.boolean('Marketing campaigns',
help="""Provides leads automation through marketing campaigns.
Campaigns can in fact be defined on any resource, not just CRM leads.
This installs the module marketing_campaign."""),
'module_marketing_campaign_crm_demo': fields.boolean('Demo Data for Marketing Campaigns',
'module_marketing_campaign_crm_demo': fields.boolean('Demo data for marketing campaigns',
help="""Installs demo data like leads, campaigns and segments for Marketing Campaigns.
This installs the module marketing_campaign_crm_demo."""),
'module_crm_profiling': fields.boolean('Track Customer Profile to Focus your Campaigns',
'module_crm_profiling': fields.boolean('Track customer profile to focus your campaigns',
help="""Allows users to perform segmentation within partners.
This installs the module crm_profiling."""),
}

View File

@ -28,11 +28,11 @@ class mrp_config_settings(osv.osv_memory):
_inherit = 'res.config.settings'
_columns = {
'module_stock_planning': fields.boolean('manage master production shedule',
'module_stock_planning': fields.boolean('Manage master production shedule',
help ="""This allows to create a manual procurement plan apart of the normal MRP scheduling,
which works automatically based on minimum stock rules.
This installs the module stock_planning."""),
'module_mrp_repair': fields.boolean("manage repairs of products ",
'module_mrp_repair': fields.boolean("Manage repairs of products ",
help="""Allows to manage all product repairs.
* Add/remove products in the reparation
* Impact for stocks
@ -41,34 +41,34 @@ class mrp_config_settings(osv.osv_memory):
* Repair quotation report
* Notes for the technician and for the final customer.
This installs the module mrp_repair."""),
'module_mrp_operations': fields.boolean("allow detailed planning of work order",
'module_mrp_operations': fields.boolean("Allow detailed planning of work order",
help="""This allows to add state, date_start,date_stop in production order operation lines (in the "Work Centers" tab).
This installs the module mrp_operations."""),
'module_mrp_subproduct': fields.boolean("produce several products from one manufacturing order",
'module_mrp_subproduct': fields.boolean("Produce several products from one manufacturing order",
help="""You can configure sub-products in the bill of material.
Without this module: A + B + C -> D.
With this module: A + B + C -> D + E.
This installs the module mrp_subproduct."""),
'module_mrp_jit': fields.boolean("generate procurement in real time",
'module_mrp_jit': fields.boolean("Generate procurement in real time",
help="""This allows Just In Time computation of procurement orders.
All procurement orders will be processed immediately, which could in some
cases entail a small performance impact.
This installs the module mrp_jit."""),
'module_stock_no_autopicking': fields.boolean("manage manual picking to fulfill manufacturing orders ",
'module_stock_no_autopicking': fields.boolean("Manage manual picking to fulfill manufacturing orders ",
help="""This module allows an intermediate picking process to provide raw materials to production orders.
For example to manage production made by your suppliers (sub-contracting).
To achieve this, set the assembled product which is sub-contracted to "No Auto-Picking"
and put the location of the supplier in the routing of the assembly operation.
This installs the module stock_no_autopicking."""),
'group_mrp_routings': fields.boolean("manage routings and work orders ",
'group_mrp_routings': fields.boolean("Manage routings and work orders ",
implied_group='mrp.group_mrp_routings',
help="""Routings allow you to create and manage the manufacturing operations that should be followed
within your work centers in order to produce a product. They are attached to bills of materials
that will define the required raw materials."""),
'group_mrp_properties': fields.boolean("allow several bill of materials per products using properties",
'group_mrp_properties': fields.boolean("Allow several bill of materials per products using properties",
implied_group='product.group_mrp_properties',
help="""The selection of the right Bill of Material to use will depend on the properties specified on the sale order and the Bill of Material."""),
'module_product_manufacturer': fields.boolean("define manufacturers on products ",
'module_product_manufacturer': fields.boolean("Define manufacturers on products ",
help="""This allows you to define the following for a product:
* Manufacturer
* Manufacturer Product Name

View File

@ -72,19 +72,19 @@ class PointOfSaleController(openerpweb.Controller):
return getattr(self, method)(request, **kwargs)
@openerpweb.jsonrequest
def scan_item_success(self, request):
def scan_item_success(self, request, ean):
"""
A product has been scanned with success
"""
print 'scan_item_success'
print 'scan_item_success: ' + str(ean)
return
@openerpweb.jsonrequest
def scan_item_error_unrecognized(self, request):
def scan_item_error_unrecognized(self, request, ean):
"""
A product has been scanned without success
"""
print 'scan_item_error_unrecognized'
print 'scan_item_error_unrecognized: ' + str(ean)
return
@openerpweb.jsonrequest
@ -166,4 +166,9 @@ class PointOfSaleController(openerpweb.Controller):
print 'print_receipt' + str(receipt)
return
@openerpweb.jsonrequest
def print_pdf_invoice(self, request, pdfinvoice):
print 'print_pdf_invoice' + str(pdfinvoice)
return

View File

@ -6,17 +6,17 @@
</record>
<record id="base.user_demo" model="res.users">
<field name="groups_id" eval="[(4,ref('group_pos_user'))]"/>
<field name="ean13">0410200000005</field>
<field name="ean13">0420100000005</field>
</record>
<record id="account.cash_journal" model="account.journal">
<field eval="True" name="journal_user"/>
</record>
<record id="base.user_root" model="res.users">
<field name="ean13">0410300000004</field>
<field name="ean13">0410100000006</field>
<field name="groups_id" eval="[(4,ref('group_pos_manager'))]"/>
</record>
<record id="base.user_demo" model="res.users">
<field name="ean13">0410400000003</field>
<field name="ean13">0420100000005</field>
<field name="groups_id" eval="[(4,ref('group_pos_manager'))]"/>
</record>
@ -189,6 +189,7 @@
<record id="citron" model="product.product">
<field name="list_price">1.98</field>
<field name="name">Lemon</field>
<field name="ean13">2301000000006</field>
<field name="to_weight">True</field>
<field name="pos_categ_id" ref="autres_agrumes"/>
<field name="uom_id" ref="product.product_uom_kgm" />

View File

@ -71,7 +71,7 @@
clear: left;
}
.point-of-sale .keyboard .lastitem {
margin-right: 0;
margin-right: 0 !important;
}
/* ---- full sized keyboard ---- */
@ -79,13 +79,13 @@
.point-of-sale .full_keyboard {
list-style: none;
font-size: 14px;
width: 680px;
width: 685px;
height: 100%;
margin-left: auto;
margin-right: auto;
margin-left: auto !important;
margin-right: auto !important;
}
.point-of-sale .full_keyboard li{
margin: 0 5px 5px 0;
margin: 0 5px 5px 0 !important;
width: 40px;
height: 40px;
line-height: 40px;
@ -115,22 +115,22 @@
.point-of-sale .simple_keyboard {
list-style: none;
font-size: 16px;
width: 545px;
width: 555px;
height: 220px;
margin-left: auto;
margin-right: auto;
margin-left: auto !important;
margin-right: auto !important;
}
.point-of-sale .simple_keyboard li{
margin: 0 5px 5px 0;
margin: 0 5px 5px 0 !important;
width: 49px;
height: 49px;
line-height: 49px;
}
.point-of-sale .simple_keyboard .firstitem.row_asdf{
margin-left:25px;
margin-left:25px !important;
}
.point-of-sale .simple_keyboard .firstitem.row_zxcv{
margin-left:55px;
margin-left:55px !important;
}
.point-of-sale .simple_keyboard .delete{
width: 103px;
@ -139,7 +139,7 @@
width: 103px;
}
.point-of-sale .simple_keyboard .space{
width:268px;
width:273px;
}
.point-of-sale .simple_keyboard .numlock{
width:103px;

View File

@ -373,7 +373,7 @@
/* ********* The product list ********* */
.point-of-sale .product-list {
padding:10px;
padding:10px !important;
}
.point-of-sale .product-list-scroller{
@ -470,7 +470,7 @@
}
.point-of-sale .category-list{
padding:10px;
padding:10px !important;
}
/* d) the category button */
@ -479,7 +479,7 @@
vertical-align: top;
display: inline-block;
font-size: 11px;
margin: 5px;
margin: 5px !important;
width: 120px;
height:120px;
background:#fff;
@ -566,7 +566,7 @@
display: inline-block;
line-height: 100px;
font-size: 11px;
margin: 5px;
margin: 5px !important;
width: 120px;
height:120px;
background:#fff;
@ -809,6 +809,8 @@
}
.point-of-sale .scale-screen .product-picture img{
max-width: 178px;
max-height:178px;
vertical-align: middle;
cursor:pointer;
}
@ -860,7 +862,7 @@
.point-of-sale .goodbye-message{
position: absolute;
left:50%;
top:30%;
top:40%;
width:500px;
height:400px;
margin-left: -250px;
@ -1100,6 +1102,79 @@
.point-of-sale .pos-actionbar .button.rightalign{
float:right;
}
/* ********* The Debug Widget ********* */
.point-of-sale .debug-widget{
z-index:100000;
position: absolute;
right: 10px;
top: 10px;
width: 200px;
font-size: 10px;
background: rgba(0,0,0,0.82);
color: white;
text-shadow: none;
padding-bottom: 10px;
box-shadow: 0px 3px 20px rgba(0,0,0,0.3);
cursor:move;
}
.point-of-sale .debug-widget .toggle{
position: absolute;
font-size: 16px;
cursor:pointer;
top:0px;
right:0px;
padding:10px;
padding-right:15px;
}
.point-of-sale .debug-widget .content{
overflow: hidden;
}
.point-of-sale .debug-widget h1{
background:black;
padding-top: 10px;
padding-left: 10px;
margin-top:0;
margin-bottom:0;
}
.point-of-sale .debug-widget .category{
background: black;
padding-left: 10px;
margin: 0px;
font-weight: bold;
padding-top:3px;
padding-bottom:3px;
}
.point-of-sale .debug-widget .button{
padding: 5px;
padding-left: 15px;
display: block;
cursor:pointer;
}
.point-of-sale .debug-widget .button:hover{
background: rgba(96,21,177,0.45);
}
.point-of-sale .debug-widget input{
margin-left:10px;
margin-top:7px;
}
.point-of-sale .debug-widget .status{
padding: 5px;
padding-left: 15px;
display: block;
cursor:default;
}
.point-of-sale .debug-widget .status.on{
background-color: #6cd11d;
}
.point-of-sale .debug-widget .event{
padding: 5px;
padding-left: 15px;
display: block;
cursor:default;
background-color: #1E1E1E;
}
/* ********* The PopupWidgets ********* */

View File

@ -21,6 +21,8 @@ function openerp_pos_db(instance, module){
this.category_childs = {};
this.category_parent = {};
this.category_search_string = {};
this.packagings_by_id = {};
this.packagings_by_product_id = {};
},
/* returns the category object from its id. If you pass a list of id as parameters, you get
* a list of category objects.
@ -116,6 +118,10 @@ function openerp_pos_db(instance, module){
if(product.ean13){
str += '|' + product.ean13;
}
var packagings = this.packagings_by_product_id[product.id] || [];
for(var i = 0; i < packagings.length; i++){
str += '|' + packagings[i].ean;
}
return str + '\n';
},
add_products: function(products){
@ -158,6 +164,16 @@ function openerp_pos_db(instance, module){
this.save('products',stored_products);
this.save('categories',stored_categories);
},
add_packagings: function(packagings){
for(var i = 0, len = packagings.length; i < len; i++){
var pack = packagings[i];
this.packagings_by_id[pack.id] = pack;
if(!this.packagings_by_product_id[pack.product_id[0]]){
this.packagings_by_product_id[pack.product_id[0]] = [];
}
this.packagings_by_product_id[pack.product_id[0]].push(pack);
}
},
/* removes all the data from the database. TODO : being able to selectively remove data */
clear: function(stores){
for(var i = 0, len = arguments.length; i < len; i++){
@ -184,6 +200,12 @@ function openerp_pos_db(instance, module){
return products[i];
}
}
for(var p in this.packagings_by_id){
var pack = this.packagings_by_id[p];
if( pack.ean === ean13){
return products[pack.product_id[0]];
}
}
return undefined;
},
get_product_by_category: function(category_id){
@ -223,9 +245,12 @@ function openerp_pos_db(instance, module){
},
remove_order: function(order_id){
var orders = this.load('orders',[]);
console.log('Remove order:',order_id);
console.log('Order count:',orders.length);
orders = _.filter(orders, function(order){
return order.id !== order_id;
});
console.log('Order count:',orders.length);
this.save('orders',orders);
},
get_orders: function(){

View File

@ -1,26 +1,6 @@
function openerp_pos_devices(instance,module){ //module is instance.point_of_sale
var debug_devices = new (instance.web.Class.extend({
active: false,
payment_status: 'waiting_for_payment',
weight: 0,
activate: function(){
this.active = true;
},
deactivate: function(){
this.active = false;
},
set_weight: function(weight){ this.activate(); this.weight = weight; },
accept_payment: function(){ this.activate(); this.payment_status = 'payment_accepted'; },
reject_payment: function(){ this.activate(); this.payment_status = 'payment_rejected'; },
delay_payment: function(){ this.activate(); this.payment_status = 'waiting_for_payment'; },
}))();
if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
window.debug_devices = debug_devices;
}
// this object interfaces with the local proxy to communicate to the various hardware devices
// connected to the Point of Sale. As the communication only goes from the POS to the proxy,
// methods are used both to signal an event, and to fetch information.
@ -38,32 +18,42 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.connection = new instance.web.JsonRPC();
this.connection.setup(url);
this.bypass_proxy = false;
this.notifications = {};
},
message : function(name,params,success_callback, error_callback){
success_callback = success_callback || function(){};
error_callback = error_callback || function(){};
if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
console.log('PROXY:',name,params);
var callbacks = this.notifications[name] || [];
for(var i = 0; i < callbacks.length; i++){
callbacks[i](params);
}
if(!(debug_devices && debug_devices.active)){
this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback);
this.connection.rpc('/pos/'+name, params || {}, success_callback, error_callback);
},
// this allows the client to be notified when a proxy call is made. The notification
// callback will be executed with the same arguments as the proxy call
add_notification: function(name, callback){
if(!this.notifications[name]){
this.notifications[name] = [];
}
this.notifications[name].push(callback);
},
//a product has been scanned and recognized with success
// ean is a parsed ean object
scan_item_success: function(ean){
this.message('scan_item_success',ean);
this.message('scan_item_success',{ean: ean});
},
// a product has been scanned but not recognized
// ean is a parsed ean object
scan_item_error_unrecognized: function(ean){
this.message('scan_item_error_unrecognized',ean);
this.message('scan_item_error_unrecognized',{ean: ean});
},
//the client is asking for help
@ -78,12 +68,12 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
//the client is starting to weight
weighting_start: function(){
this.weight = 0;
if(debug_devices){
debug_devices.weigth = 0;
if(!this.weighting){
this.weight = 0;
this.weighting = true;
this.bypass_proxy = false;
this.message('weighting_start');
}
this.weighting = true;
this.message('weighting_start');
},
//returns the weight on the scale.
@ -91,22 +81,29 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
// and a weighting_end()
weighting_read_kg: function(){
var self = this;
if(debug_devices && debug_devices.active){
return debug_devices.weight;
if(this.bypass_proxy){
return this.weight;
}else{
this.message('weighting_read_kg',{},function(weight){
if(self.weighting){
if(self.weighting && !self.bypass_proxy){
self.weight = weight;
}
});
return self.weight;
return this.weight;
}
},
// sets a custom weight, ignoring the proxy returned value until the next weighting_end
debug_set_weight: function(kg){
this.bypass_proxy = true;
this.weight = kg;
},
// the client has finished weighting products
weighting_end: function(){
this.weight = 0;
this.weighting = false;
this.bypass_proxy = false;
this.message('weighting_end');
},
@ -116,9 +113,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
payment_request: function(price, method, info){
this.paying = true;
this.payment_status = 'waiting_for_payment';
if(debug_devices){
debug_devices.payment_status = 'waiting_for_payment';
}
this.message('payment_request',{'price':price,'method':method,'info':info});
},
@ -127,18 +121,30 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
// returns 'waiting_for_payment' | 'payment_accepted' | 'payment_rejected'
is_payment_accepted: function(){
var self = this;
if(debug_devices.active){
return debug_devices.payment_status;
if(this.bypass_proxy){
this.bypass_proxy = false;
return this.payment_status;
}else{
this.message('is_payment_accepted', {}, function(payment_status){
if(self.paying){
self.payment_status = payment_status;
}
});
return self.payment_status;
return this.payment_status;
}
},
// override what the proxy says and accept the payment
debug_accept_payment: function(){
this.bypass_proxy = true;
this.payment_status = 'payment_accepted';
},
// override what the proxy says and reject the payment
debug_reject_payment: function(){
this.bypass_proxy = true;
this.payment_status = 'payment_rejected';
},
// the client cancels his payment
payment_canceled: function(){
this.paying = false;
@ -212,6 +218,11 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
print_receipt: function(receipt){
this.message('print_receipt',{receipt: receipt});
},
// asks the proxy to print an invoice in pdf form ( used to print invoices generated by the server )
print_pdf_invoice: function(pdfinvoice){
this.message('print_pdf_invoice',{pdfinvoice: pdfinvoice});
},
});
// this module interfaces with the barcode reader. It assumes the barcode reader
@ -237,6 +248,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
this.cashier_prefix_set = attributes.cashier_prefix_set || {'041':''};
this.client_prefix_set = attributes.client_prefix_set || {'042':''};
},
save_callbacks: function(){
var callbacks = {};
for(name in this.action_callback){
@ -244,6 +256,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
}
this.action_callback_stack.push(callbacks);
},
restore_callbacks: function(){
if(this.action_callback_stack.length){
var callbacks = this.action_callback_stack.pop();
@ -337,7 +350,6 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
value: 0,
unit: 'none',
};
console.log('ean',ean);
function match_prefix(prefix_set, type){
for(prefix in prefix_set){
@ -380,6 +392,23 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
return parse_result;
},
on_ean: function(ean){
var parse_result = this.parse_ean(ean);
if (parse_result.type === 'error') { //most likely a checksum error, raise warning
console.warn('WARNING: barcode checksum error:',parse_result);
}else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
if(this.action_callback['product']){
this.action_callback['product'](parse_result);
}
//this.trigger("codebar",parse_result );
}else{
if(this.action_callback[parse_result.type]){
this.action_callback[parse_result.type](parse_result);
}
}
},
// starts catching keyboard events and tries to interpret codebar
// calling the callbacks when needed.
connect: function(){
@ -410,21 +439,7 @@ function openerp_pos_devices(instance,module){ //module is instance.point_of_sal
lastTimeStamp = new Date().getTime();
if (codeNumbers.length == 13) {
//We have found what seems to be a valid codebar
var parse_result = self.parse_ean(codeNumbers.join(''));
if (parse_result.type === 'error') { //most likely a checksum error, raise warning
console.warn('WARNING: barcode checksum error:',parse_result);
}else if(parse_result.type in {'unit':'', 'weight':'', 'price':''}){ //ean is associated to a product
if(self.action_callback['product']){
self.action_callback['product'](parse_result);
}
//this.trigger("codebar",parse_result );
}else{
if(self.action_callback[parse_result.type]){
self.action_callback[parse_result.type](parse_result);
}
}
self.on_ean(codeNumbers.join(''));
codeNumbers = [];
}
} else {

View File

@ -1,9 +1,6 @@
function openerp_pos_models(instance, module){ //module is instance.point_of_sale
var QWeb = instance.web.qweb;
var fetch = function(model, fields, domain, ctx){
return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
};
// The PosModel contains the Point Of Sale's representation of the backend.
// Since the PoS must work in standalone ( Without connection to the server )
@ -27,6 +24,8 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.proxy = new module.ProxyDevice(); // used to communicate to the hardware devices via a local proxy
this.db = new module.PosLS(); // a database used to store the products and categories
this.db.clear('products','categories');
this.debug = jQuery.deparam(jQuery.param.querystring()).debug !== undefined; //debug mode
// default attributes values. If null, it will be loaded below.
this.set({
@ -51,7 +50,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
'units': null,
'units_by_id': null,
'selectedOrder': undefined,
'selectedOrder': null,
});
this.get('orders').bind('remove', function(){ self.on_removed_order(); });
@ -70,15 +69,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
});
},
// helper function to load data from the server
fetch: function(model, fields, domain, ctx){
return new instance.web.Model(model).query(fields).filter(domain).context(ctx).all()
},
// loads all the needed data on the sever. returns a deferred indicating when all the data has loaded.
load_server_data: function(){
var self = this;
var loaded = fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
var loaded = self.fetch('res.users',['name','company_id'],[['id','=',this.session.uid]])
.pipe(function(users){
self.set('user',users[0]);
return fetch('res.company',
return self.fetch('res.company',
[
'currency_id',
'email',
@ -93,15 +96,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(companies){
self.set('company',companies[0]);
return fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]);
return self.fetch('res.partner',['contact_address'],[['id','=',companies[0].partner_id[0]]]);
}).pipe(function(company_partners){
self.get('company').contact_address = company_partners[0].contact_address;
return fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]);
return self.fetch('res.currency',['symbol','position'],[['id','=',self.get('company').currency_id[0]]]);
}).pipe(function(currencies){
self.set('currency',currencies[0]);
return fetch('product.uom', null, null);
return self.fetch('product.uom', null, null);
}).pipe(function(units){
self.set('units',units);
var units_by_id = {};
@ -110,19 +113,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}
self.set('units_by_id',units_by_id);
return fetch('product.packaging', null, null);
return self.fetch('product.packaging', null, null);
}).pipe(function(packagings){
self.set('product.packaging',packagings);
return fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
return self.fetch('res.users', ['name','ean13'], [['ean13', '!=', false]]);
}).pipe(function(users){
self.set('user_list',users);
return fetch('account.tax', ['amount', 'price_include', 'type']);
return self.fetch('account.tax', ['amount', 'price_include', 'type']);
}).pipe(function(taxes){
self.set('taxes', taxes);
return fetch(
return self.fetch(
'pos.session',
['id', 'journal_ids','name','user_id','config_id','start_at','stop_at'],
[['state', '=', 'opened'], ['user_id', '=', self.session.uid]]
@ -130,7 +133,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(sessions){
self.set('pos_session', sessions[0]);
return fetch(
return self.fetch(
'pos.config',
['name','journal_ids','shop_id','journal_id',
'iface_self_checkout', 'iface_led', 'iface_cashdrawer',
@ -147,15 +150,19 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
self.iface_self_checkout = !!pos_config.iface_self_checkout;
self.iface_cashdrawer = !!pos_config.iface_cashdrawer;
return fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]);
return self.fetch('sale.shop',[],[['id','=',pos_config.shop_id[0]]]);
}).pipe(function(shops){
self.set('shop',shops[0]);
return fetch('pos.category', ['id','name','parent_id','child_id','image'])
return self.fetch('product.packaging',['ean','product_id']);
}).pipe(function(packagings){
self.db.add_packagings(packagings);
return self.fetch('pos.category', ['id','name','parent_id','child_id','image'])
}).pipe(function(categories){
self.db.add_categories(categories);
return fetch(
return self.fetch(
'product.product',
['name', 'list_price','price','pos_categ_id', 'taxes_id', 'ean13',
'to_weight', 'uom_id', 'uos_id', 'uos_coeff', 'mes_type', 'description_sale', 'description'],
@ -165,7 +172,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(products){
self.db.add_products(products);
return fetch(
return self.fetch(
'account.bank.statement',
['account_id','currency','journal_id','state','name','user_id','pos_session_id'],
[['state','=','open'],['pos_session_id', '=', self.get('pos_session').id]]
@ -173,7 +180,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
}).pipe(function(bank_statements){
self.set('bank_statements', bank_statements);
return fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]);
return self.fetch('account.journal', undefined, [['user_id','=', self.get('pos_session').user_id[0]]]);
}).pipe(function(journals){
self.set('journals',journals);
@ -226,6 +233,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// saves the order locally and try to send it to the backend. 'record' is a bizzarely defined JSON version of the Order
push_order: function(record) {
console.log('PUSHING NEW ORDER:',record);
this.db.add_order(record);
this.flush();
},
@ -241,10 +249,15 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
// and remove the successfully sent ones from the db once
// it has been confirmed that they have been sent correctly.
flush: function() {
//TODO make the mutex work
console.log('FLUSH');
//this makes sure only one _int_flush is called at the same time
/*
return this.flush_mutex.exec(_.bind(function() {
return this._flush(0);
}, this));
*/
this._flush(0);
},
// attempts to send an order of index 'index' in the list of order to send. The index
// is used to skip orders that failed. do not call this method outside the mutex provided
@ -253,6 +266,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
var self = this;
var orders = this.db.get_orders();
self.set('nbr_pending_operations',orders.length);
console.log('TRYING TO FLUSH ORDER:',index,'Of',orders.length);
var order = orders[index];
if(!order){
@ -268,6 +282,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
})
.done(function(){
//remove from db if success
console.log('Order successfully sent');
self.db.remove_order(order.id);
self._flush(index);
});
@ -301,6 +316,9 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
});
module.Product = Backbone.Model.extend({
get_image_url: function(){
return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+this.get('id');
},
});
module.ProductCollection = Backbone.Collection.extend({
@ -330,7 +348,6 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
get_discount: function(){
return this.discount;
},
// FIXME
get_product_type: function(){
return this.type;
},
@ -546,6 +563,7 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
this.pos = attributes.pos;
this.selected_orderline = undefined;
this.screen_data = {}; // see ScreenSelector
this.receipt_type = 'receipt'; // 'receipt' || 'invoice'
return this;
},
generateUniqueId: function() {
@ -617,6 +635,13 @@ function openerp_pos_models(instance, module){ //module is instance.point_of_sal
getDueLeft: function() {
return this.getTotal() - this.getPaidTotal();
},
// sets the type of receipt 'receipt'(default) or 'invoice'
set_receipt_type: function(type){
this.receipt_type = type;
},
get_receipt_type: function(){
return this.receipt_type;
},
// the client related to the current order.
set_client: function(client){
this.set('client',client);

View File

@ -65,6 +65,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
},
close_popup: function(){
if(this.current_popup){
this.current_popup.close();
this.current_popup.hide();
this.current_popup = null;
}
@ -188,7 +189,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
return true;
}
}
this.pos.proxy.scan_item_unrecognized(ean);
this.pos.proxy.scan_item_error_unrecognized(ean);
return false;
},
@ -206,7 +207,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
return true;
}
}
this.pos.proxy.scan_item_unrecognized(ean);
this.pos.proxy.scan_item_error_unrecognized(ean);
return false;
//TODO start the transaction
},
@ -336,6 +337,11 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$el.show();
}
},
/* called before hide, when a popup is closed */
close: function(){
},
/* hides the popup. keep in mind that this is called in the initialization pass of the
* pos instantiation, so you don't want to do anything fancy in here */
hide: function(){
if(this.$el){
this.$el.hide();
@ -352,9 +358,40 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.$el.find('.button').off('click').click(function(){
self.pos_widget.screen_selector.close_popup();
self.pos.proxy.help_canceled();
});
},
close:function(){
this.pos.proxy.help_canceled();
},
});
module.ChooseReceiptPopupWidget = module.PopUpWidget.extend({
template:'ChooseReceiptPopupWidget',
show: function(){
console.log('show');
this._super();
this.renderElement();
var self = this;
var currentOrder = self.pos.get('selectedOrder');
this.$('.button.receipt').off('click').click(function(){
currentOrder.set_receipt_type('receipt');
self.pos_widget.screen_selector.set_current_screen('products');
});
this.$('.button.invoice').off('click').click(function(){
currentOrder.set_receipt_type('invoice');
self.pos_widget.screen_selector.set_current_screen('products');
});
},
get_client_name: function(){
var client = this.pos.get('selectedOrder').get_client();
if( client ){
return client.name;
}else{
return '';
}
},
});
module.ErrorPopupWidget = module.PopUpWidget.extend({
@ -492,10 +529,6 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
var product = this.get_product();
return (product ? product.get('list_price') : 0) || 0;
},
get_product_image: function(){
var product = this.get_product();
return product ? product.get('image') : undefined;
},
get_product_weight: function(){
return this.weight || 0;
},
@ -527,23 +560,21 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
//we get the first cashregister marked as self-checkout
var selfCheckoutRegisters = [];
for(var i = 0; i < this.pos.get('cashRegisters').models.length; i++){
var cashregister = this.pos.get('cashRegisters').models[i];
for(var i = 0; i < self.pos.get('cashRegisters').models.length; i++){
var cashregister = self.pos.get('cashRegisters').models[i];
if(cashregister.self_checkout_payment_method){
selfCheckoutRegisters.push(cashregister);
}
}
var cashregister = selfCheckoutRegisters[0] || this.pos.get('cashRegisters').models[0];
var cashregister = selfCheckoutRegisters[0] || self.pos.get('cashRegisters').models[0];
currentOrder.addPaymentLine(cashregister);
self.pos.push_order(currentOrder.exportAsJSON()).then(function() {
currentOrder.destroy();
self.pos.proxy.transaction_end();
self.pos_widget.screen_selector.set_current_screen(self.next_screen);
});
self.pos.push_order(currentOrder.exportAsJSON())
currentOrder.destroy();
self.pos.proxy.transaction_end();
self.pos_widget.screen_selector.set_current_screen(self.next_screen);
}else if(payment === 'payment_rejected'){
clearInterval(this.intervalID);
clearInterval(self.intervalID);
//TODO show a tryagain thingie ?
}
},500);
@ -571,19 +602,35 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
show_numpad: false,
show_leftpane: false,
barcode_product_action: function(ean){
this.pos.proxy.transaction_start();
this._super(ean);
},
barcode_client_action: function(ean){
this.pos.proxy.transaction_start();
this._super(ean);
this.pos_widget.screen_selector.set_current_screen(this.next_screen);
$('.goodbye-message').hide();
this.pos_widget.screen_selector.show_popup('choose-receipt');
},
show: function(){
this._super();
var self = this;
this.add_action_button({
label: 'help',
icon: '/point_of_sale/static/src/img/icons/png48/help.png',
click: function(){
$('.goodbye-message').css({opacity:1}).hide();
self.help_button_action();
},
});
$('.goodbye-message').css({opacity:1}).show();
setTimeout(function(){
$('.goodbye-message').animate({opacity:0},500,'swing',function(){$('.goodbye-message').hide();});
},3000);
},5000);
},
});
@ -743,6 +790,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
});
this.updatePaymentSummary();
this.$('.paymentline-amout input').last().focus();
},
close: function(){
this._super();
@ -830,7 +878,7 @@ function openerp_pos_screens(instance, module){ //module is instance.point_of_sa
this.numpadState.set({mode: 'payment'});
},
set_value: function(val) {
this.currentPaymentLines.last().set({amount: val});
this.currentPaymentLines.last().set_amount(val);
},
});

View File

@ -284,12 +284,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.click_product_action = options.click_product_action;
},
// returns the url of the product thumbnail
get_image_url: function() {
return '/web/binary/image?session_id='+instance.session.session_id+'&model=product.product&field=image&id='+this.model.get('id');
},
renderElement: function() {
this._super();
this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.get_image_url()));
this.$('img').replaceWith(this.pos_widget.image_cache.get_image(this.model.get_image_url()));
var self = this;
$("a", this.$el).click(function(e){
if(self.click_product_action){
@ -667,9 +664,95 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
show: function(){ this.$el.show(); },
hide: function(){ this.$el.hide(); },
});
// The debug widget lets the user control and monitor the hardware and software status
// without the use of the proxy
module.DebugWidget = module.PosBaseWidget.extend({
template: "DebugWidget",
eans:{
admin_badge: '0410100000006',
client_badge: '0420100000005',
invalid_ean: '1232456',
soda_33cl: '5449000000996',
oranges_kg: '2100002031410',
lemon_price: '2301000001560',
unknown_product: '9900000000004',
},
events:[
'scan_item_success',
'scan_item_error_unrecognized',
'payment_request',
'open_cashbox',
'print_receipt',
'print_pdf_invoice',
'weighting_read_kg',
'is_payment_accepted',
],
minimized: false,
start: function(){
var self = this;
this.$el.draggable();
this.$('.toggle').click(function(){
var content = self.$('.content');
var bg = self.$el;
if(!self.minimized){
content.animate({'height':'0'},200);
}else{
content.css({'height':'auto'});
}
self.minimized = !self.minimized;
});
this.$('.button.accept_payment').click(function(){
self.pos.proxy.debug_accept_payment();
});
this.$('.button.reject_payment').click(function(){
self.pos.proxy.debug_reject_payment();
});
this.$('.button.set_weight').click(function(){
var kg = Number(self.$('input.weight').val());
if(!Number.isNaN(kg)){
self.pos.proxy.debug_set_weight(kg);
}
});
this.$('.button.custom_ean').click(function(){
var ean = self.pos.barcode_reader.sanitize_ean(self.$('input.ean').val() || '0');
self.$('input.ean').val(ean);
self.pos.barcode_reader.on_ean(ean);
});
_.each(this.eans, function(ean, name){
self.$('.button.'+name).click(function(){
self.$('input.ean').val(ean);
self.pos.barcode_reader.on_ean(ean);
});
});
_.each(this.events, function(name){
self.pos.proxy.add_notification(name,function(){
self.$('.event.'+name).stop().clearQueue().css({'background-color':'#6CD11D'});
self.$('.event.'+name).animate({'background-color':'#1E1E1E'},2000);
});
});
self.pos.proxy.add_notification('help_needed',function(){
self.$('.status.help_needed').addClass('on');
});
self.pos.proxy.add_notification('help_canceled',function(){
self.$('.status.help_needed').removeClass('on');
});
self.pos.proxy.add_notification('transaction_start',function(){
self.$('.status.transaction').addClass('on');
});
self.pos.proxy.add_notification('transaction_end',function(){
self.$('.status.transaction').removeClass('on');
});
self.pos.proxy.add_notification('weighting_start',function(){
self.$('.status.weighting').addClass('on');
});
self.pos.proxy.add_notification('weighting_end',function(){
self.$('.status.weighting').removeClass('on');
});
},
});
// ---------- Main Point of Sale Widget ----------
@ -698,6 +781,7 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
});
// The PosWidget is the main widget that contains all other widgets in the PointOfSale.
// It is mainly composed of :
// - a header, containing the list of orders
@ -722,14 +806,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.leftpane_width = '440px';
this.cashier_controls_visible = true;
this.image_cache = new module.ImageCache(); // for faster products image display
/*
//Epileptic mode
setInterval(function(){
$('body').css({'-webkit-filter':'hue-rotate('+Math.random()*360+'deg)' });
},100);
*/
},
start: function() {
@ -758,6 +834,8 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.screen_selector.set_default_screen();
window.screen_selector = self.screen_selector;
self.pos.barcode_reader.connect();
instance.webclient.set_content_full_screen(true);
@ -771,11 +849,6 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
self.$('.loader').animate({opacity:0},1500,'swing',function(){self.$('.loader').hide();});
self.$('.loader img').hide();
if(jQuery.deparam(jQuery.param.querystring()).debug !== undefined){
window.pos = self.pos;
window.pos_widget = self.pos_widget;
}
},function(){ // error when loading models data from the backend
self.$('.loader img').hide();
return new instance.web.Model("ir.model.data").get_func("search_read")([['name', '=', 'action_pos_session_opening']], ['res_id'])
@ -831,6 +904,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
this.error_session_popup = new module.ErrorNoSessionPopupWidget(this, {});
this.error_session_popup.appendTo($('.point-of-sale'));
this.choose_receipt_popup = new module.ChooseReceiptPopupWidget(this, {});
this.choose_receipt_popup.appendTo($('.point-of-sale'));
// -------- Misc ---------
this.notification = new module.SynchNotificationWidget(this,{});
@ -890,12 +966,17 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
'error': this.error_popup,
'error-product': this.error_product_popup,
'error-session': this.error_session_popup,
'choose-receipt': this.choose_receipt_popup,
},
default_client_screen: 'welcome',
default_cashier_screen: 'products',
default_mode: this.pos.iface_self_checkout ? 'client' : 'cashier',
});
if(this.pos.debug){
this.debug_widget = new module.DebugWidget(this);
this.debug_widget.appendTo(this.$('#content'));
}
},
changed_pending_operations: function () {
@ -965,9 +1046,9 @@ function openerp_pos_widgets(instance, module){ //module is instance.point_of_sa
},
try_close: function() {
var self = this;
self.pos.flush().then(function() {
self.close();
});
//TODO : do the close after the flush...
self.pos.flush()
self.close();
},
close: function() {
var self = this;

View File

@ -185,8 +185,8 @@
<span class="product-price">
<t t-esc="widget.format_currency(widget.get_product_price()) + '/Kg'" />
</span>
<t t-if="widget.get_product_image()">
<img t-att-src="'data:image/png;base64,'+ widget.get_product_image()" />
<t t-if="widget.get_product()">
<img t-att-src="widget.get_product().get_image_url()" />
</t>
</div>
</div>
@ -313,18 +313,16 @@
</div>
</t>
<t t-name="ReceiptPopupWidget">
<t t-name="ChooseReceiptPopupWidget">
<div class="modal-dialog">
<div class="popup popup-help">
<p class="message">Welcome Mr. John Smith <br /> Choose your type of receipt:</p>
<p class="message">Welcome <t t-esc="widget.get_client_name()" /><br /> Choose your type of receipt:</p>
<div class = "button big-left receipt">
Ticket
</div>
<div class = "button big-right invoice">
Invoice
</div>
<div class="footer">
</div>
</div>
</div>
</t>
@ -348,7 +346,8 @@
<t t-name="ErrorPopupWidget">
<div class="modal-dialog">
<div class="popup popup-help">
<p class="message"><t t-esc=" widget.message || 'Error.' " /></p>
<p class="message"><t t-esc=" widget.message || 'Error' " /></p>
<p class="comment"><t t-esc=" widget.comment || '' "/></p>
</div>
</div>
</t>
@ -415,6 +414,57 @@
</div>
</t>
<t t-name="DebugWidget">
<div class="debug-widget">
<h1>Debug Window</h1>
<div class="toggle"></div>
<div class="content">
<p class="category">Payment Terminal</p>
<ul>
<li class="button accept_payment">Accept Payment</li>
<li class="button reject_payment">Reject Payment</li>
</ul>
<p class="category">Electronic Scale</p>
<ul>
<li><input type="text" class="weight"></input></li>
<li class="button set_weight">Set Weight</li>
</ul>
<p class="category">Barcode Scanner</p>
<ul>
<li><input type="text" class="ean"></input></li>
<li class="button custom_ean">Custom Ean13</li>
<li class="button admin_badge">Admin Badge</li>
<li class="button client_badge">Client Badge</li>
<li class="button soda_33cl">Soda 33cl</li>
<li class="button oranges_kg">3.141Kg Oranges</li>
<li class="button lemon_price">1.54€ Lemon</li>
<li class="button unknown_product">Unknown Product</li>
<li class="button invalid_ean">Invalid Ean</li>
</ul>
<p class="category">Hardware Status</p>
<ul>
<li class="status help_needed">Help needed</li>
<li class="status weighting">Weighting</li>
<li class="status transaction">In Transaction</li>
</ul>
<p class="category">Hardware Events</p>
<ul>
<li class="event scan_item_success">Scan Item Success</li>
<li class="event scan_item_error_unrecognized">Scan Item Unrecognized</li>
<li class="event payment_request">Payment Request</li>
<li class="event open_cashbox">Open Cashbox</li>
<li class="event print_receipt">Print Receipt</li>
<li class="event print_pdf_invoice">Print Invoice</li>
<li class="event weighting_read_kg">Read Weighting Scale</li>
<li class="event is_payment_accepted">Check Payment</li>
</ul>
</div>
</div>
</t>
<t t-name="OrderlineWidget">
<li class="orderline">
<span class="product-name">

View File

@ -20,6 +20,7 @@
##############################################################################
import portal
import mail_mail
import wizard

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2011 OpenERP S.A (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
import tools
from osv import osv
class mail_mail_portal(osv.Model):
""" Update of mail_mail class, to add the signin URL to notifications.
"""
_name = 'mail.mail'
_inherit = ['mail.mail']
def _generate_signin_url(self, cr, uid, partner_id, portal_group_id, key, context=None):
""" Generate the signin url """
base_url = self.pool.get('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='', context=context)
return base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (partner_id, portal_group_id, key)
def send_get_mail_body(self, cr, uid, mail, partner=None, context=None):
""" Return a specific ir_email body. The main purpose of this method
is to be inherited by Portal, to add a link for signing in, in
each notification email a partner receives.
:param mail: mail.mail browse_record
:param partner: browse_record of the specific recipient partner
"""
if partner:
portal_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'portal', 'portal')
portal_id = portal_ref and portal_ref[1] or False
url = self._generate_signin_url(cr, uid, partner.id, portal_id, 1234, context=context)
body = tools.append_content_to_html(mail.body_html, url)
return body
else:
return super(mail_mail_portal, self).send_get_mail_body(cr, uid, mail, partner=partner, context=context)

View File

@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from . import test_portal
checks = [
test_portal,
]
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Business Applications
# Copyright (c) 2012-TODAY OpenERP S.A. <http://openerp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################
from openerp.addons.mail.tests import test_mail
from openerp.tools import append_content_to_html
class test_portal(test_mail.TestMailMockups):
def setUp(self):
super(test_portal, self).setUp()
self.ir_model = self.registry('ir.model')
self.mail_group = self.registry('mail.group')
self.mail_mail = self.registry('mail.mail')
self.res_users = self.registry('res.users')
self.res_partner = self.registry('res.partner')
# create a 'pigs' group that will be used through the various tests
self.group_pigs_id = self.mail_group.create(self.cr, self.uid,
{'name': 'Pigs', 'description': 'Fans of Pigs, unite !'})
def test_00_mail_invite(self):
cr, uid = self.cr, self.uid
user_admin = self.res_users.browse(cr, uid, uid)
self.mail_invite = self.registry('mail.wizard.invite')
base_url = self.registry('ir.config_parameter').get_param(cr, uid, 'web.base.url', default='')
portal_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'portal', 'portal')
portal_id = portal_ref and portal_ref[1] or False
# 0 - Admin
p_a_id = user_admin.partner_id.id
# 1 - Bert Tartopoils, with email, should receive emails for comments and emails
p_b_id = self.res_partner.create(cr, uid, {'name': 'Bert Tartopoils', 'email': 'b@b'})
# ----------------------------------------
# CASE1: generated URL
# ----------------------------------------
url = self.mail_mail._generate_signin_url(cr, uid, p_b_id, portal_id, 1234)
self.assertEqual(url, base_url + '/login?action=signin&partner_id=%s&group=%s&key=%s' % (p_b_id, portal_id, 1234),
'generated signin URL incorrect')
# ----------------------------------------
# CASE2: invite Bert
# ----------------------------------------
_sent_email_subject = 'Invitation to follow Pigs'
_sent_email_body = append_content_to_html('<div>You have been invited to follow Pigs.</div>', url)
# Do: create a mail_wizard_invite, validate it
self._init_mock_build_email()
mail_invite_id = self.mail_invite.create(cr, uid, {'partner_ids': [(4, p_b_id)]}, {'default_res_model': 'mail.group', 'default_res_id': self.group_pigs_id})
self.mail_invite.add_followers(cr, uid, [mail_invite_id])
group_pigs = self.mail_group.browse(cr, uid, self.group_pigs_id)
# Test: Pigs followers should contain Admin and Bert
follower_ids = [follower.id for follower in group_pigs.message_follower_ids]
self.assertEqual(set(follower_ids), set([p_a_id, p_b_id]), 'Pigs followers after invite is incorrect')
# Test: sent email subject, body
self.assertEqual(len(self._build_email_kwargs_list), 1, 'sent email number incorrect, should be only for Bert')
for sent_email in self._build_email_kwargs_list:
self.assertEqual(sent_email.get('subject'), _sent_email_subject, 'sent email subject incorrect')
self.assertEqual(sent_email.get('body'), _sent_email_body, 'sent email body incorrect')

View File

@ -27,38 +27,38 @@ class project_configuration(osv.osv_memory):
_inherit = 'res.config.settings'
_columns = {
'module_project_mrp': fields.boolean('generate tasks from sale orders',
'module_project_mrp': fields.boolean('Generate tasks from sale orders',
help ="""This feature automatically creates project tasks from service products in sale orders.
More precisely, tasks are created for procurement lines with product of type 'Service',
procurement method 'Make to Order', and supply method 'Produce'.
This installs the module project_mrp."""),
'module_pad': fields.boolean("use integrated collaborative note pads on task",
'module_pad': fields.boolean("Use integrated collaborative note pads on task",
help="""Lets the company customize which Pad installation should be used to link to new pads
(by default, http://ietherpad.com/).
This installs the module pad."""),
'module_project_timesheet': fields.boolean("record timesheet lines per tasks",
'module_project_timesheet': fields.boolean("Record timesheet lines per tasks",
help="""This allows you to transfer the entries under tasks defined for Project Management to
the timesheet line entries for particular date and user, with the effect of creating,
editing and deleting either ways.
This installs the module project_timesheet."""),
'module_project_long_term': fields.boolean("manage resources planning on gantt view",
'module_project_long_term': fields.boolean("Manage resources planning on gantt view",
help="""A long term project management module that tracks planning, scheduling, and resource allocation.
This installs the module project_long_term."""),
'module_project_issue': fields.boolean("track issues and bugs",
'module_project_issue': fields.boolean("Track issues and bugs",
help="""Provides management of issues/bugs in projects.
This installs the module project_issue."""),
'time_unit': fields.many2one('product.uom', 'Working time unit', required=True,
help="""This will set the unit of measure used in projects and tasks."""),
'module_project_issue_sheet': fields.boolean("invoice working time on issues",
'module_project_issue_sheet': fields.boolean("Invoice working time on issues",
help="""Provides timesheet support for the issues/bugs management in project.
This installs the module project_issue_sheet."""),
'group_tasks_work_on_tasks': fields.boolean("Log work activities on tasks",
implied_group='project.group_tasks_work_on_tasks',
help="Allows you to compute work on tasks."),
'group_time_work_estimation_tasks': fields.boolean("manage time estimation on tasks",
'group_time_work_estimation_tasks': fields.boolean("Manage time estimation on tasks",
implied_group='project.group_time_work_estimation_tasks',
help="Allows you to compute Time Estimation on tasks."),
'group_manage_delegation_task': fields.boolean("allow task delegation",
'group_manage_delegation_task': fields.boolean("Allow task delegation",
implied_group='project.group_delegate_task',
help="Allows you to delegate tasks to other users."),
}

View File

@ -26,7 +26,7 @@ class project_issue_settings(osv.osv_memory):
_inherit = ['project.config.settings', 'fetchmail.config.settings']
_columns = {
'fetchmail_issue': fields.boolean("create issues from an incoming email account ",
'fetchmail_issue': fields.boolean("Create issues from an incoming email account ",
fetchmail_model='project.issue', fetchmail_name='Incoming Issues',
help="""Allows you to configure your incoming mail server, and create issues from incoming emails."""),
}

View File

@ -29,36 +29,36 @@ class purchase_config_settings(osv.osv_memory):
_columns = {
'default_invoice_method': fields.selection(
[('manual', 'Based on Purchase Order Lines'),
('picking', 'Based on Receptions'),
('order', 'Pre-Generate Draft Invoices based on Purchase Orders'),
], 'default invoicing control method', required=True, default_model='purchase.order'),
'group_purchase_pricelist':fields.boolean("manage pricelist per supplier",
[('manual', 'Based on purchase order lines'),
('picking', 'Based on receptions'),
('order', 'Pre-generate draft invoices based on purchase orders'),
], 'Default invoicing control method', required=True, default_model='purchase.order'),
'group_purchase_pricelist':fields.boolean("Manage pricelist per supplier",
implied_group='product.group_purchase_pricelist',
help="""Allows to manage different prices based on rules per category of Supplier.
Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
'group_uom':fields.boolean("manage different units of measure for products",
'group_uom':fields.boolean("Manage different units of measure for products",
implied_group='product.group_uom',
help="""Allows you to select and maintain different units of measure for products."""),
'group_costing_method':fields.boolean("compute product cost price based on average cost",
'group_costing_method':fields.boolean("Compute product cost price based on average cost",
implied_group='product.group_costing_method',
help="""Allows you to compute product cost price based on average cost."""),
'group_purchase_delivery_address': fields.boolean("allow a different address for incoming products and invoicings",
'group_purchase_delivery_address': fields.boolean("Allow a different address for incoming products and invoicings",
implied_group='purchase.group_delivery_invoice_address',
help="Allows you to specify different delivery and invoice addresses on a purchase order."),
'module_purchase_analytic_plans': fields.boolean('allow using multiple analytic accounts on the same order',
'module_purchase_analytic_plans': fields.boolean('Allow using multiple analytic accounts on the same order',
help ="""Allows the user to maintain several analysis plans. These let you split
lines on a purchase order between several accounts and analytic plans.
This installs the module purchase_analytic_plans."""),
'module_warning': fields.boolean("alerts by products or supllier",
'module_warning': fields.boolean("Alerts by products or supplier",
help="""Allow to configure warnings on products and trigger them when a user wants to purchase a given product or a given supplier.
Example: Product: this product is deprecated, do not purchase more than 5.
Supplier: don't forget to ask for an express delivery."""),
'module_purchase_double_validation': fields.boolean("force two levels of approvals",
'module_purchase_double_validation': fields.boolean("Force two levels of approvals",
help="""Provide a double validation mechanism for purchases exceeding minimum amount.
This installs the module purchase_double_validation."""),
'module_purchase_requisition': fields.boolean("manage purchase requisitions",
'module_purchase_requisition': fields.boolean("Manage purchase requisitions",
help="""Purchase Requisitions are used when you want to request quotations from several suppliers for a given set of products.
You can configure per product if you directly do a Request for Quotation
to one supplier or if you want a purchase requisition to negotiate with several suppliers."""),
@ -74,9 +74,9 @@ class purchase_config_settings(osv.osv_memory):
class account_config_settings(osv.osv_memory):
_inherit = 'account.config.settings'
_columns = {
'module_purchase_analytic_plans': fields.boolean('use multiple analytic accounts on orders',
'module_purchase_analytic_plans': fields.boolean('Use multiple analytic accounts on orders',
help="""This allows install module purchase_analytic_plans."""),
'group_analytic_account_for_purchases': fields.boolean('analytic accounting for purchases',
'group_analytic_account_for_purchases': fields.boolean('Analytic accounting for purchases',
implied_group='purchase.group_analytic_accounting',
help="Allows you to specify an analytic account on purchase orders."),
}

View File

@ -27,70 +27,70 @@ class sale_configuration(osv.osv_memory):
_inherit = 'sale.config.settings'
_columns = {
'group_invoice_so_lines': fields.boolean('generate invoices based on the sale order',
'group_invoice_so_lines': fields.boolean('Generate invoices based on the sale order',
implied_group='sale.group_invoice_so_lines',
help="To allow your salesman to make invoices for sale order lines using the menu 'Lines to Invoice'."),
'group_invoice_deli_orders': fields.boolean('generate invoices after and based on delivery orders',
'group_invoice_deli_orders': fields.boolean('Generate invoices after and based on delivery orders',
implied_group='sale.group_invoice_deli_orders',
help="To allow your salesman to make invoices for Delivery Orders using the menu 'Deliveries to Invoice'."),
'task_work': fields.boolean("prepare invoices based on task's activities",
'task_work': fields.boolean("Prepare invoices based on task's activities",
help="""Lets you transfer the entries under tasks defined for Project Management to
the Timesheet line entries for particular date and particular user with the effect of creating, editing and deleting either ways
and to automatically creates project tasks from procurement lines.
This installs the modules project_timesheet and project_mrp."""),
'timesheet': fields.boolean('prepare invoices based on timesheets',
'timesheet': fields.boolean('Prepare invoices based on timesheets',
help = """For modifying account analytic view to show important data to project manager of services companies.
You can also view the report of account analytic summary user-wise as well as month wise.
This installs the module account_analytic_analysis."""),
'module_account_analytic_analysis': fields.boolean('use contracts management',
'module_account_analytic_analysis': fields.boolean('Use contracts management',
help = """Allows to define your customer contracts conditions: invoicing
method (fixed price, on timesheet, advance invoice), the exact pricing
(650/day for a developer), the duration (one year support contract).
You will be able to follow the progress of the contract and invoice automatically.
It installs the account_analytic_analysis module."""),
'default_order_policy': fields.selection(
[('manual', 'Invoice Based on Sales Orders'), ('picking', 'Invoice Based on Deliveries')],
[('manual', 'Invoice based on sales orders'), ('picking', 'Invoice based on deliveries')],
'The default invoicing method is', default_model='sale.order',
help="You can generate invoices based on sales orders or based on shippings."),
'module_delivery': fields.boolean('allow adding shipping costs',
'module_delivery': fields.boolean('Allow adding shipping costs',
help ="""Allows you to add delivery methods in sale orders and delivery orders.
You can define your own carrier and delivery grids for prices.
This installs the module delivery."""),
'time_unit': fields.many2one('product.uom', 'the default working time unit for services is'),
'default_picking_policy' : fields.boolean("deliver all at once when all products are available.",
'time_unit': fields.many2one('product.uom', 'The default working time unit for services is'),
'default_picking_policy' : fields.boolean("Deliver all at once when all products are available.",
help = "Sales order by default will be configured to deliver all products at once instead of delivering each product when it is available. This may have an impact on the shipping price."),
'group_sale_pricelist':fields.boolean("use pricelists to adapt your price per customers",
'group_sale_pricelist':fields.boolean("Use pricelists to adapt your price per customers",
implied_group='product.group_sale_pricelist',
help="""Allows to manage different prices based on rules per category of customers.
Example: 10% for retailers, promotion of 5 EUR on this product, etc."""),
'group_uom':fields.boolean("allow using different units of measures",
'group_uom':fields.boolean("Allow using different units of measures",
implied_group='product.group_uom',
help="""Allows you to select and maintain different units of measure for products."""),
'group_sale_delivery_address': fields.boolean("allow a different address for delivery and invoicing ",
'group_sale_delivery_address': fields.boolean("Allow a different address for delivery and invoicing ",
implied_group='sale.group_delivery_invoice_address',
help="Allows you to specify different delivery and invoice addresses on a sale order."),
'group_mrp_properties': fields.boolean('product properties on order lines',
'group_mrp_properties': fields.boolean('Product properties on order lines',
implied_group='sale.group_mrp_properties',
help="Allows you to tag sale order lines with properties."),
'group_discount_per_so_line': fields.boolean("allow setting a discount on the sale order lines",
'group_discount_per_so_line': fields.boolean("Allow setting a discount on the sale order lines",
implied_group='sale.group_discount_per_so_line',
help="Allows you to apply some discount per sale order line."),
'group_multiple_shops': fields.boolean("manage multiple shops",
'group_multiple_shops': fields.boolean("Manage multiple shops",
implied_group='stock.group_locations',
help="This allows to configure and use multiple shops."),
'module_warning': fields.boolean("allow configuring alerts by customer or products",
'module_warning': fields.boolean("Allow configuring alerts by customer or products",
help="""Allow to configure warnings on products and trigger them when a user wants to sale a given product or a given customer.
Example: Product: this product is deprecated, do not purchase more than 5.
Supplier: don't forget to ask for an express delivery."""),
'module_sale_margin': fields.boolean("display margins on sales orders",
'module_sale_margin': fields.boolean("Display margins on sales orders",
help="""This adds the 'Margin' on sales order.
This gives the profitability by calculating the difference between the Unit Price and Cost Price.
This installs the module sale_margin."""),
'module_sale_journal': fields.boolean("allow batch invoicing of delivery orders through journals",
'module_sale_journal': fields.boolean("Allow batch invoicing of delivery orders through journals",
help="""Allows you to categorize your sales and deliveries (picking lists) between different journals,
and perform batch operations on journals.
This installs the module sale_journal."""),
'module_analytic_user_function': fields.boolean("one employee can have different roles per contract",
'module_analytic_user_function': fields.boolean("One employee can have different roles per contract",
help="""Allows you to define what is the default function of a specific user on a given account.
This is mostly used when a user encodes his timesheet. The values are retrieved and the fields are auto-filled.
But the possibility to change these values is still available.
@ -172,9 +172,9 @@ class sale_configuration(osv.osv_memory):
class account_config_settings(osv.osv_memory):
_inherit = 'account.config.settings'
_columns = {
'module_sale_analytic_plans': fields.boolean('several analytic accounts on sales',
'module_sale_analytic_plans': fields.boolean('Several analytic accounts on sales',
help="""This allows install module sale_analytic_plans."""),
'group_analytic_account_for_sales': fields.boolean('analytic accounting for sales',
'group_analytic_account_for_sales': fields.boolean('Analytic accounting for sales',
implied_group='sale.group_analytic_accounting',
help="Allows you to specify an analytic account on sale orders."),
}

View File

@ -30,7 +30,7 @@ class sale_advance_payment_inv(osv.osv_memory):
'advance_payment_method':fields.selection(
[('all', 'Invoice the whole sale order'), ('percentage','Percentage'), ('fixed','Fixed price (deposit)'),
('lines', 'Some order lines')],
'Invoice Method', required=True,
'What do you want to invoice?', required=True,
help="""Use All to create the final invoice.
Use Percentage to invoice a percentage of the total amount.
Use Fixed Price to invoice a specific amound in advance.

View File

@ -26,14 +26,14 @@ class stock_config_settings(osv.osv_memory):
_inherit = 'res.config.settings'
_columns = {
'module_claim_from_delivery': fields.boolean("allow claim on deliveries",
'module_claim_from_delivery': fields.boolean("Allow claim on deliveries",
help="""Adds a Claim link to the delivery order.
This installs the module claim_from_delivery."""),
'module_stock_invoice_directly': fields.boolean("create and open the invoice when the user finish a delivery order",
'module_stock_invoice_directly': fields.boolean("Create and open the invoice when the user finish a delivery order",
help="""This allows to automatically launch the invoicing wizard if the delivery is
to be invoiced when you send or deliver goods.
This installs the module stock_invoice_directly."""),
'module_product_expiry': fields.boolean("expiry date on lots",
'module_product_expiry': fields.boolean("Expiry date on lots",
help="""Track different dates on products and serial numbers.
The following dates can be tracked:
- end of life
@ -41,36 +41,36 @@ class stock_config_settings(osv.osv_memory):
- removal date
- alert date.
This installs the module product_expiry."""),
'module_stock_location': fields.boolean("create push/pull logistic rules",
'module_stock_location': fields.boolean("Create push/pull logistic rules",
help="""Provide push and pull inventory flows. Typical uses of this feature are:
manage product manufacturing chains, manage default locations per product,
define routes within your warehouse according to business needs, etc.
This installs the module stock_location."""),
'group_uom': fields.boolean("manage units of measure on products",
'group_uom': fields.boolean("Manage units of measure on products",
implied_group='product.group_uom',
help="""Allows you to select and maintain different units of measure for products."""),
'group_uos': fields.boolean("invoice products in a different unit of measure than the sale order",
'group_uos': fields.boolean("Invoice products in a different unit of measure than the sale order",
implied_group='product.group_uos',
help="""Allows you to sell units of a product, but invoice based on a different unit of measure.
For instance, you can sell pieces of meat that you invoice based on their weight."""),
'group_stock_packaging': fields.boolean("allow to define several packaging methods on products",
'group_stock_packaging': fields.boolean("Allow to define several packaging methods on products",
implied_group='product.group_stock_packaging',
help="""Allows you to create and manage your packaging dimensions and types you want to be maintained in your system."""),
'group_stock_production_lot': fields.boolean("track serial number on products",
'group_stock_production_lot': fields.boolean("Track serial number on products",
implied_group='stock.group_production_lot',
help="""This allows you to manage products by using serial numbers.
When you select a lot, you can get the upstream or downstream traceability of the products contained in lot."""),
'group_stock_tracking_lot': fields.boolean("track serial number on logistic units (pallets)",
'group_stock_tracking_lot': fields.boolean("Track serial number on logistic units (pallets)",
implied_group='stock.group_tracking_lot',
help="""Allows you to get the upstream or downstream traceability of the products contained in lot."""),
'group_stock_inventory_valuation': fields.boolean("generate accounting entries per stock movement",
'group_stock_inventory_valuation': fields.boolean("Generate accounting entries per stock movement",
implied_group='stock.group_inventory_valuation',
help="""Allows to configure inventory valuations on products and product categories."""),
'group_stock_multiple_locations': fields.boolean("manage multiple locations and warehouses",
'group_stock_multiple_locations': fields.boolean("Manage multiple locations and warehouses",
implied_group='stock.group_locations',
help="""This allows to configure and use multiple stock locations and warehouses,
instead of having a single default one."""),
'group_product_variant': fields.boolean("support multiple variants per products ",
'group_product_variant': fields.boolean("Support multiple variants per products ",
implied_group='product.group_product_variant',
help="""Allow to manage several variants per product. As an example, if you sell T-Shirts, for the same "Linux T-Shirt", you may have variants on sizes or colors; S, M, L, XL, XXL."""),
'decimal_precision': fields.integer('Decimal precision on weight', help="As an example, a decimal precision of 2 will allow weights like: 9.99 kg, whereas a decimal precision of 4 will allow weights like: 0.0231 kg."),

View File

@ -117,6 +117,8 @@ openerp.web_linkedin = function(instance) {
"count": 25,
}).result(function(result) {
children_def.resolve(result);
}).error(function() {
children_def.reject();
});
defs.push(children_def.pipe(function(result) {
result = _.reject(result.people.values || [], function(el) {
@ -130,6 +132,8 @@ openerp.web_linkedin = function(instance) {
var p_to_change = _.toArray(arguments);
to_change.child_ids = p_to_change;
});
}, function() {
return $.when();
}));
/* TODO
to_change.linkedinUrl = _.str.sprintf("http://www.linkedin.com/company/%d", entity.id);