Merge branch 'develop' of github.com:frappe/erpnext into develop

This commit is contained in:
Suraj Shetty 2021-05-03 19:44:22 +05:30
commit ca37380d2e
99 changed files with 5066 additions and 2081 deletions

View File

@ -80,14 +80,29 @@ jobs:
env:
TYPE: ${{ matrix.TYPE }}
- name: Coverage
if: matrix.TYPE == 'server'
- name: Coverage - Pull Request
if: matrix.TYPE == 'server' && github.event_name == 'pull_request'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==3.0.1
pip install coverage==5.5
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls --service=github
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github
- name: Coverage - Push
if: matrix.TYPE == 'server' && github.event_name == 'push'
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip install coveralls==2.2.0
pip install coverage==4.5.4
coveralls --service=github-actions
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_SERVICE_NAME: github-actions

View File

@ -39,6 +39,10 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a
---
### Containerized Installation
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
### Full Install
The Easy Way: our install script for bench will install all dependencies (e.g. MariaDB). See https://github.com/frappe/bench for more details.

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
__version__ = '13.1.0'
__version__ = '13.2.0'
def get_default_company(user=None):
'''Get default company for user'''

View File

@ -592,6 +592,7 @@ class JournalEntry(AccountsController):
self.validate_total_debit_and_credit()
@frappe.whitelist()
def get_outstanding_invoices(self):
self.set('accounts', [])
total = 0

View File

@ -561,7 +561,7 @@ frappe.ui.form.on('Payment Entry', {
flt(frm.doc.received_amount) * flt(frm.doc.target_exchange_rate));
if(frm.doc.payment_type == "Pay")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.received_amount, 1);
else
frm.events.set_unallocated_amount(frm);
@ -582,7 +582,7 @@ frappe.ui.form.on('Payment Entry', {
}
if(frm.doc.payment_type == "Receive")
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount);
frm.events.allocate_party_amount_against_ref_docs(frm, frm.doc.paid_amount, 1);
else
frm.events.set_unallocated_amount(frm);
},
@ -606,9 +606,9 @@ frappe.ui.form.on('Payment Entry', {
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"},
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
}
}
},
@ -743,7 +743,7 @@ frappe.ui.form.on('Payment Entry', {
});
},
allocate_party_amount_against_ref_docs: function(frm, paid_amount) {
allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0;
var total_deductions = frappe.utils.sum($.map(frm.doc.deductions || [],
@ -800,22 +800,15 @@ frappe.ui.form.on('Payment Entry', {
//If allocate payment amount checkbox is unchecked, set zero to allocate amount
row.allocated_amount = 0;
} else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
}
} else if (frappe.flags.allocate_payment_amount != 0 && (!row.allocated_amount || paid_amount_change)) {
if (row.outstanding_amount > 0 && allocated_positive_outstanding >= 0) {
row.allocated_amount = (row.outstanding_amount >= allocated_positive_outstanding) ?
allocated_positive_outstanding : row.outstanding_amount;
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
};
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
row.allocated_amount = (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) ?
-1*allocated_negative_outstanding : row.outstanding_amount;
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
}

View File

@ -234,7 +234,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
});
if (invoices) {
this.frm.fields_dict.payment.grid.update_docfield_property(
this.frm.fields_dict.payments.grid.update_docfield_property(
'invoice_number', 'options', "\n" + invoices.join("\n")
);

View File

@ -20,10 +20,11 @@
"discount",
"section_break_9",
"payment_amount",
"outstanding",
"paid_amount",
"discounted_amount",
"column_break_3",
"outstanding",
"paid_amount"
"base_payment_amount"
],
"fields": [
{
@ -78,7 +79,8 @@
"depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount"
"label": "Paid Amount",
"options": "currency"
},
{
"fieldname": "column_break_3",
@ -97,6 +99,7 @@
"fieldname": "outstanding",
"fieldtype": "Currency",
"label": "Outstanding",
"options": "currency",
"read_only": 1
},
{
@ -145,12 +148,18 @@
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"fieldname": "base_payment_amount",
"fieldtype": "Currency",
"label": "Payment Amount (Company Currency)",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-15 21:03:12.540546",
"modified": "2021-04-28 05:41:35.084233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",

View File

@ -235,11 +235,11 @@ def get_invoice_customer_map(pos_invoices):
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 5 and closing_entry:
if len(invoices) >= 1 and closing_entry:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry)
else:
@ -252,18 +252,18 @@ def unconsolidate_pos_invoices(closing_entry):
pluck='name'
)
if len(merge_logs) >= 5:
if len(merge_logs) >= 1:
closing_entry.set_status(update=True, status='Queued')
enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry)
else:
cancel_merge_logs(merge_logs, closing_entry)
def create_merge_logs(invoice_by_customer, closing_entry={}):
def create_merge_logs(invoice_by_customer, closing_entry=None):
for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(closing_entry.get('posting_date'))
merge_log.posting_date = getdate(closing_entry.get('posting_date')) if closing_entry else nowdate()
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.pos_closing_entry = closing_entry.get('name') if closing_entry else None
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
@ -273,7 +273,7 @@ def create_merge_logs(invoice_by_customer, closing_entry={}):
closing_entry.set_status(update=True, status='Submitted')
closing_entry.update_opening_entry()
def cancel_merge_logs(merge_logs, closing_entry={}):
def cancel_merge_logs(merge_logs, closing_entry=None):
for log in merge_logs:
merge_log = frappe.get_doc('POS Invoice Merge Log', log)
merge_log.flags.ignore_permissions = True
@ -283,20 +283,20 @@ def cancel_merge_logs(merge_logs, closing_entry={}):
closing_entry.set_status(update=True, status='Cancelled')
closing_entry.update_opening_entry(for_cancel=True)
def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None):
def enqueue_job(job, **kwargs):
check_scheduler_status()
closing_entry = kwargs.get('closing_entry') or {}
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
enqueue(
job,
**kwargs,
queue="long",
timeout=10000,
event="processing_merge_logs",
job_name=job_name,
closing_entry=closing_entry,
invoice_by_customer=invoice_by_customer,
merge_logs=merge_logs,
now=frappe.conf.developer_mode or frappe.flags.in_test
)

View File

@ -0,0 +1,37 @@
{
"actions": [],
"creation": "2021-04-19 14:56:06.652327",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"field",
"fieldname"
],
"fields": [
{
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 1,
"label": "Fieldname"
},
{
"fieldname": "field",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Field"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-21 11:12:54.632093",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Search Fields",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class POSSearchFields(Document):
pass

View File

@ -1,9 +1,17 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
let search_fields_datatypes = ['Data', 'Link', 'Dynamic Link', 'Long Text', 'Select', 'Small Text', 'Text', 'Text Editor'];
let do_not_include_fields = ["naming_series", "item_code", "item_name", "stock_uom", "hub_sync_id", "asset_naming_series",
"default_material_request_type", "valuation_method", "warranty_period", "weight_uom", "batch_number_series",
"serial_no_series", "purchase_uom", "customs_tariff_number", "sales_uom", "deferred_revenue_account",
"deferred_expense_account", "quality_inspection_template", "route", "slideshow", "website_image_alt", "thumbnail",
"web_long_description", "hub_sync_id"]
frappe.ui.form.on('POS Settings', {
onload: function(frm) {
frm.trigger("get_invoice_fields");
frm.trigger("add_search_options");
},
get_invoice_fields: function(frm) {
@ -21,6 +29,38 @@ frappe.ui.form.on('POS Settings', {
);
});
},
add_search_options: function(frm) {
frappe.model.with_doctype("Item", () => {
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
if (search_fields_datatypes.includes(d.fieldtype) && !(do_not_include_fields.includes(d.fieldname))) {
return [d.label];
} else {
return null;
}
});
fields.unshift('');
frm.fields_dict.pos_search_fields.grid.update_docfield_property('field', 'options', fields);
});
}
});
frappe.ui.form.on("POS Search Fields", {
field: function(frm, doctype, name) {
var doc = frappe.get_doc(doctype, name);
var df = $.map(frappe.get_doc("DocType", "Item").fields, function(d) {
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
return d;
} else {
return null;
}
})[0];
doc.fieldname = df.fieldname;
frm.refresh_field("fields");
}
});

View File

@ -5,7 +5,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"invoice_fields"
"invoice_fields",
"pos_search_fields"
],
"fields": [
{
@ -13,11 +14,17 @@
"fieldtype": "Table",
"label": "POS Field",
"options": "POS Field"
},
{
"fieldname": "pos_search_fields",
"fieldtype": "Table",
"label": "POS Search Fields",
"options": "POS Search Fields"
}
],
"issingle": 1,
"links": [],
"modified": "2020-06-01 15:46:41.478928",
"modified": "2021-04-19 14:56:24.465218",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Settings",

View File

@ -19,7 +19,7 @@
</tr>
</thead>
<tbody>
{% for row in data %}
{% for row in data %}
<tr>
{% if(row.posting_date) %}
<td>{{ frappe.format(row.posting_date, 'Date') }}</td>
@ -60,8 +60,8 @@
</tbody>
</table>
<br><br>
{% if aging %}
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ aging.ageing_based_on }}</h3>
{% if ageing %}
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ ageing.ageing_based_on }}</h3>
<h5 class="text-center">
{{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
</h5>
@ -78,10 +78,10 @@
</thead>
<tbody>
<tr>
<td>{{ aging.range1 }}</td>
<td>{{ aging.range2 }}</td>
<td>{{ aging.range3 }}</td>
<td>{{ aging.range4 }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
</tr>
</tbody>
</table>

View File

@ -4,10 +4,12 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa
from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing
from frappe.core.doctype.communication.email import make
from erpnext import get_company_currency
from erpnext.accounts.party import get_party_account_currency
from frappe.utils.print_format import report_to_pdf
from frappe.utils.pdf import get_pdf
@ -29,7 +31,7 @@ class ProcessStatementOfAccounts(Document):
validate_template(self.body)
if not self.customers:
frappe.throw(frappe._('Customers not selected.'))
frappe.throw(_('Customers not selected.'))
if self.enable_auto_email:
self.to_date = self.start_date
@ -38,7 +40,7 @@ class ProcessStatementOfAccounts(Document):
def get_report_pdf(doc, consolidated=True):
statement_dict = {}
aging = ''
ageing = ''
base_template_path = "frappe/www/printview.html"
template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html"
@ -54,26 +56,30 @@ def get_report_pdf(doc, consolidated=True):
'range4': 120,
'customer': entry.customer
})
col1, aging = get_ageing(ageing_filters)
aging[0]['ageing_based_on'] = doc.ageing_based_on
col1, ageing = get_ageing(ageing_filters)
if ageing:
ageing[0]['ageing_based_on'] = doc.ageing_based_on
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
or doc.currency or get_company_currency(doc.company)
filters= frappe._dict({
'from_date': doc.from_date,
'to_date': doc.to_date,
'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None,
"account": doc.account if doc.account else None,
'account': doc.account if doc.account else None,
'party_type': 'Customer',
'party': [entry.customer],
'presentation_currency': presentation_currency,
'group_by': doc.group_by,
'currency': doc.currency,
'cost_center': [cc.cost_center_name for cc in doc.cost_center],
'project': [p.project_name for p in doc.project],
'show_opening_entries': 0,
'include_default_book_entries': 0,
'show_cancelled_entries': 1,
'tax_id': tax_id if tax_id else None
})
col, res = get_soa(filters)
@ -83,11 +89,14 @@ def get_report_pdf(doc, consolidated=True):
if len(res) == 3:
continue
html = frappe.render_template(template_path, \
{"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None})
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None})
html = frappe.render_template(base_template_path, {"body": html, \
"css": get_print_style(), "title": "Statement For " + entry.customer})
statement_dict[entry.customer] = html
if not bool(statement_dict):
return False
elif consolidated:
@ -167,7 +176,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == 'Sales Person':
customers = get_customers_based_on_sales_person(collection_name)
if not bool(customers):
frappe.throw('No Customers found with selected options.')
frappe.throw(_('No Customers found with selected options.'))
else:
if customer_collection == 'Sales Partner':
customers = frappe.get_list('Customer', fields=['name', 'email_id'], \
@ -199,14 +208,14 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:
frappe.throw('No billing email found for customer: '+ customer_name)
frappe.throw(_("No billing email found for customer: {0}").format(customer_name))
else:
return ''
if billing_and_primary:
primary_email = frappe.get_value('Customer', customer_name, 'email_id')
if primary_email is None and int(primary_mandatory):
frappe.throw('No primary email found for customer: '+ customer_name)
frappe.throw(_("No primary email found for customer: {0}").format(customer_name))
return [primary_email or '', billing_email[0][0]]
else:
return billing_email[0][0] or ''

View File

@ -514,6 +514,28 @@ frappe.ui.form.on("Purchase Invoice", {
}
},
refresh: function(frm) {
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.per_received < 100) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frm.events.make_purchase_receipt(frm);
}, __('Create'));
}
if (frm.doc.docstatus == 1 && frm.doc.per_received > 0) {
frm.add_custom_button(__('Purchase Receipt'), () => {
frappe.route_options = {
'purchase_invoice': frm.doc.name
}
frappe.set_route("List", "Purchase Receipt", "List")
}, __('View'));
}
},
onload: function(frm) {
if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) {
@ -539,5 +561,13 @@ frappe.ui.form.on("Purchase Invoice", {
update_stock: function(frm) {
hide_fields(frm.doc);
frm.fields_dict.items.grid.toggle_reqd("item_code", frm.doc.update_stock? true: false);
},
make_purchase_receipt: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
frm: frm,
freeze_message: __("Creating Purchase Receipt ...")
})
}
})

