Merge branch 'develop' of https://github.com/frappe/erpnext into project-link-for-all-accounts

This commit is contained in:
Deepesh Garg 2020-06-19 11:41:21 +05:30
commit 3c63f5b76b
176 changed files with 4541 additions and 2499 deletions

14
.github/workflows/docker-release.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: Trigger Docker build on release
on:
release:
types: [created]
jobs:
curl:
runs-on: ubuntu-latest
container:
image: alpine:latest
steps:
- name: curl
run: |
apk add curl bash
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests

View File

@ -3,17 +3,16 @@
# These owners will be the default owners for everything in # These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
* @nabinhait manufacturing/ @rohitwaghchaure @marination
manufacturing/ @rohitwaghchaure
accounts/ @deepeshgarg007 @nextchamp-saqib accounts/ @deepeshgarg007 @nextchamp-saqib
loan_management/ @deepeshgarg007 loan_management/ @deepeshgarg007 @rohitwaghchaure
pos* @nextchamp-saqib pos* @nextchamp-saqib @rohitwaghchaure
assets/ @nextchamp-saqib assets/ @nextchamp-saqib @deepeshgarg007
stock/ @marination @rohitwaghchaure stock/ @marination @rohitwaghchaure
buying/ @marination @rohitwaghchaure buying/ @marination @deepeshgarg007
hr/ @Anurag810 hr/ @Anurag810 @rohitwaghchaure
projects/ @hrwX projects/ @hrwX @nextchamp-saqib
support/ @hrwX support/ @hrwX @marination
healthcare/ @ruchamahabal healthcare/ @ruchamahabal @marination
erpnext_integrations/ @Mangesh-Khairnar erpnext_integrations/ @Mangesh-Khairnar @nextchamp-saqib
requirements.txt @gavindsouza requirements.txt @gavindsouza

View File

@ -5,7 +5,22 @@ import frappe
import json import json
from frappe.utils import nowdate, add_months, get_date_str from frappe.utils import nowdate, add_months, get_date_str
from frappe import _ from frappe import _
from erpnext.accounts.utils import get_fiscal_year, get_account_name from erpnext.accounts.utils import get_fiscal_year, get_account_name, FiscalYearError
def _get_fiscal_year(date=None):
try:
fiscal_year = get_fiscal_year(date=nowdate(), as_dict=True)
return fiscal_year
except FiscalYearError:
#if no fiscal year for current date then get default fiscal year
try:
fiscal_year = get_fiscal_year(as_dict=True)
return fiscal_year
except FiscalYearError:
#if still no fiscal year found then no accounting data created, return
return None
def get_company_for_dashboards(): def get_company_for_dashboards():
company = frappe.defaults.get_defaults().company company = frappe.defaults.get_defaults().company
@ -18,10 +33,16 @@ def get_company_for_dashboards():
return None return None
def get_data(): def get_data():
fiscal_year = _get_fiscal_year(nowdate())
if not fiscal_year:
return frappe._dict()
return frappe._dict({ return frappe._dict({
"dashboards": get_dashboards(), "dashboards": get_dashboards(),
"charts": get_charts(), "charts": get_charts(fiscal_year),
"number_cards": get_number_cards() "number_cards": get_number_cards(fiscal_year)
}) })
def get_dashboards(): def get_dashboards():
@ -46,10 +67,9 @@ def get_dashboards():
] ]
}] }]
def get_charts(): def get_charts(fiscal_year):
company = frappe.get_doc("Company", get_company_for_dashboards()) company = frappe.get_doc("Company", get_company_for_dashboards())
bank_account = company.default_bank_account or get_account_name("Bank", company=company.name) bank_account = company.default_bank_account or get_account_name("Bank", company=company.name)
fiscal_year = get_fiscal_year(date=nowdate())
default_cost_center = company.cost_center default_cost_center = company.cost_center
return [ return [
@ -61,8 +81,8 @@ def get_charts():
"filters_json": json.dumps({ "filters_json": json.dumps({
"company": company.name, "company": company.name,
"filter_based_on": "Fiscal Year", "filter_based_on": "Fiscal Year",
"from_fiscal_year": fiscal_year[0], "from_fiscal_year": fiscal_year.get('name'),
"to_fiscal_year": fiscal_year[0], "to_fiscal_year": fiscal_year.get('name'),
"periodicity": "Monthly", "periodicity": "Monthly",
"include_default_book_entries": 1 "include_default_book_entries": 1
}), }),
@ -158,8 +178,8 @@ def get_charts():
"report_name": "Budget Variance Report", "report_name": "Budget Variance Report",
"filters_json": json.dumps({ "filters_json": json.dumps({
"company": company.name, "company": company.name,
"from_fiscal_year": fiscal_year[0], "from_fiscal_year": fiscal_year.get('name'),
"to_fiscal_year": fiscal_year[0], "to_fiscal_year": fiscal_year.get('name'),
"period": "Monthly", "period": "Monthly",
"budget_against": "Cost Center" "budget_against": "Cost Center"
}), }),
@ -190,10 +210,10 @@ def get_charts():
}, },
] ]
def get_number_cards(): def get_number_cards(fiscal_year):
fiscal_year = get_fiscal_year(date=nowdate())
year_start_date = get_date_str(fiscal_year[1]) year_start_date = get_date_str(fiscal_year.get("year_start_date"))
year_end_date = get_date_str(fiscal_year[2]) year_end_date = get_date_str(fiscal_year.get("year_end_date"))
return [ return [
{ {
"doctype": "Number Card", "doctype": "Number Card",

View File

@ -14,6 +14,9 @@ frappe.treeview_settings["Account"] = {
on_change: function() { on_change: function() {
var me = frappe.treeview_settings['Account'].treeview; var me = frappe.treeview_settings['Account'].treeview;
var company = me.page.fields_dict.company.get_value(); var company = me.page.fields_dict.company.get_value();
if (!company) {
frappe.throw(__("Please set a Company"));
}
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.account.account.get_root_company", method: "erpnext.accounts.doctype.account.account.get_root_company",
args: { args: {

View File

@ -72,7 +72,11 @@ def make_dimension_in_accounting_doctypes(doc):
if doctype == "Budget": if doctype == "Budget":
add_dimension_to_budget_doctype(df, doc) add_dimension_to_budget_doctype(df, doc)
else: else:
create_custom_field(doctype, df) meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
if df['fieldname'] not in fieldnames:
create_custom_field(doctype, df)
count += 1 count += 1

View File

@ -14,7 +14,18 @@ frappe.ui.form.on('Cost Center', {
is_group: 1 is_group: 1
} }
} }
}) });
frm.set_query("cost_center", "distributed_cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
enable_distributed_cost_center: 0,
name: ['!=', frm.doc.name]
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {
if (!frm.is_new()) { if (!frm.is_new()) {

View File

@ -16,6 +16,9 @@
"cb0", "cb0",
"is_group", "is_group",
"disabled", "disabled",
"section_break_9",
"enable_distributed_cost_center",
"distributed_cost_center",
"lft", "lft",
"rgt", "rgt",
"old_parent" "old_parent"
@ -119,13 +122,31 @@
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "label": "Disabled"
},
{
"default": "0",
"fieldname": "enable_distributed_cost_center",
"fieldtype": "Check",
"label": "Enable Distributed Cost Center"
},
{
"depends_on": "eval:doc.is_group==0",
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"depends_on": "enable_distributed_cost_center",
"fieldname": "distributed_cost_center",
"fieldtype": "Table",
"label": "Distributed Cost Center",
"options": "Distributed Cost Center"
} }
], ],
"icon": "fa fa-money", "icon": "fa fa-money",
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-04-29 16:09:30.025214", "modified": "2020-06-17 16:09:30.025214",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Cost Center", "name": "Cost Center",

View File

@ -19,6 +19,24 @@ class CostCenter(NestedSet):
def validate(self): def validate(self):
self.validate_mandatory() self.validate_mandatory()
self.validate_parent_cost_center() self.validate_parent_cost_center()
self.validate_distributed_cost_center()
def validate_distributed_cost_center(self):
if cint(self.enable_distributed_cost_center):
if not self.distributed_cost_center:
frappe.throw(_("Please enter distributed cost center"))
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
if not self.get('__islocal'):
if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
and self.check_if_part_of_distributed_cost_center():
frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
else:
self.distributed_cost_center = []
def validate_mandatory(self): def validate_mandatory(self):
if self.cost_center_name != self.company and not self.parent_cost_center: if self.cost_center_name != self.company and not self.parent_cost_center:
@ -43,12 +61,15 @@ class CostCenter(NestedSet):
return 1 return 1
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center):
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
if self.check_if_part_of_distributed_cost_center():
frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group"))
if self.check_gle_exists(): if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group")) frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
else: self.is_group = 1
self.is_group = 1 self.save()
self.save() return 1
return 1
def check_gle_exists(self): def check_gle_exists(self):
return frappe.db.get_value("GL Entry", {"cost_center": self.name}) return frappe.db.get_value("GL Entry", {"cost_center": self.name})
@ -57,6 +78,9 @@ class CostCenter(NestedSet):
return frappe.db.sql("select name from `tabCost Center` where \ return frappe.db.sql("select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2", self.name) parent_cost_center = %s and docstatus != 2", self.name)
def check_if_part_of_distributed_cost_center(self):
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
def before_rename(self, olddn, newdn, merge=False): def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided # Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr from erpnext.setup.doctype.company.company import get_name_with_abbr
@ -100,3 +124,7 @@ def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit(): if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account new_account = account_number + " - " + new_account
return new_account return new_account
def check_if_distributed_cost_center_enabled(cost_center_list):
value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
return next((True for x in value_list if x[0]), False)

View File

@ -22,6 +22,33 @@ class TestCostCenter(unittest.TestCase):
self.assertRaises(frappe.ValidationError, cost_center.save) self.assertRaises(frappe.ValidationError, cost_center.save)
def test_validate_distributed_cost_center(self):
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
frappe.get_doc(test_records[0]).insert()
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
frappe.get_doc(test_records[1]).insert()
invalid_distributed_cost_center = frappe.get_doc({
"company": "_Test Company",
"cost_center_name": "_Test Distributed Cost Center",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC",
"enable_distributed_cost_center": 1,
"distributed_cost_center": [{
"cost_center": "_Test Cost Center - _TC",
"percentage_allocation": 40
}, {
"cost_center": "_Test Cost Center 2 - _TC",
"percentage_allocation": 50
}
]
})
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
def create_cost_center(**args): def create_cost_center(**args):
args = frappe._dict(args) args = frappe._dict(args)
if args.cost_center_name: if args.cost_center_name:

View File

@ -0,0 +1,40 @@
{
"actions": [],
"creation": "2020-03-19 12:34:01.500390",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cost_center",
"percentage_allocation"
],
"fields": [
{
"fieldname": "cost_center",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Cost Center",
"options": "Cost Center",
"reqd": 1
},
{
"fieldname": "percentage_allocation",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Percentage Allocation",
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-03-19 12:54:43.674655",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Distributed Cost Center",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@ -191,6 +191,7 @@
{ {
"fieldname": "total_debit", "fieldname": "total_debit",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1,
"label": "Total Debit", "label": "Total Debit",
"no_copy": 1, "no_copy": 1,
"oldfieldname": "total_debit", "oldfieldname": "total_debit",
@ -252,7 +253,6 @@
"fieldname": "total_amount", "fieldname": "total_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 1, "hidden": 1,
"in_list_view": 1,
"label": "Total Amount", "label": "Total Amount",
"no_copy": 1, "no_copy": 1,
"options": "total_amount_currency", "options": "total_amount_currency",
@ -503,7 +503,7 @@
"idx": 176, "idx": 176,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-29 10:55:28.240916", "modified": "2020-06-02 18:15:46.955697",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry", "name": "Journal Entry",

View File

@ -18,6 +18,7 @@
"accounting_dimensions_section", "accounting_dimensions_section",
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project",
"currency_section", "currency_section",
"account_currency", "account_currency",
"column_break_10", "column_break_10",
@ -32,7 +33,6 @@
"reference_type", "reference_type",
"reference_name", "reference_name",
"reference_due_date", "reference_due_date",
"project",
"col_break3", "col_break3",
"is_advance", "is_advance",
"user_remark", "user_remark",
@ -273,7 +273,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-25 01:47:49.060128", "modified": "2020-06-18 14:06:54.833738",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Journal Entry Account", "name": "Journal Entry Account",

View File

@ -319,7 +319,7 @@ class PaymentEntry(AccountsController):
invoice_payment_amount_map.setdefault(key, 0.0) invoice_payment_amount_map.setdefault(key, 0.0)
invoice_payment_amount_map[key] += reference.allocated_amount invoice_payment_amount_map[key] += reference.allocated_amount
if not invoice_paid_amount_map.get(reference.reference_name): if not invoice_paid_amount_map.get(key):
payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name}, payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name},
fields=['paid_amount', 'payment_amount', 'payment_term']) fields=['paid_amount', 'payment_amount', 'payment_term'])
for term in payment_schedule: for term in payment_schedule:
@ -332,12 +332,14 @@ class PaymentEntry(AccountsController):
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
else: else:
outstanding = invoice_paid_amount_map.get(key)['outstanding'] outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
if amount > outstanding: if amount > outstanding:
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s if amount and outstanding:
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
def set_status(self): def set_status(self):
if self.docstatus == 2: if self.docstatus == 2:
@ -1091,17 +1093,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
references = [] references = []
for payment_term in payment_schedule: for payment_term in payment_schedule:
references.append({ payment_term_outstanding = flt(payment_term.payment_amount - payment_term.paid_amount,
'reference_doctype': dt,
'reference_name': dn,
'bill_no': doc.get('bill_no'),
'due_date': doc.get('due_date'),
'total_amount': grand_total,
'outstanding_amount': outstanding_amount,
'payment_term': payment_term.payment_term,
'allocated_amount': flt(payment_term.payment_amount - payment_term.paid_amount,
payment_term.precision('payment_amount')) payment_term.precision('payment_amount'))
})
if payment_term_outstanding:
references.append({
'reference_doctype': dt,
'reference_name': dn,
'bill_no': doc.get('bill_no'),
'due_date': doc.get('due_date'),
'total_amount': grand_total,
'outstanding_amount': outstanding_amount,
'payment_term': payment_term.payment_term,
'allocated_amount': payment_term_outstanding
})
return references return references

View File

@ -349,9 +349,10 @@
"read_only": 1 "read_only": 1
} }
], ],
"in_create": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-08 10:23:02.815237", "modified": "2020-05-29 17:38:49.392713",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Request", "name": "Payment Request",

View File

