Merge branch 'develop' of https://github.com/frappe/erpnext into round-off-diff-allowance
This commit is contained in:
commit
3f53b87379
4
.github/workflows/ci-tests.yml
vendored
4
.github/workflows/ci-tests.yml
vendored
@ -85,8 +85,8 @@ jobs:
|
||||
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 }}
|
||||
|
@ -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.
|
||||
|
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.0.0-dev'
|
||||
__version__ = '13.1.1'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -78,8 +78,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
if (
|
||||
frm.doc.bank_account &&
|
||||
frm.doc.bank_statement_from_date &&
|
||||
frm.doc.bank_statement_to_date &&
|
||||
frm.doc.bank_statement_closing_balance
|
||||
frm.doc.bank_statement_to_date
|
||||
) {
|
||||
frm.trigger("render_chart");
|
||||
frm.trigger("render");
|
||||
|
@ -39,13 +39,13 @@
|
||||
"depends_on": "eval: doc.bank_account",
|
||||
"fieldname": "bank_statement_from_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Bank Statement From Date"
|
||||
"label": "From Date"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bank_statement_from_date",
|
||||
"fieldname": "bank_statement_to_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Bank Statement To Date"
|
||||
"label": "To Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_2",
|
||||
@ -63,11 +63,10 @@
|
||||
"depends_on": "eval: doc.bank_statement_to_date",
|
||||
"fieldname": "bank_statement_closing_balance",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Bank Statement Closing Balance",
|
||||
"label": "Closing Balance",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.bank_statement_closing_balance",
|
||||
"fieldname": "section_break_1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reconcile"
|
||||
@ -90,7 +89,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-02 01:35:53.043578",
|
||||
"modified": "2021-04-21 11:13:49.831769",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Reconciliation Tool",
|
||||
|
@ -21,21 +21,17 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
|
||||
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.docstatus==1) {
|
||||
frappe.db.get_value("Journal Entry Account", {
|
||||
'reference_type': 'Exchange Rate Revaluation',
|
||||
'reference_name': frm.doc.name,
|
||||
'docstatus': 1
|
||||
}, "sum(debit) as sum", (r) =>{
|
||||
let total_amt = 0;
|
||||
frm.doc.accounts.forEach(d=> {
|
||||
total_amt = total_amt + d['new_balance_in_base_currency'];
|
||||
});
|
||||
if(total_amt !== r.sum) {
|
||||
frappe.call({
|
||||
method: 'check_journal_entry_condition',
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.add_custom_button(__('Journal Entry'), function() {
|
||||
return frm.events.make_jv(frm);
|
||||
}, __('Create'));
|
||||
}
|
||||
}, 'Journal Entry');
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -27,6 +27,23 @@ class ExchangeRateRevaluation(Document):
|
||||
if not (self.company and self.posting_date):
|
||||
frappe.throw(_("Please select Company and Posting Date to getting entries"))
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_journal_entry_condition(self):
|
||||
total_debit = frappe.db.get_value("Journal Entry Account", {
|
||||
'reference_type': 'Exchange Rate Revaluation',
|
||||
'reference_name': self.name,
|
||||
'docstatus': 1
|
||||
}, "sum(debit) as sum")
|
||||
|
||||
total_amt = 0
|
||||
for d in self.accounts:
|
||||
total_amt = total_amt + d.new_balance_in_base_currency
|
||||
|
||||
if total_amt != total_debit:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_accounts_data(self, account=None):
|
||||
accounts = []
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
},
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -16,28 +16,8 @@ class POSClosingEntry(StatusUpdater):
|
||||
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
|
||||
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
|
||||
|
||||
self.validate_pos_closing()
|
||||
self.validate_pos_invoices()
|
||||
|
||||
def validate_pos_closing(self):
|
||||
user = frappe.db.sql("""
|
||||
SELECT name FROM `tabPOS Closing Entry`
|
||||
WHERE
|
||||
user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND
|
||||
(period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s)
|
||||
""", {
|
||||
'user': self.user,
|
||||
'profile': self.pos_profile,
|
||||
'start': self.period_start_date,
|
||||
'end': self.period_end_date
|
||||
})
|
||||
|
||||
if user:
|
||||
bold_already_exists = frappe.bold(_("already exists"))
|
||||
bold_user = frappe.bold(self.user)
|
||||
frappe.throw(_("POS Closing Entry {} against {} between selected period")
|
||||
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
|
||||
|
||||
def validate_pos_invoices(self):
|
||||
invalid_rows = []
|
||||
for d in self.pos_transactions:
|
||||
|
@ -96,30 +96,45 @@ class POSInvoice(SalesInvoice):
|
||||
if paid_amt and pay.amount != paid_amt:
|
||||
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
def validate_pos_reserved_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
filters = {"item_code": item.item_code, "warehouse": item.warehouse}
|
||||
if item.batch_no:
|
||||
filters["batch_no"] = item.batch_no
|
||||
|
||||
allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock')
|
||||
error_msg = []
|
||||
for d in self.get('items'):
|
||||
msg = ""
|
||||
if d.serial_no:
|
||||
filters = { "item_code": d.item_code, "warehouse": d.warehouse }
|
||||
if d.batch_no:
|
||||
filters["batch_no"] = d.batch_no
|
||||
reserved_serial_nos = get_pos_reserved_serial_nos(filters)
|
||||
serial_nos = get_serial_nos(d.serial_no)
|
||||
invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos]
|
||||
|
||||
bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos))
|
||||
if len(invalid_serial_nos) == 1:
|
||||
msg = (_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(d.idx, bold_invalid_serial_nos))
|
||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
elif invalid_serial_nos:
|
||||
msg = (_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(d.idx, bold_invalid_serial_nos))
|
||||
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
|
||||
|
||||
def validate_delivered_serial_nos(self, item):
|
||||
serial_nos = get_serial_nos(item.serial_no)
|
||||
delivered_serial_nos = frappe.db.get_list('Serial No', {
|
||||
'item_code': item.item_code,
|
||||
'name': ['in', serial_nos],
|
||||
'sales_invoice': ['is', 'set']
|
||||
}, pluck='name')
|
||||
|
||||
if delivered_serial_nos:
|
||||
bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos))
|
||||
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
|
||||
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
|
||||
|
||||
def validate_stock_availablility(self):
|
||||
if self.is_return:
|
||||
return
|
||||
|
||||
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
|
||||
for d in self.get('items'):
|
||||
if d.serial_no:
|
||||
self.validate_pos_reserved_serial_nos(d)
|
||||
self.validate_delivered_serial_nos(d)
|
||||
else:
|
||||
if allow_negative_stock:
|
||||
return
|
||||
@ -127,15 +142,11 @@ class POSInvoice(SalesInvoice):
|
||||
available_stock = get_stock_availability(d.item_code, d.warehouse)
|
||||
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
|
||||
if flt(available_stock) <= 0:
|
||||
msg = (_('Row #{}: Item Code: {} is not available under warehouse {}.').format(d.idx, item_code, warehouse))
|
||||
frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.')
|
||||
.format(d.idx, item_code, warehouse), title=_("Item Unavailable"))
|
||||
elif flt(available_stock) < flt(d.qty):
|
||||
msg = (_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
|
||||
.format(d.idx, item_code, warehouse, qty))
|
||||
if msg:
|
||||
error_msg.append(msg)
|
||||
|
||||
if error_msg:
|
||||
frappe.throw(error_msg, title=_("Item Unavailable"), as_list=True)
|
||||
frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.')
|
||||
.format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable"))
|
||||
|
||||
def validate_serialised_or_batched_item(self):
|
||||
error_msg = []
|
||||
@ -202,9 +213,8 @@ class POSInvoice(SalesInvoice):
|
||||
for d in self.get("items"):
|
||||
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
|
||||
if not is_stock_item:
|
||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format(
|
||||
d.idx, frappe.bold(d.item_code)
|
||||
), title=_("Invalid Item"))
|
||||
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ")
|
||||
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
|
||||
|
||||
def validate_mode_of_payment(self):
|
||||
if len(self.payments) == 0:
|
||||
|
@ -10,10 +10,12 @@ from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
class TestPOSInvoice(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item", qty=800, basic_rate=100)
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def tearDown(self):
|
||||
@ -320,6 +322,34 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos2.insert)
|
||||
|
||||
def test_delivered_serialized_item_transaction(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
||||
se = make_serialized_item(company='_Test Company',
|
||||
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
|
||||
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
|
||||
si = create_sales_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||
|
||||
si.get("items")[0].serial_no = serial_nos[0]
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
pos2 = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
|
||||
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
|
||||
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
|
||||
item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
|
||||
|
||||
pos2.get("items")[0].serial_no = serial_nos[0]
|
||||
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||
|
||||
self.assertRaises(frappe.ValidationError, pos2.insert)
|
||||
|
||||
def test_loyalty_points(self):
|
||||
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
|
||||
|
@ -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
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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
|
@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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(aging.range1, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(aging.range2, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(aging.range3, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(aging.range4, currency=filters.presentation_currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -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
|
||||
@ -58,22 +60,24 @@ def get_report_pdf(doc, consolidated=True):
|
||||
aging[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)
|
||||
@ -167,7 +171,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 +203,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 ''
|
||||
|
@ -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 ...")
|
||||
})
|
||||
}
|
||||
})
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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'
|
||||
}
|
||||
]
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -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)
|
||||
|
||||
|
129
erpnext/change_log/v13/v13_1_0.md
Normal file
129
erpnext/change_log/v13/v13_1_0.md
Normal file
@ -0,0 +1,129 @@
|
||||
# Version 13.1.0 Release Notes
|
||||
|
||||
### Features
|
||||
|
||||
- Recursive pricing rule ([#24922](https://github.com/frappe/erpnext/pull/24922))
|
||||
- Discount configuration on early payments ([#24586](https://github.com/frappe/erpnext/pull/24586))
|
||||
- Bulk e-invoice generation ([#24969](https://github.com/frappe/erpnext/pull/24969))
|
||||
- Employee Self Service ([#24408](https://github.com/frappe/erpnext/pull/24408))
|
||||
- Share doc with employee approvers if they don't have access ([#25190](https://github.com/frappe/erpnext/pull/25190))
|
||||
- Price margin in buying ([#24685](https://github.com/frappe/erpnext/pull/24685))
|
||||
- Allow changing Work Stations in Work Order & Job Card ([#24897](https://github.com/frappe/erpnext/pull/24897))
|
||||
- Add document type field for e-invoicing (Italy) ([#25256](https://github.com/frappe/erpnext/pull/25256))
|
||||
- Add checkbox for disabling leave notification in HR Settings ([#24877](https://github.com/frappe/erpnext/pull/24877))
|
||||
- Enhancements in Material Request Plan Item in Production Plan ([#25025](https://github.com/frappe/erpnext/pull/25025))
|
||||
|
||||
|
||||
### Fixes and Enhancements
|
||||
- Mode of payments disappear on loading draft pos invoice ([#24917](https://github.com/frappe/erpnext/pull/24917))
|
||||
- Sales order not saving due type mismatch in promo scheme (#24748) ([#25222](https://github.com/frappe/erpnext/pull/25222))
|
||||
- Zero amount completed delivery notes being shown in Sales Invoice get items ([#25317](https://github.com/frappe/erpnext/pull/25317))
|
||||
- Incorrect status creating PR from PO after creating PI ([#25109](https://github.com/frappe/erpnext/pull/25109))
|
||||
- Precision and formatted document for stock level in item dashboard. ([#24921](https://github.com/frappe/erpnext/pull/24921))
|
||||
- Precision issues while allocating advance amount ([#25086](https://github.com/frappe/erpnext/pull/25086))
|
||||
- Round off final tax amount instead of current tax amount ([#25188](https://github.com/frappe/erpnext/pull/25188))
|
||||
- Redesign fixes ([#24896](https://github.com/frappe/erpnext/pull/24896))
|
||||
- TDS check getting checked after reload ([#24972](https://github.com/frappe/erpnext/pull/24972))
|
||||
- Github Action not failing when tests fail ([#24867](https://github.com/frappe/erpnext/pull/24867))
|
||||
- Calculate 80g certificate amount on validate for memberships ([#24925](https://github.com/frappe/erpnext/pull/24925))
|
||||
- Purchase from registered composition dealer ([#25040](https://github.com/frappe/erpnext/pull/25040))
|
||||
- Reduce number of queries for checking if future SL entry exists ([#24881](https://github.com/frappe/erpnext/pull/24881))
|
||||
- Remove unwanted parameter in calculate_rate_and_amount ([#24883](https://github.com/frappe/erpnext/pull/24883))
|
||||
- Membership renewal validation ([#24963](https://github.com/frappe/erpnext/pull/24963))
|
||||
- Not able to save material request ([#25112](https://github.com/frappe/erpnext/pull/25112))
|
||||
- POS print receipt ([#25330](https://github.com/frappe/erpnext/pull/25330))
|
||||
- Supplier was not able to Submit RFQ due to insufficient permission ([#24622](https://github.com/frappe/erpnext/pull/24622))
|
||||
- Unequal debit and credit issue on RCM Invoice ([#24836](https://github.com/frappe/erpnext/pull/24836))
|
||||
- Picked Qty conversion from Stock Qty to Qty while creating DN from Pick List ([#25105](https://github.com/frappe/erpnext/pull/25105))
|
||||
- Salary Structure object has no attribute set_totals ([#25113](https://github.com/frappe/erpnext/pull/25113))
|
||||
- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24916](https://github.com/frappe/erpnext/pull/24916))
|
||||
- Add method for regional round off account back ([#24893](https://github.com/frappe/erpnext/pull/24893))
|
||||
- Employee profile pic upload access for erpnext user ([#25022](https://github.com/frappe/erpnext/pull/25022))
|
||||
- Make filters for payroll entry ([#25386](https://github.com/frappe/erpnext/pull/25386))
|
||||
- Fix dynamically changing grid properties ([#25310](https://github.com/frappe/erpnext/pull/25310))
|
||||
- Consider paid repayment entries in subsequent loan repayments ([#25271](https://github.com/frappe/erpnext/pull/25271))
|
||||
- Allow duplicate additional salaries ([#24842](https://github.com/frappe/erpnext/pull/24842))
|
||||
- Object referencing the same address issue ([#25159](https://github.com/frappe/erpnext/pull/25159))
|
||||
- Validating party currency with doc currency ([#24318](https://github.com/frappe/erpnext/pull/24318))
|
||||
- Non Profit fixes ([#25060](https://github.com/frappe/erpnext/pull/25060))
|
||||
- Additional Salary component amount not getting set ([#25356](https://github.com/frappe/erpnext/pull/25356))
|
||||
- Allow user to update exchange rate in Multi-currency LCV ([#24912](https://github.com/frappe/erpnext/pull/24912))
|
||||
- Allow creating stock entry based on work order for customer provided items ([#24885](https://github.com/frappe/erpnext/pull/24885))
|
||||
- Create property setters for shorter naming series on setup ([#25128](https://github.com/frappe/erpnext/pull/25128))
|
||||
- Add GST category field in Delivery Note ([#25053](https://github.com/frappe/erpnext/pull/25053))
|
||||
- Ignore Permission for Leave Ledger Entry ([#25172](https://github.com/frappe/erpnext/pull/25172))
|
||||
- Pending shortfall update on processing loan security shortfall ([#24971](https://github.com/frappe/erpnext/pull/24971))
|
||||
- Added flag for dont_fetch_price_list_rate in transaction ([#25041](https://github.com/frappe/erpnext/pull/25041))
|
||||
- Exchange Rate not getting set in Salary Slip ([#25004](https://github.com/frappe/erpnext/pull/25004))
|
||||
- Repost not completed backdated transactions ([#24980](https://github.com/frappe/erpnext/pull/24980))
|
||||
- frappe.whitelist for doc methods ([#25230](https://github.com/frappe/erpnext/pull/25230))
|
||||
- Opportunity-quotation mapping order status ([#25001](https://github.com/frappe/erpnext/pull/25001))
|
||||
- GST on freight charge in e-invoicing ([#25000](https://github.com/frappe/erpnext/pull/25000))
|
||||
- Role to override maintain same rate check in transactions ([#25193](https://github.com/frappe/erpnext/pull/25193))
|
||||
- Added blank option for status in report related to issue ([#25082](https://github.com/frappe/erpnext/pull/25082))
|
||||
- Cashier query in POS Opening/Closing Entry ([#25399](https://github.com/frappe/erpnext/pull/25399))
|
||||
- Lead Source's module ([#24583](https://github.com/frappe/erpnext/pull/24583))
|
||||
- Hide alt tag if item is not shown in website ([#24937](https://github.com/frappe/erpnext/pull/24937))
|
||||
- Ignore Customer Group Perm on All Products page ([#25397](https://github.com/frappe/erpnext/pull/25397))
|
||||
- Give first preference to loan security on repayment ([#25212](https://github.com/frappe/erpnext/pull/25212))
|
||||
- Add shortfall ratio in Loan Security Shortfall ([#25138](https://github.com/frappe/erpnext/pull/25138))
|
||||
- Condition for SLA status banner ([#25261](https://github.com/frappe/erpnext/pull/25261))
|
||||
- Component amount calculation based on formula with abbr not working ([#25117](https://github.com/frappe/erpnext/pull/25117))
|
||||
- Remove gst name validation for purchase Invoice ([#25235](https://github.com/frappe/erpnext/pull/25235))
|
||||
- Do not fetch stopped MR in production plan ([#25063](https://github.com/frappe/erpnext/pull/25063))
|
||||
- Backport missing commits to develop branch ([#25305](https://github.com/frappe/erpnext/pull/25305))
|
||||
- UOM length unit in global setup list is empty ([#24855](https://github.com/frappe/erpnext/pull/24855))
|
||||
- Round total quantity in job card ([#25240](https://github.com/frappe/erpnext/pull/25240))
|
||||
- Default total_estimated_cost to zero ([#24939](https://github.com/frappe/erpnext/pull/24939))
|
||||
- Serial no refresh issue ([#25127](https://github.com/frappe/erpnext/pull/25127))
|
||||
- Correct calculation for discount amount when margin is set ([#25179](https://github.com/frappe/erpnext/pull/25179))
|
||||
- Get correct holiday list when calculating dates; test fixes ([#24901](https://github.com/frappe/erpnext/pull/24901))
|
||||
- POS print receipt ([#24924](https://github.com/frappe/erpnext/pull/24924))
|
||||
- Condition for setting agreement status ([#25255](https://github.com/frappe/erpnext/pull/25255))
|
||||
- Loan Repayment entry cancellation on salary slip cancel ([#24879](https://github.com/frappe/erpnext/pull/24879))
|
||||
- Add company validation for e-invoicing ([#25349](https://github.com/frappe/erpnext/pull/25349))
|
||||
- Query values incorrectly escaped while back updating Quality Inspection ([#25118](https://github.com/frappe/erpnext/pull/25118))
|
||||
- Update Bin via Update Item on Purchase/Sales Order ([#23509](https://github.com/frappe/erpnext/pull/23509))
|
||||
- Declare data before assigning ([#25287](https://github.com/frappe/erpnext/pull/25287))
|
||||
- Do not set standard link in Sales Invoice as custom ([#25096](https://github.com/frappe/erpnext/pull/25096))
|
||||
- Hide serial and batch selector in Stock Entry ([#25107](https://github.com/frappe/erpnext/pull/25107))
|
||||
- Taxable value including Freight and Forwarding charges in GSTR-1 Report ([#25290](https://github.com/frappe/erpnext/pull/25290))
|
||||
- Remove nonexistent method from pick list ([#25279](https://github.com/frappe/erpnext/pull/25279))
|
||||
- Allow zero valuation in stock reconciliation ([#24888](https://github.com/frappe/erpnext/pull/24888))
|
||||
- Place of supply of e-invoicing ([#25148](https://github.com/frappe/erpnext/pull/25148))
|
||||
- Delivery note print error ([#25080](https://github.com/frappe/erpnext/pull/25080))
|
||||
- Fix Payment references from disappearing on adding Cost Center in Payment Entry ([#24831](https://github.com/frappe/erpnext/pull/24831))
|
||||
- Company field in Warehouse ([#25196](https://github.com/frappe/erpnext/pull/25196))
|
||||
- Available employee for selection ([#25378](https://github.com/frappe/erpnext/pull/25378))
|
||||
- Cannot set qty to less than zero ([#25258](https://github.com/frappe/erpnext/pull/25258))
|
||||
- Don't delete mode of payment account details while deleting comp… ([#25217](https://github.com/frappe/erpnext/pull/25217))
|
||||
- Exclude current doc while validation. ([#24914](https://github.com/frappe/erpnext/pull/24914))
|
||||
- POS Opening Entry with empty balance detail rows ([#24876](https://github.com/frappe/erpnext/pull/24876))
|
||||
- Unable to submit stock entry ([#25033](https://github.com/frappe/erpnext/pull/25033))
|
||||
- BOM cost test case ([#25242](https://github.com/frappe/erpnext/pull/25242))
|
||||
- Filter for employees in salary slip ([#25361](https://github.com/frappe/erpnext/pull/25361))
|
||||
- Added correct path in hooks ([#24862](https://github.com/frappe/erpnext/pull/24862))
|
||||
- Patch regional fields for old companies ([#24988](https://github.com/frappe/erpnext/pull/24988))
|
||||
- consolidated sales invoice posting date ([#25119](https://github.com/frappe/erpnext/pull/25119))
|
||||
- Don't set "Company:company:default_currency" as default for currency link fields ([#25095](https://github.com/frappe/erpnext/pull/25095))
|
||||
- Healthcare lab module rename fields ([#25276](https://github.com/frappe/erpnext/pull/25276))
|
||||
- Error message compensatory leave request ([#25206](https://github.com/frappe/erpnext/pull/25206))
|
||||
- Adding company link to e invoice settings patch condition ([#25301](https://github.com/frappe/erpnext/pull/25301))
|
||||
- Membership and Donation API fixes ([#24900](https://github.com/frappe/erpnext/pull/24900))
|
||||
- Set correct ack no. on irn generation ([#25251](https://github.com/frappe/erpnext/pull/25251))
|
||||
- Report Issue Summary fix for zero issues ([#24934](https://github.com/frappe/erpnext/pull/24934))
|
||||
- Validation msg for TransDocNo e-invoicing ([#25121](https://github.com/frappe/erpnext/pull/25121))
|
||||
- Correct state code for 'Other Territory' ([#24993](https://github.com/frappe/erpnext/pull/24993))
|
||||
- Commit individual SLE rename for large datasets (develop) ([#25084](https://github.com/frappe/erpnext/pull/25084))
|
||||
- Remove shipping address GSTIN validation for e-invoice ([#25153](https://github.com/frappe/erpnext/pull/25153))
|
||||
- Period list for exponential smoothing forecasting report ([#24982](https://github.com/frappe/erpnext/pull/24982))
|
||||
- Customer creation from shopping cart ([#25136](https://github.com/frappe/erpnext/pull/25136))
|
||||
- Simplified logic for additional salary ([#24824](https://github.com/frappe/erpnext/pull/24824))
|
||||
- Item wise tax rate for consolidated POS invoice ([#25029](https://github.com/frappe/erpnext/pull/25029))
|
||||
- Column width in Recruitment analytics report ([#25003](https://github.com/frappe/erpnext/pull/25003))
|
||||
- Filter Bank Account drop-down list in Bank Reconciliation Tool ([#24873](https://github.com/frappe/erpnext/pull/24873))
|
||||
- Payroll issues ([#24540](https://github.com/frappe/erpnext/pull/24540))
|
||||
- PO not created against all selected suppliers (drop shipping) ([#24863](https://github.com/frappe/erpnext/pull/24863))
|
||||
- Can't multiply sequence by non-int of type 'float' ([#25092](https://github.com/frappe/erpnext/pull/25092))
|
||||
- Make Discharge Schedule Date as Datetime ([#24940](https://github.com/frappe/erpnext/pull/24940))
|
||||
- Serial no trim issue ([#24949](https://github.com/frappe/erpnext/pull/24949))
|
@ -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"))
|
||||
|
||||
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -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,])
|
||||
|
@ -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,34 +44,35 @@ 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")){
|
||||
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")){
|
||||
} 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'));
|
||||
@ -79,13 +80,13 @@ frappe.ui.form.on('Employee Advance', {
|
||||
}
|
||||
},
|
||||
|
||||
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,7 +149,7 @@ frappe.ui.form.on('Employee Advance', {
|
||||
});
|
||||
},
|
||||
|
||||
employee: function (frm) {
|
||||
employee: function(frm) {
|
||||
if (frm.doc.employee) {
|
||||
frappe.run_serially([
|
||||
() => frm.trigger('get_employee_currency'),
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
0
erpnext/hr/doctype/employee_referral/__init__.py
Normal file
0
erpnext/hr/doctype/employee_referral/__init__.py
Normal file
68
erpnext/hr/doctype/employee_referral/employee_referral.js
Normal file
68
erpnext/hr/doctype/employee_referral/employee_referral.js
Normal 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);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
294
erpnext/hr/doctype/employee_referral/employee_referral.json
Normal file
294
erpnext/hr/doctype/employee_referral/employee_referral.json
Normal 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"
|
||||
}
|
71
erpnext/hr/doctype/employee_referral/employee_referral.py
Normal file
71
erpnext/hr/doctype/employee_referral/employee_referral.py
Normal 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
|
||||
|
@ -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']
|
||||
},
|
||||
|
||||
]
|
||||
}
|
@ -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];
|
||||
}
|
||||
},
|
||||
};
|
@ -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
|
@ -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', [])
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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`
|
||||
|
@ -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 = []
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -38,16 +38,37 @@ def execute():
|
||||
""".format(doctype), {'parentfield': parentfield})
|
||||
|
||||
# copy renamed child table fields (fields were already renamed in old doctype json, hence sql)
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_name = test_name""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_event = test_event""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_uom = test_uom""")
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_comment = test_comment""")
|
||||
rename_fields = {
|
||||
'lab_test_name': 'test_name',
|
||||
'lab_test_event': 'test_event',
|
||||
'lab_test_uom': 'test_uom',
|
||||
'lab_test_comment': 'test_comment'
|
||||
}
|
||||
|
||||
for new, old in rename_fields.items():
|
||||
if frappe.db.has_column('Normal Test Result', old):
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Result` SET {} = {}"""
|
||||
.format(new, old))
|
||||
|
||||
if frappe.db.has_column('Normal Test Template', 'test_event'):
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""")
|
||||
|
||||
if frappe.db.has_column('Normal Test Template', 'test_uom'):
|
||||
frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""")
|
||||
|
||||
if frappe.db.has_column('Descriptive Test Result', 'test_particulars'):
|
||||
frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""")
|
||||
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_template = test_template""")
|
||||
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_description = test_description""")
|
||||
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_rate = test_rate""")
|
||||
|
||||
rename_fields = {
|
||||
'lab_test_template': 'test_template',
|
||||
'lab_test_description': 'test_description',
|
||||
'lab_test_rate': 'test_rate'
|
||||
}
|
||||
|
||||
for new, old in rename_fields.items():
|
||||
if frappe.db.has_column('Lab Test Group Template', old):
|
||||
frappe.db.sql("""UPDATE `tabLab Test Group Template` SET {} = {}"""
|
||||
.format(new, old))
|
||||
|
||||
# rename field
|
||||
frappe.reload_doc('healthcare', 'doctype', 'lab_test')
|
||||
|
@ -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')""")
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
@ -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": []
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -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"
|
||||
}
|
||||
]
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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
|
||||
}
|
||||
]
|
@ -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()
|
@ -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",
|
||||
|
@ -712,7 +712,7 @@ erpnext.utils.map_current_doc = function(opts) {
|
||||
}
|
||||
|
||||
frappe.form.link_formatters['Item'] = function(value, doc) {
|
||||
if (doc && value && doc.item_name && doc.item_name !== value) {
|
||||
if (doc && value && doc.item_name && doc.item_name !== value && doc.item_code === value) {
|
||||
return value + ': ' + doc.item_name;
|
||||
} else if (!value && doc.doctype && doc.item_name) {
|
||||
// format blank value in child table
|
||||
|
@ -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",
|
||||
|
@ -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
|
@ -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() {
|
||||
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()
|
||||
callback: () => frm.reload_doc() || dialog.hide()
|
||||
});
|
||||
}
|
||||
},
|
||||
primary_action_label: __('Yes')
|
||||
});
|
||||
|
@ -340,8 +340,6 @@ def get_eway_bill_details(invoice):
|
||||
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()
|
||||
|
@ -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 = {}
|
||||
|
@ -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:
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
${invoice.customer}
|
||||
${frappe.ellipsis(invoice.customer, 20)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
|
File diff suppressed because one or more lines are too long
@ -1,4 +1,5 @@
|
||||
{
|
||||
"absolute_value": 0,
|
||||
"align_labels_right": 0,
|
||||
"creation": "2011-12-21 11:08:55",
|
||||
"custom_format": 1,
|
||||
@ -6,10 +7,10 @@
|
||||
"doc_type": "POS Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t{{ doc.select_print_heading or _(\"Invoice\") }}<br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
|
||||
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.get_formatted(\"rate\") }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t{%- for row in doc.payments -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t {{ row.mode_of_payment }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
|
||||
"idx": 1,
|
||||
"line_breaks": 0,
|
||||
"modified": "2020-04-29 16:45:58.942375",
|
||||
"modified": "2021-04-15 15:23:28.867135",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "POS Invoice",
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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 \
|
||||
|
@ -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) {
|
||||
|
@ -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):
|
||||
|
@ -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",
|
||||
|
@ -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.
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user