View File

@ -163,7 +163,8 @@
"to_date",
"column_break_114",
"auto_repeat",
"update_auto_repeat_reference"
"update_auto_repeat_reference",
"per_received"
],
"fields": [
{
@ -1364,6 +1365,15 @@
"print_hide": 1,
"print_width": "50px",
"width": "50px"
},
{
"fieldname": "per_received",
"fieldtype": "Percent",
"hidden": 1,
"label": "Per Received",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
],
"icon": "fa fa-file-text",

View File

@ -1207,3 +1207,41 @@ def make_inter_company_sales_invoice(source_name, target_doc=None):
def on_doctype_update():
frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"])
@frappe.whitelist()
def make_purchase_receipt(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.qty = flt(obj.qty) - flt(obj.received_qty)
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \
flt(obj.rate) * flt(source_parent.conversion_rate)
doc = get_mapped_doc("Purchase Invoice", source_name, {
"Purchase Invoice": {
"doctype": "Purchase Receipt",
"validation": {
"docstatus": ["=", 1],
}
},
"Purchase Invoice Item": {
"doctype": "Purchase Receipt Item",
"field_map": {
"name": "purchase_invoice_item",
"parent": "purchase_invoice",
"bom": "bom",
"purchase_order": "purchase_order",
"po_detail": "purchase_order_item",
"material_request": "material_request",
"material_request_item": "material_request_item"
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges"
}
}, target_doc)
return doc

View File

@ -397,7 +397,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.update({
"payment_schedule": get_payment_terms("_Test Payment Term Template",
pi.posting_date, pi.grand_total)
pi.posting_date, pi.grand_total, pi.base_grand_total)
})
pi.save()

View File

@ -607,6 +607,7 @@
"oldfieldname": "purchase_order",
"oldfieldtype": "Link",
"options": "Purchase Order",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
@ -853,7 +854,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-02-23 00:59:52.614805",
"modified": "2021-03-30 09:02:39.256602",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@ -36,6 +36,7 @@
"additional_discount_percentage",
"additional_discount_amount",
"sb_3",
"submit_invoice",
"invoices",
"accounting_dimensions_section",
"cost_center",
@ -45,9 +46,7 @@
{
"allow_on_submit": 1,
"fieldname": "cb_1",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "status",
@ -55,97 +54,73 @@
"label": "Status",
"no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "subscription_period",
"fieldtype": "Section Break",
"label": "Subscription Period",
"show_days": 1,
"show_seconds": 1
"label": "Subscription Period"
},
{
"fieldname": "cancelation_date",
"fieldtype": "Date",
"label": "Cancelation Date",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"allow_on_submit": 1,
"fieldname": "trial_period_start",
"fieldtype": "Date",
"label": "Trial Period Start Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"depends_on": "eval:doc.trial_period_start",
"fieldname": "trial_period_end",
"fieldtype": "Date",
"label": "Trial Period End Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "current_invoice_start",
"fieldtype": "Date",
"label": "Current Invoice Start Date",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"fieldname": "current_invoice_end",
"fieldtype": "Date",
"label": "Current Invoice End Date",
"read_only": 1,
"show_days": 1,
"show_seconds": 1
"read_only": 1
},
{
"default": "0",
"description": "Number of days that the subscriber has to pay invoices generated by this subscription",
"fieldname": "days_until_due",
"fieldtype": "Int",
"label": "Days Until Due",
"show_days": 1,
"show_seconds": 1
"label": "Days Until Due"
},
{
"default": "0",
"fieldname": "cancel_at_period_end",
"fieldtype": "Check",
"label": "Cancel At End Of Period",
"show_days": 1,
"show_seconds": 1
"label": "Cancel At End Of Period"
},
{
"default": "0",
"fieldname": "generate_invoice_at_period_start",
"fieldtype": "Check",
"label": "Generate Invoice At Beginning Of Period",
"show_days": 1,
"show_seconds": 1
"label": "Generate Invoice At Beginning Of Period"
},
{
"allow_on_submit": 1,
"fieldname": "sb_4",
"fieldtype": "Section Break",
"label": "Plans",
"show_days": 1,
"show_seconds": 1
"label": "Plans"
},
{
"allow_on_submit": 1,
@ -153,84 +128,62 @@
"fieldtype": "Table",
"label": "Plans",
"options": "Subscription Plan Detail",
"reqd": 1,
"show_days": 1,
"show_seconds": 1
"reqd": 1
},
{
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
"fieldname": "sb_1",
"fieldtype": "Section Break",
"label": "Taxes",
"show_days": 1,
"show_seconds": 1
"label": "Taxes"
},
{
"fieldname": "sb_2",
"fieldtype": "Section Break",
"label": "Discounts",
"show_days": 1,
"show_seconds": 1
"label": "Discounts"
},
{
"fieldname": "apply_additional_discount",
"fieldtype": "Select",
"label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total",
"show_days": 1,
"show_seconds": 1
"options": "\nGrand Total\nNet Total"
},
{
"fieldname": "cb_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Percent",
"label": "Additional DIscount Percentage",
"show_days": 1,
"show_seconds": 1
"label": "Additional DIscount Percentage"
},
{
"collapsible": 1,
"fieldname": "additional_discount_amount",
"fieldtype": "Currency",
"label": "Additional DIscount Amount",
"show_days": 1,
"show_seconds": 1
"label": "Additional DIscount Amount"
},
{
"depends_on": "eval:doc.invoices",
"fieldname": "sb_3",
"fieldtype": "Section Break",
"label": "Invoices",
"show_days": 1,
"show_seconds": 1
"label": "Invoices"
},
{
"collapsible": 1,
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Invoices",
"options": "Subscription Invoice",
"show_days": 1,
"show_seconds": 1
"options": "Subscription Invoice"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions",
"show_days": 1,
"show_seconds": 1
"label": "Accounting Dimensions"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
"fieldtype": "Column Break"
},
{
"fieldname": "party_type",
@ -238,9 +191,7 @@
"label": "Party Type",
"options": "DocType",
"reqd": 1,
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "party",
@ -249,27 +200,21 @@
"label": "Party",
"options": "party_type",
"reqd": 1,
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"depends_on": "eval:doc.party_type === 'Customer'",
"fieldname": "sales_tax_template",
"fieldtype": "Link",
"label": "Sales Taxes and Charges Template",
"options": "Sales Taxes and Charges Template",
"show_days": 1,
"show_seconds": 1
"options": "Sales Taxes and Charges Template"
},
{
"depends_on": "eval:doc.party_type === 'Supplier'",
"fieldname": "purchase_tax_template",
"fieldtype": "Link",
"label": "Purchase Taxes and Charges Template",
"options": "Purchase Taxes and Charges Template",
"show_days": 1,
"show_seconds": 1
"options": "Purchase Taxes and Charges Template"
},
{
"default": "0",
@ -277,55 +222,49 @@
"fieldname": "follow_calendar_months",
"fieldtype": "Check",
"label": "Follow Calendar Months",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"default": "0",
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
"fieldname": "generate_new_invoices_past_due_date",
"fieldtype": "Check",
"label": "Generate New Invoices Past Due Date",
"show_days": 1,
"show_seconds": 1
"label": "Generate New Invoices Past Due Date"
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "Subscription End Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Subscription Start Date",
"set_only_once": 1,
"show_days": 1,
"show_seconds": 1
"set_only_once": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"show_days": 1,
"show_seconds": 1
"options": "Cost Center"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"show_days": 1,
"show_seconds": 1
"options": "Company"
},
{
"default": "1",
"fieldname": "submit_invoice",
"fieldtype": "Check",
"label": "Submit Invoice Automatically"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-09 15:44:20.024789",
"modified": "2021-04-19 15:24:27.550797",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",

View File

@ -276,7 +276,7 @@ class Subscription(Document):
frappe.throw(_('Subscription End Date is mandatory to follow calendar months'))
if billing_info[0]['billing_interval'] != 'Month':
frappe.throw('Billing Interval in Subscription Plan must be Month to follow calendar months')
frappe.throw(_('Billing Interval in Subscription Plan must be Month to follow calendar months'))
def after_insert(self):
# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype?
@ -383,7 +383,9 @@ class Subscription(Document):
invoice.flags.ignore_mandatory = True
invoice.save()
invoice.submit()
if self.submit_invoice:
invoice.submit()
return invoice

View File

@ -18,7 +18,8 @@ def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, upd
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
else:
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else:
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
@ -170,7 +171,7 @@ def round_off_debit_credit(gl_map):
else:
allowance = .5
if abs(debit_credit_diff) >= allowance:
if abs(debit_credit_diff) > allowance:
frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.")
.format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff))

View File

@ -0,0 +1,29 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports['Billed Items To Be Received'] = {
'filters': [
{
'label': __('Company'),
'fieldname': 'company',
'fieldtype': 'Link',
'options': 'Company',
'reqd': 1,
'default': frappe.defaults.get_default('Company')
},
{
'label': __('As on Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'reqd': 1,
'default': get_today()
},
{
'label': __('Purchase Invoice'),
'fieldname': 'purchase_invoice',
'fieldtype': 'Link',
'options': 'Purchase Invoice'
}
]
};

View File

@ -0,0 +1,39 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-03-30 09:35:38.683028",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-03-31 08:48:30.944429",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Billed Items To Be Received",
"owner": "Administrator",
"prepared_report": 0,
"query": "",
"ref_doctype": "Purchase Invoice",
"report_name": "Billed Items To Be Received",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Purchase User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
},
{
"role": "Stock User"
}
]
}

View File

@ -0,0 +1,107 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
data = get_data(filters) or []
columns = get_columns()
return columns, data
def get_data(report_filters):
filters = get_report_filters(report_filters)
fields = get_report_fields()
return frappe.get_all('Purchase Invoice',
fields= fields, filters=filters)
def get_report_filters(report_filters):
filters = [['Purchase Invoice','company','=',report_filters.get('company')],
['Purchase Invoice','posting_date','<=',report_filters.get('posting_date')], ['Purchase Invoice','docstatus','=',1],
['Purchase Invoice','per_received','<',100], ['Purchase Invoice','update_stock','=',0]]
if report_filters.get('purchase_invoice'):
filters.append(['Purchase Invoice','per_received','in',[report_filters.get('purchase_invoice')]])
return filters
def get_report_fields():
fields = []
for p_field in ['name', 'supplier', 'company', 'posting_date', 'currency']:
fields.append('`tabPurchase Invoice`.`{}`'.format(p_field))
for c_field in ['item_code', 'item_name', 'uom', 'qty', 'received_qty', 'rate', 'amount']:
fields.append('`tabPurchase Invoice Item`.`{}`'.format(c_field))
return fields
def get_columns():
return [
{
'label': _('Purchase Invoice'),
'fieldname': 'name',
'fieldtype': 'Link',
'options': 'Purchase Invoice',
'width': 170
},
{
'label': _('Supplier'),
'fieldname': 'supplier',
'fieldtype': 'Link',
'options': 'Supplier',
'width': 120
},
{
'label': _('Posting Date'),
'fieldname': 'posting_date',
'fieldtype': 'Date',
'width': 100
},
{
'label': _('Item Code'),
'fieldname': 'item_code',
'fieldtype': 'Link',
'options': 'Item',
'width': 100
},
{
'label': _('Item Name'),
'fieldname': 'item_name',
'fieldtype': 'Data',
'width': 100
},
{
'label': _('UOM'),
'fieldname': 'uom',
'fieldtype': 'Link',
'options': 'UOM',
'width': 100
},
{
'label': _('Invoiced Qty'),
'fieldname': 'qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Received Qty'),
'fieldname': 'received_qty',
'fieldtype': 'Float',
'width': 100
},
{
'label': _('Rate'),
'fieldname': 'rate',
'fieldtype': 'Currency',
'width': 100
},
{
'label': _('Amount'),
'fieldname': 'amount',
'fieldtype': 'Currency',
'width': 100
}
]

View File

@ -435,6 +435,35 @@ class TestPurchaseOrder(unittest.TestCase):
po.load_from_db()
self.assertEqual(po.get("items")[0].received_qty, 5)
def test_purchase_order_invoice_receipt_workflow(self):
from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt
po = create_purchase_order()
pi = make_pi_from_po(po.name)
pi.submit()
pr = make_purchase_receipt(pi.name)
pr.submit()
pi.load_from_db()
self.assertEquals(pi.per_received, 100.00)
self.assertEquals(pi.items[0].qty, pi.items[0].received_qty)
po.load_from_db()
self.assertEquals(po.per_received, 100.00)
self.assertEquals(po.per_billed, 100.00)
pr.cancel()
pi.load_from_db()
pi.cancel()
po.load_from_db()
po.cancel()
def test_make_purchase_invoice(self):
po = create_purchase_order(do_not_submit=True)

View File

