Merge branch 'develop' into refactor-lab-module

This commit is contained in:
Rucha Mahabal 2020-09-10 12:36:29 +05:30 committed by GitHub
commit 42bff3289d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 1918 additions and 558 deletions

View File

@ -43,7 +43,7 @@
{
"hidden": 0,
"label": "Bank Statement",
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
"links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Transaction Entry\",\n \"name\": \"Bank Statement Transaction Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Statement Settings\",\n \"name\": \"Bank Statement Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
@ -98,7 +98,7 @@
"idx": 0,
"is_standard": 1,
"label": "Accounting",
"modified": "2020-06-19 12:42:44.054598",
"modified": "2020-09-03 10:37:07.865801",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -158,4 +158,4 @@
"type": "Dashboard"
}
]
}
}

View File

@ -91,15 +91,11 @@ class TestBankTransaction(unittest.TestCase):
self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try:
frappe.get_doc({
"doctype": "Bank",
"bank_name":"Citi Bank",
"bank_name":bank_name,
}).insert()
except frappe.DuplicateEntryError:
pass
@ -108,12 +104,19 @@ def add_transactions():
frappe.get_doc({
"doctype": "Bank Account",
"account_name":"Checking Account",
"bank": "Citi Bank",
"account": "_Test Bank - _TC"
"bank": bank_name,
"account": account_name
}).insert()
except frappe.DuplicateEntryError:
pass
def add_transactions():
if frappe.flags.test_bank_transactions_created:
return
frappe.set_user("Administrator")
create_bank_account()
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",

View File

@ -638,20 +638,12 @@ $.extend(erpnext.journal_entry, {
return { filters: filters };
},
reverse_journal_entry: function(frm) {
var me = frm.doc;
for(var i=0; i<me.accounts.length; i++) {
me.accounts[i].credit += me.accounts[i].debit;
me.accounts[i].debit = me.accounts[i].credit - me.accounts[i].debit;
me.accounts[i].credit -= me.accounts[i].debit;
me.accounts[i].credit_in_account_currency = me.accounts[i].credit;
me.accounts[i].debit_in_account_currency = me.accounts[i].debit;
me.accounts[i].reference_type = "Journal Entry";
me.accounts[i].reference_name = me.name
}
frm.copy_doc();
cur_frm.reload_doc();
}
reverse_journal_entry: function() {
frappe.model.open_mapped_doc({
method: "erpnext.accounts.doctype.journal_entry.journal_entry.make_reverse_journal_entry",
frm: cur_frm
})
},
});
$.extend(erpnext.journal_entry, {

View File

@ -1021,3 +1021,34 @@ def make_inter_company_journal_entry(name, voucher_type, company):
journal_entry.posting_date = nowdate()
journal_entry.inter_company_journal_entry_reference = name
return journal_entry.as_dict()
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent):
target.reference_type = "Journal Entry"
target.reference_name = source_parent.name
doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": {
"doctype": "Journal Entry",
"validation": {
"docstatus": ["=", 1]
}
},
"Journal Entry Account": {
"doctype": "Journal Entry Account",
"field_map": {
"account_currency": "account_currency",
"exchange_rate": "exchange_rate",
"debit_in_account_currency": "credit_in_account_currency",
"debit": "credit",
"credit_in_account_currency": "debit_in_account_currency",
"credit": "debit",
},
"postprocess": update_accounts,
},
}, target_doc)
return doclist

View File

@ -167,6 +167,49 @@ class TestJournalEntry(unittest.TestCase):
self.assertFalse(gle)
def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False)
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.get("accounts")[1].exchange_rate = 1
jv.submit()
rjv = make_reverse_journal_entry(jv.name)
rjv.posting_date = nowdate()
rjv.submit()
gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
order by account asc""", rjv.name, as_dict=1)
self.assertTrue(gl_entries)
expected_values = {
"_Test Bank USD - _TC": {
"account_currency": "USD",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
"credit_in_account_currency": 100,
},
"Sales - _TC": {
"account_currency": "INR",
"debit": 5000,
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
}
}
for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD
jv = make_journal_entry("_Test Bank USD - _TC",

View File

@ -1172,30 +1172,23 @@ def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def set_missing_values(source, target):
target.payment_order_type = "Payment Entry"
target.append('references', dict(
reference_doctype="Payment Entry",
reference_name=source.name,
bank_account=source.party_bank_account,
amount=source.paid_amount,
account=source.paid_to,
supplier=source.party,
mode_of_payment=source.mode_of_payment,
))
def update_item(source_doc, target_doc, source_parent):
target_doc.bank_account = source_parent.party_bank_account
target_doc.amount = source_doc.allocated_amount
target_doc.account = source_parent.paid_to
target_doc.payment_entry = source_parent.name
target_doc.supplier = source_parent.party
target_doc.mode_of_payment = source_parent.mode_of_payment
doclist = get_mapped_doc("Payment Entry", source_name, {
doclist = get_mapped_doc("Payment Entry", source_name, {
"Payment Entry": {
"doctype": "Payment Order",
"validation": {
"docstatus": ["=", 1]
}
},
"Payment Entry Reference": {
"doctype": "Payment Order Reference",
"validation": {
"docstatus": ["=", 1]
},
"postprocess": update_item
},
}
}, target_doc, set_missing_values)

View File

@ -21,10 +21,15 @@ class PaymentOrder(Document):
if cancel:
status = 'Initiated'
ref_field = "status" if self.payment_order_type == "Payment Request" else "payment_order_status"
if self.payment_order_type == "Payment Request":
ref_field = "status"
ref_doc_field = frappe.scrub(self.payment_order_type)
else:
ref_field = "payment_order_status"
ref_doc_field = "reference_name"
for d in self.references:
frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status)
frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@ -5,6 +5,45 @@ from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import getdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, make_payment_order
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
class TestPaymentOrder(unittest.TestCase):
pass
def setUp(self):
create_bank_account()
def tearDown(self):
for bt in frappe.get_all("Payment Order"):
doc = frappe.get_doc("Payment Order", bt.name)
doc.cancel()
doc.delete()
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC")
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
payment_entry.party_bank_account = "Checking Account - Citi Bank"
payment_entry.insert()
payment_entry.submit()
doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry")
reference_doc = doc.get("references")[0]
self.assertEquals(reference_doc.reference_name, payment_entry.name)
self.assertEquals(reference_doc.reference_doctype, "Payment Entry")
self.assertEquals(reference_doc.supplier, "_Test Supplier")
self.assertEquals(reference_doc.amount, 250)
def create_payment_order_against_payment_entry(ref_doc, order_type):
payment_order = frappe.get_doc(dict(
doctype="Payment Order",
company="_Test Company",
payment_order_type=order_type,
company_bank_account="Checking Account - Citi Bank"
))
doc = make_payment_order(ref_doc.name, payment_order)
doc.save()
doc.submit()
return doc

View File

@ -1,4 +1,5 @@
{
"actions": [],
"creation": "2018-07-20 16:38:06.630813",
"doctype": "DocType",
"editable_grid": 1,
@ -10,7 +11,6 @@
"column_break_4",
"supplier",
"payment_request",
"payment_entry",
"mode_of_payment",
"bank_account_details",
"bank_account",
@ -103,17 +103,12 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "payment_entry",
"fieldtype": "Link",
"label": "Payment Entry",
"options": "Payment Entry",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"modified": "2019-05-08 13:56:25.724557",
"links": [],
"modified": "2020-09-04 08:29:51.014390",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Order Reference",

View File

@ -10,13 +10,15 @@ frappe.ui.form.on('Process Deferred Accounting', {
}
};
});
},
if (frm.doc.company) {
type: function(frm) {
if (frm.doc.company && frm.doc.type) {
frm.set_query("account", function() {
return {
filters: {
'company': frm.doc.company,
'root_type': 'Liability',
'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset',
'is_group': 0
}
};

View File

@ -60,6 +60,7 @@
"reqd": 1
},
{
"depends_on": "eval: doc.type",
"fieldname": "account",
"fieldtype": "Link",
"label": "Account",
@ -73,9 +74,10 @@
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-02-06 18:18:09.852844",
"modified": "2020-09-03 18:07:02.463754",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Deferred Accounting",

View File

@ -25,6 +25,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
this.frm.set_df_property("credit_to", "print_hide", 0);
}
}
// Trigger supplier event on load if supplier is available
// The reason for this is PI can be created from PR or PO and supplier is pre populated
if (this.frm.doc.supplier) {
this.frm.trigger('supplier');
}
},
refresh: function(doc) {
@ -135,6 +141,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
}
});
}
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
},
unblock_invoice: function() {

View File

@ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController):
if not self.due_date:
self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date)
tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
if tds_category and not for_validate:
self.apply_tds = 1
self.tax_withholding_category = tds_category
super(PurchaseInvoice, self).set_missing_values(for_validate)
def check_conversion_rate(self):

View File

@ -1,134 +1,66 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"actions": [],
"allow_rename": 1,
"autoname": "field:title",
"beta": 0,
"creation": "2018-11-22 23:38:39.668804",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-01-15 17:14:28.951793",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-30 19:41:25.783852",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Category",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
"share": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}
"track_changes": 1
}

View File

@ -45,8 +45,8 @@ def validate_accounting_period(gl_map):
}, as_dict=1)
if accounting_periods:
frappe.throw(_("You can't create accounting entries in the closed accounting period {0}")
.format(accounting_periods[0].name), ClosedAccountingPeriod)
frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}")
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True):
if merge_entries:
@ -301,8 +301,9 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
})
if gl_entries:
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
for entry in gl_entries:
entry['name'] = None
@ -342,7 +343,7 @@ def set_as_cancel(voucher_type, voucher_no):
"""
Set is_cancelled=1 in all original gl entries for the voucher
"""
frappe.db.sql("""update `tabGL Entry` set is_cancelled = 1,
frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1,
modified=%s, modified_by=%s
where voucher_type=%s and voucher_no=%s and is_cancelled = 0""",
(now(), frappe.session.user, voucher_type, voucher_no))

View File

@ -71,7 +71,22 @@ frappe.query_reports["Budget Variance Report"] = {
fieldtype: "Check",
default: 0,
},
]
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes('variance')) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
}
erpnext.dimension_filters.forEach((dimension) => {

View File

@ -256,7 +256,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts"""
for d in reversed(accounts):
if d.parent_account:
account = d.parent_account.split('-')[0].strip()
account = d.parent_account.split(' - ')[0].strip()
if not accounts_by_name.get(account):
continue

View File