@ -582,14 +582,14 @@ class SalesInvoice(SellingController):
def validate_item_code(self): def validate_item_code(self):
for d in self.get('items'): for d in self.get('items'):
if not d.item_code: if not d.item_code and self.is_opening == "No":
msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True)
def validate_warehouse(self): def validate_warehouse(self):
super(SalesInvoice, self).validate_warehouse() super(SalesInvoice, self).validate_warehouse()
for d in self.get_item_list(): for d in self.get_item_list():
if not d.warehouse and frappe.get_cached_value("Item", d.item_code, "is_stock_item"): if not d.warehouse and d.item_code and frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code)) frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code))
def validate_delivery_note(self): def validate_delivery_note(self):
@ -1454,11 +1454,17 @@ def get_inter_company_details(doc, doctype):
parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}) parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company})
company = frappe.get_cached_value("Customer", doc.customer, "represents_company") company = frappe.get_cached_value("Customer", doc.customer, "represents_company")
if not parties:
frappe.throw(_('No Supplier found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company)))
party = get_internal_party(parties, "Supplier", doc) party = get_internal_party(parties, "Supplier", doc)
else: else:
parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}) parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company})
company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company") company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company")
if not parties:
frappe.throw(_('No Customer found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company)))
party = get_internal_party(parties, "Customer", doc) party = get_internal_party(parties, "Customer", doc)
return { return {

View File

@ -1772,53 +1772,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30") check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_error_email(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
from erpnext.accounts.deferred_revenue import convert_deferred_revenue_to_income
acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
acc_settings.acc_frozen_upto = '2019-01-31'
acc_settings.save()
pda = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda.insert()
pda.submit()
email = frappe.db.sql(""" select name from `tabEmail Queue`
where message like %(txt)s """, {
'txt': "%%%s%%" % "Error while processing deferred accounting for {0}".format(pda.name)
})
self.assertTrue(email)
acc_settings.load_from_db()
acc_settings.acc_frozen_upto = None
acc_settings.save()
def test_inter_company_transaction(self): def test_inter_company_transaction(self):
if not frappe.db.exists("Customer", "_Test Internal Customer"): if not frappe.db.exists("Customer", "_Test Internal Customer"):

View File

@ -45,7 +45,9 @@ class ShippingRule(Document):
shipping_amount = 0.0 shipping_amount = 0.0
by_value = False by_value = False
self.validate_countries(doc) if doc.get_shipping_address():
# validate country only if there is address
self.validate_countries(doc)
if self.calculate_based_on == 'Net Total': if self.calculate_based_on == 'Net Total':
value = doc.base_net_total value = doc.base_net_total

View File

@ -169,9 +169,11 @@ class ReceivablePayableReport(object):
def append_subtotal_row(self, party): def append_subtotal_row(self, party):
sub_total_row = self.total_row_map.get(party) sub_total_row = self.total_row_map.get(party)
self.data.append(sub_total_row)
self.data.append({}) if sub_total_row:
self.update_sub_total_row(sub_total_row, 'Total') self.data.append(sub_total_row)
self.data.append({})
self.update_sub_total_row(sub_total_row, 'Total')
def get_voucher_balance(self, gle): def get_voucher_balance(self, gle):
if self.filters.get("sales_person"): if self.filters.get("sales_person"):
@ -232,7 +234,8 @@ class ReceivablePayableReport(object):
if self.filters.get('group_by_party'): if self.filters.get('group_by_party'):
self.append_subtotal_row(self.previous_party) self.append_subtotal_row(self.previous_party)
self.data.append(self.total_row_map.get('Total')) if self.data:
self.data.append(self.total_row_map.get('Total'))
def append_row(self, row): def append_row(self, row):
self.allocate_future_payments(row) self.allocate_future_payments(row)

View File

@ -29,37 +29,60 @@ def execute(filters=None):
for dimension in dimensions: for dimension in dimensions:
dimension_items = cam_map.get(dimension) dimension_items = cam_map.get(dimension)
if dimension_items: if dimension_items:
for account, monthwise_data in iteritems(dimension_items): data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
row = [dimension, account] else:
totals = [0, 0, 0] DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
for year in get_fiscal_years(filters): FROM `tabDistributed Cost Center`
last_total = 0 WHERE cost_center IN %(dimension)s
for relevant_months in period_month_ranges: AND parent NOT IN %(dimension)s
period_data = [0, 0, 0] GROUP BY parent''',{'dimension':[dimension]})
for month in relevant_months: if DCC_allocation:
if monthwise_data.get(year[0]): filters['budget_against_filter'] = [DCC_allocation[0][0]]
month_data = monthwise_data.get(year[0]).get(month, {}) cam_map = get_dimension_account_month_map(filters)
for i, fieldname in enumerate(["target", "actual", "variance"]): dimension_items = cam_map.get(DCC_allocation[0][0])
value = flt(month_data.get(fieldname)) if dimension_items:
period_data[i] += value data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
totals[i] += value
period_data[0] += last_total
if filters.get("show_cumulative"):
last_total = period_data[0] - period_data[1]
period_data[2] = period_data[0] - period_data[1]
row += period_data
totals[2] = totals[0] - totals[1]
if filters["period"] != "Yearly":
row += totals
data.append(row)
chart = get_chart_data(filters, columns, data) chart = get_chart_data(filters, columns, data)
return columns, data, None, chart return columns, data, None, chart
def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation):
for account, monthwise_data in iteritems(dimension_items):
row = [dimension, account]
totals = [0, 0, 0]
for year in get_fiscal_years(filters):
last_total = 0
for relevant_months in period_month_ranges:
period_data = [0, 0, 0]
for month in relevant_months:
if monthwise_data.get(year[0]):
month_data = monthwise_data.get(year[0]).get(month, {})
for i, fieldname in enumerate(["target", "actual", "variance"]):
value = flt(month_data.get(fieldname))
period_data[i] += value
totals[i] += value
period_data[0] += last_total
if DCC_allocation:
period_data[0] = period_data[0]*(DCC_allocation/100)
period_data[1] = period_data[1]*(DCC_allocation/100)
if(filters.get("show_cumulative")):
last_total = period_data[0] - period_data[1]
period_data[2] = period_data[0] - period_data[1]
row += period_data
totals[2] = totals[0] - totals[1]
if filters["period"] != "Yearly" :
row += totals
data.append(row)
return data
def get_columns(filters): def get_columns(filters):
columns = [ columns = [
{ {
@ -366,7 +389,7 @@ def get_chart_data(filters, columns, data):
budget_values[i] += values[index] budget_values[i] += values[index]
actual_values[i] += values[index+1] actual_values[i] += values[index+1]
index += 3 index += 3
return { return {
'data': { 'data': {
'labels': labels, 'labels': labels,

View File

@ -56,9 +56,8 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_
to_date = add_months(start_date, months_to_add) to_date = add_months(start_date, months_to_add)
start_date = to_date start_date = to_date
if to_date == get_first_day(to_date): # Subtract one day from to_date, as it may be first day in next fiscal year or month
# if to_date is the first day, get the last day of previous month to_date = add_days(to_date, -1)
to_date = add_days(to_date, -1)
if to_date <= year_end_date: if to_date <= year_end_date:
# the normal case # the normal case
@ -387,11 +386,43 @@ def set_gl_entries_by_account(
key: value key: value
}) })
distributed_cost_center_query = ""
if filters and filters.get('cost_center'):
distributed_cost_center_query = """
UNION ALL
SELECT posting_date,
account,
debit*(DCC_allocation.percentage_allocation/100) as debit,
credit*(DCC_allocation.percentage_allocation/100) as credit,
is_opening,
fiscal_year,
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,
account_currency
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
AND is_cancelled = 0
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{additional_conditions}
AND posting_date <= %(to_date)s
AND cost_center = DCC_allocation.parent
""".format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
where company=%(company)s where company=%(company)s
{additional_conditions} {additional_conditions}
and posting_date <= %(to_date)s and posting_date <= %(to_date)s
order by account, posting_date""".format(additional_conditions=additional_conditions), gl_filters, as_dict=True) #nosec and is_cancelled = 0
{distributed_cost_center_query}
order by account, posting_date""".format(
additional_conditions=additional_conditions,
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
if filters and filters.get('presentation_currency'): if filters and filters.get('presentation_currency'):
convert_to_presentation_currency(gl_entries, get_currency(filters)) convert_to_presentation_currency(gl_entries, get_currency(filters))
@ -489,4 +520,4 @@ def get_columns(periodicity, period_list, accumulated_values=1, company=None):
"width": 150 "width": 150
}) })
return columns return columns

View File

@ -128,18 +128,53 @@ def get_gl_entries(filters):
filters['company_fb'] = frappe.db.get_value("Company", filters['company_fb'] = frappe.db.get_value("Company",
filters.get("company"), 'default_finance_book') filters.get("company"), 'default_finance_book')
distributed_cost_center_query = ""
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,
posting_date,
account,
party_type,
party,
voucher_type,
voucher_no,
cost_center, project,
against_voucher_type,
against_voucher,
account_currency,
remarks, against,
is_opening, `tabGL Entry`.creation {select_fields_with_percentage}
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{conditions}
AND posting_date <= %(to_date)s
AND cost_center = DCC_allocation.parent
""".format(select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql( gl_entries = frappe.db.sql(
""" """
select select
name as gl_entry, posting_date, account, party_type, party, name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, cost_center, project, voucher_type, voucher_no, cost_center, project,
against_voucher_type, against_voucher, account_currency, against_voucher_type, against_voucher, account_currency,
remarks, against, is_opening {select_fields} remarks, against, is_opening, creation {select_fields}
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s {conditions} where company=%(company)s {conditions}
{distributed_cost_center_query}
{order_by_statement} {order_by_statement}
""".format( """.format(
select_fields=select_fields, conditions=get_conditions(filters), select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query,
order_by_statement=order_by_statement order_by_statement=order_by_statement
), ),
filters, as_dict=1) filters, as_dict=1)

View File

@ -265,13 +265,6 @@ def get_columns(additional_table_columns, filters):
'fieldtype': 'Currency', 'fieldtype': 'Currency',
'options': 'currency', 'options': 'currency',
'width': 100 'width': 100
},
{
'fieldname': 'currency',
'label': _('Currency'),
'fieldtype': 'Currency',
'width': 80,
'hidden': 1
} }
] ]

View File

@ -223,7 +223,7 @@ def get_columns(additional_table_columns, filters):
} }
] ]
if filters.get('group_by') != 'Terriotory': if filters.get('group_by') != 'Territory':
columns.extend([ columns.extend([
{ {
'label': _("Territory"), 'label': _("Territory"),
@ -304,13 +304,6 @@ def get_columns(additional_table_columns, filters):
'fieldtype': 'Currency', 'fieldtype': 'Currency',
'options': 'currency', 'options': 'currency',
'width': 100 'width': 100
},
{
'fieldname': 'currency',
'label': _('Currency'),
'fieldtype': 'Currency',
'width': 80,
'hidden': 1
} }
] ]
@ -536,6 +529,13 @@ def get_tax_accounts(item_list, columns, company_currency,
'fieldtype': 'Currency', 'fieldtype': 'Currency',
'options': 'currency', 'options': 'currency',
'width': 100 'width': 100
},
{
'fieldname': 'currency',
'label': _('Currency'),
'fieldtype': 'Currency',
'width': 80,
'hidden': 1
} }
] ]

View File

@ -105,6 +105,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
def prepare_data(accounts, filters, total_row, parent_children_map, based_on): def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
data = [] data = []
new_accounts = accounts
company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency")
for d in accounts: for d in accounts:
@ -118,6 +119,19 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
"currency": company_currency, "currency": company_currency,
"based_on": based_on "based_on": based_on
} }
if based_on == 'cost_center':
cost_center_doc = frappe.get_doc("Cost Center",d.name)
if not cost_center_doc.enable_distributed_cost_center:
DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent""",{'cost_center': [d.name]})
if DCC_allocation:
for account in new_accounts:
if account['name'] == DCC_allocation[0][0]:
for value in value_fields:
d[value] += account[value]*(DCC_allocation[0][1]/100)
for key in value_fields: for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3) row[key] = flt(d.get(key, 0.0), 3)

View File

@ -111,7 +111,7 @@ def get_gle_map(filters):
# {"purchase_invoice": list of dict of all gle created for this invoice} # {"purchase_invoice": list of dict of all gle created for this invoice}
gle_map = {} gle_map = {}
gle = frappe.db.get_all('GL Entry',\ gle = frappe.db.get_all('GL Entry',\
{"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]]}, {"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]], 'is_cancelled': 0},
["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date"]) ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date"])
for d in gle: for d in gle:

View File

@ -57,6 +57,9 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
frappe.cache().hset("fiscal_years", company, fiscal_years) frappe.cache().hset("fiscal_years", company, fiscal_years)
if not transaction_date and not fiscal_year:
return fiscal_years
if transaction_date: if transaction_date:
transaction_date = getdate(transaction_date) transaction_date = getdate(transaction_date)
@ -79,6 +82,23 @@ def get_fiscal_years(transaction_date=None, fiscal_year=None, label="Date", verb
if verbose==1: frappe.msgprint(error_msg) if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg) raise FiscalYearError(error_msg)
@frappe.whitelist()
def get_fiscal_year_filter_field(company=None):
field = {
"fieldtype": "Select",
"options": [],
"operator": "Between",
"query_value": True
}
fiscal_years = get_fiscal_years(company=company)
for fiscal_year in fiscal_years:
field["options"].append({
"label": fiscal_year.name,
"value": fiscal_year.name,
"query_value": [fiscal_year.year_start_date.strftime("%Y-%m-%d"), fiscal_year.year_end_date.strftime("%Y-%m-%d")]
})
return field
def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None): def validate_fiscal_year(date, fiscal_year, company, label="Date", doc=None):
years = [f[0] for f in get_fiscal_years(date, label=_(label), company=company)] years = [f[0] for f in get_fiscal_years(date, label=_(label), company=company)]
if fiscal_year not in years: if fiscal_year not in years:
@ -935,4 +955,4 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1): tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d) gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries return gl_entries

View File

@ -5,14 +5,23 @@ import frappe
import json import json
from frappe.utils import nowdate, add_months, get_date_str from frappe.utils import nowdate, add_months, get_date_str
from frappe import _ from frappe import _
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.dashboard_fixtures import _get_fiscal_year
from erpnext.buying.dashboard_fixtures import get_company_for_dashboards
def get_data(): def get_data():
fiscal_year = _get_fiscal_year(nowdate())
if not fiscal_year:
return frappe._dict()
year_start_date = get_date_str(fiscal_year.get('year_start_date'))
year_end_date = get_date_str(fiscal_year.get('year_end_date'))
return frappe._dict({ return frappe._dict({
"dashboards": get_dashboards(), "dashboards": get_dashboards(),
"charts": get_charts(), "charts": get_charts(fiscal_year, year_start_date, year_end_date),
"number_cards": get_number_cards(), "number_cards": get_number_cards(fiscal_year, year_start_date, year_end_date),
}) })
def get_dashboards(): def get_dashboards():
@ -31,12 +40,7 @@ def get_dashboards():
] ]
}] }]
fiscal_year = get_fiscal_year(date=nowdate()) def get_charts(fiscal_year, year_start_date, year_end_date):
year_start_date = get_date_str(fiscal_year[1])
year_end_date = get_date_str(fiscal_year[2])
def get_charts():
company = get_company_for_dashboards() company = get_company_for_dashboards()
return [ return [
{ {
@ -55,8 +59,8 @@ def get_charts():
"company": company, "company": company,
"status": "In Location", "status": "In Location",
"filter_based_on": "Fiscal Year", "filter_based_on": "Fiscal Year",
"from_fiscal_year": fiscal_year[0], "from_fiscal_year": fiscal_year.get('name'),
"to_fiscal_year": fiscal_year[0], "to_fiscal_year": fiscal_year.get('name'),
"period_start_date": year_start_date, "period_start_date": year_start_date,
"period_end_date": year_end_date, "period_end_date": year_end_date,
"date_based_on": "Purchase Date", "date_based_on": "Purchase Date",
@ -134,7 +138,7 @@ def get_charts():
} }
] ]
def get_number_cards(): def get_number_cards(fiscal_year, year_start_date, year_end_date):
return [ return [
{ {
"name": "Total Assets", "name": "Total Assets",
@ -172,14 +176,4 @@ def get_number_cards():
"filters_json": "[]", "filters_json": "[]",
"doctype": "Number Card" "doctype": "Number Card"
} }
] ]
def get_company_for_dashboards():
company = frappe.defaults.get_defaults().company
if company:
return company
else:
company_list = frappe.get_list("Company")
if company_list:
return company_list[0].name
return None

View File

@ -41,7 +41,7 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
team_member = frappe.db.get_value('User', assign_to_member, "email") team_member = frappe.db.get_value('User', assign_to_member, "email")
args = { args = {
'doctype' : 'Asset Maintenance', 'doctype' : 'Asset Maintenance',
'assign_to' : team_member, 'assign_to' : [team_member],
'name' : asset_maintenance_name, 'name' : asset_maintenance_name,
'description' : maintenance_task, 'description' : maintenance_task,
'date' : next_due_date 'date' : next_due_date

View File

@ -5,13 +5,24 @@ import frappe
import json import json
from frappe import _ from frappe import _
from frappe.utils import nowdate from frappe.utils import nowdate
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.dashboard_fixtures import _get_fiscal_year
def get_data(): def get_data():
fiscal_year = _get_fiscal_year(nowdate())
if not fiscal_year:
return frappe._dict()
company = frappe.get_doc("Company", get_company_for_dashboards())
fiscal_year_name = fiscal_year.get("name")
start_date = str(fiscal_year.get("year_start_date"))
end_date = str(fiscal_year.get("year_end_date"))
return frappe._dict({ return frappe._dict({
"dashboards": get_dashboards(), "dashboards": get_dashboards(),
"charts": get_charts(), "charts": get_charts(company, fiscal_year_name, start_date, end_date),
"number_cards": get_number_cards(), "number_cards": get_number_cards(company, fiscal_year_name, start_date, end_date),
}) })
def get_company_for_dashboards(): def get_company_for_dashboards():
@ -24,12 +35,6 @@ def get_company_for_dashboards():
return company_list[0].name return company_list[0].name
return None return None
company = frappe.get_doc("Company", get_company_for_dashboards())
fiscal_year = get_fiscal_year(nowdate(), as_dict=1)
fiscal_year_name = fiscal_year.get("name")
start_date = str(fiscal_year.get("year_start_date"))
end_date = str(fiscal_year.get("year_end_date"))
def get_dashboards(): def get_dashboards():
return [{ return [{
"name": "Buying", "name": "Buying",
@ -48,7 +53,7 @@ def get_dashboards():
] ]
}] }]
def get_charts(): def get_charts(company, fiscal_year_name, start_date, end_date):
return [ return [
{ {
"name": "Purchase Order Analysis", "name": "Purchase Order Analysis",
@ -139,7 +144,7 @@ def get_charts():
} }
] ]
def get_number_cards(): def get_number_cards(company, fiscal_year_name, start_date, end_date):
return [ return [
{ {
"name": "Annual Purchase", "name": "Annual Purchase",

File diff suppressed because it is too large Load Diff

View File

@ -71,6 +71,15 @@ class PurchaseOrder(BuyingController):
"compare_fields": [["project", "="], ["item_code", "="], "compare_fields": [["project", "="], ["item_code", "="],
["uom", "="], ["conversion_factor", "="]], ["uom", "="], ["conversion_factor", "="]],
"is_child_table": True "is_child_table": True
},
"Material Request": {
"ref_dn_field": "material_request",
"compare_fields": [["company", "="]],
},
"Material Request Item": {
"ref_dn_field": "material_request_item",
"compare_fields": [["project", "="], ["item_code", "="]],
"is_child_table": True
} }
}) })

View File

@ -118,7 +118,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(po.get("items")[0].amount, 1400) self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
def test_add_new_item_in_update_child_qty_rate(self): def test_add_new_item_in_update_child_qty_rate(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4 po.items[0].qty = 4
@ -144,7 +144,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEquals(len(po.get('items')), 2) self.assertEquals(len(po.get('items')), 2)
self.assertEqual(po.status, 'To Receive and Bill') self.assertEqual(po.status, 'To Receive and Bill')
def test_remove_item_in_update_child_qty_rate(self): def test_remove_item_in_update_child_qty_rate(self):
po = create_purchase_order(do_not_save=1) po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4 po.items[0].qty = 4
@ -185,6 +185,23 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEquals(len(po.get('items')), 1) self.assertEquals(len(po.get('items')), 1)
self.assertEqual(po.status, 'To Receive and Bill') self.assertEqual(po.status, 'To Receive and Bill')
def test_update_child_qty_rate_perm(self):
po = create_purchase_order(item_code= "_Test Item", qty=4)
user = 'test@example.com'
test_user = frappe.get_doc('User', user)
test_user.add_roles("Accounts User")
frappe.set_user(user)
# update qty
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.items[0].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
# add new item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
frappe.set_user("Administrator")
def test_update_qty(self): def test_update_qty(self):
po = create_purchase_order() po = create_purchase_order()
@ -689,7 +706,7 @@ class TestPurchaseOrder(unittest.TestCase):
po.save() po.save()
self.assertEqual(po.schedule_date, add_days(nowdate(), 2)) self.assertEqual(po.schedule_date, add_days(nowdate(), 2))
def test_po_optional_blanket_order(self): def test_po_optional_blanket_order(self):
""" """
Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1. Expected result: Blanket order Ordered Quantity should only be affected on Purchase Order with against_blanket_order = 1.

View File

@ -25,6 +25,7 @@ class RequestforQuotation(BuyingController):
self.validate_duplicate_supplier() self.validate_duplicate_supplier()
self.validate_supplier_list() self.validate_supplier_list()
validate_for_items(self) validate_for_items(self)
super(RequestforQuotation, self).set_qty_as_per_stock_uom()
self.update_email_id() self.update_email_id()
def validate_duplicate_supplier(self): def validate_duplicate_supplier(self):
@ -278,6 +279,7 @@ def create_rfq_items(sq_doc, supplier, data):
"description": data.description, "description": data.description,
"qty": data.qty, "qty": data.qty,
"rate": data.rate, "rate": data.rate,
"conversion_factor": data.conversion_factor if data.conversion_factor else None,
"supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"), "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
"warehouse": data.warehouse or '', "warehouse": data.warehouse or '',
"request_for_quotation_item": data.name, "request_for_quotation_item": data.name,

View File

@ -6,12 +6,14 @@ from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
from frappe.utils import nowdate from frappe.utils import nowdate
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
class TestRequestforQuotation(unittest.TestCase): class TestRequestforQuotation(unittest.TestCase):
def test_quote_status(self): def test_quote_status(self):
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation
rfq = make_request_for_quotation() rfq = make_request_for_quotation()
self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Pending') self.assertEqual(rfq.get('suppliers')[0].quote_status, 'Pending')
@ -31,7 +33,6 @@ class TestRequestforQuotation(unittest.TestCase):
self.assertEqual(rfq.get('suppliers')[1].quote_status, 'No Quote') self.assertEqual(rfq.get('suppliers')[1].quote_status, 'No Quote')
def test_make_supplier_quotation(self): def test_make_supplier_quotation(self):
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation
rfq = make_request_for_quotation() rfq = make_request_for_quotation()
sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier) sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier)
@ -51,15 +52,13 @@ class TestRequestforQuotation(unittest.TestCase):
self.assertEqual(sq1.get('items')[0].qty, 5) self.assertEqual(sq1.get('items')[0].qty, 5)
def test_make_supplier_quotation_with_special_characters(self): def test_make_supplier_quotation_with_special_characters(self):
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation
frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1) frappe.delete_doc_if_exists("Supplier", "_Test Supplier '1", force=1)
supplier = frappe.new_doc("Supplier") supplier = frappe.new_doc("Supplier")
supplier.supplier_name = "_Test Supplier '1" supplier.supplier_name = "_Test Supplier '1"
supplier.supplier_group = "_Test Supplier Group" supplier.supplier_group = "_Test Supplier Group"
supplier.insert() supplier.insert()
rfq = make_request_for_quotation(supplier_wt_appos) rfq = make_request_for_quotation(supplier_data=supplier_wt_appos)
sq = make_supplier_quotation(rfq.name, supplier_wt_appos[0].get("supplier")) sq = make_supplier_quotation(rfq.name, supplier_wt_appos[0].get("supplier"))
sq.submit() sq.submit()
@ -76,7 +75,6 @@ class TestRequestforQuotation(unittest.TestCase):
frappe.form_dict.name = None frappe.form_dict.name = None
def test_make_supplier_quotation_from_portal(self): def test_make_supplier_quotation_from_portal(self):
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation
rfq = make_request_for_quotation() rfq = make_request_for_quotation()
rfq.get('items')[0].rate = 100 rfq.get('items')[0].rate = 100
rfq.supplier = rfq.suppliers[0].supplier rfq.supplier = rfq.suppliers[0].supplier
@ -90,12 +88,34 @@ class TestRequestforQuotation(unittest.TestCase):
self.assertEqual(supplier_quotation_doc.get('items')[0].qty, 5) self.assertEqual(supplier_quotation_doc.get('items')[0].qty, 5)
self.assertEqual(supplier_quotation_doc.get('items')[0].amount, 500) self.assertEqual(supplier_quotation_doc.get('items')[0].amount, 500)
def test_make_multi_uom_supplier_quotation(self):
item_code = "_Test Multi UOM RFQ Item"
if not frappe.db.exists('Item', item_code):
item = make_item(item_code, {'stock_uom': '_Test UOM'})
row = item.append('uoms', {
'uom': 'Kg',
'conversion_factor': 2
})
row.db_update()
def make_request_for_quotation(supplier_data=None): rfq = make_request_for_quotation(item_code="_Test Multi UOM RFQ Item", uom="Kg", conversion_factor=2)
rfq.get('items')[0].rate = 100
rfq.supplier = rfq.suppliers[0].supplier
self.assertEqual(rfq.items[0].stock_qty, 10)
supplier_quotation_name = create_supplier_quotation(rfq)
supplier_quotation = frappe.get_doc('Supplier Quotation', supplier_quotation_name)
self.assertEqual(supplier_quotation.items[0].qty, 5)
self.assertEqual(supplier_quotation.items[0].stock_qty, 10)
def make_request_for_quotation(**args):
""" """
:param supplier_data: List containing supplier data :param supplier_data: List containing supplier data
""" """
supplier_data = supplier_data if supplier_data else get_supplier_data() args = frappe._dict(args)
supplier_data = args.get("supplier_data") if args.get("supplier_data") else get_supplier_data()
rfq = frappe.new_doc('Request for Quotation') rfq = frappe.new_doc('Request for Quotation')
rfq.transaction_date = nowdate() rfq.transaction_date = nowdate()
rfq.status = 'Draft' rfq.status = 'Draft'
@ -106,11 +126,13 @@ def make_request_for_quotation(supplier_data=None):
rfq.append('suppliers', data) rfq.append('suppliers', data)
rfq.append("items", { rfq.append("items", {
"item_code": "_Test Item", "item_code": args.item_code or "_Test Item",
"description": "_Test Item", "description": "_Test Item",
"uom": "_Test UOM", "uom": args.uom or "_Test UOM",
"qty": 5, "stock_uom": args.stock_uom or "_Test UOM",
"warehouse": "_Test Warehouse - _TC", "qty": args.qty or 5,
"conversion_factor": args.conversion_factor or 1.0,
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"schedule_date": nowdate() "schedule_date": nowdate()
}) })

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2016-02-25 08:04:02.452958", "creation": "2016-02-25 08:04:02.452958",
"doctype": "DocType", "doctype": "DocType",
@ -9,6 +10,7 @@
"supplier_part_no", "supplier_part_no",
"column_break_3", "column_break_3",
"item_name", "item_name",
"schedule_date",
"section_break_5", "section_break_5",
"description", "description",
"item_group", "item_group",
@ -18,9 +20,11 @@
"image_view", "image_view",
"quantity", "quantity",
"qty", "qty",
"stock_uom",
"col_break2", "col_break2",
"schedule_date",
"uom", "uom",
"conversion_factor",
"stock_qty",
"warehouse_and_reference", "warehouse_and_reference",
"warehouse", "warehouse",
"project_name", "project_name",
@ -33,7 +37,7 @@
"fields": [ "fields": [
{ {
"bold": 1, "bold": 1,
"columns": 3, "columns": 2,
"fieldname": "item_code", "fieldname": "item_code",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -98,7 +102,7 @@
{ {
"fieldname": "quantity", "fieldname": "quantity",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Quantity" "label": "Quantity & Stock"
}, },
{ {
"bold": 1, "bold": 1,
@ -129,12 +133,12 @@
{ {
"fieldname": "uom", "fieldname": "uom",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "UOM", "label": "UOM",
"oldfieldname": "uom", "oldfieldname": "uom",
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "UOM", "options": "UOM",
"print_width": "100px", "print_width": "100px",
"read_only": 1,
"reqd": 1, "reqd": 1,
"width": "100px" "width": "100px"
}, },
@ -144,7 +148,7 @@
"label": "Warehouse and Reference" "label": "Warehouse and Reference"
}, },
{ {
"columns": 3, "columns": 2,
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -202,6 +206,7 @@
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "0",
"fieldname": "page_break", "fieldname": "page_break",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Page Break", "label": "Page Break",
@ -219,10 +224,36 @@
{ {
"fieldname": "section_break_23", "fieldname": "section_break_23",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"print_hide": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor",
"print_hide": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Qty as per Stock UOM",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-05-01 17:50:23.703801", "links": [],
"modified": "2020-06-12 19:10:36.333441",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Request for Quotation Item", "name": "Request for Quotation Item",

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt
def execute(filters=None): def execute(filters=None):
columns = get_columns(filters) columns = get_columns(filters)
@ -54,15 +55,16 @@ def get_columns(filters):
"width": 140 "width": 140
}, },
{ {
"label": _("Description"), "label": _("Item"),
"fieldname": "description", "fieldname": "item_code",
"fieldtype": "Data", "fieldtype": "Link",
"width": 200 "options": "Item",
"width": 150
}, },
{ {
"label": _("Quantity"), "label": _("Quantity"),
"fieldname": "quantity", "fieldname": "quantity",
"fieldtype": "Int", "fieldtype": "Float",
"width": 140 "width": 140
}, },
{ {
@ -118,7 +120,7 @@ def get_columns(filters):
}, },
{ {
"label": _("Purchase Order Amount(Company Currency)"), "label": _("Purchase Order Amount(Company Currency)"),
"fieldname": "purchase_order_amt_usd", "fieldname": "purchase_order_amt_in_company_currency",
"fieldtype": "Float", "fieldtype": "Float",
"width": 140 "width": 140
}, },
@ -175,17 +177,17 @@ def get_data(filters):
"requesting_site": po.warehouse, "requesting_site": po.warehouse,
"requestor": po.owner, "requestor": po.owner,
"material_request_no": po.material_request, "material_request_no": po.material_request,
"description": po.description, "item_code": po.item_code,
"quantity": po.qty, "quantity": flt(po.qty),
"unit_of_measurement": po.stock_uom, "unit_of_measurement": po.stock_uom,
"status": po.status, "status": po.status,
"purchase_order_date": po.transaction_date, "purchase_order_date": po.transaction_date,
"purchase_order": po.parent, "purchase_order": po.parent,
"supplier": po.supplier, "supplier": po.supplier,
"estimated_cost": mr_record.get('amount'), "estimated_cost": flt(mr_record.get('amount')),
"actual_cost": pi_records.get(po.name), "actual_cost": flt(pi_records.get(po.name)),
"purchase_order_amt": po.amount, "purchase_order_amt": flt(po.amount),
"purchase_order_amt_in_company_currency": po.base_amount, "purchase_order_amt_in_company_currency": flt(po.base_amount),
"expected_delivery_date": po.schedule_date, "expected_delivery_date": po.schedule_date,
"actual_delivery_date": pr_records.get(po.name) "actual_delivery_date": pr_records.get(po.name)
} }
@ -198,9 +200,14 @@ def get_mapped_mr_details(conditions):
SELECT SELECT
par.transaction_date, par.transaction_date,
par.per_ordered, par.per_ordered,
par.owner,
child.name, child.name,
child.parent, child.parent,
child.amount child.amount,
child.qty,
child.item_code,
child.uom,
par.status
FROM `tabMaterial Request` par, `tabMaterial Request Item` child FROM `tabMaterial Request` par, `tabMaterial Request Item` child
WHERE WHERE
par.per_ordered>=0 par.per_ordered>=0
@ -217,7 +224,15 @@ def get_mapped_mr_details(conditions):
procurement_record_details = dict( procurement_record_details = dict(
material_request_date=record.transaction_date, material_request_date=record.transaction_date,
material_request_no=record.parent, material_request_no=record.parent,
estimated_cost=record.amount requestor=record.owner,
item_code=record.item_code,
estimated_cost=flt(record.amount),
quantity=flt(record.qty),
unit_of_measurement=record.uom,
status=record.status,
actual_cost=0,
purchase_order_amt=0,
purchase_order_amt_in_company_currency=0
) )
procurement_record_against_mr.append(procurement_record_details) procurement_record_against_mr.append(procurement_record_details)
return mr_records, procurement_record_against_mr return mr_records, procurement_record_against_mr
@ -259,7 +274,7 @@ def get_po_entries(conditions):
child.warehouse, child.warehouse,
child.material_request, child.material_request,
child.material_request_item, child.material_request_item,
child.description, child.item_code,
child.stock_uom, child.stock_uom,
child.qty, child.qty,
child.amount, child.amount,

View File

@ -37,8 +37,6 @@ class TestProcurementTracker(unittest.TestCase):
po = make_purchase_order(mr.name) po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier" po.supplier = "_Test Supplier"
po.get("items")[0].cost_center = "Main - _TPC" po.get("items")[0].cost_center = "Main - _TPC"
po.items[0].rate = 500.0
po.save()
po.submit() po.submit()
pr = make_purchase_receipt(po.name) pr = make_purchase_receipt(po.name)
pr.get("items")[0].cost_center = "Main - _TPC" pr.get("items")[0].cost_center = "Main - _TPC"
@ -55,7 +53,7 @@ class TestProcurementTracker(unittest.TestCase):
"requesting_site": "_Test Procurement Warehouse - _TPC", "requesting_site": "_Test Procurement Warehouse - _TPC",
"requestor": "Administrator", "requestor": "Administrator",
"material_request_no": mr.name, "material_request_no": mr.name,
"description": '_Test Item 1', "item_code": '_Test Item',
"quantity": 10.0, "quantity": 10.0,
"unit_of_measurement": "_Test UOM", "unit_of_measurement": "_Test UOM",
"status": "To Bill", "status": "To Bill",
@ -63,7 +61,7 @@ class TestProcurementTracker(unittest.TestCase):
"purchase_order": po.name, "purchase_order": po.name,
"supplier": "_Test Supplier", "supplier": "_Test Supplier",
"estimated_cost": 0.0, "estimated_cost": 0.0,
"actual_cost": None, "actual_cost": 0.0,
"purchase_order_amt": po.net_total, "purchase_order_amt": po.net_total,
"purchase_order_amt_in_company_currency": po.base_net_total, "purchase_order_amt_in_company_currency": po.base_net_total,
"expected_delivery_date": date_obj, "expected_delivery_date": date_obj,

View File

@ -1137,8 +1137,8 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
child_item.item_name = item.item_name child_item.item_name = item.item_name
child_item.description = item.description child_item.description = item.description
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.uom = item.stock_uom child_item.uom = item.stock_uom
child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse: if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
@ -1157,8 +1157,8 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.item_name = item.item_name child_item.item_name = item.item_name
child_item.description = item.description child_item.description = item.description
child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.uom = item.stock_uom child_item.uom = item.stock_uom
child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation
return child_item return child_item
@ -1190,6 +1190,26 @@ def check_and_delete_children(parent, data):
@frappe.whitelist() @frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"): def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_permissions(doc, perm_type='create'):
try:
doc.check_permission(perm_type)
except:
action = "add" if perm_type == 'create' else "update"
frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions"))
def get_new_child_item(item_row):
if parent_doctype == "Sales Order":
return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
if parent_doctype == "Purchase Order":
return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
frappe.throw(_("Cannot set quantity less than delivered quantity"))
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
frappe.throw(_("Cannot set quantity less than received quantity"))
data = json.loads(trans_items) data = json.loads(trans_items)
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
@ -1201,20 +1221,29 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
new_child_flag = False new_child_flag = False
if not d.get("docname"): if not d.get("docname"):
new_child_flag = True new_child_flag = True
if parent_doctype == "Sales Order": check_permissions(parent, 'create')
child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d) child_item = get_new_child_item(d)
if parent_doctype == "Purchase Order":
child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d)
else: else:
check_permissions(parent, 'write')
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")):
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty"))
prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(d.get("conversion_factor"))
if parent_doctype == 'Sales Order':
prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date")
elif parent_doctype == 'Purchase Order':
prev_date, new_date = child_item.get("schedule_date"), d.get("schedule_date")
rate_unchanged = prev_rate == new_rate
qty_unchanged = prev_qty == new_qty
conversion_factor_unchanged = prev_con_fac == new_con_fac
date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc
if rate_unchanged and qty_unchanged and conversion_factor_unchanged and date_unchanged:
continue continue
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty): validate_quantity(child_item, d)
frappe.throw(_("Cannot set quantity less than delivered quantity"))
if parent_doctype == "Purchase Order" and flt(d.get("qty")) < flt(child_item.received_qty):
frappe.throw(_("Cannot set quantity less than received quantity"))
child_item.qty = flt(d.get("qty")) child_item.qty = flt(d.get("qty"))
precision = child_item.precision("rate") or 2 precision = child_item.precision("rate") or 2
@ -1225,6 +1254,18 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
else: else:
child_item.rate = flt(d.get("rate")) child_item.rate = flt(d.get("rate"))
if d.get("conversion_factor"):
if child_item.stock_uom == child_item.uom:
child_item.conversion_factor = 1
else:
child_item.conversion_factor = flt(d.get('conversion_factor'))
if d.get("delivery_date") and parent_doctype == 'Sales Order':
child_item.delivery_date = d.get('delivery_date')
if d.get("schedule_date") and parent_doctype == 'Purchase Order':
child_item.schedule_date = d.get('schedule_date')
if flt(child_item.price_list_rate): if flt(child_item.price_list_rate):
if flt(child_item.rate) > flt(child_item.price_list_rate): if flt(child_item.rate) > flt(child_item.price_list_rate):
# if rate is greater than price_list_rate, set margin # if rate is greater than price_list_rate, set margin

View File

@ -349,7 +349,7 @@ class BuyingController(StockController):
}) })
if not rm.rate: if not rm.rate:
rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse, rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company) self.doctype, self.name, currency=self.company_currency, company=self.company)
rm.amount = qty * flt(rm.rate) rm.amount = qty * flt(rm.rate)

View File

@ -70,7 +70,7 @@ def validate_item_variant_attributes(item, args=None):
else: else:
attributes_list = attribute_values.get(attribute.lower(), []) attributes_list = attribute_values.get(attribute.lower(), [])
validate_item_attribute_value(attributes_list, attribute, value, item.name) validate_item_attribute_value(attributes_list, attribute, value, item.name, from_variant=True)
def validate_is_incremental(numeric_attribute, attribute, value, item): def validate_is_incremental(numeric_attribute, attribute, value, item):
from_range = numeric_attribute.from_range from_range = numeric_attribute.from_range
@ -93,13 +93,20 @@ def validate_is_incremental(numeric_attribute, attribute, value, item):
.format(attribute, from_range, to_range, increment, item), .format(attribute, from_range, to_range, increment, item),
InvalidItemAttributeValueError, title=_('Invalid Attribute')) InvalidItemAttributeValueError, title=_('Invalid Attribute'))
def validate_item_attribute_value(attributes_list, attribute, attribute_value, item): def validate_item_attribute_value(attributes_list, attribute, attribute_value, item, from_variant=True):
allow_rename_attribute_value = frappe.db.get_single_value('Item Variant Settings', 'allow_rename_attribute_value') allow_rename_attribute_value = frappe.db.get_single_value('Item Variant Settings', 'allow_rename_attribute_value')
if allow_rename_attribute_value: if allow_rename_attribute_value:
pass pass
elif attribute_value not in attributes_list: elif attribute_value not in attributes_list:
frappe.throw(_("The value {0} is already assigned to an exisiting Item {2}.").format( if from_variant:
attribute_value, attribute, item), InvalidItemAttributeValueError, title=_('Rename Not Allowed')) frappe.throw(_("{0} is not a valid Value for Attribute {1} of Item {2}.").format(
frappe.bold(attribute_value), frappe.bold(attribute), frappe.bold(item)), InvalidItemAttributeValueError, title=_("Invalid Value"))
else:
msg = _("The value {0} is already assigned to an existing Item {1}.").format(
frappe.bold(attribute_value), frappe.bold(item))
msg += "<br>" + _("To still proceed with editing this Attribute Value, enable {0} in Item Variant Settings.").format(frappe.bold("Allow Rename Attribute Value"))
frappe.throw(msg, InvalidItemAttributeValueError, title=_('Edit Not Allowed'))
def get_attribute_values(item): def get_attribute_values(item):
if not frappe.flags.attribute_values: if not frappe.flags.attribute_values:

View File

@ -19,7 +19,8 @@ class QualityInspectionNotSubmittedError(frappe.ValidationError): pass
class StockController(AccountsController): class StockController(AccountsController):
def validate(self): def validate(self):
super(StockController, self).validate() super(StockController, self).validate()
self.validate_inspection() if not self.get('is_return'):
self.validate_inspection()
self.validate_serialized_batch() self.validate_serialized_batch()
self.validate_customer_provided_item() self.validate_customer_provided_item()
@ -228,7 +229,9 @@ class StockController(AccountsController):
def check_expense_account(self, item): def check_expense_account(self, item):
if not item.get("expense_account"): if not item.get("expense_account"):
frappe.throw(_("Expense Account not set for Item {0}. Please set an Expense Account for the item in the Items table").format(item.item_code)) frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \
Account in the Items table").format(item.idx, frappe.bold(item.item_code)),
title=_("Expense Account Missing"))
else: else:
is_expense_account = frappe.db.get_value("Account", is_expense_account = frappe.db.get_value("Account",

View File

@ -6,6 +6,7 @@
"creation": "2013-04-10 11:45:37", "creation": "2013-04-10 11:45:37",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"email_append_to": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"organization_lead", "organization_lead",
@ -448,7 +449,7 @@
"idx": 5, "idx": 5,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-05-11 20:27:45.868960", "modified": "2020-06-18 14:39:41.835416",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead", "name": "Lead",
@ -508,8 +509,10 @@
} }
], ],
"search_fields": "lead_name,lead_owner,status", "search_fields": "lead_name,lead_owner,status",
"sender_field": "email_id",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"subject_field": "title",
"title_field": "title" "title_field": "title"
} }

View File

@ -3,11 +3,7 @@ from frappe import _
def get_data(): def get_data():
return { return {
'fieldname': 'prevdoc_docname', 'fieldname': 'opportunity',
'non_standard_fieldnames': {
'Supplier Quotation': 'opportunity',
'Quotation': 'opportunity'
},
'transactions': [ 'transactions': [
{ {
'items': ['Quotation', 'Supplier Quotation'] 'items': ['Quotation', 'Supplier Quotation']

View File

@ -30,24 +30,32 @@
"fieldname": "text", "fieldname": "text",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Tweet", "label": "Tweet",
"mandatory_depends_on": "eval:doc.twitter ==1" "mandatory_depends_on": "eval:doc.twitter ==1",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
"label": "Image" "label": "Image",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "twitter", "fieldname": "twitter",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Twitter" "label": "Twitter",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "linkedin", "fieldname": "linkedin",
"fieldtype": "Check", "fieldtype": "Check",
"label": "LinkedIn" "label": "LinkedIn",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
@ -56,13 +64,17 @@
"no_copy": 1, "no_copy": 1,
"options": "Social Media Post", "options": "Social Media Post",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.twitter ==1", "depends_on": "eval:doc.twitter ==1",
"fieldname": "content", "fieldname": "content",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Twitter" "label": "Twitter",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -70,7 +82,9 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Post Status", "label": "Post Status",
"options": "\nScheduled\nPosted\nError", "options": "\nScheduled\nPosted\nError",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -78,7 +92,9 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Twitter Post Id", "label": "Twitter Post Id",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -86,68 +102,89 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "LinkedIn Post Id", "label": "LinkedIn Post Id",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "campaign_name", "fieldname": "campaign_name",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Campaign", "label": "Campaign",
"options": "Campaign" "options": "Campaign",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"label": "Share On" "label": "Share On",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_14", "fieldname": "column_break_14",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "tweet_preview", "fieldname": "tweet_preview",
"fieldtype": "HTML" "fieldtype": "HTML",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"depends_on": "eval:doc.linkedin==1", "depends_on": "eval:doc.linkedin==1",
"fieldname": "linkedin_section", "fieldname": "linkedin_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "LinkedIn" "label": "LinkedIn",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "attachments_section", "fieldname": "attachments_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Attachments" "label": "Attachments",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "linkedin_post", "fieldname": "linkedin_post",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Post", "label": "Post",
"mandatory_depends_on": "eval:doc.linkedin ==1" "mandatory_depends_on": "eval:doc.linkedin ==1",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_15", "fieldname": "column_break_15",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "scheduled_time", "fieldname": "scheduled_time",
"fieldtype": "Datetime", "fieldtype": "Datetime",
"label": "Scheduled Time", "label": "Scheduled Time",
"read_only_depends_on": "eval:doc.post_status == \"Posted\"" "read_only_depends_on": "eval:doc.post_status == \"Posted\"",
"show_days": 1,
"show_seconds": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-21 15:10:04.953713", "modified": "2020-06-14 10:31:33.961381",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Social Media Post", "name": "Social Media Post",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"cancel": 1,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
@ -157,6 +194,35 @@
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"share": 1, "share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"share": 1,
"submit": 1,
"write": 1
},
{
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"share": 1,
"submit": 1,
"write": 1 "write": 1
} }
], ],

View File

@ -9,6 +9,14 @@ frappe.ui.form.on('Fee Structure', {
}, },
onload: function(frm) { onload: function(frm) {
frm.set_query("academic_term", function() {
return {
"filters": {
"academic_year": frm.doc.academic_year
}
};
});
frm.set_query("receivable_account", function(doc) { frm.set_query("receivable_account", function(doc) {
return { return {
filters: { filters: {

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
@ -11,8 +12,8 @@
"program", "program",
"student_category", "student_category",
"column_break_2", "column_break_2",
"academic_term",
"academic_year", "academic_year",
"academic_term",
"section_break_4", "section_break_4",
"components", "components",
"section_break_6", "section_break_6",
@ -157,7 +158,8 @@
], ],
"icon": "fa fa-flag", "icon": "fa fa-flag",
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-05-26 09:04:17.765758", "links": [],
"modified": "2020-06-16 15:34:57.295010",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Fee Structure", "name": "Fee Structure",

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"creation": "2015-09-07 14:37:01.886859", "creation": "2015-09-07 14:37:01.886859",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@ -16,26 +17,33 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Course", "label": "Course",
"options": "Course", "options": "Course",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fetch_from": "course.course_name",
"fieldname": "course_name", "fieldname": "course_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Course Name", "label": "Course Name",
"fetch_from": "course.course_name", "read_only": 1,
"read_only":1 "show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "required", "fieldname": "required",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
"label": "Mandatory" "label": "Mandatory",
"show_days": 1,
"show_seconds": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-06-12 12:42:12.845972", "links": [],
"modified": "2020-06-09 18:56:10.213241",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Program Course", "name": "Program Course",
@ -45,4 +53,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@ -1,398 +1,119 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 1, "allow_guest_to_view": 1,
"allow_import": 0, "allow_rename": 1,
"allow_rename": 1, "creation": "2016-09-13 03:05:27.154713",
"autoname": "", "doctype": "DocType",
"beta": 0, "document_type": "Document",
"creation": "2016-09-13 03:05:27.154713", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "title",
"document_type": "Document", "route",
"editable_grid": 1, "column_break_3",
"academic_year",
"admission_start_date",
"admission_end_date",
"published",
"enable_admission_application",
"section_break_5",
"program_details",
"introduction"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "title",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "label": "Title"
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "route",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "label": "Route",
"collapsible": 0, "no_copy": 1,
"columns": 0,
"depends_on": "",
"fieldname": "route",
"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": "Route",
"length": 0,
"no_copy": 1,
"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,
"unique": 1 "unique": 1
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "application_form_route",
"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": "Application Form Route",
"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": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "academic_year",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "in_standard_filter": 1,
"columns": 0, "label": "Academic Year",
"fieldname": "column_break_3", "no_copy": 1,
"fieldtype": "Column Break", "options": "Academic Year",
"hidden": 0, "reqd": 1
"ignore_user_permissions": 0, },
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "admission_start_date",
"allow_on_submit": 0, "fieldtype": "Date",
"bold": 0, "label": "Admission Start Date",
"collapsible": 0, "no_copy": 1
"columns": 0, },
"fieldname": "academic_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Academic Year",
"length": 0,
"no_copy": 1,
"options": "Academic Year",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "admission_end_date",
"allow_on_submit": 0, "fieldtype": "Date",
"bold": 0, "label": "Admission End Date",
"collapsible": 0, "no_copy": 1
"columns": 0, },
"fieldname": "admission_start_date",
"fieldtype": "Date",
"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": "Admission Start Date",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0, "fieldname": "published",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Publish on website"
"columns": 0, },
"fieldname": "admission_end_date",
"fieldtype": "Date",
"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": "Admission End Date",
"length": 0,
"no_copy": 1,
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_5",
"allow_on_submit": 0, "fieldtype": "Section Break",
"bold": 0, "label": "Eligibility and Details"
"collapsible": 0, },
"columns": 0,
"fieldname": "published",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Publish on website",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "program_details",
"allow_on_submit": 0, "fieldtype": "Table",
"bold": 0, "label": "Eligibility and Details",
"collapsible": 0, "options": "Student Admission Program"
"columns": 0, },
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Eligibility and Details",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "introduction",
"allow_on_submit": 0, "fieldtype": "Text Editor",
"bold": 0, "label": "Introduction"
"collapsible": 0, },
"columns": 0,
"fieldname": "program_details",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Eligibility and Details",
"length": 0,
"no_copy": 0,
"options": "Student Admission Program",
"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,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_on_submit": 0, "fieldname": "enable_admission_application",
"bold": 0, "fieldtype": "Check",
"collapsible": 0, "label": "Enable Admission Application"
"columns": 0,
"fieldname": "introduction",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Introduction",
"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,
"unique": 0
} }
], ],
"has_web_view": 1, "has_web_view": 1,
"hide_heading": 0, "is_published_field": "published",
"hide_toolbar": 0, "links": [],
"idx": 0, "modified": "2020-06-15 20:18:38.591626",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Education",
"is_published_field": "published", "name": "Student Admission",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2017-11-10 18:57:34.570376",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Admission",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "delete": 1,
"cancel": 0, "email": 1,
"create": 1, "export": 1,
"delete": 1, "print": 1,
"email": 1, "read": 1,
"export": 1, "report": 1,
"if_owner": 0, "role": "Academics User",
"import": 0, "share": 1,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Academics User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "restrict_to_domain": "Education",
"read_only": 0, "route": "admissions",
"read_only_onload": 0, "show_name_in_global_search": 1,
"restrict_to_domain": "Education", "sort_field": "modified",
"route": "admissions", "sort_order": "DESC",
"show_name_in_global_search": 1, "title_field": "title"
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 0,
"track_seen": 0
} }

View File

@ -43,8 +43,8 @@
<thead> <thead>
<tr class="active"> <tr class="active">
<th style="width: 90px">Program/Std.</th> <th style="width: 90px">Program/Std.</th>
<th style="width: 170px">Minumum Age(DOB)</th> <th style="width: 170px">Minumum Age</th>
<th style="width: 170px">Maximum Age(DOB)</th> <th style="width: 170px">Maximum Age</th>
<th style="width: 100px">Application Fee</th> <th style="width: 100px">Application Fee</th>
</tr> </tr>
</thead> </thead>
@ -52,8 +52,8 @@
{% for row in program_details %} {% for row in program_details %}
<tr> <tr>
<td>{{ row.program }}</td> <td>{{ row.program }}</td>
<td>{{ row.minimum_age }}</td> <td>{{ row.min_age }}</td>
<td>{{ row.maximum_age }}</td> <td>{{ row.max_age }}</td>
<td>{{ row.application_fee }}</td> <td>{{ row.application_fee }}</td>
</tr> </tr>
{% endfor %} {% endfor %}
@ -61,12 +61,11 @@
</table> </table>
</div> </div>
{% endif %} {% endif %}
{%- if doc.enable_admission_application -%}
{%- if application_form_route -%}
<br> <br>
<p> <p>
<a class='btn btn-primary' <a class='btn btn-primary'
href='/{{ doc.application_form_route }}?new=1'> href='/student-applicant?new=1&student_admission={{doc.name}}'>
{{ _("Apply Now") }}</a> {{ _("Apply Now") }}</a>
</p> </p>
{% endif %} {% endif %}

View File

@ -11,7 +11,7 @@ QUnit.test('Test: Student Admission', function(assert) {
{admission_start_date: '2016-04-20'}, {admission_start_date: '2016-04-20'},
{admission_end_date: '2016-05-31'}, {admission_end_date: '2016-05-31'},
{title: '2016-17 Admissions'}, {title: '2016-17 Admissions'},
{application_form_route: 'student-applicant'}, {enable_admission_application: 1},
{introduction: 'Test intro'}, {introduction: 'Test intro'},
{program_details: [ {program_details: [
[ [
@ -28,7 +28,7 @@ QUnit.test('Test: Student Admission', function(assert) {
assert.ok(cur_frm.doc.admission_start_date == '2016-04-20'); assert.ok(cur_frm.doc.admission_start_date == '2016-04-20');
assert.ok(cur_frm.doc.admission_end_date == '2016-05-31'); assert.ok(cur_frm.doc.admission_end_date == '2016-05-31');
assert.ok(cur_frm.doc.title == '2016-17 Admissions'); assert.ok(cur_frm.doc.title == '2016-17 Admissions');
assert.ok(cur_frm.doc.application_form_route == 'student-applicant'); assert.ok(cur_frm.doc.enable_admission_application == 1);
assert.ok(cur_frm.doc.introduction == 'Test intro'); assert.ok(cur_frm.doc.introduction == 'Test intro');
assert.ok(cur_frm.doc.program_details[0].program == 'Standard Test', 'Program correctly selected'); assert.ok(cur_frm.doc.program_details[0].program == 'Standard Test', 'Program correctly selected');
assert.ok(cur_frm.doc.program_details[0].application_fee == 1000); assert.ok(cur_frm.doc.program_details[0].application_fee == 1000);

View File

@ -1,237 +1,77 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "creation": "2017-09-15 12:59:43.207923",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "editable_grid": 1,
"allow_rename": 0, "engine": "InnoDB",
"autoname": "", "field_order": [
"beta": 0, "program",
"creation": "2017-09-15 12:59:43.207923", "min_age",
"custom": 0, "max_age",
"docstatus": 0, "column_break_4",
"doctype": "DocType", "application_fee",
"document_type": "", "applicant_naming_series"
"editable_grid": 1, ],
"engine": "InnoDB",
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "program",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Program",
"collapsible": 0, "options": "Program",
"columns": 0, "show_days": 1,
"fieldname": "program", "show_seconds": 1
"fieldtype": "Link", },
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Program",
"length": 0,
"no_copy": 0,
"options": "Program",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_4",
"allow_in_quick_entry": 0, "fieldtype": "Column Break",
"allow_on_submit": 0, "show_days": 1,
"bold": 0, "show_seconds": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "minimum_age",
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Minimum Age",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "application_fee",
"allow_in_quick_entry": 0, "fieldtype": "Currency",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Application Fee",
"collapsible": 0, "show_days": 1,
"columns": 0, "show_seconds": 1
"fieldname": "maximum_age", },
"fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Maximum Age",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "applicant_naming_series",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Naming Series (for Student Applicant)",
"collapsible": 0, "show_days": 1,
"columns": 0, "show_seconds": 1
"fieldname": "column_break_4", },
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "min_age",
"allow_in_quick_entry": 0, "fieldtype": "Int",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Minimum Age",
"collapsible": 0, "show_days": 1,
"columns": 0, "show_seconds": 1
"fieldname": "application_fee", },
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Application Fee",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "max_age",
"allow_in_quick_entry": 0, "fieldtype": "Int",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Maximum Age",
"collapsible": 0, "show_days": 1,
"columns": 0, "show_seconds": 1
"fieldname": "applicant_naming_series",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Naming Series (for Student Applicant)",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2020-06-10 23:06:30.037404",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Education",
"in_create": 0, "name": "Student Admission Program",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "restrict_to_domain": "Education",
"modified": "2018-11-04 03:37:17.408427", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Education", "track_changes": 1
"name": "Student Admission Program",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@ -6,7 +6,7 @@ from __future__ import print_function, unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate from frappe.utils import getdate, add_years, nowdate, date_diff
class StudentApplicant(Document): class StudentApplicant(Document):
def autoname(self): def autoname(self):
@ -31,6 +31,7 @@ class StudentApplicant(Document):
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
if self.student_admission and self.program and self.date_of_birth: if self.student_admission and self.program and self.date_of_birth:
self.validation_from_student_admission() self.validation_from_student_admission()
@ -48,16 +49,16 @@ class StudentApplicant(Document):
frappe.throw(_("Please select Student Admission which is mandatory for the paid student applicant")) frappe.throw(_("Please select Student Admission which is mandatory for the paid student applicant"))
def validation_from_student_admission(self): def validation_from_student_admission(self):
student_admission = get_student_admission_data(self.student_admission, self.program) student_admission = get_student_admission_data(self.student_admission, self.program)
# different validation for minimum and maximum age so that either min/max can also work independently. if student_admission and student_admission.min_age and \
if student_admission and student_admission.minimum_age and \ date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.min_age)) < 0:
getdate(student_admission.minimum_age) < getdate(self.date_of_birth): frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth"))
frappe.throw(_("Not eligible for the admission in this program as per DOB"))
if student_admission and student_admission.maximum_age and \ if student_admission and student_admission.max_age and \
getdate(student_admission.maximum_age) > getdate(self.date_of_birth): date_diff(nowdate(), add_years(getdate(self.date_of_birth), student_admission.max_age)) > 0:
frappe.throw(_("Not eligible for the admission in this program as per DOB")) frappe.throw(_("Not eligible for the admission in this program as per Date Of Birth"))
def on_payment_authorized(self, *args, **kwargs): def on_payment_authorized(self, *args, **kwargs):
@ -65,10 +66,12 @@ class StudentApplicant(Document):
def get_student_admission_data(student_admission, program): def get_student_admission_data(student_admission, program):
student_admission = frappe.db.sql("""select sa.admission_start_date, sa.admission_end_date, student_admission = frappe.db.sql("""select sa.admission_start_date, sa.admission_end_date,
sap.program, sap.minimum_age, sap.maximum_age, sap.applicant_naming_series sap.program, sap.min_age, sap.max_age, sap.applicant_naming_series
from `tabStudent Admission` sa, `tabStudent Admission Program` sap from `tabStudent Admission` sa, `tabStudent Admission Program` sap
where sa.name = sap.parent and sa.name = %s and sap.program = %s""", (student_admission, program), as_dict=1) where sa.name = sap.parent and sa.name = %s and sap.program = %s""", (student_admission, program), as_dict=1)
if student_admission: if student_admission:
return student_admission[0] return student_admission[0]
else: else:

View File

@ -1,200 +1,248 @@
{ {
"accept_payment": 0, "accept_payment": 0,
"allow_comments": 0, "allow_comments": 0,
"allow_delete": 0, "allow_delete": 0,
"allow_edit": 1, "allow_edit": 1,
"allow_incomplete": 0, "allow_incomplete": 0,
"allow_multiple": 1, "allow_multiple": 1,
"allow_print": 0, "allow_print": 0,
"amount": 0.0, "amount": 0.0,
"amount_based_on_field": 0, "amount_based_on_field": 0,
"creation": "2016-09-22 13:10:10.792735", "creation": "2016-09-22 13:10:10.792735",
"doc_type": "Student Applicant", "doc_type": "Student Applicant",
"docstatus": 0, "docstatus": 0,
"doctype": "Web Form", "doctype": "Web Form",
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"login_required": 1, "login_required": 1,
"max_attachment_size": 0, "max_attachment_size": 0,
"modified": "2017-02-21 05:44:46.022738", "modified": "2020-06-11 22:53:45.875310",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "student-applicant", "name": "student-applicant",
"owner": "Administrator", "owner": "Administrator",
"payment_button_label": "Buy Now", "payment_button_label": "Buy Now",
"published": 1, "published": 1,
"route": "student-applicant", "route": "student-applicant",
"show_sidebar": 1, "route_to_success_link": 0,
"sidebar_items": [], "show_attachments": 0,
"success_url": "/student-applicant", "show_in_grid": 0,
"title": "Student Applicant", "show_sidebar": 1,
"sidebar_items": [],
"success_url": "/student-applicant",
"title": "Student Applicant",
"web_form_fields": [ "web_form_fields": [
{ {
"fieldname": "first_name", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "first_name",
"hidden": 0, "fieldtype": "Data",
"label": "First Name", "hidden": 0,
"max_length": 0, "label": "First Name",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 1 "read_only": 0,
}, "reqd": 1,
"show_in_filter": 0
},
{ {
"fieldname": "middle_name", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "middle_name",
"hidden": 0, "fieldtype": "Data",
"label": "Middle Name", "hidden": 0,
"max_length": 0, "label": "Middle Name",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "last_name", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "last_name",
"hidden": 0, "fieldtype": "Data",
"label": "Last Name", "hidden": 0,
"max_length": 0, "label": "Last Name",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "image", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "image",
"hidden": 0, "fieldtype": "Data",
"label": "Image", "hidden": 0,
"max_length": 0, "label": "Image",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "program", "allow_read_on_all_link_options": 0,
"fieldtype": "Link", "fieldname": "program",
"hidden": 0, "fieldtype": "Link",
"label": "Program", "hidden": 0,
"max_length": 0, "label": "Program",
"max_value": 0, "max_length": 0,
"options": "Program", "max_value": 0,
"read_only": 0, "options": "Program",
"reqd": 1 "read_only": 0,
}, "reqd": 1,
"show_in_filter": 0
},
{ {
"fieldname": "academic_year", "allow_read_on_all_link_options": 0,
"fieldtype": "Link", "fieldname": "academic_year",
"hidden": 0, "fieldtype": "Link",
"label": "Academic Year", "hidden": 0,
"max_length": 0, "label": "Academic Year",
"max_value": 0, "max_length": 0,
"options": "Academic Year", "max_value": 0,
"read_only": 0, "options": "Academic Year",
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "date_of_birth", "allow_read_on_all_link_options": 0,
"fieldtype": "Date", "fieldname": "date_of_birth",
"hidden": 0, "fieldtype": "Date",
"label": "Date of Birth", "hidden": 0,
"max_length": 0, "label": "Date of Birth",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "blood_group", "allow_read_on_all_link_options": 0,
"fieldtype": "Select", "fieldname": "blood_group",
"hidden": 0, "fieldtype": "Select",
"label": "Blood Group", "hidden": 0,
"max_length": 0, "label": "Blood Group",
"max_value": 0, "max_length": 0,
"options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-", "max_value": 0,
"read_only": 0, "options": "\nA+\nA-\nB+\nB-\nO+\nO-\nAB+\nAB-",
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "student_email_id", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "student_email_id",
"hidden": 0, "fieldtype": "Data",
"label": "Student Email ID", "hidden": 0,
"max_length": 0, "label": "Student Email ID",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "student_mobile_number", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "student_mobile_number",
"hidden": 0, "fieldtype": "Data",
"label": "Student Mobile Number", "hidden": 0,
"max_length": 0, "label": "Student Mobile Number",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"default": "INDIAN", "allow_read_on_all_link_options": 0,
"fieldname": "nationality", "default": "INDIAN",
"fieldtype": "Data", "fieldname": "nationality",
"hidden": 0, "fieldtype": "Data",
"label": "Nationality", "hidden": 0,
"max_length": 0, "label": "Nationality",
"max_value": 0, "max_length": 0,
"options": "", "max_value": 0,
"read_only": 0, "options": "",
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "address_line_1", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "address_line_1",
"hidden": 0, "fieldtype": "Data",
"label": "Address Line 1", "hidden": 0,
"max_length": 0, "label": "Address Line 1",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "address_line_2", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "address_line_2",
"hidden": 0, "fieldtype": "Data",
"label": "Address Line 2", "hidden": 0,
"max_length": 0, "label": "Address Line 2",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "pincode", "allow_read_on_all_link_options": 0,
"fieldtype": "Data", "fieldname": "pincode",
"hidden": 0, "fieldtype": "Data",
"label": "Pincode", "hidden": 0,
"max_length": 0, "label": "Pincode",
"max_value": 0, "max_length": 0,
"read_only": 0, "max_value": 0,
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "guardians", "allow_read_on_all_link_options": 0,
"fieldtype": "Table", "fieldname": "guardians",
"hidden": 0, "fieldtype": "Table",
"label": "Guardians", "hidden": 0,
"max_length": 0, "label": "Guardians",
"max_value": 0, "max_length": 0,
"options": "Student Guardian", "max_value": 0,
"read_only": 0, "options": "Student Guardian",
"reqd": 0 "read_only": 0,
}, "reqd": 0,
"show_in_filter": 0
},
{ {
"fieldname": "siblings", "allow_read_on_all_link_options": 0,
"fieldtype": "Table", "fieldname": "siblings",
"hidden": 0, "fieldtype": "Table",
"label": "Siblings", "hidden": 0,
"max_length": 0, "label": "Siblings",
"max_value": 0, "max_length": 0,
"options": "Student Sibling", "max_value": 0,
"read_only": 0, "options": "Student Sibling",
"reqd": 0 "read_only": 0,
"reqd": 0,
"show_in_filter": 0
},
{
"allow_read_on_all_link_options": 0,
"fieldname": "student_admission",
"fieldtype": "Link",
"hidden": 0,
"label": "Student Admission",
"max_length": 0,
"max_value": 0,
"options": "Student Admission",
"read_only": 0,
"reqd": 0,
"show_in_filter": 0
} }
] ]
} }

View File

@ -241,14 +241,17 @@ def get_order_taxes(shopify_order, shopify_settings):
return taxes return taxes
def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings): def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings):
"""Shipping lines represents the shipping details,
each such shipping detail consists of a list of tax_lines"""
for shipping_charge in shipping_lines: for shipping_charge in shipping_lines:
taxes.append({ for tax in shipping_charge.get("tax_lines"):
"charge_type": _("Actual"), taxes.append({
"account_head": get_tax_account_head(shipping_charge), "charge_type": _("Actual"),
"description": shipping_charge["title"], "account_head": get_tax_account_head(tax),
"tax_amount": shipping_charge["price"], "description": tax["title"],
"cost_center": shopify_settings.cost_center "tax_amount": tax["price"],
}) "cost_center": shopify_settings.cost_center
})
return taxes return taxes

View File

@ -73,10 +73,16 @@ def link_customer_and_address(raw_billing_data, raw_shipping_data, customer_name
if customer_exists: if customer_exists:
frappe.rename_doc("Customer", old_name, customer_name) frappe.rename_doc("Customer", old_name, customer_name)
billing_address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": "Billing"}) for address_type in ("Billing", "Shipping",):
shipping_address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": "Shipping"}) try:
rename_address(billing_address, customer) address = frappe.get_doc("Address", {"woocommerce_email": customer_woo_com_email, "address_type": address_type})
rename_address(shipping_address, customer) rename_address(address, customer)
except (
frappe.DoesNotExistError,
frappe.DuplicateEntryError,
frappe.ValidationError,
):
pass
else: else:
create_address(raw_billing_data, customer, "Billing") create_address(raw_billing_data, customer, "Billing")
create_address(raw_shipping_data, customer, "Shipping") create_address(raw_shipping_data, customer, "Shipping")

View File

@ -8,6 +8,7 @@ import json
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import get_request_session from frappe.utils import get_request_session
from requests.exceptions import HTTPError
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.erpnext_integrations.utils import get_webhook_address from erpnext.erpnext_integrations.utils import get_webhook_address
from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log from erpnext.erpnext_integrations.doctype.shopify_log.shopify_log import make_shopify_log
@ -29,19 +30,24 @@ class ShopifySettings(Document):
webhooks = ["orders/create", "orders/paid", "orders/fulfilled"] webhooks = ["orders/create", "orders/paid", "orders/fulfilled"]
# url = get_shopify_url('admin/webhooks.json', self) # url = get_shopify_url('admin/webhooks.json', self)
created_webhooks = [d.method for d in self.webhooks] created_webhooks = [d.method for d in self.webhooks]
url = get_shopify_url('admin/api/2019-04/webhooks.json', self) url = get_shopify_url('admin/api/2020-04/webhooks.json', self)
for method in webhooks: for method in webhooks:
session = get_request_session() session = get_request_session()
try: try:
d = session.post(url, data=json.dumps({ res = session.post(url, data=json.dumps({
"webhook": { "webhook": {
"topic": method, "topic": method,
"address": get_webhook_address(connector_name='shopify_connection', method='store_request_data'), "address": get_webhook_address(connector_name='shopify_connection', method='store_request_data'),
"format": "json" "format": "json"
} }
}), headers=get_header(self)) }), headers=get_header(self))
d.raise_for_status() res.raise_for_status()
self.update_webhook_table(method, d.json()) self.update_webhook_table(method, res.json())
except HTTPError as e:
error_message = res.json().get('errors', e)
make_shopify_log(status="Warning", exception=error_message, rollback=True)
except Exception as e: except Exception as e:
make_shopify_log(status="Warning", exception=e, rollback=True) make_shopify_log(status="Warning", exception=e, rollback=True)
@ -50,13 +56,18 @@ class ShopifySettings(Document):
deleted_webhooks = [] deleted_webhooks = []
for d in self.webhooks: for d in self.webhooks:
url = get_shopify_url('admin/api/2019-04/webhooks/{0}.json'.format(d.webhook_id), self) url = get_shopify_url('admin/api/2020-04/webhooks/{0}.json'.format(d.webhook_id), self)
try: try:
res = session.delete(url, headers=get_header(self)) res = session.delete(url, headers=get_header(self))
res.raise_for_status() res.raise_for_status()
deleted_webhooks.append(d) deleted_webhooks.append(d)
except HTTPError as e:
error_message = res.json().get('errors', e)
make_shopify_log(status="Warning", exception=error_message, rollback=True)
except Exception as e: except Exception as e:
frappe.log_error(message=frappe.get_traceback(), title=e) frappe.log_error(message=e, title='Shopify Webhooks Issue')
for d in deleted_webhooks: for d in deleted_webhooks:
self.remove(d) self.remove(d)
@ -125,4 +136,3 @@ def setup_custom_fields():
} }
create_custom_fields(custom_fields) create_custom_fields(custom_fields)

View File

@ -8,7 +8,7 @@ from erpnext.erpnext_integrations.doctype.shopify_settings.shopify_settings impo
shopify_variants_attr_list = ["option1", "option2", "option3"] shopify_variants_attr_list = ["option1", "option2", "option3"]
def sync_item_from_shopify(shopify_settings, item): def sync_item_from_shopify(shopify_settings, item):
url = get_shopify_url("admin/api/2019-04/products/{0}.json".format(item.get("product_id")), shopify_settings) url = get_shopify_url("admin/api/2020-04/products/{0}.json".format(item.get("product_id")), shopify_settings)
session = get_request_session() session = get_request_session()
try: try:

View File

@ -1,7 +1,9 @@
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Tally Migration', { frappe.provide("erpnext.tally_migration");
frappe.ui.form.on("Tally Migration", {
onload: function (frm) { onload: function (frm) {
let reload_status = true; let reload_status = true;
frappe.realtime.on("tally_migration_progress_update", function (data) { frappe.realtime.on("tally_migration_progress_update", function (data) {
@ -35,7 +37,17 @@ frappe.ui.form.on('Tally Migration', {
} }
}); });
}, },
refresh: function (frm) { refresh: function (frm) {
frm.trigger("show_logs_preview");
erpnext.tally_migration.failed_import_log = JSON.parse(frm.doc.failed_import_log);
erpnext.tally_migration.fixed_errors_log = JSON.parse(frm.doc.fixed_errors_log);
["default_round_off_account", "default_warehouse", "default_cost_center"].forEach(account => {
frm.toggle_reqd(account, frm.doc.is_master_data_imported === 1)
frm.toggle_enable(account, frm.doc.is_day_book_data_processed != 1)
})
if (frm.doc.master_data && !frm.doc.is_master_data_imported) { if (frm.doc.master_data && !frm.doc.is_master_data_imported) {
if (frm.doc.is_master_data_processed) { if (frm.doc.is_master_data_processed) {
if (frm.doc.status != "Importing Master Data") { if (frm.doc.status != "Importing Master Data") {
@ -47,6 +59,7 @@ frappe.ui.form.on('Tally Migration', {
} }
} }
} }
if (frm.doc.day_book_data && !frm.doc.is_day_book_data_imported) { if (frm.doc.day_book_data && !frm.doc.is_day_book_data_imported) {
if (frm.doc.is_day_book_data_processed) { if (frm.doc.is_day_book_data_processed) {
if (frm.doc.status != "Importing Day Book Data") { if (frm.doc.status != "Importing Day Book Data") {
@ -59,6 +72,17 @@ frappe.ui.form.on('Tally Migration', {
} }
} }
}, },
erpnext_company: function (frm) {
frappe.db.exists("Company", frm.doc.erpnext_company).then(exists => {
if (exists) {
frappe.msgprint(
__("Company {0} already exists. Continuing will overwrite the Company and Chart of Accounts", [frm.doc.erpnext_company]),
);
}
});
},
add_button: function (frm, label, method) { add_button: function (frm, label, method) {
frm.add_custom_button( frm.add_custom_button(
label, label,
@ -71,5 +95,255 @@ frappe.ui.form.on('Tally Migration', {
frm.reload_doc(); frm.reload_doc();
} }
); );
},
render_html_table(frm, shown_logs, hidden_logs, field) {
if (shown_logs && shown_logs.length > 0) {
frm.toggle_display(field, true);
} else {
frm.toggle_display(field, false);
return
}
let rows = erpnext.tally_migration.get_html_rows(shown_logs, field);
let rows_head, table_caption;
let table_footer = (hidden_logs && (hidden_logs.length > 0)) ? `<tr class="text-muted">
<td colspan="4">And ${hidden_logs.length} more others</td>
</tr>`: "";
if (field === "fixed_error_log_preview") {
rows_head = `<th width="75%">${__("Meta Data")}</th>
<th width="10%">${__("Unresolve")}</th>`
table_caption = "Resolved Issues"
} else {
rows_head = `<th width="75%">${__("Error Message")}</th>
<th width="10%">${__("Create")}</th>`
table_caption = "Error Log"
}
frm.get_field(field).$wrapper.html(`
<table class="table table-bordered">
<caption>${table_caption}</caption>
<tr class="text-muted">
<th width="5%">${__("#")}</th>
<th width="10%">${__("DocType")}</th>
${rows_head}
</tr>
${rows}
${table_footer}
</table>
`);
},
show_error_summary(frm) {
let summary = erpnext.tally_migration.failed_import_log.reduce((summary, row) => {
if (row.doc) {
if (summary[row.doc.doctype]) {
summary[row.doc.doctype] += 1;
} else {
summary[row.doc.doctype] = 1;
}
}
return summary
}, {});
console.table(summary);
},
show_logs_preview(frm) {
let empty = "[]";
let import_log = frm.doc.failed_import_log || empty;
let completed_log = frm.doc.fixed_errors_log || empty;
let render_section = !(import_log === completed_log && import_log === empty);
frm.toggle_display("import_log_section", render_section);
if (render_section) {
frm.trigger("show_error_summary");
frm.trigger("show_errored_import_log");
frm.trigger("show_fixed_errors_log");
}
},
show_errored_import_log(frm) {
let import_log = erpnext.tally_migration.failed_import_log;
let logs = import_log.slice(0, 20);
let hidden_logs = import_log.slice(20);
frm.events.render_html_table(frm, logs, hidden_logs, "failed_import_preview");
},
show_fixed_errors_log(frm) {
let completed_log = erpnext.tally_migration.fixed_errors_log;
let logs = completed_log.slice(0, 20);
let hidden_logs = completed_log.slice(20);
frm.events.render_html_table(frm, logs, hidden_logs, "fixed_error_log_preview");
} }
}); });
erpnext.tally_migration.getError = (traceback) => {
/* Extracts the Error Message from the Python Traceback or Solved error */
let is_multiline = traceback.trim().indexOf("\n") != -1;
let message;
if (is_multiline) {
let exc_error_idx = traceback.trim().lastIndexOf("\n") + 1
let error_line = traceback.substr(exc_error_idx)
let split_str_idx = (error_line.indexOf(':') > 0) ? error_line.indexOf(':') + 1 : 0;
message = error_line.slice(split_str_idx).trim();
} else {
message = traceback;
}
return message
}
erpnext.tally_migration.cleanDoc = (obj) => {
/* Strips all null and empty values of your JSON object */
let temp = obj;
$.each(temp, function(key, value){
if (value === "" || value === null){
delete obj[key];
} else if (Object.prototype.toString.call(value) === '[object Object]') {
erpnext.tally_migration.cleanDoc(value);
} else if ($.isArray(value)) {
$.each(value, function (k,v) { erpnext.tally_migration.cleanDoc(v); });
}
});
return temp;
}
erpnext.tally_migration.unresolve = (document) => {
/* Mark document migration as unresolved ie. move to failed error log */
let frm = cur_frm;
let failed_log = erpnext.tally_migration.failed_import_log;
let fixed_log = erpnext.tally_migration.fixed_errors_log;
let modified_fixed_log = fixed_log.filter(row => {
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
return row
}
});
failed_log.push({ doc: document, exc: `Marked unresolved on ${Date()}` });
frm.doc.failed_import_log = JSON.stringify(failed_log);
frm.doc.fixed_errors_log = JSON.stringify(modified_fixed_log);
frm.dirty();
frm.save();
}
erpnext.tally_migration.resolve = (document) => {
/* Mark document migration as resolved ie. move to fixed error log */
let frm = cur_frm;
let failed_log = erpnext.tally_migration.failed_import_log;
let fixed_log = erpnext.tally_migration.fixed_errors_log;
let modified_failed_log = failed_log.filter(row => {
if (!frappe.utils.deep_equal(erpnext.tally_migration.cleanDoc(row.doc), document)) {
return row
}
});
fixed_log.push({ doc: document, exc: `Solved on ${Date()}` });
frm.doc.failed_import_log = JSON.stringify(modified_failed_log);
frm.doc.fixed_errors_log = JSON.stringify(fixed_log);
frm.dirty();
frm.save();
}
erpnext.tally_migration.create_new_doc = (document) => {
/* Mark as resolved and create new document */
erpnext.tally_migration.resolve(document);
return frappe.call({
type: "POST",
method: 'erpnext.erpnext_integrations.doctype.tally_migration.tally_migration.new_doc',
args: {
document
},
freeze: true,
callback: function(r) {
if(!r.exc) {
frappe.model.sync(r.message);
frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = true;
frappe.set_route("Form", r.message.doctype, r.message.name);
}
}
});
}
erpnext.tally_migration.get_html_rows = (logs, field) => {
let index = 0;
let rows = logs
.map(({ doc, exc }) => {
let id = frappe.dom.get_unique_id();
let traceback = exc;
let error_message = erpnext.tally_migration.getError(traceback);
index++;
let show_traceback = `
<button class="btn btn-default btn-xs m-3" type="button" data-toggle="collapse" data-target="#${id}-traceback" aria-expanded="false" aria-controls="${id}-traceback">
${__("Show Traceback")}
</button>
<div class="collapse margin-top" id="${id}-traceback">
<div class="well">
<pre style="font-size: smaller;">${traceback}</pre>
</div>
</div>`;
let show_doc = `
<button class='btn btn-default btn-xs m-3' type='button' data-toggle='collapse' data-target='#${id}-doc' aria-expanded='false' aria-controls='${id}-doc'>
${__("Show Document")}
</button>
<div class="collapse margin-top" id="${id}-doc">
<div class="well">
<pre style="font-size: smaller;">${JSON.stringify(erpnext.tally_migration.cleanDoc(doc), null, 1)}</pre>
</div>
</div>`;
let create_button = `
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.create_new_doc(${JSON.stringify(doc)})'>
${__("Create Document")}
</button>`
let mark_as_unresolved = `
<button class='btn btn-default btn-xs m-3' type='button' onclick='erpnext.tally_migration.unresolve(${JSON.stringify(doc)})'>
${__("Mark as unresolved")}
</button>`
if (field === "fixed_error_log_preview") {
return `<tr>
<td>${index}</td>
<td>
<div>${doc.doctype}</div>
</td>
<td>
<div>${error_message}</div>
<div>${show_doc}</div>
</td>
<td>
<div>${mark_as_unresolved}</div>
</td>
</tr>`;
} else {
return `<tr>
<td>${index}</td>
<td>
<div>${doc.doctype}</div>
</td>
<td>
<div>${error_message}</div>
<div>${show_traceback}</div>
<div>${show_doc}</div>
</td>
<td>
<div>${create_button}</div>
</td>
</tr>`;
}
}).join("");
return rows
}

View File

@ -28,14 +28,19 @@
"vouchers", "vouchers",
"accounts_section", "accounts_section",
"default_warehouse", "default_warehouse",
"round_off_account", "default_round_off_account",
"column_break_21", "column_break_21",
"default_cost_center", "default_cost_center",
"day_book_section", "day_book_section",
"day_book_data", "day_book_data",
"column_break_27", "column_break_27",
"is_day_book_data_processed", "is_day_book_data_processed",
"is_day_book_data_imported" "is_day_book_data_imported",
"import_log_section",
"failed_import_log",
"fixed_errors_log",
"failed_import_preview",
"fixed_error_log_preview"
], ],
"fields": [ "fields": [
{ {
@ -57,6 +62,7 @@
"fieldname": "tally_creditors_account", "fieldname": "tally_creditors_account",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Tally Creditors Account", "label": "Tally Creditors Account",
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
"reqd": 1 "reqd": 1
}, },
{ {
@ -69,6 +75,7 @@
"fieldname": "tally_debtors_account", "fieldname": "tally_debtors_account",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Tally Debtors Account", "label": "Tally Debtors Account",
"read_only_depends_on": "eval:doc.is_master_data_processed==1",
"reqd": 1 "reqd": 1
}, },
{ {
@ -92,7 +99,7 @@
"fieldname": "erpnext_company", "fieldname": "erpnext_company",
"fieldtype": "Data", "fieldtype": "Data",
"label": "ERPNext Company", "label": "ERPNext Company",
"read_only_depends_on": "eval:doc.is_master_data_processed == 1" "read_only_depends_on": "eval:doc.is_master_data_processed==1"
}, },
{ {
"fieldname": "processed_files_section", "fieldname": "processed_files_section",
@ -136,6 +143,7 @@
}, },
{ {
"depends_on": "is_master_data_imported", "depends_on": "is_master_data_imported",
"description": "The accounts are set by the system automatically but do confirm these defaults",
"fieldname": "accounts_section", "fieldname": "accounts_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounts" "label": "Accounts"
@ -146,12 +154,6 @@
"label": "Default Warehouse", "label": "Default Warehouse",
"options": "Warehouse" "options": "Warehouse"
}, },
{
"fieldname": "round_off_account",
"fieldtype": "Link",
"label": "Round Off Account",
"options": "Account"
},
{ {
"fieldname": "column_break_21", "fieldname": "column_break_21",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -212,11 +214,47 @@
"fieldname": "default_uom", "fieldname": "default_uom",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Default UOM", "label": "Default UOM",
"options": "UOM" "options": "UOM",
"read_only_depends_on": "eval:doc.is_master_data_imported==1"
},
{
"default": "[]",
"fieldname": "failed_import_log",
"fieldtype": "Code",
"hidden": 1,
"options": "JSON"
},
{
"fieldname": "failed_import_preview",
"fieldtype": "HTML",
"label": "Failed Import Log"
},
{
"fieldname": "import_log_section",
"fieldtype": "Section Break",
"label": "Import Log"
},
{
"fieldname": "default_round_off_account",
"fieldtype": "Link",
"label": "Default Round Off Account",
"options": "Account"
},
{
"default": "[]",
"fieldname": "fixed_errors_log",
"fieldtype": "Code",
"hidden": 1,
"options": "JSON"
},
{
"fieldname": "fixed_error_log_preview",
"fieldtype": "HTML",
"label": "Fixed Error Log"
} }
], ],
"links": [], "links": [],
"modified": "2020-04-16 13:03:28.894919", "modified": "2020-04-28 00:29:18.039826",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Tally Migration", "name": "Tally Migration",

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import json import json
import re import re
import sys
import traceback import traceback
import zipfile import zipfile
from decimal import Decimal from decimal import Decimal
@ -15,18 +16,34 @@ from bs4 import BeautifulSoup as bs
import frappe import frappe
from erpnext import encode_company_abbr from erpnext import encode_company_abbr
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
from erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer import unset_existing_data
from frappe import _ from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import getseries, revert_series_if_last from frappe.model.naming import getseries, revert_series_if_last
from frappe.utils.data import format_datetime from frappe.utils.data import format_datetime
PRIMARY_ACCOUNT = "Primary" PRIMARY_ACCOUNT = "Primary"
VOUCHER_CHUNK_SIZE = 500 VOUCHER_CHUNK_SIZE = 500
@frappe.whitelist()
def new_doc(document):
document = json.loads(document)
doctype = document.pop("doctype")
document.pop("name", None)
doc = frappe.new_doc(doctype)
doc.update(document)
return doc
class TallyMigration(Document): class TallyMigration(Document):
def validate(self):
failed_import_log = json.loads(self.failed_import_log)
sorted_failed_import_log = sorted(failed_import_log, key=lambda row: row["doc"]["creation"])
self.failed_import_log = json.dumps(sorted_failed_import_log)
def autoname(self): def autoname(self):
if not self.name: if not self.name:
self.name = "Tally Migration on " + format_datetime(self.creation) self.name = "Tally Migration on " + format_datetime(self.creation)
@ -65,9 +82,17 @@ class TallyMigration(Document):
"attached_to_name": self.name, "attached_to_name": self.name,
"content": json.dumps(value), "content": json.dumps(value),
"is_private": True "is_private": True
}).insert() })
try:
f.insert()
except frappe.DuplicateEntryError:
pass
setattr(self, key, f.file_url) setattr(self, key, f.file_url)
def set_account_defaults(self):
self.default_cost_center, self.default_round_off_account = frappe.db.get_value("Company", self.erpnext_company, ["cost_center", "round_off_account"])
self.default_warehouse = frappe.db.get_value("Stock Settings", "Stock Settings", "default_warehouse")
def _process_master_data(self): def _process_master_data(self):
def get_company_name(collection): def get_company_name(collection):
return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip() return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip()
@ -84,7 +109,11 @@ class TallyMigration(Document):
children, parents = get_children_and_parent_dict(accounts) children, parents = get_children_and_parent_dict(accounts)
group_set = [acc[1] for acc in accounts if acc[2]] group_set = [acc[1] for acc in accounts if acc[2]]
children, customers, suppliers = remove_parties(parents, children, group_set) children, customers, suppliers = remove_parties(parents, children, group_set)
coa = traverse({}, children, roots, roots, group_set)
try:
coa = traverse({}, children, roots, roots, group_set)
except RecursionError:
self.log(_("Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name"))
for account in coa: for account in coa:
coa[account]["root_type"] = root_type_map[account] coa[account]["root_type"] = root_type_map[account]
@ -126,14 +155,18 @@ class TallyMigration(Document):
def remove_parties(parents, children, group_set): def remove_parties(parents, children, group_set):
customers, suppliers = set(), set() customers, suppliers = set(), set()
for account in parents: for account in parents:
found = False
if self.tally_creditors_account in parents[account]: if self.tally_creditors_account in parents[account]:
children.pop(account, None) found = True
if account not in group_set: if account not in group_set:
suppliers.add(account) suppliers.add(account)
elif self.tally_debtors_account in parents[account]: if self.tally_debtors_account in parents[account]:
children.pop(account, None) found = True
if account not in group_set: if account not in group_set:
customers.add(account) customers.add(account)
if found:
children.pop(account, None)
return children, customers, suppliers return children, customers, suppliers
def traverse(tree, children, accounts, roots, group_set): def traverse(tree, children, accounts, roots, group_set):
@ -151,6 +184,7 @@ class TallyMigration(Document):
parties, addresses = [], [] parties, addresses = [], []
for account in collection.find_all("LEDGER"): for account in collection.find_all("LEDGER"):
party_type = None party_type = None
links = []
if account.NAME.string.strip() in customers: if account.NAME.string.strip() in customers:
party_type = "Customer" party_type = "Customer"
parties.append({ parties.append({
@ -161,7 +195,9 @@ class TallyMigration(Document):
"territory": "All Territories", "territory": "All Territories",
"customer_type": "Individual", "customer_type": "Individual",
}) })
elif account.NAME.string.strip() in suppliers: links.append({"link_doctype": party_type, "link_name": account["NAME"]})
if account.NAME.string.strip() in suppliers:
party_type = "Supplier" party_type = "Supplier"
parties.append({ parties.append({
"doctype": party_type, "doctype": party_type,
@ -170,6 +206,8 @@ class TallyMigration(Document):
"supplier_group": "All Supplier Groups", "supplier_group": "All Supplier Groups",
"supplier_type": "Individual", "supplier_type": "Individual",
}) })
links.append({"link_doctype": party_type, "link_name": account["NAME"]})
if party_type: if party_type:
address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")]) address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")])
addresses.append({ addresses.append({
@ -183,7 +221,7 @@ class TallyMigration(Document):
"mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
"phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None, "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
"gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None, "gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None,
"links": [{"link_doctype": party_type, "link_name": account["NAME"]}], "links": links
}) })
return parties, addresses return parties, addresses
@ -242,12 +280,18 @@ class TallyMigration(Document):
def create_company_and_coa(coa_file_url): def create_company_and_coa(coa_file_url):
coa_file = frappe.get_doc("File", {"file_url": coa_file_url}) coa_file = frappe.get_doc("File", {"file_url": coa_file_url})
frappe.local.flags.ignore_chart_of_accounts = True frappe.local.flags.ignore_chart_of_accounts = True
company = frappe.get_doc({
"doctype": "Company", try:
"company_name": self.erpnext_company, company = frappe.get_doc({
"default_currency": "INR", "doctype": "Company",
"enable_perpetual_inventory": 0, "company_name": self.erpnext_company,
}).insert() "default_currency": "INR",
"enable_perpetual_inventory": 0,
}).insert()
except frappe.DuplicateEntryError:
company = frappe.get_doc("Company", self.erpnext_company)
unset_existing_data(self.erpnext_company)
frappe.local.flags.ignore_chart_of_accounts = False frappe.local.flags.ignore_chart_of_accounts = False
create_charts(company.name, custom_chart=json.loads(coa_file.get_content())) create_charts(company.name, custom_chart=json.loads(coa_file.get_content()))
company.create_default_warehouses() company.create_default_warehouses()
@ -256,36 +300,35 @@ class TallyMigration(Document):
parties_file = frappe.get_doc("File", {"file_url": parties_file_url}) parties_file = frappe.get_doc("File", {"file_url": parties_file_url})
for party in json.loads(parties_file.get_content()): for party in json.loads(parties_file.get_content()):
try: try:
frappe.get_doc(party).insert() party_doc = frappe.get_doc(party)
party_doc.insert()
except: except:
self.log(party) self.log(party_doc)
addresses_file = frappe.get_doc("File", {"file_url": addresses_file_url}) addresses_file = frappe.get_doc("File", {"file_url": addresses_file_url})
for address in json.loads(addresses_file.get_content()): for address in json.loads(addresses_file.get_content()):
try: try:
frappe.get_doc(address).insert(ignore_mandatory=True) address_doc = frappe.get_doc(address)
address_doc.insert(ignore_mandatory=True)
except: except:
try: self.log(address_doc)
gstin = address.pop("gstin", None)
frappe.get_doc(address).insert(ignore_mandatory=True)
self.log({"address": address, "message": "Invalid GSTIN: {}. Address was created without GSTIN".format(gstin)})
except:
self.log(address)
def create_items_uoms(items_file_url, uoms_file_url): def create_items_uoms(items_file_url, uoms_file_url):
uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url}) uoms_file = frappe.get_doc("File", {"file_url": uoms_file_url})
for uom in json.loads(uoms_file.get_content()): for uom in json.loads(uoms_file.get_content()):
if not frappe.db.exists(uom): if not frappe.db.exists(uom):
try: try:
frappe.get_doc(uom).insert() uom_doc = frappe.get_doc(uom)
uom_doc.insert()
except: except:
self.log(uom) self.log(uom_doc)
items_file = frappe.get_doc("File", {"file_url": items_file_url}) items_file = frappe.get_doc("File", {"file_url": items_file_url})
for item in json.loads(items_file.get_content()): for item in json.loads(items_file.get_content()):
try: try:
frappe.get_doc(item).insert() item_doc = frappe.get_doc(item)
item_doc.insert()
except: except:
self.log(item) self.log(item_doc)
try: try:
self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4) self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4)
@ -299,10 +342,13 @@ class TallyMigration(Document):
self.publish("Import Master Data", _("Done"), 4, 4) self.publish("Import Master Data", _("Done"), 4, 4)
self.set_account_defaults()
self.is_master_data_imported = 1 self.is_master_data_imported = 1
frappe.db.commit()
except: except:
self.publish("Import Master Data", _("Process Failed"), -1, 5) self.publish("Import Master Data", _("Process Failed"), -1, 5)
frappe.db.rollback()
self.log() self.log()
finally: finally:
@ -323,7 +369,9 @@ class TallyMigration(Document):
processed_voucher = function(voucher) processed_voucher = function(voucher)
if processed_voucher: if processed_voucher:
vouchers.append(processed_voucher) vouchers.append(processed_voucher)
frappe.db.commit()
except: except:
frappe.db.rollback()
self.log(voucher) self.log(voucher)
return vouchers return vouchers
@ -349,6 +397,7 @@ class TallyMigration(Document):
journal_entry = { journal_entry = {
"doctype": "Journal Entry", "doctype": "Journal Entry",
"tally_guid": voucher.GUID.string.strip(), "tally_guid": voucher.GUID.string.strip(),
"tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "",
"posting_date": voucher.DATE.string.strip(), "posting_date": voucher.DATE.string.strip(),
"company": self.erpnext_company, "company": self.erpnext_company,
"accounts": accounts, "accounts": accounts,
@ -377,6 +426,7 @@ class TallyMigration(Document):
"doctype": doctype, "doctype": doctype,
party_field: voucher.PARTYNAME.string.strip(), party_field: voucher.PARTYNAME.string.strip(),
"tally_guid": voucher.GUID.string.strip(), "tally_guid": voucher.GUID.string.strip(),
"tally_voucher_no": voucher.VOUCHERNUMBER.string.strip() if voucher.VOUCHERNUMBER else "",
"posting_date": voucher.DATE.string.strip(), "posting_date": voucher.DATE.string.strip(),
"due_date": voucher.DATE.string.strip(), "due_date": voucher.DATE.string.strip(),
"items": get_voucher_items(voucher, doctype), "items": get_voucher_items(voucher, doctype),
@ -468,14 +518,21 @@ class TallyMigration(Document):
oldest_year = new_year oldest_year = new_year
def create_custom_fields(doctypes): def create_custom_fields(doctypes):
for doctype in doctypes: tally_guid_df = {
df = { "fieldtype": "Data",
"fieldtype": "Data", "fieldname": "tally_guid",
"fieldname": "tally_guid", "read_only": 1,
"read_only": 1, "label": "Tally GUID"
"label": "Tally GUID" }
} tally_voucher_no_df = {
create_custom_field(doctype, df) "fieldtype": "Data",
"fieldname": "tally_voucher_no",
"read_only": 1,
"label": "Tally Voucher Number"
}
for df in [tally_guid_df, tally_voucher_no_df]:
for doctype in doctypes:
create_custom_field(doctype, df)
def create_price_list(): def create_price_list():
frappe.get_doc({ frappe.get_doc({
@ -490,7 +547,7 @@ class TallyMigration(Document):
try: try:
frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable") frappe.db.set_value("Account", encode_company_abbr(self.tally_creditors_account, self.erpnext_company), "account_type", "Payable")
frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable") frappe.db.set_value("Account", encode_company_abbr(self.tally_debtors_account, self.erpnext_company), "account_type", "Receivable")
frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.round_off_account) frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.default_round_off_account)
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers}) vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
vouchers = json.loads(vouchers_file.get_content()) vouchers = json.loads(vouchers_file.get_content())
@ -521,11 +578,14 @@ class TallyMigration(Document):
for index, voucher in enumerate(chunk, start=start): for index, voucher in enumerate(chunk, start=start):
try: try:
doc = frappe.get_doc(voucher).insert() voucher_doc = frappe.get_doc(voucher)
doc.submit() voucher_doc.insert()
voucher_doc.submit()
self.publish("Importing Vouchers", _("{} of {}").format(index, total), index, total) self.publish("Importing Vouchers", _("{} of {}").format(index, total), index, total)
frappe.db.commit()
except: except:
self.log(voucher) frappe.db.rollback()
self.log(voucher_doc)
if is_last: if is_last:
self.status = "" self.status = ""
@ -551,9 +611,22 @@ class TallyMigration(Document):
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
def log(self, data=None): def log(self, data=None):
data = data or self.status if isinstance(data, frappe.model.document.Document):
message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) if sys.exc_info()[1].__class__ != frappe.DuplicateEntryError:
return frappe.log_error(title="Tally Migration Error", message=message) failed_import_log = json.loads(self.failed_import_log)
doc = data.as_dict()
failed_import_log.append({
"doc": doc,
"exc": traceback.format_exc()
})
self.failed_import_log = json.dumps(failed_import_log, separators=(',', ':'))
self.save()
frappe.db.commit()
else:
data = data or self.status
message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
return frappe.log_error(title="Tally Migration Error", message=message)
def set_status(self, status=""): def set_status(self, status=""):
self.status = status self.status = status

View File

@ -70,6 +70,7 @@ def validate_service_item(item, msg):
if frappe.db.get_value('Item', item, 'is_stock_item'): if frappe.db.get_value('Item', item, 'is_stock_item'):
frappe.throw(_(msg)) frappe.throw(_(msg))
@frappe.whitelist()
def get_practitioner_list(doctype, txt, searchfield, start, page_len, filters=None): def get_practitioner_list(doctype, txt, searchfield, start, page_len, filters=None):
fields = ['name', 'practitioner_name', 'mobile_phone'] fields = ['name', 'practitioner_name', 'mobile_phone']

View File

@ -15,6 +15,7 @@ class TestInpatientRecord(unittest.TestCase):
patient = create_patient() patient = create_patient()
# Schedule Admission # Schedule Admission
ip_record = create_inpatient(patient) ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True) ip_record.save(ignore_permissions = True)
self.assertEqual(ip_record.name, frappe.db.get_value("Patient", patient, "inpatient_record")) self.assertEqual(ip_record.name, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(ip_record.status, frappe.db.get_value("Patient", patient, "inpatient_status")) self.assertEqual(ip_record.status, frappe.db.get_value("Patient", patient, "inpatient_status"))
@ -26,7 +27,7 @@ class TestInpatientRecord(unittest.TestCase):
self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) self.assertEqual("Occupied", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
# Discharge # Discharge
schedule_discharge(patient=patient) schedule_discharge(frappe.as_json({'patient': patient}))
self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status")) self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name) ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
@ -44,8 +45,10 @@ class TestInpatientRecord(unittest.TestCase):
patient = create_patient() patient = create_patient()
ip_record = create_inpatient(patient) ip_record = create_inpatient(patient)
ip_record.expected_length_of_stay = 0
ip_record.save(ignore_permissions = True) ip_record.save(ignore_permissions = True)
ip_record_new = create_inpatient(patient) ip_record_new = create_inpatient(patient)
ip_record_new.expected_length_of_stay = 0
self.assertRaises(frappe.ValidationError, ip_record_new.save) self.assertRaises(frappe.ValidationError, ip_record_new.save)
service_unit = get_healthcare_service_unit() service_unit = get_healthcare_service_unit()

View File

@ -41,7 +41,7 @@ boot_session = "erpnext.startup.boot.boot_session"
notification_config = "erpnext.startup.notifications.get_notification_config" notification_config = "erpnext.startup.notifications.get_notification_config"
get_help_messages = "erpnext.utilities.activation.get_help_messages" get_help_messages = "erpnext.utilities.activation.get_help_messages"
leaderboards = "erpnext.startup.leaderboard.get_leaderboards" leaderboards = "erpnext.startup.leaderboard.get_leaderboards"
filters_config = "erpnext.startup.filters.get_filters_config"
on_session_creation = [ on_session_creation = [
"erpnext.portal.utils.create_customer_or_supplier", "erpnext.portal.utils.create_customer_or_supplier",
@ -238,6 +238,9 @@ doc_events = {
"on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission"
}, },
"Purchase Invoice": {
"on_submit": "erpnext.regional.india.utils.make_reverse_charge_entries"
},
"Payment Entry": { "Payment Entry": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status"], "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status"],
"on_trash": "erpnext.regional.check_deletion_permission" "on_trash": "erpnext.regional.check_deletion_permission"
@ -320,8 +323,7 @@ scheduler_events = {
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans" "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans"
], ],
"monthly_long": [ "monthly_long": [
"erpnext.accounts.deferred_revenue.convert_deferred_revenue_to_income", "erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.accounts.deferred_revenue.convert_deferred_expense_to_expense",
"erpnext.hr.utils.allocate_earned_leaves", "erpnext.hr.utils.allocate_earned_leaves",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
] ]

View File

@ -18,7 +18,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Leaves", "label": "Leaves",
"links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]" "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -93,7 +93,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "HR", "label": "HR",
"modified": "2020-05-28 13:36:07.710600", "modified": "2020-06-16 19:20:50.976045",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR", "name": "HR",
@ -126,7 +126,7 @@
}, },
{ {
"label": "Salary Structure", "label": "Salary Structure",
"link_to": "Payroll Entry", "link_to": "Salary Structure",
"type": "DocType" "type": "DocType"
}, },
{ {

View File

@ -19,11 +19,15 @@
"attendance_date", "attendance_date",
"company", "company",
"department", "department",
"shift",
"attendance_request", "attendance_request",
"amended_from", "details_section",
"shift",
"in_time",
"out_time",
"column_break_18",
"late_entry", "late_entry",
"early_exit" "early_exit",
"amended_from"
], ],
"fields": [ "fields": [
{ {
@ -172,13 +176,36 @@
"fieldname": "early_exit", "fieldname": "early_exit",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Early Exit" "label": "Early Exit"
},
{
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Details"
},
{
"depends_on": "shift",
"fieldname": "in_time",
"fieldtype": "Datetime",
"label": "In Time",
"read_only": 1
},
{
"depends_on": "shift",
"fieldname": "out_time",
"fieldtype": "Datetime",
"label": "Out Time",
"read_only": 1
},
{
"fieldname": "column_break_18",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-ok", "icon": "fa fa-ok",
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-11 11:40:14.319496", "modified": "2020-05-29 13:51:37.177231",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Attendance", "name": "Attendance",

View File

@ -19,7 +19,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
approvers = [] approvers = []
department_details = {} department_details = {}
department_list = [] department_list = []
employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True) employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver", "expense_approver"], as_dict=True)
employee_department = filters.get("department") or employee.department employee_department = filters.get("department") or employee.department
if employee_department: if employee_department:
@ -33,10 +33,16 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
if filters.get("doctype") == "Leave Application" and employee.leave_approver: if filters.get("doctype") == "Leave Application" and employee.leave_approver:
approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name'])) approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name']))
if filters.get("doctype") == "Expense Claim" and employee.expense_approver:
approvers.append(frappe.db.get_value("User", employee.expense_approver, ['name', 'first_name', 'last_name']))
if filters.get("doctype") == "Leave Application": if filters.get("doctype") == "Leave Application":
parentfield = "leave_approvers" parentfield = "leave_approvers"
field_name = "Leave Approver"
else: else:
parentfield = "expense_approvers" parentfield = "expense_approvers"
field_name = "Expense Approver"
if department_list: if department_list:
for d in department_list: for d in department_list:
approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from
@ -46,4 +52,12 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
and approver.parentfield = %s and approver.parentfield = %s
and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
if len(approvers) == 0:
frappe.throw(_("Please set {0} for the Employee or for Department: {1}").
format(
field_name, frappe.bold(employee_department),
frappe.bold(employee.name)
),
title=_(field_name + " Missing"))
return set(tuple(approver) for approver in approvers) return set(tuple(approver) for approver in approvers)

View File

@ -62,6 +62,7 @@
"salary_mode", "salary_mode",
"payroll_cost_center", "payroll_cost_center",
"column_break_52", "column_break_52",
"expense_approver",
"bank_name", "bank_name",
"bank_ac_no", "bank_ac_no",
"health_insurance_section", "health_insurance_section",
@ -205,7 +206,7 @@
"label": "Status", "label": "Status",
"oldfieldname": "status", "oldfieldname": "status",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nActive\nLeft", "options": "Active\nLeft",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1
}, },
@ -667,6 +668,7 @@
"oldfieldtype": "Date" "oldfieldtype": "Date"
}, },
{ {
"depends_on": "eval:doc.status == \"Left\"",
"fieldname": "relieving_date", "fieldname": "relieving_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Relieving Date", "label": "Relieving Date",
@ -797,13 +799,21 @@
{ {
"fieldname": "column_break_52", "fieldname": "column_break_52",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "expense_approver",
"fieldtype": "Link",
"label": "Expense Approver",
"options": "User",
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2020-05-05 18:51:03.152503", "modified": "2020-06-18 18:01:27.223535",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@ -139,13 +139,13 @@ frappe.ui.form.on('Employee Advance', {
employee: function (frm) { employee: function (frm) {
if (frm.doc.employee) { if (frm.doc.employee) {
return frappe.call({ return frappe.call({
method: "erpnext.hr.doctype.employee_advance.employee_advance.get_due_advance_amount", method: "erpnext.hr.doctype.employee_advance.employee_advance.get_pending_amount",
args: { args: {
"employee": frm.doc.employee, "employee": frm.doc.employee,
"posting_date": frm.doc.posting_date "posting_date": frm.doc.posting_date
}, },
callback: function(r) { callback: function(r) {
frm.set_value("due_advance_amount",r.message); frm.set_value("pending_amount",r.message);
} }
}); });
} }

View File

@ -19,7 +19,7 @@
"column_break_11", "column_break_11",
"advance_amount", "advance_amount",
"paid_amount", "paid_amount",
"due_advance_amount", "pending_amount",
"claimed_amount", "claimed_amount",
"return_amount", "return_amount",
"section_break_7", "section_break_7",
@ -102,14 +102,6 @@
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
{
"depends_on": "eval:cur_frm.doc.employee",
"fieldname": "due_advance_amount",
"fieldtype": "Currency",
"label": "Due Advance Amount",
"options": "Company:company:default_currency",
"read_only": 1
},
{ {
"fieldname": "claimed_amount", "fieldname": "claimed_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
@ -177,11 +169,19 @@
"fieldname": "repay_unclaimed_amount_from_salary", "fieldname": "repay_unclaimed_amount_from_salary",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Repay unclaimed amount from salary" "label": "Repay unclaimed amount from salary"
},
{
"depends_on": "eval:cur_frm.doc.employee",
"fieldname": "pending_amount",
"fieldtype": "Currency",
"label": "Pending Amount",
"options": "Company:company:default_currency",
"read_only": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-03-06 15:11:33.747535", "modified": "2020-06-12 12:42:39.833818",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Advance", "name": "Employee Advance",

View File

@ -95,7 +95,7 @@ class EmployeeAdvance(Document):
frappe.db.set_value("Employee Advance", self.name, "status", self.status) frappe.db.set_value("Employee Advance", self.name, "status", self.status)
@frappe.whitelist() @frappe.whitelist()
def get_due_advance_amount(employee, posting_date): def get_pending_amount(employee, posting_date):
employee_due_amount = frappe.get_all("Employee Advance", \ employee_due_amount = frappe.get_all("Employee Advance", \
filters = {"employee":employee, "docstatus":1, "posting_date":("<=", posting_date)}, \ filters = {"employee":employee, "docstatus":1, "posting_date":("<=", posting_date)}, \
fields = ["advance_amount", "paid_amount"]) fields = ["advance_amount", "paid_amount"])

View File

@ -72,7 +72,7 @@ def add_log_based_on_employee_field(employee_field_value, timestamp, device_id=N
return doc return doc
def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, shift=None): def mark_attendance_and_link_log(logs, attendance_status, attendance_date, working_hours=None, late_entry=False, early_exit=False, in_time=None, out_time=None, shift=None):
"""Creates an attendance and links the attendance to the Employee Checkin. """Creates an attendance and links the attendance to the Employee Checkin.
Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown. Note: If attendance is already present for the given date, the logs are marked as skipped and no exception is thrown.
@ -100,7 +100,9 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
'company': employee_doc.company, 'company': employee_doc.company,
'shift': shift, 'shift': shift,
'late_entry': late_entry, 'late_entry': late_entry,
'early_exit': early_exit 'early_exit': early_exit,
'in_time': in_time,
'out_time': out_time
} }
attendance = frappe.get_doc(doc_dict).insert() attendance = frappe.get_doc(doc_dict).insert()
attendance.submit() attendance.submit()

View File

@ -243,7 +243,6 @@ frappe.ui.form.on("Expense Claim", {
}, },
update_employee_advance_claimed_amount: function(frm) { update_employee_advance_claimed_amount: function(frm) {
console.log("update_employee_advance_claimed_amount")
let amount_to_be_allocated = frm.doc.grand_total; let amount_to_be_allocated = frm.doc.grand_total;
$.each(frm.doc.advances || [], function(i, advance){ $.each(frm.doc.advances || [], function(i, advance){
if (amount_to_be_allocated >= advance.unclaimed_amount){ if (amount_to_be_allocated >= advance.unclaimed_amount){
@ -295,6 +294,16 @@ frappe.ui.form.on("Expense Claim", {
frm.events.get_advances(frm); frm.events.get_advances(frm);
}, },
cost_center: function(frm) {
frm.events.set_child_cost_center(frm);
},
set_child_cost_center: function(frm){
(frm.doc.expenses || []).forEach(function(d) {
if (!d.cost_center){
d.cost_center = frm.doc.cost_center;
}
});
},
get_taxes: function(frm) { get_taxes: function(frm) {
if(frm.doc.taxes) { if(frm.doc.taxes) {
frappe.call({ frappe.call({
@ -338,8 +347,7 @@ frappe.ui.form.on("Expense Claim", {
frappe.ui.form.on("Expense Claim Detail", { frappe.ui.form.on("Expense Claim Detail", {
expenses_add: function(frm, cdt, cdn) { expenses_add: function(frm, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn); frm.events.set_child_cost_center(frm);
frm.script_manager.copy_from_first_row("expenses", row, ["cost_center"]);
}, },
amount: function(frm, cdt, cdn) { amount: function(frm, cdt, cdn) {
var child = locals[cdt][cdn]; var child = locals[cdt][cdn];

View File

@ -66,6 +66,7 @@
"fieldname": "employee", "fieldname": "employee",
"fieldtype": "Link", "fieldtype": "Link",
"in_global_search": 1, "in_global_search": 1,
"in_standard_filter": 1,
"label": "From Employee", "label": "From Employee",
"oldfieldname": "employee", "oldfieldname": "employee",
"oldfieldtype": "Link", "oldfieldtype": "Link",
@ -164,6 +165,7 @@
"default": "Today", "default": "Today",
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"in_standard_filter": 1,
"label": "Posting Date", "label": "Posting Date",
"oldfieldname": "posting_date", "oldfieldname": "posting_date",
"oldfieldtype": "Date", "oldfieldtype": "Date",
@ -236,6 +238,7 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company", "label": "Company",
"oldfieldname": "company", "oldfieldname": "company",
"oldfieldtype": "Link", "oldfieldtype": "Link",
@ -368,7 +371,7 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2019-12-14 23:52:05.388458", "modified": "2020-06-15 12:43:04.099803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Expense Claim", "name": "Expense Claim",

View File

@ -38,11 +38,15 @@ frappe.ui.form.on("Leave Application", {
}, },
validate: function(frm) { validate: function(frm) {
if (frm.doc.from_date == frm.doc.to_date && frm.doc.half_day == 1){
frm.doc.half_day_date = frm.doc.from_date;
}
frm.toggle_reqd("half_day_date", frm.doc.half_day == 1); frm.toggle_reqd("half_day_date", frm.doc.half_day == 1);
}, },
make_dashboard: function(frm) { make_dashboard: function(frm) {
var leave_details; var leave_details;
let lwps;
if (frm.doc.employee) { if (frm.doc.employee) {
frappe.call({ frappe.call({
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details", method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
@ -58,6 +62,7 @@ frappe.ui.form.on("Leave Application", {
if (!r.exc && r.message['leave_approver']) { if (!r.exc && r.message['leave_approver']) {
frm.set_value('leave_approver', r.message['leave_approver']); frm.set_value('leave_approver', r.message['leave_approver']);
} }
lwps = r.message["lwps"];
} }
}); });
$("div").remove(".form-dashboard-section.custom"); $("div").remove(".form-dashboard-section.custom");
@ -67,6 +72,18 @@ frappe.ui.form.on("Leave Application", {
}) })
); );
frm.dashboard.show(); frm.dashboard.show();
let allowed_leave_types = Object.keys(leave_details);
// lwps should be allowed, lwps don't have any allocation
allowed_leave_types = allowed_leave_types.concat(lwps);
frm.set_query('leave_type', function(){
return {
filters : [
['leave_type_name', 'in', allowed_leave_types]
]
};
});
} }
}, },

View File

@ -19,7 +19,6 @@ class NotAnOptionalHoliday(frappe.ValidationError): pass
from frappe.model.document import Document from frappe.model.document import Document
class LeaveApplication(Document): class LeaveApplication(Document):
def get_feed(self): def get_feed(self):
return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type)
@ -33,6 +32,7 @@ class LeaveApplication(Document):
self.validate_block_days() self.validate_block_days()
self.validate_salary_processed_days() self.validate_salary_processed_days()
self.validate_attendance() self.validate_attendance()
self.set_half_day_date()
if frappe.db.get_value("Leave Type", self.leave_type, 'is_optional_leave'): if frappe.db.get_value("Leave Type", self.leave_type, 'is_optional_leave'):
self.validate_optional_leave() self.validate_optional_leave()
self.validate_applicable_after() self.validate_applicable_after()
@ -131,8 +131,6 @@ class LeaveApplication(Document):
for dt in daterange(getdate(self.from_date), getdate(self.to_date)): for dt in daterange(getdate(self.from_date), getdate(self.to_date)):
date = dt.strftime("%Y-%m-%d") date = dt.strftime("%Y-%m-%d")
status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave" status = "Half Day" if getdate(date) == getdate(self.half_day_date) else "On Leave"
print("-------->>>", status)
# frappe.throw("Hello")
attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee, attendance_name = frappe.db.exists('Attendance', dict(employee = self.employee,
attendance_date = date, docstatus = ('!=', 2))) attendance_date = date, docstatus = ('!=', 2)))
@ -292,6 +290,10 @@ class LeaveApplication(Document):
frappe.throw(_("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday) frappe.throw(_("{0} is not in Optional Holiday List").format(formatdate(day)), NotAnOptionalHoliday)
day = add_days(day, 1) day = add_days(day, 1)
def set_half_day_date(self):
if self.from_date == self.to_date and self.half_day == 1:
self.half_day_date = self.from_date
def notify_employee(self): def notify_employee(self):
employee = frappe.get_doc("Employee", self.employee) employee = frappe.get_doc("Employee", self.employee)
if not employee.user_id: if not employee.user_id:
@ -442,6 +444,7 @@ def get_leave_details(employee, date):
total_allocated_leaves = frappe.db.get_value('Leave Allocation', { total_allocated_leaves = frappe.db.get_value('Leave Allocation', {
'from_date': ('<=', date), 'from_date': ('<=', date),
'to_date': ('>=', date), 'to_date': ('>=', date),
'employee': employee,
'leave_type': allocation.leave_type, 'leave_type': allocation.leave_type,
}, 'SUM(total_leaves_allocated)') or 0 }, 'SUM(total_leaves_allocated)') or 0
@ -459,9 +462,14 @@ def get_leave_details(employee, date):
"pending_leaves": leaves_pending, "pending_leaves": leaves_pending,
"remaining_leaves": remaining_leaves} "remaining_leaves": remaining_leaves}
#is used in set query
lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1})
lwps = [lwp.name for lwp in lwps]
ret = { ret = {
'leave_allocation': leave_allocation, 'leave_allocation': leave_allocation,
'leave_approver': get_leave_approver(employee) 'leave_approver': get_leave_approver(employee),
'lwps': lwps
} }
return ret return ret

View File

@ -1,5 +1,5 @@
{% if data %} {% if not jQuery.isEmptyObject(data) %}
<h5 style="margin-top: 20px;"> {{ __("Allocated Leaves") }} </h5> <h5 style="margin-top: 20px;"> {{ __("Allocated Leaves") }} </h5>
<table class="table table-bordered small"> <table class="table table-bordered small">
<thead> <thead>
@ -11,7 +11,6 @@
<th style="width: 16%" class="text-right">{{ __("Pending Leaves") }}</th> <th style="width: 16%" class="text-right">{{ __("Pending Leaves") }}</th>
<th style="width: 16%" class="text-right">{{ __("Available Leaves") }}</th> <th style="width: 16%" class="text-right">{{ __("Available Leaves") }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for(const [key, value] of Object.entries(data)) { %} {% for(const [key, value] of Object.entries(data)) { %}
@ -26,6 +25,6 @@
{% } %} {% } %}
</tbody> </tbody>
</table> </table>
{% } else { %} {% else %}
<p style="margin-top: 30px;"> No Leaves have been allocated. </p> <p style="margin-top: 30px;"> No Leaves have been allocated. </p>
{% } %} {% endif %}

View File

@ -28,13 +28,14 @@ class ShiftType(Document):
logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time") logs = frappe.db.get_list('Employee Checkin', fields="*", filters=filters, order_by="employee,time")
for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])): for key, group in itertools.groupby(logs, key=lambda x: (x['employee'], x['shift_actual_start'])):
single_shift_logs = list(group) single_shift_logs = list(group)
attendance_status, working_hours, late_entry, early_exit = self.get_attendance(single_shift_logs) attendance_status, working_hours, late_entry, early_exit, in_time, out_time = self.get_attendance(single_shift_logs)
mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, self.name) mark_attendance_and_link_log(single_shift_logs, attendance_status, key[1].date(), working_hours, late_entry, early_exit, in_time, out_time, self.name)
for employee in self.get_assigned_employee(self.process_attendance_after, True): for employee in self.get_assigned_employee(self.process_attendance_after, True):
self.mark_absent_for_dates_with_no_attendance(employee) self.mark_absent_for_dates_with_no_attendance(employee)
def get_attendance(self, logs): def get_attendance(self, logs):
"""Return attendance_status, working_hours for a set of logs belonging to a single shift. """Return attendance_status, working_hours, late_entry, early_exit, in_time, out_time
for a set of logs belonging to a single shift.
Assumtion: Assumtion:
1. These logs belongs to an single shift, single employee and is not in a holiday date. 1. These logs belongs to an single shift, single employee and is not in a holiday date.
2. Logs are in chronological order 2. Logs are in chronological order
@ -48,10 +49,10 @@ class ShiftType(Document):
early_exit = True early_exit = True
if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent: if self.working_hours_threshold_for_absent and total_working_hours < self.working_hours_threshold_for_absent:
return 'Absent', total_working_hours, late_entry, early_exit return 'Absent', total_working_hours, late_entry, early_exit, in_time, out_time
if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day: if self.working_hours_threshold_for_half_day and total_working_hours < self.working_hours_threshold_for_half_day:
return 'Half Day', total_working_hours, late_entry, early_exit return 'Half Day', total_working_hours, late_entry, early_exit, in_time, out_time
return 'Present', total_working_hours, late_entry, early_exit return 'Present', total_working_hours, late_entry, early_exit, in_time, out_time
def mark_absent_for_dates_with_no_attendance(self, employee): def mark_absent_for_dates_with_no_attendance(self, employee):
"""Marks Absents for the given employee on working days in this shift which have no attendance marked. """Marks Absents for the given employee on working days in this shift which have no attendance marked.

View File

@ -73,11 +73,11 @@ class EmployeeBoardingController(Document):
def assign_task_to_users(self, task, users): def assign_task_to_users(self, task, users):
for user in users: for user in users:
args = { args = {
'assign_to' : user, 'assign_to': [user],
'doctype' : task.doctype, 'doctype': task.doctype,
'name' : task.name, 'name': task.name,
'description' : task.description or task.subject, 'description': task.description or task.subject,
'notify': self.notify_users_by_email 'notify': self.notify_users_by_email
} }
assign_to.add(args) assign_to.add(args)

View File

@ -23,7 +23,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Reports", "label": "Reports",
"links": "[\n {\n \"dependencies\": [\n \"Loan Repayment\"\n ],\n \"doctype\": \"Loan Repayment\",\n \"incomplete_dependencies\": [\n \"Loan Repayment\"\n ],\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Loan Security Pledge\"\n ],\n \"doctype\": \"Loan Security Pledge\",\n \"incomplete_dependencies\": [\n \"Loan Security Pledge\"\n ],\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]" "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]"
} }
], ],
"category": "Modules", "category": "Modules",
@ -34,11 +34,10 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Desk Page", "doctype": "Desk Page",
"extends_another_page": 0, "extends_another_page": 0,
"hide_custom": 0,
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Loan", "label": "Loan",
"modified": "2020-05-28 13:37:42.017709", "modified": "2020-06-07 19:42:14.947902",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",

View File

@ -27,6 +27,7 @@ class LoanDisbursement(AccountsController):
def on_cancel(self): def on_cancel(self):
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
def set_missing_values(self): def set_missing_values(self):
if not self.disbursement_date: if not self.disbursement_date:

View File

@ -31,6 +31,7 @@ class LoanInterestAccrual(AccountsController):
self.update_is_accrued() self.update_is_accrued()
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
def update_is_accrued(self): def update_is_accrued(self):
frappe.db.set_value('Repayment Schedule', self.repayment_schedule_name, 'is_accrued', 0) frappe.db.set_value('Repayment Schedule', self.repayment_schedule_name, 'is_accrued', 0)
@ -176,21 +177,23 @@ def get_term_loans(date, term_loan=None, loan_type=None):
return term_loans return term_loans
def make_loan_interest_accrual_entry(args): def make_loan_interest_accrual_entry(args):
loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") precision = cint(frappe.db.get_default("currency_precision")) or 2
loan_interest_accrual.loan = args.loan
loan_interest_accrual.applicant_type = args.applicant_type
loan_interest_accrual.applicant = args.applicant
loan_interest_accrual.interest_income_account = args.interest_income_account
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, 2)
loan_interest_accrual.interest_amount = flt(args.interest_amount, 2)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name
loan_interest_accrual.payable_principal_amount = args.payable_principal
loan_interest_accrual.save() loan_interest_accrual = frappe.new_doc("Loan Interest Accrual")
loan_interest_accrual.submit() loan_interest_accrual.loan = args.loan
loan_interest_accrual.applicant_type = args.applicant_type
loan_interest_accrual.applicant = args.applicant
loan_interest_accrual.interest_income_account = args.interest_income_account
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name
loan_interest_accrual.payable_principal_amount = args.payable_principal
loan_interest_accrual.save()
loan_interest_accrual.submit()
def get_no_of_days_for_interest_accural(loan, posting_date): def get_no_of_days_for_interest_accural(loan, posting_date):

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import json import json
from frappe import _ from frappe import _
from frappe.utils import flt, getdate from frappe.utils import flt, getdate, cint
from six import iteritems from six import iteritems
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day, get_datetime from frappe.utils import date_diff, add_days, getdate, add_months, get_first_day, get_datetime
@ -29,8 +29,11 @@ class LoanRepayment(AccountsController):
def on_cancel(self): def on_cancel(self):
self.mark_as_unpaid() self.mark_as_unpaid()
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
self.ignore_linked_doctypes = ['GL Entry']
def set_missing_values(self, amounts): def set_missing_values(self, amounts):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if not self.posting_date: if not self.posting_date:
self.posting_date = get_datetime() self.posting_date = get_datetime()
@ -38,24 +41,26 @@ class LoanRepayment(AccountsController):
self.cost_center = erpnext.get_default_cost_center(self.company) self.cost_center = erpnext.get_default_cost_center(self.company)
if not self.interest_payable: if not self.interest_payable:
self.interest_payable = flt(amounts['interest_amount'], 2) self.interest_payable = flt(amounts['interest_amount'], precision)
if not self.penalty_amount: if not self.penalty_amount:
self.penalty_amount = flt(amounts['penalty_amount'], 2) self.penalty_amount = flt(amounts['penalty_amount'], precision)
if not self.pending_principal_amount: if not self.pending_principal_amount:
self.pending_principal_amount = flt(amounts['pending_principal_amount'], 2) self.pending_principal_amount = flt(amounts['pending_principal_amount'], precision)
if not self.payable_principal_amount and self.is_term_loan: if not self.payable_principal_amount and self.is_term_loan:
self.payable_principal_amount = flt(amounts['payable_principal_amount'], 2) self.payable_principal_amount = flt(amounts['payable_principal_amount'], precision)
if not self.payable_amount: if not self.payable_amount:
self.payable_amount = flt(amounts['payable_amount'], 2) self.payable_amount = flt(amounts['payable_amount'], precision)
if amounts.get('due_date'): if amounts.get('due_date'):
self.due_date = amounts.get('due_date') self.due_date = amounts.get('due_date')
def validate_amount(self): def validate_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
if not self.amount_paid: if not self.amount_paid:
frappe.throw(_("Amount paid cannot be zero")) frappe.throw(_("Amount paid cannot be zero"))
@ -63,11 +68,13 @@ class LoanRepayment(AccountsController):
msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount)
frappe.throw(msg) frappe.throw(msg)
if self.payment_type == "Loan Closure" and flt(self.amount_paid, 2) < flt(self.payable_amount, 2): if self.payment_type == "Loan Closure" and flt(self.amount_paid, precision) < flt(self.payable_amount, precision):
msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount) msg = _("Amount of {0} is required for Loan closure").format(self.payable_amount)
frappe.throw(msg) frappe.throw(msg)
def update_paid_amount(self): def update_paid_amount(self):
precision = cint(frappe.db.get_default("currency_precision")) or 2
loan = frappe.get_doc("Loan", self.against_loan) loan = frappe.get_doc("Loan", self.against_loan)
for payment in self.repayment_details: for payment in self.repayment_details:
@ -75,9 +82,9 @@ class LoanRepayment(AccountsController):
SET paid_principal_amount = `paid_principal_amount` + %s, SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""", WHERE name = %s""",
(flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual)) (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual))
if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2): if flt(loan.total_principal_paid + self.principal_amount_paid, precision) >= flt(loan.total_payment, precision):
if loan.is_secured_loan: if loan.is_secured_loan:
frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
else: else:
@ -253,6 +260,7 @@ def get_accrued_interest_entries(against_loan):
# So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable
def get_amounts(amounts, against_loan, posting_date, payment_type): def get_amounts(amounts, against_loan, posting_date, payment_type):
precision = cint(frappe.db.get_default("currency_precision")) or 2
against_loan_doc = frappe.get_doc("Loan", against_loan) against_loan_doc = frappe.get_doc("Loan", against_loan)
loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type)
@ -282,8 +290,8 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
payable_principal_amount += entry.payable_principal_amount payable_principal_amount += entry.payable_principal_amount
pending_accrual_entries.setdefault(entry.name, { pending_accrual_entries.setdefault(entry.name, {
'interest_amount': flt(entry.interest_amount), 'interest_amount': flt(entry.interest_amount, precision),
'payable_principal_amount': flt(entry.payable_principal_amount) 'payable_principal_amount': flt(entry.payable_principal_amount, precision)
}) })
if not final_due_date: if not final_due_date:
@ -301,11 +309,11 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365 per_day_interest = (payable_principal_amount * (loan_type_details.rate_of_interest / 100))/365
total_pending_interest += (pending_days * per_day_interest) total_pending_interest += (pending_days * per_day_interest)
amounts["pending_principal_amount"] = pending_principal_amount amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = payable_principal_amount amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
amounts["interest_amount"] = total_pending_interest amounts["interest_amount"] = flt(total_pending_interest, precision)
amounts["penalty_amount"] = penalty_amount amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
if final_due_date: if final_due_date:

View File

@ -41,6 +41,7 @@
"options": "Company:company:default_currency" "options": "Company:company:default_currency"
}, },
{ {
"default": "0",
"fieldname": "rate_of_interest", "fieldname": "rate_of_interest",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Rate of Interest (%) Yearly", "label": "Rate of Interest (%) Yearly",
@ -143,7 +144,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-15 00:24:43.259963", "modified": "2020-06-07 18:55:59.346292",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Type", "name": "Loan Type",

View File

@ -76,7 +76,8 @@ def get_columns(filters):
"fieldtype": "Link", "fieldtype": "Link",
"fieldname": "currency", "fieldname": "currency",
"options": "Currency", "options": "Currency",
"width": 50 "width": 50,
"hidden": 1
} }
] ]
@ -84,17 +85,13 @@ def get_columns(filters):
def get_data(filters): def get_data(filters):
loan_security_price_map = frappe._dict(frappe.get_all("Loan Security",
fields=["name", "loan_security_price"], as_list=1
))
data = [] data = []
conditions = get_conditions(filters) conditions = get_conditions(filters)
loan_security_pledges = frappe.db.sql(""" loan_security_pledges = frappe.db.sql("""
SELECT SELECT
p.name, p.applicant, p.loan, p.status, p.pledge_time, p.name, p.applicant, p.loan, p.status, p.pledge_time,
c.loan_security, c.qty c.loan_security, c.qty, c.loan_security_price, c.amount
FROM FROM
`tabLoan Security Pledge` p, `tabPledge` c `tabLoan Security Pledge` p, `tabPledge` c
WHERE WHERE
@ -115,8 +112,8 @@ def get_data(filters):
row["pledge_time"] = pledge.pledge_time row["pledge_time"] = pledge.pledge_time
row["loan_security"] = pledge.loan_security row["loan_security"] = pledge.loan_security
row["qty"] = pledge.qty row["qty"] = pledge.qty
row["loan_security_price"] = loan_security_price_map.get(pledge.loan_security) row["loan_security_price"] = pledge.loan_security_price
row["loan_security_value"] = row["loan_security_price"] * pledge.qty row["loan_security_value"] = pledge.amount
row["currency"] = default_currency row["currency"] = default_currency
data.append(row) data.append(row)

View File

@ -4,7 +4,6 @@
import frappe, erpnext, json import frappe, erpnext, json
from frappe import _ from frappe import _
from frappe.utils import nowdate, get_first_day, get_last_day, add_months from frappe.utils import nowdate, get_first_day, get_last_day, add_months
from erpnext.accounts.utils import get_fiscal_year
def get_data(): def get_data():
return frappe._dict({ return frappe._dict({

View File

@ -112,8 +112,16 @@ class BOM(WebsiteGenerator):
if self.routing: if self.routing:
self.set("operations", []) self.set("operations", [])
for d in frappe.get_all("BOM Operation", fields = ["*"], for d in frappe.get_all("BOM Operation", fields = ["*"],
filters = {'parenttype': 'Routing', 'parent': self.routing}): filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="idx"):
child = self.append('operations', d) child = self.append('operations', {
"operation": d.operation,
"workstation": d.workstation,
"description": d.description,
"time_in_mins": d.time_in_mins,
"batch_size": d.batch_size,
"operating_cost": d.operating_cost,
"idx": d.idx
})
child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2) child.hour_rate = flt(d.hour_rate / self.conversion_rate, 2)
def set_bom_material_details(self): def set_bom_material_details(self):

View File

@ -78,6 +78,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:parent.doctype == 'BOM'",
"fieldname": "base_hour_rate", "fieldname": "base_hour_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Base Hour Rate(Company Currency)", "label": "Base Hour Rate(Company Currency)",
@ -87,6 +88,7 @@
}, },
{ {
"default": "5", "default": "5",
"depends_on": "eval:parent.doctype == 'BOM'",
"fieldname": "base_operating_cost", "fieldname": "base_operating_cost",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Operating Cost(Company Currency)", "label": "Operating Cost(Company Currency)",
@ -108,12 +110,12 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-07-16 22:35:55.374037", "modified": "2020-06-16 17:01:11.128420",
"modified_by": "govindsmenokee@gmail.com", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "name": "BOM Operation",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC"
} }

View File

@ -44,7 +44,6 @@ frappe.ui.form.on('BOM Operation', {
name: d.workstation name: d.workstation
}, },
callback: function (data) { callback: function (data) {
frappe.model.set_value(d.doctype, d.name, "base_hour_rate", data.message.hour_rate);
frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate); frappe.model.set_value(d.doctype, d.name, "hour_rate", data.message.hour_rate);
frm.events.calculate_operating_cost(frm, d); frm.events.calculate_operating_cost(frm, d);
} }

View File

@ -217,6 +217,8 @@ class ForecastingReport(ExponentialSmoothingForecast):
} }
def get_summary_data(self): def get_summary_data(self):
if not self.data: return
return [ return [
{ {
"value": sum(self.total_demand), "value": sum(self.total_demand),

View File

@ -77,6 +77,7 @@ def create_customer(user_details):
customer = frappe.new_doc("Customer") customer = frappe.new_doc("Customer")
customer.customer_name = user_details.fullname customer.customer_name = user_details.fullname
customer.customer_type = "Individual" customer.customer_type = "Individual"
customer.flags.ignore_mandatory = True
customer.insert(ignore_permissions=True) customer.insert(ignore_permissions=True)
try: try:
@ -91,7 +92,11 @@ def create_customer(user_details):
"link_name": customer.name "link_name": customer.name
}) })
contact.insert() contact.save()
except frappe.DuplicateEntryError:
return customer.name
except Exception as e: except Exception as e:
frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed")) frappe.log_error(frappe.get_traceback(), _("Contact Creation Failed"))
pass pass

View File

@ -62,11 +62,26 @@ def get_member_based_on_subscription(subscription_id, email):
'subscription_id': subscription_id, 'subscription_id': subscription_id,
'email_id': email 'email_id': email
}, order_by="creation desc") }, order_by="creation desc")
return frappe.get_doc("Member", members[0]['name']) try:
return frappe.get_doc("Member", members[0]['name'])
except:
return None
def verify_signature(data):
signature = frappe.request.headers.get('X-Razorpay-Signature')
settings = frappe.get_doc("Membership Settings")
key = settings.get_webhook_secret()
controller = frappe.get_doc("Razorpay Settings")
controller.verify_signature(data, signature, key)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs): def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data() data = frappe.request.get_data(as_text=True)
verify_signature(data)
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
data = json.loads(data) data = json.loads(data)
@ -84,7 +99,10 @@ def trigger_razorpay_subscription(*args, **kwargs):
except Exception as e: except Exception as e:
error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed"))
notify_failure(error_log) notify_failure(error_log)
raise e return False
if not member:
return False
if data.event == "subscription.activated": if data.event == "subscription.activated":
member.customer_id = payment.customer_id member.customer_id = payment.customer_id
@ -113,7 +131,6 @@ def trigger_razorpay_subscription(*args, **kwargs):
return True return True
def notify_failure(log): def notify_failure(log):
try: try:
content = """Dear System Manager, content = """Dear System Manager,

View File

@ -1,8 +1,30 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Membership Settings', { frappe.ui.form.on("Membership Settings", {
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.webhook_secret) {
frm.add_custom_button(__("Revoke <Key></Key>"), () => {
frm.call("revoke_key").then(() => {
frm.refresh();
})
});
}
frm.trigger("add_generate_button");
},
} add_generate_button: function(frm) {
let label;
if (frm.doc.webhook_secret) {
label = __("Regenerate Webhook Secret");
} else {
label = __("Generate Webhook Secret");
}
frm.add_custom_button(label, () => {
frm.call("generate_webhook_key").then(() => {
frm.refresh();
});
});
},
}); });

View File

@ -8,7 +8,8 @@
"enable_razorpay", "enable_razorpay",
"razorpay_settings_section", "razorpay_settings_section",
"billing_cycle", "billing_cycle",
"billing_frequency" "billing_frequency",
"webhook_secret"
], ],
"fields": [ "fields": [
{ {
@ -34,11 +35,17 @@
"fieldname": "billing_frequency", "fieldname": "billing_frequency",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Billing Frequency" "label": "Billing Frequency"
},
{
"fieldname": "webhook_secret",
"fieldtype": "Password",
"label": "Webhook Secret",
"read_only": 1
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-04-07 18:42:51.496807", "modified": "2020-05-22 12:38:27.103759",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership Settings", "name": "Membership Settings",

View File

@ -4,11 +4,27 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe import _
from frappe.integrations.utils import get_payment_gateway_controller from frappe.integrations.utils import get_payment_gateway_controller
from frappe.model.document import Document from frappe.model.document import Document
class MembershipSettings(Document): class MembershipSettings(Document):
pass def generate_webhook_key(self):
key = frappe.generate_hash(length=20)
self.webhook_secret = key
self.save()
frappe.msgprint(
_("Here is your webhook secret, this will be shown to you only once.") + "<br><br>" + key,
_("Webhook Secret")
);
def revoke_key(self):
self.webhook_secret = None;
self.save()
def get_webhook_secret(self):
return self.get_password(fieldname="webhook_secret", raise_exception=False)
@frappe.whitelist() @frappe.whitelist()
def get_plans_for_membership(*args, **kwargs): def get_plans_for_membership(*args, **kwargs):

View File

@ -680,6 +680,8 @@ erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v12_0.retain_permission_rules_for_video_doctype
erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22 erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22
erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
execute:frappe.reload_doc("HR", "doctype", "Employee Advance")
erpnext.patches.v12_0.move_due_advance_amount_to_pending_amount
execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.delete_doc_if_exists("Page", "appointment-analytic")
execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True)
erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price
@ -695,4 +697,7 @@ execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True)
erpnext.patches.v12_0.update_uom_conversion_factor erpnext.patches.v12_0.update_uom_conversion_factor
erpnext.patches.v13_0.delete_old_purchase_reports erpnext.patches.v13_0.delete_old_purchase_reports
erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions erpnext.patches.v12_0.set_italian_import_supplier_invoice_permissions
erpnext.patches.v12_0.unhide_cost_center_field erpnext.patches.v12_0.unhide_cost_center_field
erpnext.patches.v13_0.update_sla_enhancements
erpnext.patches.v12_0.update_address_template_for_india
erpnext.patches.v12_0.set_multi_uom_in_rfq

View File

@ -4,7 +4,7 @@ import frappe
def execute(): def execute():
frappe.reload_doc('accounts', 'doctype', 'bank', force=1) frappe.reload_doc('accounts', 'doctype', 'bank', force=1)
if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account'): if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'):
frappe.db.sql(""" frappe.db.sql("""
UPDATE `tabBank` b, `tabBank Account` ba UPDATE `tabBank` b, `tabBank Account` ba
SET b.swift_number = ba.swift_number, b.branch_code = ba.branch_code SET b.swift_number = ba.swift_number, b.branch_code = ba.branch_code
@ -12,4 +12,4 @@ def execute():
""") """)
frappe.reload_doc('accounts', 'doctype', 'bank_account') frappe.reload_doc('accounts', 'doctype', 'bank_account')
frappe.reload_doc('accounts', 'doctype', 'payment_request') frappe.reload_doc('accounts', 'doctype', 'payment_request')

View File

@ -0,0 +1,11 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
''' Move from due_advance_amount to pending_amount '''
if frappe.db.has_column("Employee Advance", "due_advance_amount"):
frappe.db.sql(''' UPDATE `tabEmployee Advance` SET pending_amount=due_advance_amount ''')

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