ERPNext Customize Job Openings jobs Autogenerated /Jobs and Job Opening Pages in ERPNext
The ERPNext Jobs Page Problem Overview
By default the /jobs page is just one long mess. With a website containing many job openings, this becomes unusable very quickly.
I am attempting to clean up this mess into something usable by prospective employees.
Note for this posting I am working with ERPNext version 13.
Historical Background
After years of using Odoo (and previous incarnation of it's predecessors OpenERP and TinyERP, by version 14 Odoo's Community Edition (CE) became so crippled in features that were only available in the commercial editions it was no longer usable. Up to Odoo 13, you could find methods of manually installing the missing Enterprise Edition (EE) features from the git repos if you were willing to do the integration work, which wasn't too difficult and didn't require any custom coding, just systems administration skills. By version 14 however, key addons/modules/applications were no longer available in the CE edition period. So, I have advised all of the companies I encouraged (and implemented) going to Odoo over the years to abandon Odoo and go elsewhere. This has been painful but necessary. Unfortunately Odoo is not the only opensource project suffering from the extremely harmful mentality of developers of the past 5-10 years or so.
One of the candidates we're evaluating, but getting increasingly concerned about, is ERPNext.
Unfortunately, after several months of asking a variety of questions on the ERPNext web forum, and noticing how little they respond to other users as well, I've had to resort to trying to hack through and figure it out myself. This does not bode well for long term use of ERPNext, but is a trend of the last 10 years in developers who do not seem to actually understand what opensource means as they have become both incredibly mercenary and too developer-focused inward navel gazers. :-(
This doesn't bode well for long term use of ERPNext. We may have to once again search for other alternatives (if any are left). I am concerned we are increasingly ending up back in the "bad old days" of the 1970s and 80s before Opensource changed and improved everything.
As someone directly involved with opensource development since 1979, it breaks my heart to see how many projects are being overrun/ruined by this new generation of clueless developers who don't understand how much damage they are doing long-term. Anyway, I digress...
The Problem In More Detail
The current defaults for ERPNext 13 for Job Openings is as follows:
<domain>/jobs is one long concatenated mess of a single page long list with all Job Openings, and all fields for each opening all on one page. Worse yet, these are all listed without any clear separation between job listings. There is an "Apply Now" button for each listing, which jumps straight to the generic job applicant page (there are options in the Job Opening creation web form to have select a custom route to a custom job application page, but that is for later).
Worse yet, after a certain number of job listings (50?), there is just a little Gray Button stating "More", but so far no job applicants notice that More button, and so all of the other job postings go unseen.
Furthermore, unless I'm missing something somewhere (entirely likely), there is no way to send a link to the specific Job Opening itself (without all the other job openings), so that a prospective candidate can look at just that position and not have to wade through the whole page trying to find the one they are told to consider.
What is Desired
The biggest problem, and first to fix, is this hideous and unusable all-on-one-page-but-not-all-viewable layout.
Next is having a separate URL for the specific Job Opening that prospective candidates can view by itself instead of having to wade through a huge single page trying to find the single position you referred them to look at.
Finally, from the detailed version sending them to the Job Applicant form and receiving the applicant knowing which job they are actually applying for so we have full context in the hiring process.
I want the /jobs page to be a summary of all jobs available, with a clearer indicator that there are more pages of listings for sites (like mine) with many positions open), with a link for each summary to the Job Openings details for just that position, and then they can apply from that details page.
The details will not included on the /jobs page, rather it is a summary grid/matrix of just Title and Job Summary, with a "Learn More" button going to the full job description with all fields as its own dedicated page just for that single job opening. Then they can click an "Apply Now" button that takes them to either the generic job applicant form or the custom route/form if desired.
What I want is a clean grid or matrix view of Job opening on the /jobs page, that lists the Job Title and Job Summary, with a link/button to "Learn More", which then loads the full Job Opening page for just that specific job and no others, with full details, salary, and all other fields. And from that Job Opening Details page, there is a link to "Apply Now".
Attempted Temporary Workaround UI Hacks
I tried some simple hacks to just add horizonal lines and table borders around the job descriptions to at least add some clearer separation between the Job Description and the correct Apply Now button (users were getting confused sometimes about which button to click when the Job Descriptions were short and clustered together.
As you can see, the jobs all run together, and which Apply Now button isn't always clear, and the "More" button is barely noticeable.
This gets even worse with job descriptions that are filled in with full details, further cluttering up the page horrifically:
By adding the crude hack of simple <hr> tags and table borders, this at least becomes much clearer separation between each Job Opening, though still not pretty, and still not solving the "More" job openings button being missed by most users:
The UI Crude Hack Steps
Through SSH to the web server (so much for "no code" claims of ERPNext):
erpnuser@aws1:/srv/venv_erpnext/frappe-bench/apps/erpnext/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
Changed the default code from here:
<div class="my-5">
<h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p>
{%- if doc.publish_salary_range -%}
<p><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}</p>
{% endif %}
<div>
{%- if doc.job_application_route -%}
<a class='btn btn-primary'
href='/{{doc.job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% else %}
<a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% endif %}
</div>
</div>
To the following dirty hack adding simple horizontal rules and table borders for each job listing to be more clearly separated:
<hr>
<br />
<table border="1" style="border-spacing: 50px">
<tr>
<td >
<div class="my-5">
<h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p>
{%- if doc.publish_salary_range -%}
<p><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}</p>
{% endif %}
<div>
{%- if doc.job_application_route -%}
<a class='btn btn-primary'
href='/{{doc.job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% else %}
<a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% endif %}
</div>
</div>
</td>
</tr>
</table>
<br />
<hr style="border-width:5px;height:50px">
Enhancing the almost invisible "More" Button
erpnuser@aws1:/srv/venv_erpnext/frappe-bench/apps$ grep -r more-block *
frappe/frappe/templates/includes/list/list.html: <div class="more-block mt-6 {% if not show_more -%} hidden {%- endif %}">
frappe/frappe/templates/includes/list/list.js: $(".website-list .more-block").addClass("hide");
Find the More text, and just before it modify the button class entry, for example change it from class="btn btn-light btn-more btn-sm" changing btn-light to btn-dark:
Before:
{% if sub_title %}
<h4 class="text-muted">{{ sub_title }}</h4>
{% endif %}
{% if not result -%}
<div class="text-muted" style="min-height: 300px;">
{{ no_result_message or _("Nothing to show") }}
</div>
{% else %}
<div class="website-list" data-doctype="{{ doctype }}"
data-txt="{{ txt or '[notxt]' | e }}">
<!-- {% if not hide_filters -%}
{% include "templates/includes/list/filters.html" %}
{%- endif %} -->
{% if result_heading_template %}{% include result_heading_template %}{% endif %}
<div class="result">
{% for item in result %}
{{ item }}
{% endfor %}
</div>
<div class="more-block mt-6 {% if not show_more -%} hidden {%- endif %}">
<button class="btn btn-light btn-more btn-sm">{{ _("More") }}</button>
</div>
</div>
{%- endif %}
After:
{% if sub_title %}
<h4 class="text-muted">{{ sub_title }}</h4>
{% endif %}
{% if not result -%}
<div class="text-muted" style="min-height: 300px;">
{{ no_result_message or _("Nothing to show") }}
</div>
{% else %}
<div class="website-list" data-doctype="{{ doctype }}"
data-txt="{{ txt or '[notxt]' | e }}">
<!-- {% if not hide_filters -%}
{% include "templates/includes/list/filters.html" %}
{%- endif %} -->
{% if result_heading_template %}{% include result_heading_template %}{% endif %}
<div class="result">
{% for item in result %}
{{ item }}
{% endfor %}
</div>
<div class="more-block mt-6 {% if not show_more -%} hidden {%- endif %}">
<button class="btn btn-dark btn-more btn-sm">{{ _("More") }}</button>
</div>
</div>
{%- endif %}
It is still too small, and needs more text, so I did the following to try to make it stand out more clearly and provide a more useful explanation:
<button class="btn btn-primary btn-more">{{ _("THERE ARE EVEN MORE LISTINGS. CLICK THIS BUTTON TO SEE MORE JOB OPENINGS") }}</button>
Which makes the button now look like this:
Reference Example - Doesn't Fix, but Perhaps Helpful Clues Toward Fixes?
This framework instruction example customization page: https://frappeframework.com/docs/v13/user/en/guides/portal-development/generators claims we're supposed to edit the DocType, but when attempting to do so, the DocType for Job Opening clearly states you cannot edit it directly, you must edit a Customized Form instead.
So I am checking here instead:
https://rpgresearch.com/app/customize-form for the Job Opening Web Form.
The instructions (apparently out of date/sync) state to add published and route to the DocType (Form), but they already exist with the same specifications.
Next the instructions state to add to the list of "generator hooks", looking for website_generators or adding it to hooks.py (with no information on where I should find this hooks.py file, so I had to run via SSH a find to track it down:
erpnuser@aws1:/srv/venv_erpnext$ find ./ -iname "*hooks.py*"
./lib/python3.10/site-packages/pip/_vendor/requests/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/IPython/core/tests/test_hooks.py
./frappe-bench/env/lib/python3.10/site-packages/IPython/core/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/IPython/lib/tests/test_editorhooks.py
./frappe-bench/env/lib/python3.10/site-packages/IPython/lib/editorhooks.py
./frappe-bench/env/lib/python3.10/site-packages/tests/integration/test_webhooks.py
./frappe-bench/env/lib/python3.10/site-packages/plaid/api/webhooks.py
./frappe-bench/env/lib/python3.10/site-packages/botocore/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/requests/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/jedi/third_party/typeshed/third_party/2and3/requests/hooks.pyi
./frappe-bench/env/lib/python3.10/site-packages/pbr/tests/testpackage/pbr_testpackage/_setup_hooks.py
./frappe-bench/env/lib/python3.10/site-packages/pbr/tests/test_hooks.py
./frappe-bench/env/lib/python3.10/site-packages/gocardless_pro/webhooks.py
./frappe-bench/env/lib/python3.10/site-packages/pip/_vendor/requests/hooks.py
./frappe-bench/apps/erpnext/erpnext/hooks.py
./frappe-bench/apps/frappe/frappe/tests/test_hooks.py
./frappe-bench/apps/frappe/frappe/hooks.py
My initial assumption is to follow the instructions and focus on the listing for hooks.py, but as you can see there are multiple hooks.py, in:
./lib/python3.10/site-packages/pip/_vendor/requests/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/IPython/core/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/botocore/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/requests/hooks.py
./frappe-bench/env/lib/python3.10/site-packages/pip/_vendor/requests/hooks.py
./frappe-bench/apps/erpnext/erpnext/hooks.py
./frappe-bench/apps/frappe/frappe/hooks.py
Not to mention the webhooks.py floating around as well.
My assumption, based on context process of elimination is to go with
./frappe-bench/apps/erpnext/erpnext/hooks.py
This appears to already exist for Job Opening, so no need to make any edits on this file:
website_generators = [
"Item Group",
"Website Item",
"BOM",
"Sales Partner",
"Job Opening",
"Student Admission",
]
In the instructions, step 3 states to modify the job_opening.py file (again without specifying where to find this file, so yet again another find search):
erpnuser@aws1:/srv/venv_erpnext$ find ./ -iname "*job_opening.py*"
./frappe-bench/apps/erpnext/erpnext/hr/doctype/job_opening/job_opening.py
From instructions:
" add the website property to the JobOpening class in job_opening.py
In get_context, parents property will indicate the breadcrumbs"
Example in instructions is:
# subclass from WebsiteGenerator, not Document
class JobOpening(WebsiteGenerator):
website = frappe._dict(
template = "templates/generators/job_opening.html",
condition_field = "published",
page_title_field = "job_title",
)
But on server condition_field = is "publish" not "published", do I change this? - NO
Also instructions example shows this section as:
def get_context(self, context):
# show breadcrumbs
context.parents = [{'name': 'jobs', 'title': _('All Jobs') }]
But actual code on server is:
def get_context(self, context):
context.parents = [{"route": "jobs", "title": _("All Jobs")}]
Do I leave the server as-is, with "route" instead of "name"? Or do I change it from route to name? - NO
Instructions state:
Note: Once you do this, you should see the "See in Website" link on the document form.
But I see no such changes appear.
Step 4 of instructions indicate to:
Add the template in erpnext/templates/generators/job_opening.html
At least this time they mentioned the contest of where the file should be.
Which already exists...
Clearly the instructions are significantly out of date, even though both for version 13 of ERPNext.
The code in the instructions is thus:
{% raw %}{% extends "templates/web.html" %}
{% block breadcrumbs %}
{% include "templates/includes/breadcrumbs.html" %}
{% endblock %}
{% block header %}
<h1>{{ job_title }}</h1>
{% endblock %}
{% block page_content %}
<div>{{ description }}</div>
<a class="btn btn-primary" href="/job_application?job_title={{ doc.job_title }}">
{{ _("Apply Now") }}</a>
{% endblock %}{% endraw %}
While the code on the server has more if/else options, but is basically the same:
{% extends "templates/web.html" %}
{% block breadcrumbs %}
{% include "templates/includes/breadcrumbs.html" %}
{% endblock %}
{% block header %}
<h1>{{ job_title }}</h1>
{% endblock %}
{% block page_content %}
{%- if description -%}
<div>{{ description }}</div>
{% endif %}
{%- if publish_salary_range -%}
<div><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(lower_range), currency=currency) }} - {{ frappe.format_value(frappe.utils.flt(upper_range), currency=currency) }}</div>
{% endif %}
<p style='margin-top: 30px'>
{%- if job_application_route -%}
<a class='btn btn-primary'
href='/{{job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% else %}
<a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
{% endif %}
</p>
{% endblock %}
Step 5 in the instructions indicates:
If you add a method get_list_view
in the controller file (job_opening.py), you can set properties for the listview
def get_list_context(context):
context.title = _("Jobs")
context.introduction = _('Current Job Openings')
Once again without providing full context about location...
erpnuser@aws1:/srv/venv_erpnext$ find ./ -iname "*job_opening.py"
./frappe-bench/apps/erpnext/erpnext/hr/doctype/job_opening/job_opening.py
Which on the server already looks like this (similar but some difference with additional line):
def get_list_context(context):
context.title = _("Jobs")
context.introduction = _("Current Job Openings")
context.get_list = get_job_openings
So, the instructions don't solve what I want to solve, but hopefully these different pieces are what I need to know to try to hack together the desired changes...
Now Trying to Hack Desired Fixes
I think, for the main listing page, the key is (at least in part) hacking the generator template?
/srv/venv_erpnext/frappe-bench/apps/erpnext/erpnext/templates/generators/job_opening.html
First, I am going to pair down to only include the Job Title job_title and remove description from the block, as well as the salary listings.
Leaving initially only the Job Title and Apply Now Button.
No changes showing up, so tried:
service supervisor restart
service redis restart
service nginx restart
Still a work in progress. Will update this page as further progress is made.