@ -43,8 +43,11 @@ def execute(filters=None):
def validate_filters(filters, account_details):
if not filters.get('company'):
frappe.throw(_('{0} is mandatory').format(_('Company')))
if not filters.get("company"):
frappe.throw(_("{0} is mandatory").format(_("Company")))
if not filters.get("from_date") and not filters.get("to_date"):
frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))))
if filters.get("account") and not account_details.get(filters.account):
frappe.throw(_("Account {0} does not exists").format(filters.account))
@ -141,7 +144,7 @@ def get_gl_entries(filters, accounting_dimensions):
if filters and filters.get('cost_center'):
select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """
UNION ALL
SELECT name as gl_entry,
@ -155,7 +158,7 @@ def get_gl_entries(filters, accounting_dimensions):
against_voucher_type,
against_voucher,
account_currency,
remarks, against,
remarks, against,
is_opening, `tabGL Entry`.creation {select_fields_with_percentage}
FROM `tabGL Entry`,
(

View File

@ -1,24 +1,23 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-02-25 17:03:34",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-02-24 20:12:22.464240",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",
"owner": "Administrator",
"ref_doctype": "Sales Invoice",
"report_name": "Gross Profit",
"report_type": "Script Report",
"add_total_row": 1,
"creation": "2013-02-25 17:03:34",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2020-08-13 11:26:39.112352",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",
"owner": "Administrator",
"ref_doctype": "Sales Invoice",
"report_name": "Gross Profit",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts Manager"
},
},
{
"role": "Accounts User"
}

View File

@ -146,7 +146,7 @@ class Asset(AccountsController):
'assets': assets,
'purpose': 'Receipt',
'company': self.company,
'transaction_date': getdate(nowdate()),
'transaction_date': getdate(self.purchase_date),
'reference_doctype': reference_doctype,
'reference_name': reference_docname
}).insert()

View File

@ -11,6 +11,8 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
class TestRequestforQuotation(unittest.TestCase):
def test_quote_status(self):
@ -110,6 +112,23 @@ class TestRequestforQuotation(unittest.TestCase):
self.assertEqual(supplier_quotation.items[0].qty, 5)
self.assertEqual(supplier_quotation.items[0].stock_qty, 10)
def test_make_rfq_from_opportunity(self):
opportunity = make_opportunity(with_items=1)
supplier_data = get_supplier_data()
rfq = make_rfq(opportunity.name)
self.assertEqual(len(rfq.get("items")), len(opportunity.get("items")))
rfq.message_for_supplier = 'Please supply the specified items at the best possible rates.'
for item in rfq.items:
item.warehouse = "_Test Warehouse - _TC"
for data in supplier_data:
rfq.append('suppliers', data)
rfq.status = 'Draft'
rfq.submit()
def make_request_for_quotation(**args):
"""
:param supplier_data: List containing supplier data

View File