@ -0,0 +1,56 @@
# Version 13.2.0 Release Notes
### Features & Enhancements
- Employee Hours Utilization Report ([#25209](https://github.com/frappe/erpnext/pull/25209))
- Delayed Tasks Summary Report ([#25024](https://github.com/frappe/erpnext/pull/25024))
- Project Profitability Report ([#24944](https://github.com/frappe/erpnext/pull/24944))
- Timer in LMS Quiz ([#24246](https://github.com/frappe/erpnext/pull/24246))
- Role to allow over billing, delivery, receipt ([#24854](https://github.com/frappe/erpnext/pull/24854))
- Auto calculate distance for e-way bill generations ([#25480](https://github.com/frappe/erpnext/pull/25480))
- Add total available stock field in PO ([#24878](https://github.com/frappe/erpnext/pull/24878))
- Refactored Setup Taxes and Charges ([#24805](https://github.com/frappe/erpnext/pull/24805))
- Inpatient Occupancy Table Editable for Healthcare Admin ([#24989](https://github.com/frappe/erpnext/pull/24989))
- Added Disable Rounded Total in sales transactions ([#25362](https://github.com/frappe/erpnext/pull/25362))
### Fixes
- Incorrect GL Entry validation ([#25474](https://github.com/frappe/erpnext/pull/25474))
- Cannot create item variants ([#25433](https://github.com/frappe/erpnext/pull/25433))
- Leave policy in leave allocation ([#25334](https://github.com/frappe/erpnext/pull/25334))
- Let Administrator delete company transactions ([#25300](https://github.com/frappe/erpnext/pull/25300))
- Display reconcile tool when closing balance 0 ([#25417](https://github.com/frappe/erpnext/pull/25417))
- Bulk Salary Structure Assignment ([#25389](https://github.com/frappe/erpnext/pull/25389))
- Payment amount showing in foreign currency ([#25518](https://github.com/frappe/erpnext/pull/25518))
- Commit changes to shipment status in database ([#25374](https://github.com/frappe/erpnext/pull/25374))
- Add amend perm for loan and system manager for loan doctypes ([#25393](https://github.com/frappe/erpnext/pull/25393))
- Cashier query in POS Opening/Closing Entry ([#25398](https://github.com/frappe/erpnext/pull/25398))
- Apply single transaction threshold on net_total instead of supplier credit amount ([#25243](https://github.com/frappe/erpnext/pull/25243))
- Update allocated amount after paid amount is changed in PE ([#25528](https://github.com/frappe/erpnext/pull/25528))
- Remove non-standard module cards from Home Workspace ([#25391](https://github.com/frappe/erpnext/pull/25391))
- Cannot scan spacebar character in pos ([#25479](https://github.com/frappe/erpnext/pull/25479))
- Permission error after submitting exchange rate revaluation ([#25432](https://github.com/frappe/erpnext/pull/25432))
- Equality check instead of assignment in cart ([#25372](https://github.com/frappe/erpnext/pull/25372))
- Disable auto naming of customer during import ([#25152](https://github.com/frappe/erpnext/pull/25152))
- Additional Salary component amount not getting set ([#25355](https://github.com/frappe/erpnext/pull/25355))
- Round off values near to zero ([#25304](https://github.com/frappe/erpnext/pull/25304))
- Allow to cancel loan with cancelled repayment entry ([#25508](https://github.com/frappe/erpnext/pull/25508))
- Currency symbol in bank transaction list view ([#25336](https://github.com/frappe/erpnext/pull/25336))
- Incorrect batch picked in subcontracted purchase receipt ([#25186](https://github.com/frappe/erpnext/pull/25186))
- Issue in project custom status ([#25452](https://github.com/frappe/erpnext/pull/25452))
- Shipment pickup_to, pickup_from functionality. ([#25359](https://github.com/frappe/erpnext/pull/25359))
- Stock ledger entry created against draft stock entry ([#25539](https://github.com/frappe/erpnext/pull/25539))
- Ageing errors in PSOA ([#25529](https://github.com/frappe/erpnext/pull/25529))
- Permission error while adding weekly holidays ([#25450](https://github.com/frappe/erpnext/pull/25450))
- Filter for employees in salary slip ([#25360](https://github.com/frappe/erpnext/pull/25360))
- Backward compatibility for GSTR-1 report ([#25444](https://github.com/frappe/erpnext/pull/25444))
- Incorrect incoming rate for the sales return ([#25145](https://github.com/frappe/erpnext/pull/25145))
- POS print receipt ([#25328](https://github.com/frappe/erpnext/pull/25328))
- Laboratory Module patch ([#25431](https://github.com/frappe/erpnext/pull/25431))
- Performance: fetching exchange rate on every line item slows down PO ([#25345](https://github.com/frappe/erpnext/pull/25345))
- Presentation currency in statement of accounts ([#25367](https://github.com/frappe/erpnext/pull/25367))
- Serial No not updated correctly via Inter Company Stock Transfer ([#25006](https://github.com/frappe/erpnext/pull/25006))
- Ignore Customer Group Perm on All Products page ([#25396](https://github.com/frappe/erpnext/pull/25396))
- Change subcontracted item display ([#25425](https://github.com/frappe/erpnext/pull/25425))
- Add company validation for e-invoicing ([#25348](https://github.com/frappe/erpnext/pull/25348))

View File

@ -90,6 +90,8 @@ class AccountsController(TransactionBase):
self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year()
self.validate_party_accounts()
self.validate_inter_company_reference()
self.set_incoming_rate()
@ -233,6 +235,23 @@ class AccountsController(TransactionBase):
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
self.meta.get_label(date_field), self)
def validate_party_accounts(self):
if self.doctype not in ('Sales Invoice', 'Purchase Invoice'):
return
if self.doctype == 'Sales Invoice':
party_account_field = 'debit_to'
item_field = 'income_account'
else:
party_account_field = 'credit_to'
item_field = 'expense_account'
for item in self.get('items'):
if item.get(item_field) == self.get(party_account_field):
frappe.throw(_("Row {0}: {1} {2} cannot be same as {3} (Party Account) {4}").format(item.idx,
frappe.bold(frappe.unscrub(item_field)), item.get(item_field),
frappe.bold(frappe.unscrub(party_account_field)), self.get(party_account_field)))
def validate_inter_company_reference(self):
if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
return
@ -240,7 +259,7 @@ class AccountsController(TransactionBase):
if self.is_internal_transfer():
if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference')
or self.get('inter_company_order_reference')):
msg = _("Internal Sale or Delivery Reference missing. ")
msg = _("Internal Sale or Delivery Reference missing.")
msg += _("Please create purchase from internal sale or delivery document itself")
frappe.throw(msg, title=_("Internal Sales Reference Missing"))
@ -904,29 +923,34 @@ class AccountsController(TransactionBase):
date = self.get("due_date")
due_date = date or posting_date
if party_account_currency == self.company_currency:
grand_total = self.get("base_rounded_total") or self.base_grand_total
else:
grand_total = self.get("rounded_total") or self.grand_total
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if self.get("total_advance"):
grand_total -= self.get("total_advance")
if party_account_currency == self.company_currency:
base_grand_total -= self.get("total_advance")
grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total"))
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
if not self.get("payment_schedule"):
if self.get("payment_terms_template"):
data = get_payment_terms(self.payment_terms_template, posting_date, grand_total)
data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total)
for item in data:
self.append("payment_schedule", item)
else:
data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total)
data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total)
self.append("payment_schedule", data)
else:
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.outstanding = d.payment_amount
def set_due_date(self):
@ -963,22 +987,28 @@ class AccountsController(TransactionBase):
if self.get("payment_schedule"):
total = 0
base_total = 0
for d in self.get("payment_schedule"):
total += flt(d.payment_amount)
base_total += flt(d.base_payment_amount)
if party_account_currency == self.company_currency:
total = flt(total, self.precision("base_grand_total"))
grand_total = flt(self.get("base_rounded_total") or self.base_grand_total, self.precision('base_grand_total'))
else:
total = flt(total, self.precision("grand_total"))
grand_total = flt(self.get("rounded_total") or self.grand_total, self.precision('grand_total'))
if self.get("total_advance"):
grand_total -= self.get("total_advance")
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
if total != flt(grand_total, self.precision("grand_total")):
if self.get("total_advance"):
if party_account_currency == self.company_currency:
base_grand_total -= self.get("total_advance")
grand_total = flt(base_grand_total / self.get("conversion_rate"), self.precision("grand_total"))
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
print(grand_total, base_grand_total)
if total != flt(grand_total, self.precision("grand_total")) or \
base_total != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self):
@ -1218,7 +1248,7 @@ def update_invoice_status():
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, bill_date=None):
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
if not terms_template:
return
@ -1226,14 +1256,14 @@ def get_payment_terms(terms_template, posting_date=None, grand_total=None, bill_
schedule = []
for d in terms_doc.get("terms"):
term_details = get_payment_term_details(d, posting_date, grand_total, bill_date)
term_details = get_payment_term_details(d, posting_date, grand_total, base_grand_total, bill_date)
schedule.append(term_details)
return schedule
@frappe.whitelist()
def get_payment_term_details(term, posting_date=None, grand_total=None, bill_date=None):
def get_payment_term_details(term, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
term_details = frappe._dict()
if isinstance(term, text_type):
term = frappe.get_doc("Payment Term", term)
@ -1242,9 +1272,9 @@ def get_payment_term_details(term, posting_date=None, grand_total=None, bill_dat
term_details.description = term.description
term_details.invoice_portion = term.invoice_portion
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
term_details.base_payment_amount = flt(term.invoice_portion) * flt(base_grand_total) / 100
term_details.discount_type = term.discount_type
term_details.discount = term.discount
# term_details.discounted_amount = flt(grand_total) * (term.discount / 100) if term.discount_type == 'Percentage' else discount
term_details.outstanding = term_details.payment_amount
term_details.mode_of_payment = term.mode_of_payment

View File

@ -262,7 +262,8 @@ def copy_attributes_to_variant(item, variant):
# copy non no-copy fields
exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website",
"show_variant_in_website", "opening_stock", "variant_of", "valuation_rate"]
"show_variant_in_website", "opening_stock", "variant_of", "valuation_rate",
"has_variants", "attributes"]
if item.variant_based_on=='Manufacturer':
# don't copy manufacturer values if based on part no

View File

@ -17,10 +17,12 @@ class AmazonMWSSettings(Document):
else:
self.enable_sync = 0
@frappe.whitelist()
def get_products_details(self):
if self.enable_amazon == 1:
frappe.enqueue('erpnext.erpnext_integrations.doctype.amazon_mws_settings.amazon_methods.get_products_details')
@frappe.whitelist()
def get_order_details(self):
if self.enable_amazon == 1:
after_date = dateutil.parser.parse(self.after_date).strftime("%Y-%m-%d")
@ -40,4 +42,4 @@ def setup_custom_fields():
fieldtype='Data', insert_after='title', read_only=1, print_hide=1)]
}
create_custom_fields(custom_fields)
create_custom_fields(custom_fields)

View File

@ -50,6 +50,7 @@ class TherapyType(Document):
self.db_set('change_in_item', 0)
@frappe.whitelist()
def add_exercises(self):
exercises = self.get_exercises_for_body_parts()
last_idx = max([cint(d.idx) for d in self.get('exercises')] or [0,])

View File

@ -34,7 +34,7 @@ frappe.ui.form.on('Employee Advance', {
};
});
frm.set_query('salary_component', function(doc) {
frm.set_query('salary_component', function() {
return {
filters: {
"type": "Deduction"
@ -44,48 +44,49 @@ frappe.ui.form.on('Employee Advance', {
},
refresh: function(frm) {
if (frm.doc.docstatus===1
&& (flt(frm.doc.paid_amount) < flt(frm.doc.advance_amount))
&& frappe.model.can_create("Payment Entry")) {
if (frm.doc.docstatus === 1 &&
(flt(frm.doc.paid_amount) < flt(frm.doc.advance_amount)) &&
frappe.model.can_create("Payment Entry")) {
frm.add_custom_button(__('Payment'),
function() { frm.events.make_payment_entry(frm); }, __('Create'));
}
else if (
frm.doc.docstatus === 1
&& flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount)
&& frappe.model.can_create("Expense Claim")
function () {
frm.events.make_payment_entry(frm);
}, __('Create'));
} else if (
frm.doc.docstatus === 1 &&
flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount) &&
frappe.model.can_create("Expense Claim")
) {
frm.add_custom_button(
__("Expense Claim"),
function() {
function () {
frm.events.make_expense_claim(frm);
},
__('Create')
);
}
if (frm.doc.docstatus === 1
&& (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
if (frm.doc.docstatus === 1 &&
(flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) {
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")){
frm.add_custom_button(__("Return"), function() {
if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")) {
frm.add_custom_button(__("Return"), function() {
frm.trigger('make_return_entry');
}, __('Create'));
}else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){
frm.add_custom_button(__("Deduction from salary"), function() {
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
frm.add_custom_button(__("Deduction from salary"), function() {
frm.events.make_deduction_via_additional_salary(frm);
}, __('Create'));
}
}
},
make_deduction_via_additional_salary: function(frm){
make_deduction_via_additional_salary: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.employee_advance.employee_advance.create_return_through_additional_salary",
args: {
doc: frm.doc
},
callback: function (r){
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
@ -94,7 +95,7 @@ frappe.ui.form.on('Employee Advance', {
make_payment_entry: function(frm) {
var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry";
if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) {
if (frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) {
method = "erpnext.hr.doctype.employee_advance.employee_advance.make_bank_entry";
}
return frappe.call({
@ -148,11 +149,11 @@ frappe.ui.form.on('Employee Advance', {
});
},
employee: function (frm) {
employee: function(frm) {
if (frm.doc.employee) {
frappe.run_serially([
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_pending_amount')
() => frm.trigger('get_employee_currency'),
() => frm.trigger('get_pending_amount')
]);
}
},
@ -199,7 +200,7 @@ frappe.ui.form.on('Employee Advance', {
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
frm.set_df_property("exchange_rate", "description", "" );
frm.set_df_property("exchange_rate", "description", "");
}
frm.refresh_fields();
}
@ -215,8 +216,8 @@ frappe.ui.form.on('Employee Advance', {
callback: function(r) {
frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
+ " = [?] " + company_currency);
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency +
" = [?] " + company_currency);
}
});
}

View File

@ -4,10 +4,10 @@ from frappe import _
def get_data():
return {
'fieldname': 'employee_advance',
'non_standard_fieldnames': {
'Payment Entry': 'reference_name',
'Journal Entry': 'reference_name'
},
'non_standard_fieldnames': {
'Payment Entry': 'reference_name',
'Journal Entry': 'reference_name'
},
'transactions': [
{
'items': ['Expense Claim']

View File

@ -0,0 +1,68 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on("Employee Referral", {
refresh: function(frm) {
if (frm.doc.docstatus === 1 && frm.doc.status === "Pending") {
frm.add_custom_button(__("Reject Employee Referral"), function() {
frappe.confirm(
__("Are you sure you want to reject the Employee Referral?"),
function() {
frm.doc.status = "Rejected";
frm.dirty();
frm.save_or_update();
},
function() {
window.close();
}
);
});
frm.add_custom_button(__("Create Job Applicant"), function() {
frm.events.create_job_applicant(frm);
}).addClass("btn-primary");
}
// To check whether Payment is done or not
if (frm.doc.docstatus === 1 && frm.doc.status === "Accepted") {
frappe.db.get_list("Additional Salary", {
filters: {
ref_docname: cur_frm.doc.name,
docstatus: 1
},
fields: ["count(name) as additional_salary_count"]
}).then((data) => {
let additional_salary_count = data[0].additional_salary_count;
if (frm.doc.is_applicable_for_referral_bonus && !additional_salary_count) {
frm.add_custom_button(__("Create Additional Salary"), function() {
frm.events.create_additional_salary(frm);
}).addClass("btn-primary");
}
});
}
},
create_job_applicant: function(frm) {
frappe.model.open_mapped_doc({
method: "erpnext.hr.doctype.employee_referral.employee_referral.create_job_applicant",
frm: frm
});
},
create_additional_salary: function(frm) {
frappe.call({
method: "erpnext.hr.doctype.employee_referral.employee_referral.create_additional_salary",
args: {
doc: frm.doc
},
callback: function (r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
},
});

View File

@ -0,0 +1,294 @@
{
"actions": [],
"autoname": "format:HR-REF-{####}",
"creation": "2021-03-23 14:54:45.047051",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"first_name",
"last_name",
"full_name",
"email",
"contact_no",
"resume",
"resume_link",
"column_break_6",
"date",
"status",
"for_designation",
"current_employer",
"current_job_title",
"referrer_details_section",
"referrer",
"referrer_name",
"column_break_14",
"is_applicable_for_referral_bonus",
"referral_payment_status",
"department",
"additional_information_section",
"qualification_reason",
"work_references",
"amended_from"
],
"fields": [
{
"fieldname": "first_name",
"fieldtype": "Data",
"label": "First Name ",
"reqd": 1
},
{
"fieldname": "last_name",
"fieldtype": "Data",
"label": "Last Name",
"reqd": 1
},
{
"fieldname": "full_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Full Name",
"read_only": 1
},
{
"fieldname": "contact_no",
"fieldtype": "Data",
"in_standard_filter": 1,
"label": "Contact No.",
"options": "Phone"
},
{
"fieldname": "current_employer",
"fieldtype": "Data",
"label": "Current Employer "
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_standard_filter": 1,
"label": "Date",
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
"options": "Pending\nIn Process\nAccepted\nRejected",
"permlevel": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "current_job_title",
"fieldtype": "Data",
"label": "Current Job Title"
},
{
"fieldname": "resume",
"fieldtype": "Attach",
"label": "Resume"
},
{
"fieldname": "referrer_details_section",
"fieldtype": "Section Break",
"label": "Referrer Details"
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fieldname": "additional_information_section",
"fieldtype": "Section Break",
"label": "Additional Information "
},
{
"fieldname": "work_references",
"fieldtype": "Text Editor",
"label": "Work References"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Referral",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"fieldname": "for_designation",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "For Designation ",
"options": "Designation",
"reqd": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Email",
"options": "Email",
"reqd": 1,
"unique": 1
},
{
"default": "1",
"fieldname": "is_applicable_for_referral_bonus",
"fieldtype": "Check",
"label": "Is Applicable for Referral Bonus"
},
{
"fieldname": "qualification_reason",
"fieldtype": "Text Editor",
"label": "Why is this Candidate Qualified for this Position?"
},
{
"fieldname": "referrer",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Referrer",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "referrer.employee_name",
"fieldname": "referrer_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Referrer Name",
"read_only": 1
},
{
"fieldname": "resume_link",
"fieldtype": "Data",
"label": "Resume Link"
},
{
"fieldname": "referral_payment_status",
"fieldtype": "Select",
"label": "Referral Bonus Payment Status",
"options": "\nUnpaid\nPaid",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-26 21:21:38.094086",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Referral",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"amend": 1,
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"delete": 1,
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"permlevel": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "full_name"
}

View File

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import get_link_to_form
from frappe.model.document import Document
class EmployeeReferral(Document):
def validate(self):
self.set_full_name()
self.set_referral_bonus_payment_status()
def set_full_name(self):
self.full_name = " ".join(filter(None, [self.first_name, self.last_name]))
def set_referral_bonus_payment_status(self):
if not self.is_applicable_for_referral_bonus:
self.referral_payment_status = ""
else:
if not self.referral_payment_status:
self.referral_payment_status = "Unpaid"
@frappe.whitelist()
def create_job_applicant(source_name, target_doc=None):
emp_ref = frappe.get_doc("Employee Referral", source_name)
#just for Api call if some set status apart from default Status
status = emp_ref.status
if emp_ref.status in ["Pending", "In process"]:
status = "Open"
job_applicant = frappe.new_doc("Job Applicant")
job_applicant.employee_referral = emp_ref.name
job_applicant.status = status
job_applicant.applicant_name = emp_ref.full_name
job_applicant.email_id = emp_ref.email
job_applicant.phone_number = emp_ref.contact_no
job_applicant.resume_attachment = emp_ref.resume
job_applicant.resume_link = emp_ref.resume_link
job_applicant.save()
frappe.msgprint(_("Job Applicant {0} created successfully.").format(
get_link_to_form("Job Applicant", job_applicant.name)),
title=_("Success"), indicator="green")
emp_ref.db_set("status", "In Process")
return job_applicant
@frappe.whitelist()
def create_additional_salary(doc):
import json
from six import string_types
if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc))
if not frappe.db.exists("Additional Salary", {"ref_docname": doc.name}):
additional_salary = frappe.new_doc("Additional Salary")
additional_salary.employee = doc.referrer
additional_salary.company = frappe.db.get_value("Employee", doc.referrer, "company")
additional_salary.overwrite_salary_structure_amount = 0
additional_salary.ref_doctype = doc.doctype
additional_salary.ref_docname = doc.name
return additional_salary

View File

@ -0,0 +1,15 @@
from __future__ import unicode_literals
def get_data():
return {
'fieldname': 'employee_referral',
'non_standard_fieldnames': {
'Additional Salary': 'ref_docname'
},
'transactions': [
{
'items': ['Job Applicant', 'Additional Salary']
},
]
}

View File

@ -0,0 +1,14 @@
frappe.listview_settings['Employee Referral'] = {
add_fields: ["status"],
get_indicator: function (doc) {
if (doc.status == "Pending") {
return [__(doc.status), "grey", "status,=," + doc.status];
} else if (doc.status == "In Process") {
return [__(doc.status), "orange", "status,=," + doc.status];
} else if (doc.status == "Accepted") {
return [__(doc.status), "green", "status,=," + doc.status];
} else if (doc.status == "Rejected") {
return [__(doc.status), "red", "status,=," + doc.status];
}
},
};

View File

@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import today
from erpnext.hr.doctype.designation.test_designation import create_designation
from erpnext.hr.doctype.employee_referral.employee_referral import create_job_applicant, create_additional_salary
from erpnext.hr.doctype.employee.test_employee import make_employee
import unittest
class TestEmployeeReferral(unittest.TestCase):
def test_workflow_and_status_sync(self):
emp_ref = create_employee_referral()
#Check Initial status
self.assertTrue(emp_ref.status, "Pending")
job_applicant = create_job_applicant(emp_ref.name)
#Check status sync
emp_ref.reload()
self.assertTrue(emp_ref.status, "In Process")
job_applicant.reload()
job_applicant.status = "Rejected"
job_applicant.save()
emp_ref.reload()
self.assertTrue(emp_ref.status, "Rejected")
job_applicant.reload()
job_applicant.status = "Accepted"
job_applicant.save()
emp_ref.reload()
self.assertTrue(emp_ref.status, "Accepted")
# Check for Referral reference in additional salary
add_sal = create_additional_salary(emp_ref)
self.assertTrue(add_sal.ref_docname, emp_ref.name)
def create_employee_referral():
emp_ref = frappe.new_doc("Employee Referral")
emp_ref.first_name = "Mahesh"
emp_ref.last_name = "Singh"
emp_ref.email = "a@b.c"
emp_ref.date = today()
emp_ref.for_designation = create_designation().name
emp_ref.referrer = make_employee("testassetmovemp@example.com", company="_Test Company")
emp_ref.is_applicable_for_employee_referral_compensation = 1
emp_ref.save()
emp_ref.submit()
return emp_ref

View File

@ -1,626 +1,177 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "HR-EMP-SEP-.YYYY.-.#####",
"beta": 0,
"creation": "2018-05-10 02:29:16.740490",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"autoname": "HR-EMP-SEP-.YYYY.-.#####",
"creation": "2018-05-10 02:29:16.740490",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"department",
"designation",
"employee_grade",
"column_break_7",
"company",
"boarding_status",
"resignation_letter_date",
"project",
"table_for_activity",
"employee_separation_template",
"activities",
"notify_users_by_email",
"section_break_14",
"exit_interview",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "employee",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee",
"length": 0,
"no_copy": 0,
"options": "Employee",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee Name",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.resignation_letter_date",
"fieldname": "resignation_letter_date",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Resignation Letter Date",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "boarding_status",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nPending\nIn Process\nCompleted",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "employee",
"fieldtype": "Link",
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_bulk_edit": 0,
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_in_quick_entry": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "notify_users_by_email",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Notify users by email",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "employee_separation_template",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Separation Template",
"length": 0,
"no_copy": 0,
"options": "Employee Separation Template",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Employee Name",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "project",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Project",
"length": 0,
"no_copy": 0,
"options": "Project",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fetch_from": "employee.resignation_letter_date",
"fieldname": "resignation_letter_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Resignation Letter Date",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Department",
"length": 0,
"no_copy": 0,
"options": "Department",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"allow_on_submit": 1,
"fieldname": "boarding_status",
"fieldtype": "Select",
"label": "Status",
"options": "\nPending\nIn Process\nCompleted",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.designation",
"fieldname": "designation",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Designation",
"length": 0,
"no_copy": 0,
"options": "Designation",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"allow_on_submit": 1,
"default": "0",
"fieldname": "notify_users_by_email",
"fieldtype": "Check",
"label": "Notify users by email"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.grade",
"fieldname": "employee_grade",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Grade",
"length": 0,
"no_copy": 0,
"options": "Employee Grade",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_7",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "table_for_activity",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "employee_separation_template",
"fieldtype": "Link",
"label": "Employee Separation Template",
"options": "Employee Separation Template"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 1,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "activities",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Activities",
"length": 0,
"no_copy": 0,
"options": "Employee Boarding Activity",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_14",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "exit_interview",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Exit Interview Summary",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Employee Separation",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fetch_from": "employee.designation",
"fieldname": "designation",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fetch_from": "employee.grade",
"fieldname": "employee_grade",
"fieldtype": "Link",
"label": "Employee Grade",
"options": "Employee Grade",
"read_only": 1
},
{
"fieldname": "table_for_activity",
"fieldtype": "Section Break",
"label": "Separation Activities"
},
{
"allow_on_submit": 1,
"fieldname": "activities",
"fieldtype": "Table",
"label": "Activities",
"options": "Employee Boarding Activity"
},
{
"fieldname": "section_break_14",
"fieldtype": "Section Break"
},
{
"fieldname": "exit_interview",
"fieldtype": "Text Editor",
"label": "Exit Interview Summary"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Separation",
"print_hide": 1,
"read_only": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-08-03 16:15:39.025898",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Separation",
"name_case": "",
"owner": "Administrator",
],
"is_submittable": 1,
"links": [],
"modified": "2021-04-28 15:58:36.020196",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Separation",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
}

View File

@ -18,7 +18,7 @@ class TestEmployeeSeparation(unittest.TestCase):
'activity_name': 'Deactivate Employee',
'role': 'HR User'
})
separation.status = 'Pending'
separation.boarding_status = 'Pending'
separation.insert()
separation.submit()
self.assertEqual(separation.docstatus, 1)

View File

@ -16,6 +16,7 @@ class HolidayList(Document):
self.validate_days()
self.total_holidays = len(self.holidays)
@frappe.whitelist()
def get_weekly_off_dates(self):
self.validate_values()
date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
@ -61,6 +62,7 @@ class HolidayList(Document):
return date_list
@frappe.whitelist()
def clear_table(self):
self.set('holidays', [])

View File

@ -10,6 +10,7 @@
"retirement_age",
"emp_created_by",
"column_break_4",
"standard_working_hours",
"stop_birthday_reminders",
"expense_approver_mandatory_in_expense_claim",
"leave_settings",
@ -143,13 +144,18 @@
"fieldname": "send_leave_notification",
"fieldtype": "Check",
"label": "Send Leave Notification"
},
{
"fieldname": "standard_working_hours",
"fieldtype": "Int",
"label": "Standard Working Hours"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-14 02:04:22.907159",
"modified": "2021-04-26 10:52:56.192773",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

@ -18,6 +18,7 @@
"job_title",
"source",
"source_name",
"employee_referral",
"applicant_rating",
"section_break_6",
"notes",
@ -152,13 +153,20 @@
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "employee_referral",
"fieldtype": "Link",
"label": "Employee Referral",
"options": "Employee Referral",
"read_only": 1
}
],
"icon": "fa fa-user",
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-09-18 12:39:02.557563",
"modified": "2021-03-24 15:51:11.117517",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",

View File

@ -28,10 +28,21 @@ class JobApplicant(Document):
if self.email_id:
validate_email_address(self.email_id, True)
if self.employee_referral:
self.set_status_for_employee_referral()
if not self.applicant_name and self.email_id:
guess = self.email_id.split('@')[0]
self.applicant_name = ' '.join([p.capitalize() for p in guess.split('.')])
def set_status_for_employee_referral(self):
emp_ref = frappe.get_doc("Employee Referral", self.employee_referral)
if self.status in ["Open", "Replied", "Hold"]:
emp_ref.db_set("status", "In Process")
elif self.status in ["Accepted", "Rejected"]:
emp_ref.db_set("status", self.status)
def check_email_id_is_unique(self):
if self.email_id:
names = frappe.db.sql_list("""select name from `tabJob Applicant`

View File

@ -29,6 +29,7 @@ class LeaveControlPanel(Document):
frappe.throw(_("{0} is required").format(self.meta.get_label(f)))
self.validate_from_to_dates('from_date', 'to_date')
@frappe.whitelist()
def allocate_leave(self):
self.validate_values()
leave_allocated_for = []

View File

@ -32,13 +32,15 @@ class EmployeeBoardingController(Document):
project_name += self.job_applicant
else:
project_name += self.employee
project = frappe.get_doc({
"doctype": "Project",
"project_name": project_name,
"expected_start_date": self.date_of_joining if self.doctype == "Employee Onboarding" else self.resignation_letter_date,
"department": self.department,
"company": self.company
}).insert(ignore_permissions=True)
}).insert(ignore_permissions=True, ignore_mandatory=True)
self.db_set("project", project.name)
self.db_set("boarding_status", "Pending")
self.reload()

View File

@ -520,6 +520,15 @@
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Referral",
"link_to": "Employee Referral",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
@ -814,7 +823,7 @@
"type": "Link"
}
],
"modified": "2021-03-24 17:35:21.483297",
"modified": "2021-04-26 13:36:15.413819",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",

View File

@ -44,6 +44,7 @@ class Loan(AccountsController):
def on_cancel(self):
self.unlink_loan_security_pledge()
self.ignore_linked_doctypes = ['GL Entry']
def set_missing_fields(self):
if not self.company:
@ -70,7 +71,6 @@ class Loan(AccountsController):
frappe.throw(_("Repay From Salary can be selected only for term loans"))
def make_repayment_schedule(self):
if not self.repayment_start_date:
frappe.throw(_("Repayment Start Date is mandatory for term loans"))
@ -78,10 +78,9 @@ class Loan(AccountsController):
payment_date = self.repayment_start_date
balance_amount = self.loan_amount
while(balance_amount > 0):
interest_amount = rounded(balance_amount * flt(self.rate_of_interest) / (12*100))
interest_amount = flt(balance_amount * flt(self.rate_of_interest) / (12*100))
principal_amount = self.monthly_repayment_amount - interest_amount
balance_amount = rounded(balance_amount + interest_amount - self.monthly_repayment_amount)
balance_amount = flt(balance_amount + interest_amount - self.monthly_repayment_amount)
if balance_amount < 0:
principal_amount += balance_amount
balance_amount = 0.0
@ -195,7 +194,8 @@ def request_loan_closure(loan, posting_date=None):
posting_date = getdate()
amounts = calculate_amounts(loan, posting_date)
pending_amount = amounts['payable_amount'] + amounts['unaccrued_interest']
pending_amount = amounts['pending_principal_amount'] + amounts['unaccrued_interest'] + \
amounts['interest_amount'] + amounts['penalty_amount']
loan_type = frappe.get_value('Loan', loan, 'loan_type')
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
@ -359,4 +359,4 @@ def get_shortfall_applicants():
return {
"value": len(applicants),
"fieldtype": "Int"
}
}

View File

@ -56,25 +56,25 @@ class TestLoan(unittest.TestCase):
def test_loan(self):
loan = frappe.get_doc("Loan", {"applicant":self.applicant1})
self.assertEquals(loan.monthly_repayment_amount, 15052)
self.assertEquals(loan.total_interest_payable, 21034)
self.assertEquals(loan.total_payment, 301034)
self.assertEquals(flt(loan.total_interest_payable, 0), 21034)
self.assertEquals(flt(loan.total_payment, 0), 301034)
schedule = loan.repayment_schedule
self.assertEqual(len(schedule), 20)
for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227079], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
self.assertEqual(schedule[idx].principal_amount, principal_amount)
self.assertEqual(schedule[idx].interest_amount, interest_amount)
self.assertEqual(schedule[idx].balance_loan_amount, balance_loan_amount)
for idx, principal_amount, interest_amount, balance_loan_amount in [[3, 13369, 1683, 227080], [19, 14941, 105, 0], [17, 14740, 312, 29785]]:
self.assertEqual(flt(schedule[idx].principal_amount, 0), principal_amount)
self.assertEqual(flt(schedule[idx].interest_amount, 0), interest_amount)
self.assertEqual(flt(schedule[idx].balance_loan_amount, 0), balance_loan_amount)
loan.repayment_method = "Repay Fixed Amount per Period"
loan.monthly_repayment_amount = 14000
loan.save()
self.assertEquals(len(loan.repayment_schedule), 22)
self.assertEquals(loan.total_interest_payable, 22712)
self.assertEquals(loan.total_payment, 302712)
self.assertEquals(flt(loan.total_interest_payable, 0), 22712)
self.assertEquals(flt(loan.total_payment, 0), 302712)
def test_loan_with_security(self):

View File

@ -435,7 +435,6 @@ def get_amounts(amounts, against_loan, posting_date):
@frappe.whitelist()
def calculate_amounts(against_loan, posting_date, payment_type=''):
amounts = {
'penalty_amount': 0.0,
'interest_amount': 0.0,

View File

@ -773,3 +773,4 @@ erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021
erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting

View File

@ -22,5 +22,7 @@ def execute():
frappe.delete_doc("Page", "bank-reconciliation", force=1)
frappe.reload_doc('accounts', 'doctype', 'bank_transaction')
rename_field("Bank Transaction", "debit", "deposit")
rename_field("Bank Transaction", "credit", "withdrawal")

View File

@ -0,0 +1,8 @@
import frappe
def execute():
"""Remove has_variants and attribute fields from item variant settings."""
frappe.reload_doc("stock", "doctype", "Item Variant Settings")
frappe.db.sql("""delete from `tabVariant Field`
where field_name in ('attributes', 'has_variants')""")

View File

@ -13,12 +13,19 @@ class AdditionalSalary(Document):
if self.ref_doctype == "Employee Advance" and self.ref_docname:
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
self.update_employee_referral()
def on_cancel(self):
self.update_employee_referral(cancel=True)
def validate(self):
self.validate_dates()
self.validate_salary_structure()
self.validate_recurring_additional_salary_overlap()
self.validate_employee_referral()
if self.amount < 0:
frappe.throw(_("Amount should not be less than zero."))
frappe.throw(_("Amount should not be less than zero"))
def validate_salary_structure(self):
if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}):
@ -70,6 +77,27 @@ class AdditionalSalary(Document):
if self.payroll_date and getdate(self.payroll_date) > getdate(relieving_date):
frappe.throw(_("Payroll date can not be greater than employee's relieving date."))
def validate_employee_referral(self):
if self.ref_doctype == "Employee Referral":
referral_details = frappe.db.get_value("Employee Referral", self.ref_docname,
["is_applicable_for_referral_bonus", "status"], as_dict=1)
if not referral_details.is_applicable_for_referral_bonus:
frappe.throw(_("Employee Referral {0} is not applicable for referral bonus.").format(
self.ref_docname))
if self.type == "Deduction":
frappe.throw(_("Earning Salary Component is required for Employee Referral Bonus."))
if referral_details.status != "Accepted":
frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format(
frappe.bold("Accepted")))
def update_employee_referral(self, cancel=False):
if self.ref_doctype == "Employee Referral":
status = "Unpaid" if cancel else "Paid"
frappe.db.set_value("Employee Referral", self.ref_docname, "referral_payment_status", status)
def get_amount(self, sal_start_date, sal_end_date):
start_date = getdate(sal_start_date)
end_date = getdate(sal_end_date)
@ -110,8 +138,7 @@ def get_additional_salaries(employee, start_date, end_date, component_type):
for d in additional_salary_list:
if d.overwrite:
if d.component in components_to_overwrite:
frappe.throw(_("Multiple Additional Salaries with overwrite "
"property exist for Salary Component {0} between {1} and {2}.").format(
frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}.").format(
frappe.bold(d.component), start_date, end_date), title=_("Error"))
components_to_overwrite.append(d.component)

View File

@ -16,11 +16,11 @@ frappe.ui.form.on('Salary Structure', {
onload: function(frm) {
let help_button = $(`<a class = 'control-label'>
Condition and Formula Help
${__("Condition and Formula Help")}
</a>`).click(()=>{
let d = new frappe.ui.Dialog({
title: 'Condition and Formula Help',
title: __('Condition and Formula Help'),
fields: [
{
fieldname: 'msg_wrapper',

View File

@ -179,9 +179,6 @@ class Project(Document):
if self.percent_complete == 100:
self.status = "Completed"
else:
self.status = "Open"
def update_costing(self):
from_time_sheet = frappe.db.sql("""select
sum(costing_amount) as costing_amount,

View File

@ -32,7 +32,8 @@ frappe.ui.form.on("Task", {
frm.set_query("parent_task", function () {
let filters = {
"is_group": 1
"is_group": 1,
"name": ["!=", frm.doc.name]
};
if (frm.doc.project) filters["project"] = frm.doc.project;
return {

View File

@ -151,11 +151,11 @@ class TestTimesheet(unittest.TestCase):
settings.save()
def make_salary_structure_for_timesheet(employee):
def make_salary_structure_for_timesheet(employee, company=None):
salary_structure_name = "Timesheet Salary Structure Test"
frequency = "Monthly"
salary_structure = make_salary_structure(salary_structure_name, frequency, dont_submit=True)
salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
salary_structure.salary_component = "Timesheet Component"
salary_structure.salary_slip_based_on_timesheet = 1
salary_structure.hour_rate = 50.0

View File

@ -0,0 +1,48 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Employee Hours Utilization Based On Timesheet"] = {
"filters": [
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company"),
reqd: 1
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
fieldname:"to_date",
label: __("To Date"),
fieldtype: "Date",
default: frappe.datetime.now_date(),
reqd: 1
},
{
fieldname: "employee",
label: __("Employee"),
fieldtype: "Link",
options: "Employee"
},
{
fieldname: "department",
label: __("Department"),
fieldtype: "Link",
options: "Department"
},
{
fieldname: "project",
label: __("Project"),
fieldtype: "Link",
options: "Project"
}
]
};

View File

@ -0,0 +1,22 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-04-05 19:23:43.838623",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-04-05 19:23:43.838623",
"modified_by": "Administrator",
"module": "Projects",
"name": "Employee Hours Utilization Based On Timesheet",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Timesheet",
"report_name": "Employee Hours Utilization Based On Timesheet",
"report_type": "Script Report",
"roles": []
}

View File

@ -0,0 +1,280 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt, getdate
from six import iteritems
def execute(filters=None):
return EmployeeHoursReport(filters).run()
class EmployeeHoursReport:
'''Employee Hours Utilization Report Based On Timesheet'''
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
self.from_date = getdate(self.filters.from_date)
self.to_date = getdate(self.filters.to_date)
self.validate_dates()
self.validate_standard_working_hours()
def validate_dates(self):
self.day_span = (self.to_date - self.from_date).days
if self.day_span <= 0:
frappe.throw(_('From Date must come before To Date'))
def validate_standard_working_hours(self):
self.standard_working_hours = frappe.db.get_single_value('HR Settings', 'standard_working_hours')
if not self.standard_working_hours:
msg = _('The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.').format(
frappe.bold('Standard Working Hours'), frappe.utils.get_link_to_form('HR Settings', 'HR Settings'))
frappe.throw(msg)
def run(self):
self.generate_columns()
self.generate_data()
self.generate_report_summary()
self.generate_chart_data()
return self.columns, self.data, None, self.chart, self.report_summary
def generate_columns(self):
self.columns = [
{
'label': _('Employee'),
'options': 'Employee',
'fieldname': 'employee',
'fieldtype': 'Link',
'width': 230
},
{
'label': _('Department'),
'options': 'Department',
'fieldname': 'department',
'fieldtype': 'Link',
'width': 120
},
{
'label': _('Total Hours (T)'),
'fieldname': 'total_hours',
'fieldtype': 'Float',
'width': 120
},
{
'label': _('Billed Hours (B)'),
'fieldname': 'billed_hours',
'fieldtype': 'Float',
'width': 170
},
{
'label': _('Non-Billed Hours (NB)'),
'fieldname': 'non_billed_hours',
'fieldtype': 'Float',
'width': 170
},
{
'label': _('Untracked Hours (U)'),
'fieldname': 'untracked_hours',
'fieldtype': 'Float',
'width': 170
},
{
'label': _('% Utilization (B + NB) / T'),
'fieldname': 'per_util',
'fieldtype': 'Percentage',
'width': 200
},
{
'label': _('% Utilization (B / T)'),
'fieldname': 'per_util_billed_only',
'fieldtype': 'Percentage',
'width': 200
}
]
def generate_data(self):
self.generate_filtered_time_logs()
self.generate_stats_by_employee()
self.set_employee_department_and_name()
if self.filters.department:
self.filter_stats_by_department()
self.calculate_utilizations()
self.data = []
for emp, data in iteritems(self.stats_by_employee):
row = frappe._dict()
row['employee'] = emp
row.update(data)
self.data.append(row)
# Sort by descending order of percentage utilization
self.data.sort(key=lambda x: x['per_util'], reverse=True)
def filter_stats_by_department(self):
filtered_data = frappe._dict()
for emp, data in self.stats_by_employee.items():
if data['department'] == self.filters.department:
filtered_data[emp] = data
# Update stats
self.stats_by_employee = filtered_data
def generate_filtered_time_logs(self):
additional_filters = ''
filter_fields = ['employee', 'project', 'company']
for field in filter_fields:
if self.filters.get(field):
if field == 'project':
additional_filters += f"AND ttd.{field} = '{self.filters.get(field)}'"
else:
additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'"
self.filtered_time_logs = frappe.db.sql('''
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project
FROM `tabTimesheet Detail` AS ttd
JOIN `tabTimesheet` AS tt
ON ttd.parent = tt.name
WHERE tt.employee IS NOT NULL
AND tt.start_date BETWEEN '{0}' AND '{1}'
AND tt.end_date BETWEEN '{0}' AND '{1}'
{2}
'''.format(self.filters.from_date, self.filters.to_date, additional_filters))
def generate_stats_by_employee(self):
self.stats_by_employee = frappe._dict()
for emp, hours, billable, project in self.filtered_time_logs:
self.stats_by_employee.setdefault(
emp, frappe._dict()
).setdefault('billed_hours', 0.0)
self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0)
if billable:
self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2)
else:
self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2)
def set_employee_department_and_name(self):
for emp in self.stats_by_employee:
emp_name = frappe.db.get_value(
'Employee', emp, 'employee_name'
)
emp_dept = frappe.db.get_value(
'Employee', emp, 'department'
)
self.stats_by_employee[emp]['department'] = emp_dept
self.stats_by_employee[emp]['employee_name'] = emp_name
def calculate_utilizations(self):
TOTAL_HOURS = flt(self.standard_working_hours * self.day_span, 2)
for emp, data in iteritems(self.stats_by_employee):
data['total_hours'] = TOTAL_HOURS
data['untracked_hours'] = flt(TOTAL_HOURS - data['billed_hours'] - data['non_billed_hours'], 2)
# To handle overtime edge-case
if data['untracked_hours'] < 0:
data['untracked_hours'] = 0.0
data['per_util'] = flt(((data['billed_hours'] + data['non_billed_hours']) / TOTAL_HOURS) * 100, 2)
data['per_util_billed_only'] = flt((data['billed_hours'] / TOTAL_HOURS) * 100, 2)
def generate_report_summary(self):
self.report_summary = []
if not self.data:
return
avg_utilization = 0.0
avg_utilization_billed_only = 0.0
total_billed, total_non_billed = 0.0, 0.0
total_untracked = 0.0
for row in self.data:
avg_utilization += row['per_util']
avg_utilization_billed_only += row['per_util_billed_only']
total_billed += row['billed_hours']
total_non_billed += row['non_billed_hours']
total_untracked += row['untracked_hours']
avg_utilization /= len(self.data)
avg_utilization = flt(avg_utilization, 2)
avg_utilization_billed_only /= len(self.data)
avg_utilization_billed_only = flt(avg_utilization_billed_only, 2)
THRESHOLD_PERCENTAGE = 70.0
self.report_summary = [
{
'value': f'{avg_utilization}%',
'indicator': 'Red' if avg_utilization < THRESHOLD_PERCENTAGE else 'Green',
'label': _('Avg Utilization'),
'datatype': 'Percentage'
},
{
'value': f'{avg_utilization_billed_only}%',
'indicator': 'Red' if avg_utilization_billed_only < THRESHOLD_PERCENTAGE else 'Green',
'label': _('Avg Utilization (Billed Only)'),
'datatype': 'Percentage'
},
{
'value': total_billed,
'label': _('Total Billed Hours'),
'datatype': 'Float'
},
{
'value': total_non_billed,
'label': _('Total Non-Billed Hours'),
'datatype': 'Float'
}
]
def generate_chart_data(self):
self.chart = {}
labels = []
billed_hours = []
non_billed_hours = []
untracked_hours = []
for row in self.data:
labels.append(row.get('employee_name'))
billed_hours.append(row.get('billed_hours'))
non_billed_hours.append(row.get('non_billed_hours'))
untracked_hours.append(row.get('untracked_hours'))
self.chart = {
'data': {
'labels': labels[:30],
'datasets': [
{
'name': _('Billed Hours'),
'values': billed_hours[:30]
},
{
'name': _('Non-Billed Hours'),
'values': non_billed_hours[:30]
},
{
'name': _('Untracked Hours'),
'values': untracked_hours[:30]
}
]
},
'type': 'bar',
'barOptions': {
'stacked': True
}
}

View File

@ -0,0 +1,198 @@
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils.make_random import get_random
from erpnext.projects.report.employee_hours_utilization_based_on_timesheet.employee_hours_utilization_based_on_timesheet import execute
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.project.test_project import make_project
class TestEmployeeUtilization(unittest.TestCase):
@classmethod
def setUpClass(cls):
# Create test employee
cls.test_emp1 = make_employee("test1@employeeutil.com", "_Test Company")
cls.test_emp2 = make_employee("test2@employeeutil.com", "_Test Company")
# Create test project
cls.test_project = make_project({"project_name": "_Test Project"})
# Create test timesheets
cls.create_test_timesheets()
frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 9)
@classmethod
def create_test_timesheets(cls):
timesheet1 = frappe.new_doc("Timesheet")
timesheet1.employee = cls.test_emp1
timesheet1.company = '_Test Company'
timesheet1.append("time_logs", {
"activity_type": get_random("Activity Type"),
"hours": 5,
"billable": 1,
"from_time": '2021-04-01 13:30:00.000000',
"to_time": '2021-04-01 18:30:00.000000'
})
timesheet1.save()
timesheet1.submit()
timesheet2 = frappe.new_doc("Timesheet")
timesheet2.employee = cls.test_emp2
timesheet2.company = '_Test Company'
timesheet2.append("time_logs", {
"activity_type": get_random("Activity Type"),
"hours": 10,
"billable": 0,
"from_time": '2021-04-01 13:30:00.000000',
"to_time": '2021-04-01 23:30:00.000000',
"project": cls.test_project.name
})
timesheet2.save()
timesheet2.submit()
@classmethod
def tearDownClass(cls):
# Delete time logs
frappe.db.sql("""
DELETE FROM `tabTimesheet Detail`
WHERE parent IN (
SELECT name
FROM `tabTimesheet`
WHERE company = '_Test Company'
)
""")
frappe.db.sql("DELETE FROM `tabTimesheet` WHERE company='_Test Company'")
frappe.db.sql(f"DELETE FROM `tabProject` WHERE name='{cls.test_project.name}'")
def test_utilization_report_with_required_filters_only(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03"
}
report = execute(filters)
expected_data = self.get_expected_data_for_test_employees()
self.assertEqual(report[1], expected_data)
def test_utilization_report_for_single_employee(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03",
"employee": self.test_emp1
}
report = execute(filters)
emp1_data = frappe.get_doc('Employee', self.test_emp1)
expected_data = [
{
'employee': self.test_emp1,
'employee_name': 'test1@employeeutil.com',
'billed_hours': 5.0,
'non_billed_hours': 0.0,
'department': emp1_data.department,
'total_hours': 18.0,
'untracked_hours': 13.0,
'per_util': 27.78,
'per_util_billed_only': 27.78
}
]
self.assertEqual(report[1], expected_data)
def test_utilization_report_for_project(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03",
"project": self.test_project.name
}
report = execute(filters)
emp2_data = frappe.get_doc('Employee', self.test_emp2)
expected_data = [
{
'employee': self.test_emp2,
'employee_name': 'test2@employeeutil.com',
'billed_hours': 0.0,
'non_billed_hours': 10.0,
'department': emp2_data.department,
'total_hours': 18.0,
'untracked_hours': 8.0,
'per_util': 55.56,
'per_util_billed_only': 0.0
}
]
self.assertEqual(report[1], expected_data)
def test_utilization_report_for_department(self):
emp1_data = frappe.get_doc('Employee', self.test_emp1)
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03",
"department": emp1_data.department
}
report = execute(filters)
expected_data = self.get_expected_data_for_test_employees()
self.assertEqual(report[1], expected_data)
def test_report_summary_data(self):
filters = {
"company": "_Test Company",
"from_date": "2021-04-01",
"to_date": "2021-04-03"
}
report = execute(filters)
summary = report[4]
expected_summary_values = ['41.67%', '13.89%', 5.0, 10.0]
self.assertEqual(len(summary), 4)
for i in range(4):
self.assertEqual(
summary[i]['value'], expected_summary_values[i]
)
def get_expected_data_for_test_employees(self):
emp1_data = frappe.get_doc('Employee', self.test_emp1)
emp2_data = frappe.get_doc('Employee', self.test_emp2)
return [
{
'employee': self.test_emp2,
'employee_name': 'test2@employeeutil.com',
'billed_hours': 0.0,
'non_billed_hours': 10.0,
'department': emp2_data.department,
'total_hours': 18.0,
'untracked_hours': 8.0,
'per_util': 55.56,
'per_util_billed_only': 0.0
},
{
'employee': self.test_emp1,
'employee_name': 'test1@employeeutil.com',
'billed_hours': 5.0,
'non_billed_hours': 0.0,
'department': emp1_data.department,
'total_hours': 18.0,
'untracked_hours': 13.0,
'per_util': 27.78,
'per_util_billed_only': 27.78
}
]

View File

@ -0,0 +1,48 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Project Profitability"] = {
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname": "start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1)
},
{
"fieldname": "end_date",
"label": __("End Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.now_date()
},
{
"fieldname": "customer_name",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer"
},
{
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "Link",
"options": "Project"
}
]
};

View File

@ -0,0 +1,44 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-04-16 15:50:28.914872",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-04-16 15:50:48.490866",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Profitability",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Timesheet",
"report_name": "Project Profitability",
"report_type": "Script Report",
"roles": [
{
"role": "HR User"
},
{
"role": "Accounts User"
},
{
"role": "Employee"
},
{
"role": "Projects User"
},
{
"role": "Manufacturing User"
},
{
"role": "Employee Self Service"
},
{
"role": "HR Manager"
}
]
}

View File

@ -0,0 +1,210 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
def execute(filters=None):
columns, data = [], []
data = get_data(filters)
columns = get_columns()
charts = get_chart_data(data)
return columns, data, None, charts
def get_data(filters):
data = get_rows(filters)
data = calculate_cost_and_profit(data)
return data
def get_rows(filters):
conditions = get_conditions(filters)
standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours")
if not standard_working_hours:
msg = _("The metrics for this report are calculated based on the Standard Working Hours. Please set {0} in {1}.").format(
frappe.bold("Standard Working Hours"), frappe.utils.get_link_to_form("HR Settings", "HR Settings"))
frappe.msgprint(msg)
return []
sql = """
SELECT
*
FROM
(SELECT
si.customer_name,si.base_grand_total,
si.name as voucher_no,tabTimesheet.employee,
tabTimesheet.title as employee_name,tabTimesheet.parent_project as project,
tabTimesheet.start_date,tabTimesheet.end_date,
tabTimesheet.total_billed_hours,tabTimesheet.name as timesheet,
ss.base_gross_pay,ss.total_working_days,
tabTimesheet.total_billed_hours/(ss.total_working_days * {0}) as utilization
FROM
`tabSalary Slip Timesheet` as sst join `tabTimesheet` on tabTimesheet.name = sst.time_sheet
join `tabSales Invoice Timesheet` as sit on sit.time_sheet = tabTimesheet.name
join `tabSales Invoice` as si on si.name = sit.parent and si.status != "Cancelled"
join `tabSalary Slip` as ss on ss.name = sst.parent and ss.status != "Cancelled" """.format(standard_working_hours)
if conditions:
sql += """
WHERE
{0}) as t""".format(conditions)
return frappe.db.sql(sql,filters, as_dict=True)
def calculate_cost_and_profit(data):
for row in data:
row.fractional_cost = row.base_gross_pay * row.utilization
row.profit = row.base_grand_total - row.base_gross_pay * row.utilization
return data
def get_conditions(filters):
conditions = []
if filters.get("company"):
conditions.append("tabTimesheet.company={0}".format(frappe.db.escape(filters.get("company"))))
if filters.get("start_date"):
conditions.append("tabTimesheet.start_date>='{0}'".format(filters.get("start_date")))
if filters.get("end_date"):
conditions.append("tabTimesheet.end_date<='{0}'".format(filters.get("end_date")))
if filters.get("customer_name"):
conditions.append("si.customer_name={0}".format(frappe.db.escape(filters.get("customer_name"))))
if filters.get("employee"):
conditions.append("tabTimesheet.employee={0}".format(frappe.db.escape(filters.get("employee"))))
if filters.get("project"):
conditions.append("tabTimesheet.parent_project={0}".format(frappe.db.escape(filters.get("project"))))
conditions = " and ".join(conditions)
return conditions
def get_chart_data(data):
if not data:
return None
labels = []
utilization = []
for entry in data:
labels.append(entry.get("employee_name") + " - " + str(entry.get("end_date")))
utilization.append(entry.get("utilization"))
charts = {
"data": {
"labels": labels,
"datasets": [
{
"name": "Utilization",
"values": utilization
}
]
},
"type": "bar",
"colors": ["#84BDD5"]
}
return charts
def get_columns():
return [
{
"fieldname": "customer_name",
"label": _("Customer"),
"fieldtype": "Link",
"options": "Customer",
"width": 150
},
{
"fieldname": "employee",
"label": _("Employee"),
"fieldtype": "Link",
"options": "Employee",
"width": 130
},
{
"fieldname": "employee_name",
"label": _("Employee Name"),
"fieldtype": "Data",
"width": 120
},
{
"fieldname": "voucher_no",
"label": _("Sales Invoice"),
"fieldtype": "Link",
"options": "Sales Invoice",
"width": 120
},
{
"fieldname": "timesheet",
"label": _("Timesheet"),
"fieldtype": "Link",
"options": "Timesheet",
"width": 120
},
{
"fieldname": "project",
"label": _("Project"),
"fieldtype": "Link",
"options": "Project",
"width": 100
},
{
"fieldname": "base_grand_total",
"label": _("Bill Amount"),
"fieldtype": "Currency",
"options": "currency",
"width": 100
},
{
"fieldname": "base_gross_pay",
"label": _("Cost"),
"fieldtype": "Currency",
"options": "currency",
"width": 100
},
{
"fieldname": "profit",
"label": _("Profit"),
"fieldtype": "Currency",
"options": "currency",
"width": 100
},
{
"fieldname": "utilization",
"label": _("Utilization"),
"fieldtype": "Percentage",
"width": 100
},
{
"fieldname": "fractional_cost",
"label": _("Fractional Cost"),
"fieldtype": "Int",
"width": 120
},
{
"fieldname": "total_billed_hours",
"label": _("Total Billed Hours"),
"fieldtype": "Int",
"width": 150
},
{
"fieldname": "start_date",
"label": _("Start Date"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "end_date",
"label": _("End Date"),
"fieldtype": "Date",
"width": 100
},
{
"label": _("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"width": 80
}
]

View File

@ -0,0 +1,58 @@
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import getdate, nowdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import make_salary_structure_for_timesheet, make_timesheet
from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_sales_invoice
from erpnext.projects.report.project_profitability.project_profitability import execute
class TestProjectProfitability(unittest.TestCase):
@classmethod
def setUp(self):
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
make_salary_structure_for_timesheet(emp, company='_Test Company')
self.timesheet = make_timesheet(emp, simulate = True, billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
self.sales_invoice.due_date = nowdate()
self.sales_invoice.submit()
frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 8)
def test_project_profitability(self):
filters = {
'company': '_Test Company',
'start_date': getdate(),
'end_date': getdate()
}
report = execute(filters)
row = report[1][0]
timesheet = frappe.get_doc("Timesheet", self.timesheet.name)
self.assertEqual(self.sales_invoice.customer, row.customer_name)
self.assertEqual(timesheet.title, row.employee_name)
self.assertEqual(self.sales_invoice.base_grand_total, row.base_grand_total)
self.assertEqual(self.salary_slip.base_gross_pay, row.base_gross_pay)
self.assertEqual(timesheet.total_billed_hours, row.total_billed_hours)
self.assertEqual(self.salary_slip.total_working_days, row.total_working_days)
standard_working_hours = frappe.db.get_single_value("HR Settings", "standard_working_hours")
utilization = timesheet.total_billed_hours/(self.salary_slip.total_working_days * standard_working_hours)
self.assertEqual(utilization, row.utilization)
profit = self.sales_invoice.base_grand_total - self.salary_slip.base_gross_pay * utilization
self.assertEqual(profit, row.profit)
fractional_cost = self.salary_slip.base_gross_pay * utilization
self.assertEqual(fractional_cost, row.fractional_cost)
def tearDown(self):
frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel()
frappe.get_doc("Salary Slip", self.salary_slip.name).cancel()
frappe.get_doc("Timesheet", self.timesheet.name).cancel()

View File

@ -131,25 +131,25 @@ def get_report_summary(data):
{
"value": avg_completion,
"indicator": "Green" if avg_completion > 50 else "Red",
"label": "Average Completion",
"label": _("Average Completion"),
"datatype": "Percent",
},
{
"value": total,
"indicator": "Blue",
"label": "Total Tasks",
"label": _("Total Tasks"),
"datatype": "Int",
},
{
"value": completed,
"indicator": "Green",
"label": "Completed Tasks",
"label": _("Completed Tasks"),
"datatype": "Int",
},
{
"value": total_overdue,
"indicator": "Green" if total_overdue == 0 else "Red",
"label": "Overdue Tasks",
"label": _("Overdue Tasks"),
"datatype": "Int",
}
]

View File

@ -130,6 +130,26 @@
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Timesheet",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Hours Utilization",
"link_to": "Employee Hours Utilization Based On Timesheet",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Timesheet, Sales Invoice, Salary Slip",
"hidden": 0,
"is_query_report": 1,
"label": "Project Profitability",
"link_to": "Project Profitability",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Project",
"hidden": 0,
@ -161,7 +181,7 @@
"type": "Link"
}
],
"modified": "2021-03-26 16:32:00.628561",
"modified": "2021-04-25 16:27:16.548780",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects",

View File

@ -562,7 +562,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
weight_uom: item.weight_uom,
manufacturer: item.manufacturer,
stock_uom: item.stock_uom,
pos_profile: me.frm.doc.doctype == 'Sales Invoice' ? me.frm.doc.pos_profile : '',
pos_profile: cint(me.frm.doc.is_pos) ? me.frm.doc.pos_profile : '',
cost_center: item.cost_center,
tax_category: me.frm.doc.tax_category,
item_tax_template: item.item_tax_template,
@ -640,6 +640,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
let key = item.name;
me.apply_rule_on_other_items({key: item});
}
},
() => {
var company_currency = me.get_company_currency();
me.update_item_grid_labels(company_currency);
}
]);
}
@ -1321,11 +1325,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
change_grid_labels: function(company_currency) {
var me = this;
this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount", "base_rate_with_margin"],
company_currency, "items");
this.update_item_grid_labels(company_currency);
this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate", "rate_with_margin"],
this.frm.doc.currency, "items");
this.toggle_item_grid_columns(company_currency);
if(this.frm.fields_dict["operations"]) {
this.frm.set_currency_labels(["operating_cost", "hour_rate"], this.frm.doc.currency, "operations");
@ -1360,6 +1362,39 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.frm.doc.party_account_currency, "advances");
}
this.update_payment_schedule_grid_labels(company_currency);
},
update_item_grid_labels: function(company_currency) {
this.frm.set_currency_labels([
"base_rate", "base_net_rate", "base_price_list_rate",
"base_amount", "base_net_amount", "base_rate_with_margin"
], company_currency, "items");
this.frm.set_currency_labels([
"rate", "net_rate", "price_list_rate", "amount",
"net_amount", "stock_uom_rate", "rate_with_margin"
], this.frm.doc.currency, "items");
},
update_payment_schedule_grid_labels: function(company_currency) {
const me = this;
if (this.frm.fields_dict["payment_schedule"]) {
this.frm.set_currency_labels(["base_payment_amount", "base_outstanding", "base_paid_amount"],
company_currency, "payment_schedule");
this.frm.set_currency_labels(["payment_amount", "outstanding", "paid_amount"],
this.frm.doc.currency, "payment_schedule");
var schedule_grid = this.frm.fields_dict["payment_schedule"].grid;
$.each(["base_payment_amount", "base_outstanding", "base_paid_amount"], function(i, fname) {
if (frappe.meta.get_docfield(schedule_grid.doctype, fname))
schedule_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
});
}
},
toggle_item_grid_columns: function(company_currency) {
const me = this;
// toggle columns
var item_grid = this.frm.fields_dict["items"].grid;
$.each(["base_rate", "base_price_list_rate", "base_amount", "base_rate_with_margin"], function(i, fname) {
@ -1379,9 +1414,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, (show && (me.frm.doc.currency != company_currency)));
});
// set labels
var $wrapper = $(this.frm.wrapper);
},
recalculate: function() {
@ -1995,11 +2027,14 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
terms_template: doc.payment_terms_template,
posting_date: posting_date,
grand_total: doc.rounded_total || doc.grand_total,
base_grand_total: doc.base_rounded_total || doc.base_grand_total,
bill_date: doc.bill_date
},
callback: function(r) {
if(r.message && !r.exc) {
me.frm.set_value("payment_schedule", r.message);
const company_currency = me.get_company_currency();
this.update_payment_schedule_grid_labels(company_currency);
}
}
})
@ -2007,6 +2042,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
},
payment_term: function(doc, cdt, cdn) {
const me = this;
var row = locals[cdt][cdn];
if(row.payment_term) {
frappe.call({
@ -2015,12 +2051,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
term: row.payment_term,
bill_date: this.frm.doc.bill_date,
posting_date: this.frm.doc.posting_date || this.frm.doc.transaction_date,
grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total
grand_total: this.frm.doc.rounded_total || this.frm.doc.grand_total,
base_grand_total: this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total
},
callback: function(r) {
if(r.message && !r.exc) {
for (var d in r.message) {
frappe.model.set_value(cdt, cdn, d, r.message[d]);
const company_currency = me.get_company_currency();
me.update_payment_schedule_grid_labels(company_currency);
}
}
}

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2019-10-15 12:33:21.845329",
"doctype": "DocType",
"editable_grid": 1,
@ -86,12 +87,14 @@
"reqd": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "upload_xml_invoices_section",
"fieldtype": "Section Break",
"label": "Upload XML Invoices"
}
],
"modified": "2020-05-25 21:32:49.064579",
"links": [],
"modified": "2021-04-24 10:33:12.250687",
"modified_by": "Administrator",
"module": "Regional",
"name": "Import Supplier Invoice",

View File

@ -28,14 +28,19 @@ class ImportSupplierInvoice(Document):
self.name = "Import Invoice on " + format_datetime(self.creation)
def import_xml_data(self):
import_file = frappe.get_doc("File", {"file_url": self.zip_file})
zip_file = frappe.get_doc("File", {
"file_url": self.zip_file,
"attached_to_doctype": self.doctype,
"attached_to_name": self.name
})
self.publish("File Import", _("Processing XML Files"), 1, 3)
self.file_count = 0
self.purchase_invoices_count = 0
self.default_uom = frappe.db.get_value("Stock Settings", fieldname="stock_uom")
with zipfile.ZipFile(get_full_path(self.zip_file)) as zf:
with zipfile.ZipFile(zip_file.get_full_path()) as zf:
for file_name in zf.namelist():
content = get_file_content(file_name, zf)
file_content = bs(content, "xml")
@ -124,9 +129,9 @@ class ImportSupplierInvoice(Document):
if disc_line.find("Percentuale"):
invoices_args["total_discount"] += flt((flt(disc_line.Percentuale.text) / 100) * (rate * qty))
@frappe.whitelist()
def process_file_data(self):
self.status = "Processing File Data"
self.save()
self.db_set("status", "Processing File Data", notify=True, commit=True)
frappe.enqueue_doc(self.doctype, self.name, "import_xml_data", queue="long", timeout=3600)
def publish(self, title, message, count, total):
@ -380,24 +385,3 @@ def create_uom(uom):
new_uom.uom_name = uom
new_uom.save()
return new_uom.uom_name
def get_full_path(file_name):
"""Returns file path from given file name"""
file_path = file_name
if "/" not in file_path:
file_path = "/files/" + file_path
if file_path.startswith("/private/files/"):
file_path = get_files_path(*file_path.split("/private/files/", 1)[1].split("/"), is_private=1)
elif file_path.startswith("/files/"):
file_path = get_files_path(*file_path.split("/files/", 1)[1].split("/"))
elif file_path.startswith("http"):
pass
elif not self.file_url:
frappe.throw(_("There is some problem with the file url: {0}").format(file_path))
return file_path

View File

@ -115,17 +115,19 @@ erpnext.setup_einvoice_actions = (doctype) => {
message += '<br><br>';
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
frappe.msgprint({
const dialog = frappe.msgprint({
title: __('Update E-Way Bill Cancelled Status?'),
message: message,
indicator: 'orange',
primary_action: function() {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: { doctype, docname: name },
freeze: true,
callback: () => frm.reload_doc()
});
primary_action: {
action: function() {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
args: { doctype, docname: name },
freeze: true,
callback: () => frm.reload_doc() || dialog.hide()
});
}
},
primary_action_label: __('Yes')
});

View File

@ -339,9 +339,7 @@ def get_eway_bill_details(invoice):
if invoice.is_return:
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'),
title=_('Invalid Fields'))
if not invoice.distance:
frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field'))
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
@ -450,7 +448,7 @@ def make_einvoice(invoice):
if invoice.is_return:
prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter and flt(invoice.distance) and not invoice.is_return:
if invoice.transporter and not invoice.is_return:
eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented
@ -1027,12 +1025,12 @@ def generate_eway_bill(doctype, docname, **kwargs):
gsp_connector.generate_eway_bill(**kwargs)
@frappe.whitelist()
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
def cancel_eway_bill(doctype, docname):
# TODO: uncomment when eway_bill api from Adequare is enabled
# gsp_connector = GSPConnector(doctype, docname)
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
# update cancelled status only, to be able to cancel irn next
frappe.db.set_value(doctype, docname, 'ewaybill', '')
frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1)
@frappe.whitelist()

View File

@ -199,7 +199,7 @@ class Gstr1Report(object):
self.item_tax_rate = frappe._dict()
items = frappe.db.sql("""
select item_code, parent, taxable_value, item_tax_rate
select item_code, parent, taxable_value, base_net_amount, item_tax_rate
from `tab%s Item`
where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@ -207,7 +207,7 @@ class Gstr1Report(object):
for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
sum(i.get('taxable_value', 0) for i in items
sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items
if i.item_code == d.item_code and i.parent == d.parent))
item_tax_rate = {}

View File

@ -12,6 +12,7 @@ from frappe.model.document import Document
from frappe.core.doctype.sms_settings.sms_settings import send_sms
class SMSCenter(Document):
@frappe.whitelist()
def create_receiver_list(self):
rec, where_clause = '', ''
if self.send_to == 'All Customer Contact':
@ -73,6 +74,7 @@ class SMSCenter(Document):
return receiver_nos
@frappe.whitelist()
def send_sms(self):
receiver_list = []
if not self.message:

View File

@ -23,7 +23,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
if search_value:
data = search_serial_or_batch_or_barcode_number(search_value)
item_code = data.get("item_code") if data.get("item_code") else search_value
serial_no = data.get("serial_no") if data.get("serial_no") else ""
batch_no = data.get("batch_no") if data.get("batch_no") else ""
@ -31,7 +31,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
if data:
item_info = frappe.db.get_value(
"Item", data.get("item_code"),
"Item", data.get("item_code"),
["name as item_code", "item_name", "description", "stock_uom", "image as item_image", "is_stock_item"]
, as_dict=1)
item_info.setdefault('serial_no', serial_no)
@ -139,8 +139,24 @@ def get_conditions(item_code, serial_no, batch_no, barcode):
if serial_no or batch_no or barcode:
return "item.name = {0}".format(frappe.db.escape(item_code))
return """(item.name like {item_code}
or item.item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
return make_condition(item_code)
def make_condition(item_code):
condition = "("
condition += """item.name like {item_code}
or item.item_name like {item_code}""".format(item_code = frappe.db.escape('%' + item_code + '%'))
condition += add_search_fields_condition(item_code)
condition += ")"
return condition
def add_search_fields_condition(item_code):
condition = ''
search_fields = frappe.get_all('POS Search Fields', fields = ['fieldname'])
if search_fields:
for field in search_fields:
condition += " or item.{0} like {1}".format(field['fieldname'], frappe.db.escape('%' + item_code + '%'))
return condition
def get_item_group_condition(pos_profile):
cond = "and 1=1"
@ -257,4 +273,4 @@ def set_customer_info(fieldname, customer, value=""):
elif fieldname == 'mobile_no':
contact_doc.set('phone_nos', [{ 'phone': value, 'is_primary_mobile_no': 1}])
frappe.db.set_value('Customer', customer, 'mobile_no', value)
contact_doc.save()
contact_doc.save()

View File

@ -168,6 +168,7 @@ erpnext.PointOfSale.ItemSelector = class {
case (iCode >= 160 && iCode <= 164) || iCode == 170: // ^ ! # $ *
case iCode >= 186 && iCode <= 194: // (; = , - . / `)
case iCode >= 219 && iCode <= 222: // ([ \ ] ')
case iCode == 32: // spacebar
if (oEvent.key !== undefined && oEvent.key !== '') {
return oEvent.key;
}

View File

@ -159,6 +159,7 @@ class NamingSeries(Document):
if frappe.db.get_value('Series', series, 'name', order_by="name") == None:
frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series))
@frappe.whitelist()
def update_series_start(self):
if self.prefix:
prefix = self.parse_naming_series()

View File

@ -46,9 +46,6 @@ frappe.ui.form.on("Item", {
}, __("View"));
}
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
if (frm.doc.is_fixed_asset) {
frm.trigger('is_fixed_asset');
@ -96,6 +93,10 @@ frappe.ui.form.on("Item", {
erpnext.item.edit_prices_button(frm);
erpnext.item.toggle_attributes(frm);
if (!frm.doc.is_fixed_asset) {
erpnext.item.make_dashboard(frm);
}
frm.add_custom_button(__('Duplicate'), function() {
var new_item = frappe.model.copy_doc(frm.doc);
@ -473,11 +474,15 @@ $.extend(erpnext.item, {
me.multiple_variant_dialog.get_primary_btn().html(__('Create Variants'));
me.multiple_variant_dialog.disable_primary_action();
} else {
let no_of_combinations = lengths.reduce((a, b) => a * b, 1);
me.multiple_variant_dialog.get_primary_btn()
.html(__(
`Make ${no_of_combinations} Variant${no_of_combinations === 1 ? '' : 's'}`
));
let msg;
if (no_of_combinations === 1) {
msg = __("Make {0} Variant", [no_of_combinations]);
} else {
msg = __("Make {0} Variants", [no_of_combinations]);
}
me.multiple_variant_dialog.get_primary_btn().html(msg);
me.multiple_variant_dialog.enable_primary_action();
}
}

View File

@ -63,7 +63,7 @@ class Item(WebsiteGenerator):
if self.variant_of:
if not self.item_code:
template_item_name = frappe.db.get_value("Item", self.variant_of, "item_name")
self.item_code = make_variant_item_code(self.variant_of, template_item_name, self)
make_variant_item_code(self.variant_of, template_item_name, self)
else:
from frappe.model.naming import set_name_by_naming_series
set_name_by_naming_series(self)
@ -674,10 +674,10 @@ class Item(WebsiteGenerator):
if not records: return
document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations")
msg = _("The items {0} and {1} are present in the following {2} : ").format(
msg = _("The items {0} and {1} are present in the following {2} :").format(
frappe.bold(old_name), frappe.bold(new_name), document)
msg += '<br>'
msg += ' <br>'
msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(

View File

@ -13,10 +13,11 @@ class ItemVariantSettings(Document):
def set_default_fields(self):
self.fields = []
fields = frappe.get_meta('Item').fields
exclude_fields = ["naming_series", "item_code", "item_name", "show_in_website",
exclude_fields = {"naming_series", "item_code", "item_name", "show_in_website",
"show_variant_in_website", "standard_rate", "opening_stock", "image", "description",
"variant_of", "valuation_rate", "description", "barcodes",
"website_image", "thumbnail", "website_specifiations", "web_long_description"]
"website_image", "thumbnail", "website_specifiations", "web_long_description",
"has_variants", "attributes"}
for d in fields:
if not d.no_copy and d.fieldname not in exclude_fields and \

View File

@ -433,13 +433,21 @@ erpnext.buying.MaterialRequestController = erpnext.buying.BuyingController.exten
if (doc.material_request_type == "Customer Provided") {
return{
query: "erpnext.controllers.queries.item_query",
filters:{ 'customer': me.frm.doc.customer }
filters:{
'customer': me.frm.doc.customer,
'is_stock_item':1
}
}
} else if (doc.material_request_type != "Manufacture") {
} else if (doc.material_request_type == "Purchase") {
return{
query: "erpnext.controllers.queries.item_query",
filters: {'is_purchase_item': 1}
}
} else {
return{
query: "erpnext.controllers.queries.item_query",
filters: {'is_stock_item': 1}
}
}
});
},

View File

@ -73,6 +73,34 @@ frappe.ui.form.on("Purchase Receipt", {
})
}, __('Create'));
}
frm.events.add_custom_buttons(frm);
},
add_custom_buttons: function(frm) {
if (frm.doc.docstatus == 0) {
frm.add_custom_button(__('Purchase Invoice'), function () {
if (!frm.doc.supplier) {
frappe.throw({
title: __("Mandatory"),
message: __("Please Select a Supplier")
});
}
erpnext.utils.map_current_doc({
method: "erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
source_doctype: "Purchase Invoice",
target: frm,
setters: {
supplier: frm.doc.supplier,
},
get_query_filters: {
docstatus: 1,
per_received: ["<", 100],
company: frm.doc.company
}
})
}, __("Get Items From"));
}
},
company: function(frm) {

View File

@ -53,7 +53,20 @@ class PurchaseReceipt(BuyingController):
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty',
'percent_join_field': 'material_request'
},
{
'source_dt': 'Purchase Receipt Item',
'target_dt': 'Purchase Invoice Item',
'join_field': 'purchase_invoice_item',
'target_field': 'received_qty',
'target_parent_dt': 'Purchase Invoice',
'target_parent_field': 'per_received',
'target_ref_field': 'qty',
'source_field': 'received_qty',
'percent_join_field': 'purchase_invoice',
'overflow_type': 'receipt'
}]
if cint(self.is_return):
self.status_updater.extend([
{
@ -221,6 +234,7 @@ class PurchaseReceipt(BuyingController):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches()
@frappe.whitelist()
def get_current_stock(self):
for d in self.get('supplied_items'):
if self.supplier_warehouse:
@ -513,7 +527,9 @@ class PurchaseReceipt(BuyingController):
def update_billing_status(self, update_modified=True):
updated_pr = [self.name]
for d in self.get("items"):
if d.purchase_order_item:
if d.purchase_invoice and d.purchase_invoice_item:
d.db_set('billed_amt', d.amount, update_modified=update_modified)
elif d.purchase_order_item:
updated_pr += update_billed_amount_based_on_po(d.purchase_order_item, update_modified)
for pr in set(updated_pr):

View File

@ -72,16 +72,18 @@
"warehouse",
"rejected_warehouse",
"from_warehouse",
"purchase_order",
"material_request",
"purchase_order",
"purchase_invoice",
"column_break_40",
"is_fixed_asset",
"asset_location",
"asset_category",
"schedule_date",
"quality_inspection",
"purchase_order_item",
"material_request_item",
"purchase_order_item",
"purchase_invoice_item",
"purchase_receipt_item",
"delivery_note_item",
"putaway_rule",
@ -937,7 +939,21 @@
"fieldname": "base_rate_with_margin",
"fieldtype": "Currency",
"label": "Rate With Margin (Company Currency)",
"options": "Company:company:default_currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"label": "Purchase Invoice",
"options": "Purchase Invoice",
"read_only": 1
},
{
"fieldname": "purchase_invoice_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}
@ -945,7 +961,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-02-23 00:59:14.360847",
"modified": "2021-03-29 04:17:00.336298",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@ -398,8 +398,12 @@ class StockEntry(StockController):
and item_code = %s
and ifnull(s_warehouse,'')='' """ % (", ".join(["%s" * len(other_ste)]), "%s"), args)[0][0]
if fg_qty_already_entered and fg_qty_already_entered >= qty:
frappe.throw(_("Stock Entries already created for Work Order ")
+ self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError)
frappe.throw(
_("Stock Entries already created for Work Order {0}: {1}").format(
self.work_order, ", ".join(other_ste)
),
DuplicateEntryForWorkOrderError,
)
def set_actual_qty(self):
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
@ -435,6 +439,7 @@ class StockEntry(StockController):
if transferred_serial_no:
d.serial_no = transferred_serial_no
@frappe.whitelist()
def get_stock_and_rate(self):
"""
Updates rate and availability of all the items.

View File

@ -469,7 +469,7 @@ class StockReconciliation(StockController):
def submit(self):
if len(self.items) > 100:
msgprint(_("The task has been enqueued as a background job. In case there is any issue on processing in background, the system will add a comment about the error on this Stock Reconciliation and revert to the Draft stage"))
self.queue_action('submit')
self.queue_action('submit', timeout=2000)
else:
self._submit()

View File

@ -86,7 +86,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
elif out.get("warehouse"):
out.update(get_bin_details(args.item_code, out.warehouse))
out.update(get_bin_details(args.item_code, out.warehouse, args.company))
# update args with out, if key or value not exists
for key, value in iteritems(out):

View File

@ -416,7 +416,7 @@ class update_entries_after(object):
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
# Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no)
stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no, for_update=True)
stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
stock_entry.db_update()
for d in stock_entry.items:

View File

@ -172,7 +172,7 @@ def get_bin(item_code, warehouse):
bin_obj.flags.ignore_permissions = 1
bin_obj.insert()
else:
bin_obj = frappe.get_cached_doc('Bin', bin)
bin_obj = frappe.get_doc('Bin', bin, for_update=True)
bin_obj.flags.ignore_permissions = True
return bin_obj

View File

@ -12,7 +12,7 @@
"url": "https://github.com/frappe/erpnext/issues"
},
"devDependencies": {
"snyk": "^1.290.1"
"snyk": "^1.518.0"
},
"dependencies": {
"onscan.js": "^1.5.2"

View File

@ -1,4 +1,4 @@
frappe
# frappe # https://github.com/frappe/frappe is installed during bench-init
gocardless-pro~=1.22.0
googlemaps # used in ERPNext, but dependency is defined in Frappe
pandas~=1.1.5

3606
yarn.lock

File diff suppressed because it is too large Load Diff