@ -12,7 +12,22 @@ frappe.query_reports["Quoted Item Comparison"] = {
"reqd": 1
},
{
reqd: 1,
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
"width": "80",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
"fieldname":"to_date",
"label": __("To Date"),
"fieldtype": "Date",
"width": "80",
"reqd": 1,
"default": frappe.datetime.get_today()
},
{
default: "",
options: "Item",
label: __("Item"),
@ -45,13 +60,12 @@ frappe.query_reports["Quoted Item Comparison"] = {
}
},
{
fieldtype: "Link",
fieldtype: "MultiSelectList",
label: __("Supplier Quotation"),
options: "Supplier Quotation",
fieldname: "supplier_quotation",
default: "",
get_query: () => {
return { filters: { "docstatus": ["<", 2] } }
get_data: function(txt) {
return frappe.db.get_link_options('Supplier Quotation', txt, {'docstatus': ["<", 2]});
}
},
{
@ -63,9 +77,30 @@ frappe.query_reports["Quoted Item Comparison"] = {
get_query: () => {
return { filters: { "docstatus": ["<", 2] } }
}
},
{
fieldtype: "Check",
label: __("Include Expired"),
fieldname: "include_expired",
default: 0
}
],
formatter: (value, row, column, data, default_formatter) => {
value = default_formatter(value, row, column, data);
if(column.fieldname === "valid_till" && data.valid_till){
if(frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 1){
value = `<div style="color:red">${value}</div>`;
}
else if (frappe.datetime.get_diff(data.valid_till, frappe.datetime.nowdate()) <= 7){
value = `<div style="color:darkorange">${value}</div>`;
}
}
return value;
},
onload: (report) => {
// Create a button for setting the default supplier
report.page.add_inner_button(__("Select Default Supplier"), () => {

View File

@ -16,44 +16,49 @@ def execute(filters=None):
supplier_quotation_data = get_data(filters, conditions)
columns = get_columns()
data, chart_data = prepare_data(supplier_quotation_data)
data, chart_data = prepare_data(supplier_quotation_data, filters)
message = get_message()
return columns, data, None, chart_data
return columns, data, message, chart_data
def get_conditions(filters):
conditions = ""
if filters.get("item_code"):
conditions += " AND sqi.item_code = %(item_code)s"
if filters.get("supplier_quotation"):
conditions += " AND sqi.parent = %(supplier_quotation)s"
conditions += " AND sqi.parent in %(supplier_quotation)s"
if filters.get("request_for_quotation"):
conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s"
if filters.get("supplier"):
conditions += " AND sq.supplier in %(supplier)s"
if not filters.get("include_expired"):
conditions += " AND sq.status != 'Expired'"
return conditions
def get_data(filters, conditions):
if not filters.get("item_code"):
return []
supplier_quotation_data = frappe.db.sql("""SELECT
sqi.parent, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation,
sq.supplier
sqi.parent, sqi.item_code, sqi.qty, sqi.rate, sqi.uom, sqi.request_for_quotation,
sqi.lead_time_days, sq.supplier, sq.valid_till
FROM
`tabSupplier Quotation Item` sqi,
`tabSupplier Quotation` sq
WHERE
sqi.item_code = %(item_code)s
AND sqi.parent = sq.name
sqi.parent = sq.name
AND sqi.docstatus < 2
AND sq.company = %(company)s
AND sq.status != 'Expired'
{0}""".format(conditions), filters, as_dict=1)
AND sq.transaction_date between %(from_date)s and %(to_date)s
{0}
order by sq.transaction_date, sqi.item_code""".format(conditions), filters, as_dict=1)
return supplier_quotation_data
def prepare_data(supplier_quotation_data):
out, suppliers, qty_list = [], [], []
def prepare_data(supplier_quotation_data, filters):
out, suppliers, qty_list, chart_data = [], [], [], []
supplier_wise_map = defaultdict(list)
supplier_qty_price_map = {}
@ -70,20 +75,24 @@ def prepare_data(supplier_quotation_data):
exchange_rate = 1
row = {
"item_code": data.get('item_code'),
"quotation": data.get("parent"),
"qty": data.get("qty"),
"price": flt(data.get("rate") * exchange_rate, float_precision),
"uom": data.get("uom"),
"request_for_quotation": data.get("request_for_quotation"),
"valid_till": data.get('valid_till'),
"lead_time_days": data.get('lead_time_days')
}
# map for report view of form {'supplier1':[{},{},...]}
supplier_wise_map[supplier].append(row)
# map for chart preparation of the form {'supplier1': {'qty': 'price'}}
if not supplier in supplier_qty_price_map:
supplier_qty_price_map[supplier] = {}
supplier_qty_price_map[supplier][row["qty"]] = row["price"]
if filters.get("item_code"):
if not supplier in supplier_qty_price_map:
supplier_qty_price_map[supplier] = {}
supplier_qty_price_map[supplier][row["qty"]] = row["price"]
suppliers.append(supplier)
qty_list.append(data.get("qty"))
@ -97,7 +106,8 @@ def prepare_data(supplier_quotation_data):
for entry in supplier_wise_map[supplier]:
out.append(entry)
chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map)
if filters.get("item_code"):
chart_data = prepare_chart_data(suppliers, qty_list, supplier_qty_price_map)
return out, chart_data
@ -117,9 +127,10 @@ def prepare_chart_data(suppliers, qty_list, supplier_qty_price_map):
data_points_map[qty].append(None)
dataset = []
currency_symbol = frappe.db.get_value("Currency", frappe.db.get_default("currency"), "symbol")
for qty in qty_list:
datapoints = {
"name": _("Price for Qty ") + str(qty),
"name": currency_symbol + " (Qty " + str(qty) + " )",
"values": data_points_map[qty]
}
dataset.append(datapoints)
@ -140,14 +151,21 @@ def get_columns():
"label": _("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
"width": 150
},
{
"fieldname": "item_code",
"label": _("Item"),
"fieldtype": "Link",
"options": "Item",
"width": 200
},
{
"fieldname": "quotation",
"label": _("Supplier Quotation"),
"fieldname": "uom",
"label": _("UOM"),
"fieldtype": "Link",
"options": "Supplier Quotation",
"width": 200
"options": "UOM",
"width": 90
},
{
"fieldname": "qty",
@ -163,19 +181,43 @@ def get_columns():
"width": 110
},
{
"fieldname": "uom",
"label": _("UOM"),
"fieldname": "quotation",
"label": _("Supplier Quotation"),
"fieldtype": "Link",
"options": "UOM",
"width": 90
"options": "Supplier Quotation",
"width": 200
},
{
"fieldname": "valid_till",
"label": _("Valid Till"),
"fieldtype": "Date",
"width": 100
},
{
"fieldname": "lead_time_days",
"label": _("Lead Time (Days)"),
"fieldtype": "Int",
"width": 100
},
{
"fieldname": "request_for_quotation",
"label": _("Request for Quotation"),
"fieldtype": "Link",
"options": "Request for Quotation",
"width": 200
"width": 150
}
]
return columns
return columns
def get_message():
return """<span class="indicator">
Valid till : &nbsp;&nbsp;
</span>
<span class="indicator orange">
Expires in a week or less
</span>
&nbsp;&nbsp;
<span class="indicator red">
Expires today / Already Expired
</span>"""

View File

@ -1,4 +1,5 @@
{
"actions": [],
"autoname": "field:id",
"creation": "2019-06-05 12:07:02.634534",
"doctype": "DocType",
@ -14,6 +15,7 @@
"contact",
"contact_name",
"column_break_10",
"customer",
"lead",
"lead_name",
"section_break_5",
@ -28,7 +30,8 @@
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
"fieldtype": "Section Break",
"label": "Call Details"
},
{
"fieldname": "id",
@ -125,10 +128,19 @@
"in_list_view": 1,
"label": "Lead Name",
"read_only": 1
},
{
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer",
"read_only": 1
}
],
"in_create": 1,
"modified": "2019-08-06 05:46:53.144683",
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-08-25 17:08:34.085731",
"modified_by": "Administrator",
"module": "Communication",
"name": "Call Log",

View File

@ -16,6 +16,9 @@ class CallLog(Document):
self.contact = get_contact_with_phone_number(number)
self.lead = get_lead_with_phone_number(number)
contact = frappe.get_doc("Contact", self.contact)
self.customer = contact.get_link_for("Customer")
def after_insert(self):
self.trigger_call_popup()

View File

@ -255,7 +255,7 @@ class StatusUpdater(Document):
args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
from `tab%(second_source_dt)s`
where `%(second_join_field)s`="%(detail_id)s"
and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args
and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args
if args['detail_id']:
if not args.get("extra_cond"): args["extra_cond"] = ""

View File

@ -267,6 +267,9 @@ def make_quotation(source_name, target_doc=None):
@frappe.whitelist()
def make_request_for_quotation(source_name, target_doc=None):
def update_item(obj, target, source_parent):
target.conversion_factor = 1.0
doclist = get_mapped_doc("Opportunity", source_name, {
"Opportunity": {
"doctype": "Request for Quotation"
@ -277,7 +280,8 @@ def make_request_for_quotation(source_name, target_doc=None):
["name", "opportunity_item"],
["parent", "opportunity"],
["uom", "uom"]
]
],
"postprocess": update_item
}
}, target_doc)

View File

@ -82,7 +82,8 @@ def make_opportunity(**args):
if args.with_items:
opp_doc.append('items', {
"item_code": args.item_code or "_Test Item",
"qty": args.qty or 1
"qty": args.qty or 1,
"uom": "_Test UOM"
})
opp_doc.insert()

View File

@ -0,0 +1,40 @@
{
"cards": [
{
"hidden": 0,
"label": "Marketplace",
"links": "[\n {\n \"description\": \"Woocommerce marketplace settings\",\n \"label\": \"Woocommerce Settings\",\n \"name\": \"Woocommerce Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Amazon MWS settings\",\n \"label\": \"Amazon MWS Settings\",\n \"name\": \"Amazon MWS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Shopify settings\",\n \"label\": \"Shopify Settings\",\n \"name\": \"Shopify Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Payments",
"links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }\n]"
},
{
"hidden": 0,
"label": "Settings",
"links": "[\n {\n \"description\": \"Plaid settings\",\n \"label\": \"Plaid Settings\",\n \"name\": \"Plaid Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exotel settings\",\n \"label\": \"Exotel Settings\",\n \"name\": \"Exotel Settings\",\n \"type\": \"doctype\"\n }\n]"
}
],
"category": "Modules",
"charts": [],
"creation": "2020-08-20 19:30:48.138801",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Desk Page",
"extends": "Integrations",
"extends_another_page": 1,
"hide_custom": 1,
"idx": 0,
"is_standard": 1,
"label": "ERPNext Integrations",
"modified": "2020-08-23 16:30:51.494655",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"shortcuts": []
}

View File

@ -98,7 +98,8 @@ def add_attendance(events, start, end, conditions=None):
e = {
"name": d.name,
"doctype": "Attendance",
"date": d.attendance_date,
"start": d.attendance_date,
"end": d.attendance_date,
"title": cstr(d.status),
"docstatus": d.docstatus
}

View File

@ -1,12 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.views.calendar["Attendance"] = {
field_map: {
"start": "attendance_date",
"end": "attendance_date",
"id": "name",
"docstatus": 1
},
options: {
header: {
left: 'prev,next today',

View File

@ -1,5 +1,4 @@
{
"actions": [],
"creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType",
"engine": "InnoDB",
@ -54,6 +53,7 @@
{
"fieldname": "transaction_type",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Transaction Type",
"options": "DocType"
},
@ -109,9 +109,9 @@
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-02-27 14:40:10.502605",
"modified": "2020-09-04 12:16:36.569066",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Ledger Entry",

View File

@ -0,0 +1,13 @@
frappe.listview_settings['Leave Ledger Entry'] = {
onload: function(listview) {
if(listview.page.fields_dict.transaction_type) {
listview.page.fields_dict.transaction_type.get_query = function() {
return {
"filters": {
"name": ["in", ["Leave Allocation", "Leave Application", "Leave Encashment"]],
}
};
};
}
}
};

View File

@ -103,7 +103,7 @@ def add_assignments(events, start, end, conditions=None):
"doctype": "Shift Assignment",
"start_date": d.start_date,
"end_date": d.end_date if d.end_date else nowdate(),
"title": cstr(d.employee_name) + \
"title": cstr(d.employee_name) + ": "+ \
cstr(d.shift_type),
"docstatus": d.docstatus
}

View File

@ -73,8 +73,8 @@ frappe.ui.form.on('Loan', {
loan_type: function(frm) {
frm.toggle_reqd("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_method", 1 - frm.doc.is_term_loan);
frm.toggle_display("repayment_periods", s1 - frm.doc.is_term_loan);
frm.toggle_display("repayment_method", frm.doc.is_term_loan);
frm.toggle_display("repayment_periods", frm.doc.is_term_loan);
},
@ -119,12 +119,10 @@ frappe.ui.form.on('Loan', {
create_loan_security_unpledge: function(frm) {
frappe.call({
method: "erpnext.loan_management.doctype.loan.loan.create_loan_security_unpledge",
method: "erpnext.loan_management.doctype.loan.loan.unpledge_security",
args : {
"loan": frm.doc.name,
"applicant_type": frm.doc.applicant_type,
"applicant": frm.doc.applicant,
"company": frm.doc.company
"as_dict": 1
},
callback: function(r) {
if (r.message)

View File

@ -7,7 +7,7 @@ import frappe, math, json
import erpnext
from frappe import _
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.controllers.accounts_controller import AccountsController
class Loan(AccountsController):
@ -223,30 +223,56 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as
return repayment_entry
@frappe.whitelist()
def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1):
loan_security_pledge_details = frappe.db.sql("""
SELECT p.loan_security, sum(p.qty) as qty
FROM `tabLoan Security Pledge` lsp , `tabPledge` p
WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1
GROUP BY p.loan_security
""",(loan), as_dict=1)
def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0):
# if loan is passed it will be considered as full unpledge
if loan:
pledge_qty_map = get_pledged_security_qty(loan)
loan_doc = frappe.get_doc('Loan', loan)
unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company,
loan_doc.applicant_type, loan_doc.applicant)
# will unpledge qty based on loan security pledge
elif loan_security_pledge:
security_map = {}
pledge_doc = frappe.get_doc('Loan Security Pledge', loan_security_pledge)
for security in pledge_doc.securities:
security_map.setdefault(security.loan_security, security.qty)
unpledge_request = create_loan_security_unpledge(security_map, pledge_doc.loan,
pledge_doc.company, pledge_doc.applicant_type, pledge_doc.applicant)
if save:
unpledge_request.save()
if submit:
unpledge_request.submit()
if approve:
if unpledge_request.docstatus == 1:
unpledge_request.status = 'Approved'
unpledge_request.save()
else:
frappe.throw(_('Only submittted unpledge requests can be approved'))
if as_dict:
return unpledge_request
else:
return unpledge_request
def create_loan_security_unpledge(unpledge_map, loan, company, applicant_type, applicant):
unpledge_request = frappe.new_doc("Loan Security Unpledge")
unpledge_request.applicant_type = applicant_type
unpledge_request.applicant = applicant
unpledge_request.loan = loan
unpledge_request.company = company
for loan_security in loan_security_pledge_details:
unpledge_request.append('securities', {
"loan_security": loan_security.loan_security,
"qty": loan_security.qty
})
for security, qty in unpledge_map.items():
if qty:
unpledge_request.append('securities', {
"loan_security": security,
"qty": qty
})
if as_dict:
return unpledge_request.as_dict()
else:
return unpledge_request
return unpledge_request

View File

@ -14,9 +14,11 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
process_loan_interest_accrual_for_term_loans)
from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year
from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall
from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge
from erpnext.loan_management.doctype.loan.loan import unpledge_security
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.loan_management.doctype.loan_application.loan_application import create_pledge
from erpnext.loan_management.doctype.loan_disbursement.loan_disbursement import get_disbursal_amount
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts
class TestLoan(unittest.TestCase):
def setUp(self):
@ -193,18 +195,13 @@ class TestLoan(unittest.TestCase):
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
amount = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['sum(paid_interest_amount)'])
unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \
/ (days_in_year(get_datetime(first_date).year) * 100)
self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3),
flt(accrued_interest_amount, 3))
self.assertEquals(flt(amount, 2),flt(accrued_interest_amount, 2))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
loan.load_from_db()
@ -306,13 +303,10 @@ class TestLoan(unittest.TestCase):
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0)
unpledge_request = unpledge_security(loan=loan.name, save=1)
unpledge_request.submit()
unpledge_request.status = 'Approved'
unpledge_request.save()
@ -323,6 +317,102 @@ class TestLoan(unittest.TestCase):
self.assertEqual(loan.status, 'Closed')
self.assertEquals(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
self.assertEqual(amounts['pending_principal_amount'], 0)
self.assertEqual(amounts['payable_principal_amount'], 0)
self.assertEqual(amounts['interest_amount'], 0)
def test_disbursal_check_with_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
}]
loan_application = create_loan_application('_Test Company', self.applicant2,
'Stock Loan', pledges, "Repay Over Number of Periods", 12)
create_pledge(loan_application)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
loan.submit()
#Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
where loan_security='Test Security 2'""")
create_process_loan_security_shortfall()
loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
self.assertTrue(loan_security_shortfall)
self.assertEqual(get_disbursal_amount(loan.name), 0)
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250
where loan_security='Test Security 2'""")
def test_disbursal_check_without_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
"qty": 8000.00,
"haircut": 50,
}]
loan_application = create_loan_application('_Test Company', self.applicant2,
'Stock Loan', pledges, "Repay Over Number of Periods", 12)
create_pledge(loan_application)
loan = create_loan_with_security(self.applicant2, "Stock Loan", "Repay Over Number of Periods", 12, loan_application)
loan.submit()
#Disbursing 7,00,000 from the allowed 10,00,000 according to security pledge
make_loan_disbursement_entry(loan.name, 700000)
self.assertEqual(get_disbursal_amount(loan.name), 300000)
def test_pending_loan_amount_after_closure_request(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
}]
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
create_pledge(loan_application)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
no_of_days += 6
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 6),
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.submit()
amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 6), "Regular Repayment")
self.assertEquals(amounts['pending_principal_amount'], 0.0)
def create_loan_accounts():
if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"):

View File

@ -33,18 +33,18 @@ frappe.ui.form.on('Loan Application', {
if (frm.doc.is_secured_loan) {
frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) {
if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Loan Security Pledge'), function() {
frm.trigger('create_loan_security_pledge')
frm.trigger('create_loan_security_pledge');
},__('Create'))
}
});
}
frappe.db.get_value("Loan", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => {
if (!r) {
if (Object.keys(r).length === 0) {
frm.add_custom_button(__('Loan'), function() {
frm.trigger('create_loan')
frm.trigger('create_loan');
},__('Create'))
} else {
frm.set_df_property('status', 'read_only', 1);
@ -54,7 +54,7 @@ frappe.ui.form.on('Loan Application', {
},
create_loan: function(frm) {
if (frm.doc.status != "Approved") {
frappe.throw(__("Cannot create loan until application is approved"))
frappe.throw(__("Cannot create loan until application is approved"));
}
frappe.model.open_mapped_doc({

View File

@ -67,28 +67,10 @@ class LoanDisbursement(AccountsController):
disbursed_amount = self.disbursed_amount + loan_details.disbursed_amount
total_payment = loan_details.total_payment
if disbursed_amount > loan_details.loan_amount and loan_details.is_term_loan:
frappe.throw(_("Disbursed Amount cannot be greater than loan amount"))
possible_disbursal_amount = get_disbursal_amount(self.against_loan)
if loan_details.status == 'Disbursed':
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = loan_details.disbursed_amount
security_value = 0.0
if loan_details.is_secured_loan:
security_value = get_total_pledged_security_value(self.against_loan)
if not security_value:
security_value = loan_details.loan_amount
if pending_principal_amount + self.disbursed_amount > flt(security_value):
allowed_amount = security_value - pending_principal_amount
if allowed_amount < 0:
allowed_amount = 0
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(allowed_amount))
if self.disbursed_amount > possible_disbursal_amount:
frappe.throw(_("Disbursed Amount cannot be greater than {0}").format(possible_disbursal_amount))
if loan_details.status == "Disbursed" and not loan_details.is_term_loan:
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(self.disbursement_date, -1),
@ -176,3 +158,32 @@ def get_total_pledged_security_value(loan):
security_value += (loan_security_price_map.get(security) * qty * hair_cut_map.get(security))/100
return security_value
@frappe.whitelist()
def get_disbursal_amount(loan):
loan_details = frappe.get_all("Loan", fields = ["loan_amount", "disbursed_amount", "total_payment",
"total_principal_paid", "total_interest_payable", "status", "is_term_loan", "is_secured_loan"],
filters= { "name": loan })[0]
if loan_details.is_secured_loan and frappe.get_all('Loan Security Shortfall', filters={'loan': loan,
'status': 'Pending'}):
return 0
if loan_details.status == 'Disbursed':
pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
- flt(loan_details.total_principal_paid)
else:
pending_principal_amount = flt(loan_details.disbursed_amount)
security_value = 0.0
if loan_details.is_secured_loan:
security_value = get_total_pledged_security_value(loan)
if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount)
disbursal_amount = flt(security_value) - flt(pending_principal_amount)
return disbursal_amount

View File

@ -85,8 +85,11 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
if no_of_days <= 0:
return
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
if loan.status == 'Disbursed':
pending_principal_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
else:
pending_principal_amount = loan.disbursed_amount
interest_per_day = (pending_principal_amount * loan.rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100)
payable_interest = interest_per_day * no_of_days
@ -107,7 +110,7 @@ def calculate_accrual_amount_for_demand_loans(loan, posting_date, process_loan_i
def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_interest, open_loans=None, loan_type=None):
query_filters = {
"status": "Disbursed",
"status": ('in', ['Disbursed', 'Partially Disbursed']),
"docstatus": 1
}
@ -118,8 +121,9 @@ def make_accrual_interest_entry_for_demand_loans(posting_date, process_loan_inte
if not open_loans:
open_loans = frappe.get_all("Loan",
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account", "is_term_loan",
"disbursement_date", "applicant_type", "applicant", "rate_of_interest", "total_interest_payable", "repayment_start_date"],
fields=["name", "total_payment", "total_amount_paid", "loan_account", "interest_income_account",
"is_term_loan", "status", "disbursement_date", "disbursed_amount", "applicant_type", "applicant",
"rate_of_interest", "total_interest_payable", "total_principal_paid", "repayment_start_date"],
filters=query_filters)
for loan in open_loans:
@ -209,7 +213,8 @@ def get_last_accural_date_in_current_month(loan):
WHERE loan = %s""", (loan.name))
if last_posting_date[0][0]:
return last_posting_date[0][0]
# interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1)
else:
return loan.disbursement_date

View File

@ -13,6 +13,7 @@ from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import update_shortfall_status
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_demand_loans
class LoanRepayment(AccountsController):
@ -22,6 +23,9 @@ class LoanRepayment(AccountsController):
self.validate_amount()
self.allocate_amounts(amounts['pending_accrual_entries'])
def before_submit(self):
self.book_unaccrued_interest()
def on_submit(self):
self.update_paid_amount()
self.make_gl_entries()
@ -72,6 +76,26 @@ class LoanRepayment(AccountsController):
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
frappe.throw(msg)
def book_unaccrued_interest(self):
if self.payment_type == 'Loan Closure':
total_interest_paid = 0
for payment in self.repayment_details:
total_interest_paid += payment.paid_interest_amount
if total_interest_paid < self.interest_payable:
if not self.is_term_loan:
process = process_loan_interest_accrual_for_demand_loans(posting_date=self.posting_date,
loan=self.against_loan)
lia = frappe.db.get_value('Loan Interest Accrual', {'process_loan_interest_accrual':
process}, ['name', 'interest_amount', 'payable_principal_amount'], as_dict=1)
self.append('repayment_details', {
'loan_interest_accrual': lia.name,
'paid_interest_amount': lia.interest_amount,
'paid_principal_amount': lia.payable_principal_amount
})
def update_paid_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
@ -116,6 +140,7 @@ class LoanRepayment(AccountsController):
def allocate_amounts(self, paid_entries):
self.set('repayment_details', [])
self.principal_amount_paid = 0
total_interest_paid = 0
interest_paid = self.amount_paid - self.penalty_amount
if self.amount_paid - self.penalty_amount > 0 and paid_entries:
@ -137,12 +162,17 @@ class LoanRepayment(AccountsController):
interest_paid = 0
paid_principal=0
total_interest_paid += interest_amount
self.append('repayment_details', {
'loan_interest_accrual': lia,
'paid_interest_amount': interest_amount,
'paid_principal_amount': paid_principal
})
if self.payment_type == 'Loan Closure' and total_interest_paid < self.interest_payable:
unaccrued_interest = self.interest_payable - total_interest_paid
interest_paid -= unaccrued_interest
if interest_paid:
self.principal_amount_paid += interest_paid
@ -281,7 +311,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
due_date = add_days(entry.posting_date, 1)
no_of_late_days = date_diff(posting_date,
add_days(due_date, loan_type_details.grace_period_in_days))
add_days(due_date, loan_type_details.grace_period_in_days))
if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary):
penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days)/365
@ -297,7 +327,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
if not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid - against_loan_doc.total_interest_payable
else:
pending_principal_amount = against_loan_doc.disbursed_amount
if payment_type == "Loan Closure":
if due_date:

View File

@ -21,6 +21,10 @@
"total_security_value",
"column_break_11",
"maximum_loan_value",
"more_information_section",
"reference_no",
"column_break_18",
"description",
"amended_from"
],
"fields": [
@ -129,11 +133,34 @@
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-07-02 23:38:24.002382",
"modified": "2020-09-04 22:38:19.894488",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Pledge",

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import get_datetime
from frappe.utils import get_datetime, flt
from frappe.model.document import Document
from six import iteritems
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
@ -51,13 +51,19 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
"valid_upto": (">=", update_time)
}, as_list=1))
loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid'],
filters={'status': 'Disbursed', 'is_secured_loan': 1})
loans = frappe.get_all('Loan', fields=['name', 'loan_amount', 'total_principal_paid', 'total_payment',
'total_interest_payable', 'disbursed_amount', 'status'],
filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1})
loan_security_map = {}
for loan in loans:
outstanding_amount = loan.loan_amount - loan.total_principal_paid
if loan.status == 'Disbursed':
outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
else:
outstanding_amount = loan.disbursed_amount
pledged_securities = get_pledged_security_qty(loan.name)
ltv_ratio = ''
security_value = 0.0

View File

@ -16,6 +16,10 @@
"status",
"loan_security_details_section",
"securities",
"more_information_section",
"reference_no",
"column_break_13",
"description",
"amended_from"
],
"fields": [
@ -95,11 +99,34 @@
"label": "Applicant Type",
"options": "Employee\nMember\nCustomer",
"reqd": 1
},
{
"collapsible": 1,
"fieldname": "more_information_section",
"fieldtype": "Section Break",
"label": "More Information"
},
{
"allow_on_submit": 1,
"fieldname": "reference_no",
"fieldtype": "Data",
"label": "Reference No"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "description",
"fieldtype": "Text",
"label": "Description"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-05-05 07:23:18.440058",
"modified": "2020-09-04 22:39:57.756146",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Unpledge",

View File

@ -17,7 +17,6 @@ class LoanSecurityUnpledge(Document):
self.validate_unpledge_qty()
def on_cancel(self):
self.update_loan_security_pledge(cancel=1)
self.update_loan_status(cancel=1)
self.db_set('status', 'Requested')
@ -43,13 +42,14 @@ class LoanSecurityUnpledge(Document):
"valid_upto": (">=", get_datetime())
}, as_list=1))
loan_amount, principal_paid = frappe.get_value("Loan", self.loan, ['loan_amount', 'total_principal_paid'])
pending_principal_amount = loan_amount - principal_paid
total_payment, principal_paid, interest_payable = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
'total_interest_payable'])
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid)
security_value = 0
for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security)
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
if security.qty > pledged_qty:
frappe.throw(_("""Row {0}: {1} {2} of {3} is pledged against Loan {4}.
You are trying to unpledge more""").format(security.idx, pledged_qty, security.uom,
@ -58,16 +58,23 @@ class LoanSecurityUnpledge(Document):
qty_after_unpledge = pledged_qty - security.qty
ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
security_value += qty_after_unpledge * loan_security_price_map.get(security.loan_security)
current_price = loan_security_price_map.get(security.loan_security)
if not current_price:
frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security)))
if not security_value and pending_principal_amount > 0:
security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
if security_value and (pending_principal_amount/security_value) * 100 > ltv_ratio:
if security_value and flt(pending_principal_amount/security_value) * 100 > ltv_ratio:
frappe.throw("Cannot Unpledge, loan to value ratio is breaching")
def on_update_after_submit(self):
if self.status == "Approved":
self.approve()
def approve(self):
if self.status == "Approved" and not self.unpledge_time:
self.update_loan_status()
self.db_set('unpledge_time', get_datetime())

View File

@ -36,6 +36,8 @@ def process_loan_interest_accrual_for_demand_loans(posting_date=None, loan_type=
loan_process.submit()
return loan_process.name
def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=None, loan=None):
if not term_loan_accrual_pending(posting_date or nowdate()):
@ -49,6 +51,8 @@ def process_loan_interest_accrual_for_term_loans(posting_date=None, loan_type=No
loan_process.submit()
return loan_process.name
def term_loan_accrual_pending(date):
pending_accrual = frappe.db.get_value('Repayment Schedule', {
'payment_date': ('<=', date),

View File

@ -67,16 +67,16 @@ class MaintenanceSchedule(TransactionBase):
for key in scheduled_date:
description =frappe._("Reference: {0}, Item Code: {1} and Customer: {2}").format(self.name, d.item_code, self.customer)
frappe.get_doc({
event = frappe.get_doc({
"doctype": "Event",
"owner": email_map.get(d.sales_person, self.owner),
"subject": description,
"description": description,
"starts_on": cstr(key["scheduled_date"]) + " 10:00:00",
"event_type": "Private",
"ref_type": self.doctype,
"ref_name": self.name
}).insert(ignore_permissions=1)
})
event.add_participant(self.doctype, self.name)
event.insert(ignore_permissions=1)
frappe.db.set(self, 'status', 'Submitted')

View File

@ -2,6 +2,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
from frappe.utils.data import get_datetime, add_days
import frappe
import unittest
@ -9,4 +10,39 @@ import unittest
# test_records = frappe.get_test_records('Maintenance Schedule')
class TestMaintenanceSchedule(unittest.TestCase):
pass
def test_events_should_be_created_and_deleted(self):
ms = make_maintenance_schedule()
ms.generate_schedule()
ms.submit()
all_events = get_events(ms)
self.assertTrue(len(all_events) > 0)
ms.cancel()
events_after_cancel = get_events(ms)
self.assertTrue(len(events_after_cancel) == 0)
def get_events(ms):
return frappe.get_all("Event Participants", filters={
"reference_doctype": ms.doctype,
"reference_docname": ms.name,
"parenttype": "Event"
})
def make_maintenance_schedule():
ms = frappe.new_doc("Maintenance Schedule")
ms.company = "_Test Company"
ms.customer = "_Test Customer"
ms.transaction_date = get_datetime()
ms.append("items", {
"item_code": "_Test Item",
"start_date": get_datetime(),
"end_date": add_days(get_datetime(), 32),
"periodicity": "Weekly",
"no_of_visits": 4,
"sales_person": "Sales Team",
})
ms.insert(ignore_permissions=True)
return ms

View File

@ -2,6 +2,17 @@
// For license information, please see license.txt
frappe.ui.form.on('Job Card', {
setup: function(frm) {
frm.set_query('operation', function() {
return {
query: 'erpnext.manufacturing.doctype.job_card.job_card.get_operations',
filters: {
'work_order': frm.doc.work_order
}
};
});
},
refresh: function(frm) {
frappe.flags.pause_job = 0;
frappe.flags.resume_job = 0;
@ -20,12 +31,60 @@ frappe.ui.form.on('Job Card', {
}
}
frm.trigger("toggle_operation_number");
if (frm.doc.docstatus == 0 && (frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (!frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
}
},
operation: function(frm) {
frm.trigger("toggle_operation_number");
if (frm.doc.operation && frm.doc.work_order) {
frappe.call({
method: "erpnext.manufacturing.doctype.job_card.job_card.get_operation_details",
args: {
"work_order":frm.doc.work_order,
"operation":frm.doc.operation
},
callback: function (r) {
if (r.message) {
if (r.message.length == 1) {
frm.set_value("operation_id", r.message[0].name);
} else {
let args = [];
r.message.forEach((row) => {
args.push({ "label": row.idx, "value": row.name });
});
let description = __("Operation {0} added multiple times in the work order {1}",
[frm.doc.operation, frm.doc.work_order]);
frm.set_df_property("operation_row_number", "options", args);
frm.set_df_property("operation_row_number", "description", description);
}
frm.trigger("toggle_operation_number");
}
}
})
}
},
operation_row_number(frm) {
if (frm.doc.operation_row_number) {
frm.set_value("operation_id", frm.doc.operation_row_number);
}
},
toggle_operation_number(frm) {
frm.toggle_display("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
frm.toggle_reqd("operation_row_number", !frm.doc.operation_id && frm.doc.operation);
},
prepare_timer_buttons: function(frm) {
frm.trigger("make_dashboard");
if (!frm.doc.job_started) {
@ -35,9 +94,9 @@ frappe.ui.form.on('Job Card', {
fieldname: 'employee'}, d => {
if (d.employee) {
frm.set_value("employee", d.employee);
} else {
frm.events.start_job(frm);
}
frm.events.start_job(frm);
}, __("Enter Value"), __("Start"));
} else {
frm.events.start_job(frm);
@ -82,9 +141,7 @@ frappe.ui.form.on('Job Card', {
frm.set_value('current_time' , 0);
}
frm.save("Save", () => {}, "", () => {
frm.doc.time_logs.pop(-1);
});
frm.save();
},
complete_job: function(frm, completed_time, completed_qty) {
@ -116,6 +173,8 @@ frappe.ui.form.on('Job Card', {
employee: function(frm) {
if (frm.doc.job_started && !frm.doc.current_time) {
frm.trigger("reset_timer");
} else {
frm.events.start_job(frm);
}
},

View File

@ -11,6 +11,7 @@
"bom_no",
"workstation",
"operation",
"operation_row_number",
"column_break_4",
"posting_date",
"company",
@ -291,11 +292,15 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "operation_row_number",
"fieldtype": "Select",
"label": "Operation Row Number"
}
],
"is_submittable": 1,
"links": [],
"modified": "2020-04-20 15:14:00.273441",
"modified": "2020-08-24 15:21:21.398267",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card",
@ -347,7 +352,6 @@
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "operation",

View File

@ -15,10 +15,13 @@ from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings
class OverlapError(frappe.ValidationError): pass
class OperationMismatchError(frappe.ValidationError): pass
class JobCard(Document):
def validate(self):
self.validate_time_logs()
self.set_status()
self.validate_operation_id()
def validate_time_logs(self):
self.total_completed_qty = 0.0
@ -209,11 +212,10 @@ class JobCard(Document):
for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], []
field = "operation_id" if self.operation_id else "operation"
field = "operation_id"
data = frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order,
"workstation": self.workstation, field: self.get(field)})
filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)})
if data and len(data) > 0:
for_quantity = data[0].completed_qty
@ -226,14 +228,13 @@ class JobCard(Document):
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
jctl.parent = jc.name and jc.work_order = %s
and jc.workstation = %s and jc.{0} = %s and jc.docstatus = 1
""".format(field), (self.work_order, self.workstation, self.get(field)), as_dict=1)
and jc.{0} = %s and jc.docstatus = 1
""".format(field), (self.work_order, self.get(field)), as_dict=1)
wo = frappe.get_doc('Work Order', self.work_order)
work_order_field = "name" if field == "operation_id" else field
for data in wo.operations:
if data.get(work_order_field) == self.get(field) and data.workstation == self.workstation:
if data.get("name") == self.get(field):
data.completed_qty = for_quantity
data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None
@ -306,6 +307,37 @@ class JobCard(Document):
if update_status:
self.db_set('status', self.status)
def validate_operation_id(self):
if (self.get("operation_id") and self.get("operation_row_number") and self.operation and self.work_order and
frappe.get_cached_value("Work Order Operation", self.operation_row_number, "name") != self.operation_id):
work_order = frappe.bold(get_link_to_form("Work Order", self.work_order))
frappe.throw(_("Operation {0} does not belong to the work order {1}")
.format(frappe.bold(self.operation), work_order), OperationMismatchError)
@frappe.whitelist()
def get_operation_details(work_order, operation):
if work_order and operation:
return frappe.get_all("Work Order Operation", fields = ["name", "idx"],
filters = {
"parent": work_order,
"operation": operation
}
)
@frappe.whitelist()
def get_operations(doctype, txt, searchfield, start, page_len, filters):
if filters.get("work_order"):
args = {"parent": filters.get("work_order")}
if txt:
args["operation"] = ("like", "%{0}%".format(txt))
return frappe.get_all("Work Order Operation",
filters = args,
fields = ["distinct operation as operation"],
limit_start = start,
limit_page_length = page_len,
order_by="idx asc", as_list=1)
@frappe.whitelist()
def make_material_request(source_name, target_doc=None):
def update_item(obj, target, source_parent):

View File

@ -4,6 +4,72 @@
from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import random_string
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError
class TestJobCard(unittest.TestCase):
pass
def test_job_card(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name}, fields = ["operation_id", "name"])
if job_cards:
job_card = job_cards[0]
frappe.db.set_value("Job Card", job_card.name, "operation_row_number", job_card.operation_id)
doc = frappe.get_doc("Job Card", job_card.name)
doc.operation_id = "Test Data"
self.assertRaises(OperationMismatchError, doc.save)
for d in job_cards:
frappe.delete_doc("Job Card", d.name)
def test_job_card_with_different_work_station(self):
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data:
bom, bom_item = data
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
job_cards = frappe.get_all('Job Card',
filters = {'work_order': work_order.name},
fields = ["operation_id", "workstation", "name", "for_quantity"])
job_card = job_cards[0]
if job_card:
workstation = frappe.db.get_value("Workstation",
{"name": ("not in", [job_card.workstation])}, "name")
if not workstation or job_card.workstation == workstation:
workstation = make_workstation(workstation_name=random_string(5)).name
doc = frappe.get_doc("Job Card", job_card.name)
doc.workstation = workstation
doc.append("time_logs", {
"from_time": "2009-01-01 12:06:25",
"to_time": "2009-01-01 12:37:25",
"time_in_mins": "31.00002",
"completed_qty": job_card.for_quantity
})
doc.submit()
completed_qty = frappe.db.get_value("Work Order Operation", job_card.operation_id, "completed_qty")
self.assertEqual(completed_qty, job_card.for_quantity)
doc.cancel()
for d in job_cards:
frappe.delete_doc("Job Card", d.name)

View File

@ -7,7 +7,7 @@ import unittest
import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.utils import get_bin

View File

@ -20,3 +20,18 @@ class TestWorkstation(unittest.TestCase):
"_Test Workstation 1", "Operation 1", "2013-02-02 05:00:00", "2013-02-02 20:00:00")
self.assertRaises(WorkstationHolidayError, check_if_within_operating_hours,
"_Test Workstation 1", "Operation 1", "2013-02-01 10:00:00", "2013-02-02 20:00:00")
def make_workstation(**args):
args = frappe._dict(args)
try:
doc = frappe.get_doc({
"doctype": "Workstation",
"workstation_name": args.workstation_name
})
doc.insert()
return doc
except frappe.DuplicateEntryError:
return frappe.get_doc("Workstation", args.workstation_name)

View File

@ -133,7 +133,8 @@
{
"fieldname": "email_id",
"fieldtype": "Data",
"label": "Email Address"
"label": "Email Address",
"options": "Email"
},
{
"fieldname": "subscription_id",
@ -176,7 +177,7 @@
],
"image_field": "image",
"links": [],
"modified": "2020-04-07 14:20:33.215700",
"modified": "2020-08-06 10:06:01.153564",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Member",

View File

@ -9,6 +9,7 @@ from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.utils import cint
from frappe.integrations.utils import get_payment_gateway_controller
from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type
class Member(Document):
def onload(self):
@ -74,19 +75,23 @@ def get_or_create_member(user_details):
return create_member(user_details)
def create_member(user_details):
user_details = frappe._dict(user_details)
member = frappe.new_doc("Member")
member.update({
"member_name": user_details.fullname,
"email_id": user_details.email,
"pan_number": user_details.pan,
"pan_number": user_details.pan or None,
"membership_type": user_details.plan_id,
"customer": create_customer(user_details)
"subscription_id": user_details.subscription_id or None
})
member.insert(ignore_permissions=True)
member.customer = create_customer(user_details, member.name)
member.save(ignore_permissions=True)
return member
def create_customer(user_details):
def create_customer(user_details, member=None):
customer = frappe.new_doc("Customer")
customer.customer_name = user_details.fullname
customer.customer_type = "Individual"
@ -107,7 +112,13 @@ def create_customer(user_details):
"link_name": customer.name
})
contact.save()
if member:
contact.append("links", {
"link_doctype": "Member",
"link_name": member
})
contact.save(ignore_permissions=True)
except frappe.DuplicateEntryError:
return customer.name
@ -139,12 +150,31 @@ def create_member_subscription_order(user_details):
user_details = frappe._dict(user_details)
member = get_or_create_member(user_details)
if not member:
member = create_member(user_details)
subscription = member.setup_subscription()
member.subscription_id = subscription.get('subscription_id')
member.save(ignore_permissions=True)
return subscription
return subscription
@frappe.whitelist()
def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None):
plan = get_membership_type(rzpay_plan_id)
if not plan:
raise frappe.DoesNotExistError
member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id })
if member:
return member
else:
member = create_member(dict(
fullname=fullname,
email=email,
plan_id=plan,
subscription_id=subscription_id,
pan=pan,
mobile=mobile
))
return member.name

View File

@ -8,6 +8,24 @@ frappe.ui.form.on('Membership', {
})
},
refresh: function(frm) {
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
frm.call("generate_invoice", {
save: true
}).then(() => {
frm.reload_doc();
});
});
frappe.db.get_single_value("Membership Settings", "send_email").then(val => {
if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => {
frm.reload_doc();
});
});
})
},
onload: function(frm) {
frm.add_fetch('membership_type', 'amount', 'amount');
}

View File

@ -19,10 +19,10 @@
"paid",
"currency",
"amount",
"invoice",
"razorpay_details_section",
"subscription_id",
"payment_id",
"webhook_payload"
"payment_id"
],
"fields": [
{
@ -118,17 +118,15 @@
"read_only": 1
},
{
"fieldname": "webhook_payload",
"fieldtype": "Code",
"hidden": 1,
"label": "Webhook Payload",
"options": "JSON",
"read_only": 1
"fieldname": "invoice",
"fieldtype": "Link",
"label": "Invoice",
"options": "Sales Invoice"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-07-27 14:28:11.532696",
"modified": "2020-07-31 13:57:02.328995",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",

View File

@ -10,6 +10,7 @@ from datetime import datetime
from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers
from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form
from erpnext.non_profit.doctype.member.member import create_member
from frappe import _
import erpnext
@ -57,11 +58,95 @@ class Membership(Document):
self.load_from_db()
self.db_set('paid', 1)
def generate_invoice(self, save=True):
if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
if self.invoice:
frappe.throw(_("An invoice is already linked to this document"))
member = frappe.get_doc("Member", self.member)
plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings")
if not member.customer:
frappe.throw(_("No customer linked to member {}", [member.name]))
if not settings.debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings"))
if not settings.company:
frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name
if save:
self.save()
return invoice
def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings")
if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in Membership Settings"))
member = frappe.get_doc("Member", self.member)
plan = frappe.get_doc("Membership Type", self.membership_type)
email = member.email_id if member.email_id else member.email
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
if self.invoice and settings.send_invoice:
attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format))
email_template = frappe.get_doc("Email Template", settings.email_template)
context = { "doc": self, "member": member}
email_args = {
"recipients": [email],
"message": frappe.render_template(email_template.get("response"), context),
"subject": frappe.render_template(email_template.get("subject"), context),
"attachments": attachments,
"reference_doctype": self.doctype,
"reference_name": self.name
}
if not frappe.flags.in_test:
frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
else:
frappe.sendmail(**email_args)
def generate_and_send_invoice(self):
invoice = self.generate_invoice(False)
self.send_acknowlement()
def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
'doctype': 'Sales Invoice',
'customer': member.customer,
'debit_to': settings.debit_account,
'currency': membership.currency,
'is_pos': 0,
'items': [
{
'item_code': plan.linked_item,
'rate': membership.amount,
'qty': 1
}
]
})
invoice.insert(ignore_permissions=True)
invoice.submit()
return invoice
def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={
'subscription_id': subscription_id,
'email_id': email
}, order_by="creation desc")
try:
return frappe.get_doc("Member", members[0]['name'])
except:
@ -77,16 +162,15 @@ def verify_signature(data):
controller.verify_signature(data, signature, key)
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
try:
verify_signature(data)
except Exception as e:
signature = frappe.request.headers.get('X-Razorpay-Signature')
log = "{0} \n\n {1} \n\n {2} \n\n {3}".format(e, frappe.get_traceback(), signature, data)
frappe.log_error(e, "Webhook Verification Error")
log = frappe.log_error(e, "Webhook Verification Error")
notify_failure(log)
return { 'status': 'Failed', 'reason': e}
if isinstance(data, six.string_types):
data = json.loads(data)
@ -99,35 +183,42 @@ def trigger_razorpay_subscription(*args, **kwargs):
payment = frappe._dict(payment)
try:
data_json = json.dumps(data, indent=4, sort_keys=True)
if not data.event == "subscription.charged":
return
member = get_member_based_on_subscription(subscription.id, payment.email)
except Exception as e:
error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed"))
notify_failure(error_log)
return { status: 'Failed' }
if not member:
member = create_member(frappe._dict({
'fullname': payment.email,
'email': payment.email,
'plan_id': get_plan_from_razorpay_id(subscription.plan_id)
}))
if not member:
return { status: 'Failed' }
try:
if data.event == "subscription.activated":
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
elif data.event == "subscription.charged":
membership = frappe.new_doc("Membership")
membership.update({
"member": member.name,
"membership_status": "Current",
"membership_type": member.membership_type,
"currency": "INR",
"paid": 1,
"payment_id": payment.id,
"webhook_payload": data_json,
"from_date": datetime.fromtimestamp(subscription.current_start),
"to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise
})
membership.insert(ignore_permissions=True)
if subscription.notes and type(subscription.notes) == dict:
notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items())
member.add_comment("Comment", notes)
elif subscription.notes and type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes)
# Update these values anyway
# Update Membership
membership = frappe.new_doc("Membership")
membership.update({
"member": member.name,
"membership_status": "Current",
"membership_type": member.membership_type,
"currency": "INR",
"paid": 1,
"payment_id": payment.id,
"from_date": datetime.fromtimestamp(subscription.current_start),
"to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise
})
membership.insert(ignore_permissions=True)
# Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1
@ -135,9 +226,9 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e:
log = frappe.log_error(e, "Error creating membership entry")
notify_failure(log)
return { status: 'Failed' }
return { 'status': 'Failed', 'reason': e}
return { status: 'Success' }
return { 'status': 'Success' }
def notify_failure(log):
@ -152,3 +243,11 @@ Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except:
pass
def get_plan_from_razorpay_id(plan_id):
plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc")
try:
return plan[0]['name']
except:
return None

View File

@ -10,7 +10,39 @@ frappe.ui.form.on("Membership Settings", {
})
});
}
frm.set_query('inv_print_format', function(doc) {
return {
filters: {
"doc_type": "Sales Invoice"
}
};
});
frm.set_query('membership_print_format', function(doc) {
return {
filters: {
"doc_type": "Membership"
}
};
});
frm.set_query('debit_account', function(doc) {
return {
filters: {
'account_type': 'Receivable',
'is_group': 0,
'company': frm.doc.company
}
};
});
let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
frm.trigger("add_generate_button");
frm.trigger("add_copy_buttonn");
},
add_generate_button: function(frm) {
@ -27,4 +59,12 @@ frappe.ui.form.on("Membership Settings", {
});
});
},
add_copy_buttonn: function(frm) {
if (frm.doc.webhook_secret) {
frm.add_custom_button(__("Copy Webhook URL"), () => {
frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
});
}
}
});

View File

@ -9,7 +9,17 @@
"razorpay_settings_section",
"billing_cycle",
"billing_frequency",
"webhook_secret"
"webhook_secret",
"column_break_6",
"enable_auto_invoicing",
"company",
"debit_account",
"column_break_9",
"send_email",
"send_invoice",
"membership_print_format",
"inv_print_format",
"email_template"
],
"fields": [
{
@ -41,11 +51,79 @@
"fieldtype": "Password",
"label": "Webhook Secret",
"read_only": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Section Break",
"label": "Invoicing"
},
{
"default": "0",
"fieldname": "enable_auto_invoicing",
"fieldtype": "Check",
"label": "Enable Auto Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Account"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_auto_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Company"
},
{
"default": "0",
"depends_on": "eval:doc.enable_auto_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
},
{
"default": "0",
"fieldname": "send_email",
"fieldtype": "Check",
"label": "Send Membership Acknowledgement"
},
{
"depends_on": "eval: doc.send_invoice",
"fieldname": "inv_print_format",
"fieldtype": "Link",
"label": "Invoice Print Format",
"mandatory_depends_on": "eval: doc.send_invoice",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "membership_print_format",
"fieldtype": "Link",
"label": "Membership Print Format",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "email_template",
"fieldtype": "Link",
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
}
],
"issingle": 1,
"links": [],
"modified": "2020-05-22 12:38:27.103759",
"modified": "2020-08-05 17:26:37.287395",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",
@ -60,6 +138,23 @@
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Member",
"share": 1
}
],
"quick_entry": 1,

View File

@ -5,6 +5,10 @@ frappe.ui.form.on('Membership Type', {
refresh: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
})
});
frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false);
});
}
});

View File

@ -8,7 +8,8 @@
"field_order": [
"membership_type",
"amount",
"razorpay_plan_id"
"razorpay_plan_id",
"linked_item"
],
"fields": [
{
@ -33,10 +34,17 @@
"hidden": 1,
"label": "Razorpay Plan ID",
"unique": 1
},
{
"fieldname": "linked_item",
"fieldtype": "Link",
"label": "Linked Item",
"options": "Item",
"unique": 1
}
],
"links": [],
"modified": "2020-03-30 12:54:07.850857",
"modified": "2020-08-05 15:21:43.595745",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Type",

View File

@ -4,6 +4,10 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
class MembershipType(Document):
pass
def get_membership_type(razorpay_id):
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})

View File

@ -632,7 +632,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source')
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field')
erpnext.patches.v12_0.remove_bank_remittance_custom_fields
erpnext.patches.v12_0.generate_leave_ledger_entries
erpnext.patches.v12_0.generate_leave_ledger_entries #27-08-2020
execute:frappe.delete_doc_if_exists("Report", "Loan Repayment")
erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit
erpnext.patches.v12_0.add_variant_of_in_item_attribute_table
@ -697,7 +697,7 @@ erpnext.patches.v12_0.update_bom_in_so_mr
execute:frappe.delete_doc("Report", "Department Analytics")
execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True)
erpnext.patches.v12_0.update_uom_conversion_factor
execute:frappe.delete_doc_if_exists("Page", "pos") #29-05-2020
erpnext.patches.v13_0.replace_pos_page_with_point_of_sale_page
erpnext.patches.v13_0.delete_old_purchase_reports
erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions
erpnext.patches.v13_0.update_subscription
@ -722,4 +722,6 @@ erpnext.patches.v13_0.add_standard_navbar_items #4
erpnext.patches.v13_0.stock_entry_enhancements
erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v13_0.drop_razorpay_payload_column
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports

View File

@ -36,8 +36,7 @@ def generate_allocation_ledger_entries():
for allocation in allocation_list:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}):
allocation.update(dict(doctype="Leave Allocation"))
allocation_obj = frappe.get_doc(allocation)
allocation_obj = frappe.get_doc("Leave Allocation", allocation)
allocation_obj.create_leave_ledger_entry()
def generate_application_leave_ledger_entries():
@ -46,8 +45,7 @@ def generate_application_leave_ledger_entries():
for application in leave_applications:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}):
application.update(dict(doctype="Leave Application"))
frappe.get_doc(application).create_leave_ledger_entry()
frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry()
def generate_encashment_leave_ledger_entries():
''' fix ledger entries for missing leave encashment transaction '''
@ -55,8 +53,7 @@ def generate_encashment_leave_ledger_entries():
for encashment in leave_encashments:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}):
encashment.update(dict(doctype="Leave Encashment"))
frappe.get_doc(encashment).create_leave_ledger_entry()
frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry()
def generate_expiry_allocation_ledger_entries():
''' fix ledger entries for missing leave allocation transaction '''
@ -65,24 +62,16 @@ def generate_expiry_allocation_ledger_entries():
for allocation in allocation_list:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}):
allocation.update(dict(doctype="Leave Allocation"))
allocation_obj = frappe.get_doc(allocation)
allocation_obj = frappe.get_doc("Leave Allocation", allocation)
if allocation_obj.to_date <= getdate(today()):
expire_allocation(allocation_obj)
def get_allocation_records():
return frappe.get_all("Leave Allocation", filters={
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated',
'unused_leaves', 'from_date', 'to_date', 'carry_forward'
], order_by='to_date ASC')
return frappe.get_all("Leave Allocation", filters={"docstatus": 1},
fields=['name'], order_by='to_date ASC')
def get_leaves_application_records():
return frappe.get_all("Leave Application", filters={
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date'])
return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name'])
def get_leave_encashment_records():
return frappe.get_all("Leave Encashment", filters={
"docstatus": 1
}, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date'])
return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name'])

View File

@ -0,0 +1,7 @@
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists("DocType", "Membership"):
if 'webhook_payload' in frappe.db.get_table_columns("Membership"):
frappe.db.sql("alter table `tabMembership` drop column webhook_payload")

View File

@ -0,0 +1,6 @@
from __future__ import unicode_literals
import frappe
def execute():
if frappe.db.exists("Page", "point-of-sale"):
frappe.rename_doc("Page", "pos", "point-of-sale", 1, 1)

View File

@ -0,0 +1,10 @@
from __future__ import unicode_literals
import frappe
from erpnext.regional.india.setup import add_custom_roles_for_reports
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
add_custom_roles_for_reports()

View File

@ -24,4 +24,8 @@ def execute():
if not frappe.db.exists('Warehouse Type', 'Transit'):
doc = frappe.new_doc('Warehouse Type')
doc.name = 'Transit'
doc.insert()
doc.insert()
frappe.reload_doc("stock", "doctype", "stock_entry_type")
frappe.delete_doc_if_exists("Stock Entry Type", "Send to Warehouse")
frappe.delete_doc_if_exists("Stock Entry Type", "Receive at Warehouse")

View File

@ -5,8 +5,8 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.utils import getdate, date_diff
from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate
class AdditionalSalary(Document):
@ -22,9 +22,37 @@ class AdditionalSalary(Document):
def validate(self):
self.validate_dates()
self.validate_recurring_additional_salary_overlap()
if self.amount < 0:
frappe.throw(_("Amount should not be less than zero."))
def validate_recurring_additional_salary_overlap(self):
if self.is_recurring:
additional_salaries = frappe.db.sql("""
SELECT
name
FROM `tabAdditional Salary`
WHERE
employee=%s
AND name <> %s
AND docstatus=1
AND is_recurring=1
AND salary_component = %s
AND to_date >= %s
AND from_date <= %s""",
(self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1)
additional_salaries = [salary.name for salary in additional_salaries]
if additional_salaries and len(additional_salaries):
frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format(
bold(comma_and(additional_salaries)),
bold(self.salary_component),
bold(formatdate(self.from_date)),
bold(formatdate(self.to_date)
)))
def validate_dates(self):
date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee,
["date_of_joining", "relieving_date"])

View File

@ -13,13 +13,15 @@ def get_field_filter_data():
for f in fields:
doctype = f.get_link_doctype()
# apply enable/disable filter
# apply enable/disable/show_in_website filter
meta = frappe.get_meta(doctype)
filters = {}
if meta.has_field('enabled'):
filters['enabled'] = 1
if meta.has_field('disabled'):
filters['disabled'] = 0
if meta.has_field('show_in_website'):
filters['show_in_website'] = 1
values = [d.name for d in frappe.get_all(doctype, filters)]
filter_data.append([f, values])

View File

@ -503,11 +503,11 @@ erpnext.buying.get_items_from_product_bundle = function(frm) {
if(!r.exc && r.message) {
remove_empty_first_row(frm);
for ( var i=0; i< r.message.length; i++ ) {
for (var i=0; i< r.message.length; i++) {
var d = frm.add_child("items");
var item = r.message[i];
for ( var key in item) {
if ( !is_null(item[key]) ) {
for (var key in item) {
if (!is_null(item[key]) && key !== "doctype") {
d[key] = item[key];
}
}

View File

@ -781,10 +781,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
else var date = this.frm.doc.transaction_date;
if (frappe.meta.get_docfield(this.frm.doctype, "shipping_address") &&
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)){
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){
set_party_account(set_pricing);
})
// Get default company billing address in Purchase Invoice, Order and Receipt
frappe.call({
'method': 'frappe.contacts.doctype.address.address.get_default_address',
'args': {
'doctype': 'Company',
'name': this.frm.doc.company
},
'callback': function(r) {
me.frm.set_value('billing_address', r.message);
}
});
} else {
set_party_account(set_pricing);
}

View File

@ -73,6 +73,19 @@ def add_custom_roles_for_reports():
]
)).insert()
for report_name in ('HSN-wise-summary of outward supplies', 'GSTR-1', 'GSTR-2'):
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
doctype='Custom Role',
report=report_name,
roles= [
dict(role='Accounts User'),
dict(role='Accounts Manager'),
dict(role='Auditor')
]
)).insert()
def add_permissions():
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'):
add_permission(doctype, 'All', 0)

View File

@ -6,6 +6,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
shipping_address: function(frm) {
frm.trigger('get_tax_template');
},
supplier_address: function(frm) {
frm.trigger('get_tax_template');
},
tax_category: function(frm) {
frm.trigger('get_tax_template');
},

View File

@ -7,7 +7,7 @@
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2019-06-30 19:33:59.769385",
"modified": "2019-09-03 19:33:59.769385",
"modified_by": "Administrator",
"module": "Regional",
"name": "GSTR-1",
@ -16,15 +16,5 @@
"ref_doctype": "GL Entry",
"report_name": "GSTR-1",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
"roles": []
}

View File

@ -1,29 +1,19 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2018-01-29 12:59:55.650445",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2018-01-29 12:59:55.650445",
"modified_by": "Administrator",
"module": "Regional",
"name": "GSTR-2",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "GSTR-2",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2018-01-29 12:59:55.650445",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2018-09-03 12:59:55.650445",
"modified_by": "Administrator",
"module": "Regional",
"name": "GSTR-2",
"owner": "Administrator",
"ref_doctype": "GL Entry",
"report_name": "GSTR-2",
"report_type": "Script Report",
"roles": []
}

View File

@ -46,5 +46,28 @@ frappe.query_reports["HSN-wise-summary of outward supplies"] = {
],
onload: (report) => {
fetch_gstins(report);
report.page.add_inner_button(__("Download JSON"), function () {
var filters = report.get_values();
frappe.call({
method: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.get_json',
args: {
data: report.data,
report_name: report.report_name,
filters: filters
},
callback: function(r) {
if (r.message) {
const args = {
cmd: 'erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies.download_json_file',
data: r.message.data,
report_name: r.message.report_name
};
open_url_post(frappe.request.url, args);
}
}
});
});
}
};

View File

@ -1,28 +1,18 @@
{
"add_total_row": 0,
"creation": "2018-04-26 10:49:29.159400",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2019-04-26 12:59:38.603649",
"modified_by": "Administrator",
"module": "Regional",
"name": "HSN-wise-summary of outward supplies",
"owner": "Administrator",
"ref_doctype": "Sales Invoice",
"report_name": "HSN-wise-summary of outward supplies",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
"add_total_row": 0,
"creation": "2018-04-26 10:49:29.159400",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2019-09-03 12:59:38.603649",
"modified_by": "Administrator",
"module": "Regional",
"name": "HSN-wise-summary of outward supplies",
"owner": "Administrator",
"ref_doctype": "Sales Invoice",
"report_name": "HSN-wise-summary of outward supplies",
"report_type": "Script Report",
"roles": []
}

View File

@ -4,11 +4,13 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe import _
from frappe.utils import flt
from frappe.utils import flt, getdate, cstr
from frappe.model.meta import get_field_precision
from frappe.utils.xlsxutils import handle_html
from six import iteritems
import json
from erpnext.regional.india.utils import get_gst_accounts
from erpnext.regional.report.gstr_1.gstr_1 import get_company_gstin_number
def execute(filters=None):
return _execute(filters)
@ -141,7 +143,7 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
tax_details = frappe.db.sql("""
select
parent, description, item_wise_tax_detail,
parent, account_head, item_wise_tax_detail,
base_tax_amount_after_discount_amount
from `tab%s`
where
@ -153,11 +155,11 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
""" % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions),
tuple([doctype] + list(invoice_item_row)))
for parent, description, item_wise_tax_detail, tax_amount in tax_details:
description = handle_html(description)
if description not in tax_columns and tax_amount:
for parent, account_head, item_wise_tax_detail, tax_amount in tax_details:
if account_head not in tax_columns and tax_amount:
# as description is text editor earlier and markup can break the column convention in reports
tax_columns.append(description)
tax_columns.append(account_head)
if item_wise_tax_detail:
try:
@ -175,17 +177,17 @@ def get_tax_accounts(item_list, columns, company_currency, doctype="Sales Invoic
for d in item_row_map.get(parent, {}).get(item_code, []):
item_tax_amount = tax_amount
if item_tax_amount:
itemised_tax.setdefault((parent, item_code), {})[description] = frappe._dict({
itemised_tax.setdefault((parent, item_code), {})[account_head] = frappe._dict({
"tax_amount": flt(item_tax_amount, tax_amount_precision)
})
except ValueError:
continue
tax_columns.sort()
for desc in tax_columns:
for account_head in tax_columns:
columns.append({
"label": desc,
"fieldname": frappe.scrub(desc),
"label": account_head,
"fieldname": frappe.scrub(account_head),
"fieldtype": "Float",
"width": 110
})
@ -212,3 +214,76 @@ def get_merged_data(columns, data):
return result
@frappe.whitelist()
def get_json(filters, report_name, data):
filters = json.loads(filters)
report_data = json.loads(data)
gstin = filters.get('company_gstin') or get_company_gstin_number(filters["company"])
if not filters.get('from_date') or not filters.get('to_date'):
frappe.throw(_("Please enter From Date and To Date to generate JSON"))
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
gst_json = {"version": "GST2.3.4",
"hash": "hash", "gstin": gstin, "fp": fp}
gst_json["hsn"] = {
"data": get_hsn_wise_json_data(filters, report_data)
}
return {
'report_name': report_name,
'data': gst_json
}
@frappe.whitelist()
def download_json_file():
'''download json content in a file'''
data = frappe._dict(frappe.local.form_dict)
frappe.response['filename'] = frappe.scrub("{0}".format(data['report_name'])) + '.json'
frappe.response['filecontent'] = data['data']
frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download'
def get_hsn_wise_json_data(filters, report_data):
filters = frappe._dict(filters)
gst_accounts = get_gst_accounts(filters.company)
data = []
count = 1
for hsn in report_data:
row = {
"num": count,
"hsn_sc": hsn.get("gst_hsn_code"),
"desc": hsn.get("description"),
"uqc": hsn.get("stock_uom").upper(),
"qty": hsn.get("stock_qty"),
"val": flt(hsn.get("total_amount"), 2),
"txval": flt(hsn.get("taxable_amount", 2)),
"iamt": 0.0,
"camt": 0.0,
"samt": 0.0,
"csamt": 0.0
}
for account in gst_accounts.get('igst_account'):
row['iamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
for account in gst_accounts.get('cgst_account'):
row['camt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
for account in gst_accounts.get('sgst_account'):
row['samt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
for account in gst_accounts.get('cess_account'):
row['csamt'] += flt(hsn.get(frappe.scrub(cstr(account)), 0.0), 2)
data.append(row)
count +=1
return data

View File

@ -16,9 +16,15 @@ from frappe.utils.jinja import render_template
def execute(filters=None):
filters = filters if isinstance(filters, _dict) else _dict(filters)
if not filters:
filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0])
filters.setdefault('company', frappe.db.get_default("company"))
region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company })
if region != 'United States':
return [],[]
data = []
columns = get_columns()
data = frappe.db.sql("""

View File

@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase):
def test_irs_1099_report(self):
make_payment_entry_to_irs_1099_supplier()
filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"})
filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"})
columns, data = execute_1099_report(filters)
print(columns, data)
expected_row = {'supplier': '_US 1099 Test Supplier',
@ -42,10 +42,10 @@ def make_payment_entry_to_irs_1099_supplier():
pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Pay"
pe.company = "_Test Company"
pe.company = "_Test Company 1"
pe.posting_date = "2016-01-10"
pe.paid_from = "_Test Bank USD - _TC"
pe.paid_to = "_Test Payable USD - _TC"
pe.paid_from = "_Test Bank USD - _TC1"
pe.paid_to = "_Test Payable USD - _TC1"
pe.paid_amount = 100
pe.received_amount = 100
pe.reference_no = "For IRS 1099 testing"

View File

@ -184,10 +184,10 @@ class Customer(TransactionBase):
def validate_credit_limit_on_change(self):
if self.get("__islocal") or not self.credit_limits:
return
past_credit_limits = [d.credit_limit
for d in frappe.db.get_all("Customer Credit Limit", filters={'parent': self.name}, fields=["credit_limit"], order_by="company")]
current_credit_limits = [d.credit_limit for d in sorted(self.credit_limits, key=lambda k: k.company)]
if past_credit_limits == current_credit_limits:
@ -396,13 +396,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager")
# form a list of emails and names to show to the user
credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users]
if not credit_controller_users:
credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users]
if not credit_controller_users_formatted:
frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer)))
message = """Please contact any of the following users to extend the credit limits for {0}:
<br><br><ul><li>{1}</li></ul>""".format(customer, '<li>'.join(credit_controller_users))
<br><br><ul><li>{1}</li></ul>""".format(customer, '<li>'.join(credit_controller_users_formatted))
# if the current user does not have permissions to override credit limit,
# prompt them to send out an email to the controller users
@ -427,7 +426,7 @@ def send_emails(args):
subject = (_("Credit limit reached for customer {0}").format(args.get('customer')))
message = (_("Credit limit has been crossed for customer {0} ({1}/{2})")
.format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit')))
frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message)
frappe.sendmail(recipients=args.get('credit_controller_users_list'), subject=subject, message=message)
def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None):
# Outstanding based on GL Entries

View File

@ -20,29 +20,28 @@ def get_funnel_data(from_date, to_date, company):
validate_filters(from_date, to_date, company)
active_leads = frappe.db.sql("""select count(*) from `tabLead`
where (date(`modified`) between %s and %s)
and status != "Do Not Contact" and company=%s""", (from_date, to_date, company))[0][0]
active_leads += frappe.db.sql("""select count(distinct contact.name) from `tabContact` contact
left join `tabDynamic Link` dl on (dl.parent=contact.name) where dl.link_doctype='Customer'
and (date(contact.modified) between %s and %s) and status != "Passive" """, (from_date, to_date))[0][0]
where (date(`creation`) between %s and %s)
and company=%s""", (from_date, to_date, company))[0][0]
opportunities = frappe.db.sql("""select count(*) from `tabOpportunity`
where (date(`creation`) between %s and %s)
and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0]
and opportunity_from='Lead' and company=%s""", (from_date, to_date, company))[0][0]
quotations = frappe.db.sql("""select count(*) from `tabQuotation`
where docstatus = 1 and (date(`creation`) between %s and %s)
and status != "Lost" and company=%s""", (from_date, to_date, company))[0][0]
and (opportunity!="" or quotation_to="Lead") and company=%s""", (from_date, to_date, company))[0][0]
converted = frappe.db.sql("""select count(*) from `tabCustomer`
JOIN `tabLead` ON `tabLead`.name = `tabCustomer`.lead_name
WHERE (date(`tabCustomer`.creation) between %s and %s)
and `tabLead`.company=%s""", (from_date, to_date, company))[0][0]
sales_orders = frappe.db.sql("""select count(*) from `tabSales Order`
where docstatus = 1 and (date(`creation`) between %s and %s) and company=%s""", (from_date, to_date, company))[0][0]
return [
{ "title": _("Active Leads / Customers"), "value": active_leads, "color": "#B03B46" },
{ "title": _("Active Leads"), "value": active_leads, "color": "#B03B46" },
{ "title": _("Opportunities"), "value": opportunities, "color": "#F09C00" },
{ "title": _("Quotations"), "value": quotations, "color": "#006685" },
{ "title": _("Sales Orders"), "value": sales_orders, "color": "#00AD65" }
{ "title": _("Converted"), "value": converted, "color": "#00AD65" }
]
@frappe.whitelist()

View File

@ -63,13 +63,13 @@ def get_columns(filters, period_list, partner_doctype):
"label": _(partner_doctype),
"fieldtype": "Link",
"options": partner_doctype,
"width": 100
"width": 150
}, {
"fieldname": "item_group",
"label": _("Item Group"),
"fieldtype": "Link",
"options": "Item Group",
"width": 100
"width": 150
}]
for period in period_list:
@ -81,19 +81,19 @@ def get_columns(filters, period_list, partner_doctype):
"label": _("Target ({})").format(period.label),
"fieldtype": fieldtype,
"options": options,
"width": 100
"width": 150
}, {
"fieldname": period.key,
"label": _("Achieved ({})").format(period.label),
"fieldtype": fieldtype,
"options": options,
"width": 100
"width": 150
}, {
"fieldname": variance_key,
"label": _("Variance ({})").format(period.label),
"fieldtype": fieldtype,
"options": options,
"width": 100
"width": 150
}])
columns.extend([{
@ -101,19 +101,19 @@ def get_columns(filters, period_list, partner_doctype):
"label": _("Total Target"),
"fieldtype": fieldtype,
"options": options,
"width": 100
"width": 150
}, {
"fieldname": "total_achieved",
"label": _("Total Achieved"),
"fieldtype": fieldtype,
"options": options,
"width": 100
"width": 150
}, {
"fieldname": "total_variance",
"label": _("Total Variance"),
"fieldtype": fieldtype,
"options": options,
"width": 100
"width": 150
}])
return columns
@ -154,10 +154,10 @@ def prepare_data(filters, sales_users_data, actual_data, date_field, period_list
if (r.get(sales_field) == d.parent and r.item_group == d.item_group and
period.from_date <= r.get(date_field) and r.get(date_field) <= period.to_date):
details[p_key] += r.get(qty_or_amount_field, 0)
details[variance_key] = details.get(target_key) - details.get(p_key)
details[variance_key] = details.get(p_key) - details.get(target_key)
details["total_achieved"] += details.get(p_key)
details["total_variance"] = details.get("total_target") - details.get("total_achieved")
details["total_variance"] = details.get("total_achieved") - details.get("total_target")
return rows

View File

@ -44,5 +44,20 @@ frappe.query_reports["Sales Partner Target Variance based on Item Group"] = {
options: "Quantity\nAmount",
default: "Quantity"
},
]
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes('variance')) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
}

View File

@ -44,5 +44,20 @@ frappe.query_reports["Sales Person Target Variance Based On Item Group"] = {
options: "Quantity\nAmount",
default: "Quantity"
},
]
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes('variance')) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
}

View File

@ -44,5 +44,20 @@ frappe.query_reports["Territory Target Variance Based On Item Group"] = {
options: "Quantity\nAmount",
default: "Quantity"
},
]
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname.includes('variance')) {
if (data[column.fieldname] < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data[column.fieldname] > 0) {
value = "<span style='color:green'>" + value + "</span>";
}
}
return value;
}
}

View File

@ -20,6 +20,7 @@ def after_install():
frappe.get_doc({'doctype': "Role", "role_name": "Analytics"}).insert()
set_single_defaults()
create_compact_item_print_custom_field()
create_print_uom_after_qty_custom_field()
create_print_zero_amount_taxes_custom_field()
add_all_roles_to("Administrator")
create_default_cash_flow_mapper_templates()
@ -66,6 +67,16 @@ def create_compact_item_print_custom_field():
})
def create_print_uom_after_qty_custom_field():
create_custom_field('Print Settings', {
'label': _('Print UOM after Quantity'),
'fieldname': 'print_uom_after_quantity',
'fieldtype': 'Check',
'default': 0,
'insert_after': 'compact_item_print'
})
def create_print_zero_amount_taxes_custom_field():
create_custom_field('Print Settings', {
'label': _('Print taxes with zero amount'),

View File

@ -123,7 +123,8 @@ def get_all_suppliers(date_range, company, field, limit = None):
if field == "outstanding_amount":
filters = [['docstatus', '=', '1'], ['company', '=', company]]
if date_range:
filters.append(['posting_date', 'between' [date_range[0], date_range[1]]])
date_range = frappe.parse_json(date_range)
filters.append(['posting_date', 'between', [date_range[0], date_range[1]]])
return frappe.db.get_all('Purchase Invoice',
fields = ['supplier as name', 'sum(outstanding_amount) as value'],
filters = filters,

View File

@ -33,7 +33,7 @@
{
"hidden": 0,
"label": "Key Reports",
"links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]"
"links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Qty vs Serial No Count\",\n \"name\": \"Stock Qty vs Serial No Count\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]"
},
{
"hidden": 0,
@ -58,7 +58,7 @@
"idx": 0,
"is_standard": 1,
"label": "Stock",
"modified": "2020-05-30 17:32:11.062681",
"modified": "2020-08-11 17:29:32.626067",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",

View File

@ -513,7 +513,7 @@ class StockEntry(StockController):
d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
d.basic_amount = d.basic_rate * d.qty
d.basic_amount = d.basic_rate * flt(d.qty)
def distribute_additional_costs(self):
if self.purpose == "Material Issue":

View File

@ -258,6 +258,7 @@ class StockReconciliation(StockController):
sl_entries.append(args)
qty_after_transaction = 0
for serial_no in serial_nos:
args = self.get_sle_for_items(row, [serial_no])
@ -271,11 +272,19 @@ class StockReconciliation(StockController):
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
# If serial no exists in different warehouse
warehouse = previous_sle.get("warehouse", '') or row.warehouse
if not qty_after_transaction:
qty_after_transaction = get_stock_balance(row.item_code,
warehouse, self.posting_date, self.posting_time)
qty_after_transaction -= 1
new_args = args.copy()
new_args.update({
'actual_qty': -1,
'qty_after_transaction': cint(previous_sle.get('qty_after_transaction')) - 1,
'warehouse': previous_sle.get("warehouse", '') or row.warehouse,
'qty_after_transaction': qty_after_transaction,
'warehouse': warehouse,
'valuation_rate': previous_sle.get("valuation_rate")
})

View File

@ -0,0 +1,42 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Stock Qty vs Serial No Count"] = {
"filters": [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"warehouse",
"label": __("Warehouse"),
"fieldtype": "Link",
"options": "Warehouse",
"get_query": function() {
const company = frappe.query_report.get_filter_value('company');
return {
filters: { 'company': company }
}
},
"reqd": 1
},
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.fieldname == "difference" && data) {
if (data.difference > 0) {
value = "<span style='color:red'>" + value + "</span>";
}
else if (data.difference < 0) {
value = "<span style='color:red'>" + value + "</span>";
}
}
return value;
}
};

View File

@ -0,0 +1,27 @@
{
"add_total_row": 0,
"creation": "2020-07-23 19:31:32.395011",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 0,
"is_standard": "Yes",
"modified": "2020-07-23 19:32:02.168185",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Qty vs Serial No Count",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Item",
"report_name": "Stock Qty vs Serial No Count",
"report_type": "Script Report",
"roles": [
{
"role": "Stock Manager"
},
{
"role": "Stock User"
}
]
}

View File

@ -0,0 +1,80 @@
# 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 = get_columns()
data = get_data(filters.warehouse)
return columns, data
def get_columns():
columns = [
{
"label": _("Item Code"),
"fieldname": "item_code",
"fieldtype": "Link",
"options": "Item",
"width": 200
},
{
"label": _("Item Name"),
"fieldname": "item_name",
"fieldtype": "Data",
"width": 200
},
{
"label": _("Serial No Count"),
"fieldname": "total",
"fieldtype": "Float",
"width": 150
},
{
"label": _("Stock Qty"),
"fieldname": "stock_qty",
"fieldtype": "Float",
"width": 150
},
{
"label": _("Difference"),
"fieldname": "difference",
"fieldtype": "Float",
"width": 150
},
]
return columns
def get_data(warehouse):
serial_item_list = frappe.get_all("Item", filters={
'has_serial_no': True,
}, fields=['item_code', 'item_name'])
status_list = ['Active', 'Expired']
data = []
for item in serial_item_list:
total_serial_no = frappe.db.count("Serial No",
filters={"item_code": item.item_code, "status": ("in", status_list), "warehouse": warehouse})
actual_qty = frappe.db.get_value('Bin', fieldname=['actual_qty'],
filters={"warehouse": warehouse, "item_code": item.item_code})
# frappe.db.get_value returns null if no record exist.
if not actual_qty:
actual_qty = 0
difference = total_serial_no - actual_qty
row = {
"item_code": item.item_code,
"item_name": item.item_name,
"total": total_serial_no,
"stock_qty": actual_qty,
"difference": difference,
}
data.append(row)
return data

View File

@ -27,22 +27,25 @@
{% endif %}
</div>
{% endif %}
{% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
<div class="mt-3">
<a href="/cart"
class="btn btn-light btn-view-in-cart {% if not product_info.qty %}hidden{% endif %}"
role="button"
>
{{ _("View in Cart") }}
</a>
<button
data-item-code="{{item_code}}"
class="btn btn-outline-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %}"
>
{{ _("Add to Cart") }}
</button>
{% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
<a href="/cart"
class="btn btn-light btn-view-in-cart {% if not product_info.qty %}hidden{% endif %}"
role="button"
>
{{ _("View in Cart") }}
</a>
<button
data-item-code="{{item_code}}"
class="btn btn-outline-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %}"
>
{{ _("Add to Cart") }}
</button>
{% endif %}
{% if cart_settings.show_contact_us_button %}
{% include "templates/generators/item/item_inquiry.html" %}
{% endif %}
</div>
{% endif %}
</div>
</div>

View File

@ -10,14 +10,11 @@
{{ _('Configure') }}
</button>
{% endif %}
{% if cart_settings.show_contact_us_button | int %}
<button class="btn btn-link btn-inquiry" data-item-code="{{ doc.name }}">
{{ _('Contact Us') }}
</button>
{% if cart_settings.show_contact_us_button %}
{% include "templates/generators/item/item_inquiry.html" %}
{% endif %}
</div>
<script>
{% include "templates/generators/item/item_configure.js" %}
{% include "templates/generators/item/item_inquiry.js" %}
</script>
{% endif %}

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