Merge branch 'develop' into sla-enhancements

This commit is contained in:
Rucha Mahabal 2020-06-10 12:33:19 +05:30 committed by GitHub
commit 5b6d2c2e7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1126 additions and 361 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.org/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

@ -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,6 +122,24 @@
"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",

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

@ -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

@ -387,11 +387,41 @@ 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
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 {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 +519,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

@ -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

@ -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 exisiting 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

@ -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

@ -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

@ -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

@ -320,8 +320,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

@ -38,6 +38,9 @@ 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);
}, },
@ -67,6 +70,13 @@ frappe.ui.form.on("Leave Application", {
}) })
); );
frm.dashboard.show(); frm.dashboard.show();
frm.set_query('leave_type', function(){
return {
filters : [
['leave_type_name', 'in', Object.keys(leave_details)]
]
}
});
} }
}, },

View File

@ -33,6 +33,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 +132,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 +291,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 +445,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

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

@ -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

@ -112,8 +112,15 @@ 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,
"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

@ -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

@ -64,9 +64,21 @@ def get_member_based_on_subscription(subscription_id, email):
}, order_by="creation desc") }, order_by="creation desc")
return frappe.get_doc("Member", members[0]['name']) return frappe.get_doc("Member", members[0]['name'])
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()
verify_signature(data)
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
data = json.loads(data) data = json.loads(data)
@ -113,7 +125,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

@ -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

@ -14,15 +14,21 @@ def execute():
frappe.reload_doc("hr", "doctype", doctype) frappe.reload_doc("hr", "doctype", doctype)
standard_tax_exemption_amount_exists = frappe.db.has_column("Payroll Period", "standard_tax_exemption_amount")
select_fields = "name, start_date, end_date"
if standard_tax_exemption_amount_exists:
select_fields = "name, start_date, end_date, standard_tax_exemption_amount"
for company in frappe.get_all("Company"): for company in frappe.get_all("Company"):
payroll_periods = frappe.db.sql(""" payroll_periods = frappe.db.sql("""
SELECT SELECT
name, start_date, end_date, standard_tax_exemption_amount {0}
FROM FROM
`tabPayroll Period` `tabPayroll Period`
WHERE company=%s WHERE company=%s
ORDER BY start_date DESC ORDER BY start_date DESC
""", company.name, as_dict = 1) """.format(select_fields), company.name, as_dict = 1)
for i, period in enumerate(payroll_periods): for i, period in enumerate(payroll_periods):
income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab = frappe.new_doc("Income Tax Slab")
@ -36,7 +42,8 @@ def execute():
income_tax_slab.effective_from = period.start_date income_tax_slab.effective_from = period.start_date
income_tax_slab.company = company.name income_tax_slab.company = company.name
income_tax_slab.allow_tax_exemption = 1 income_tax_slab.allow_tax_exemption = 1
income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount if standard_tax_exemption_amount_exists:
income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount
income_tax_slab.flags.ignore_mandatory = True income_tax_slab.flags.ignore_mandatory = True
income_tax_slab.submit() income_tax_slab.submit()

View File

@ -18,7 +18,7 @@ frappe.ui.form.on("Project", {
}; };
}, },
onload: function (frm) { onload: function (frm) {
var so = frm.get_docfield("Project", "sales_order"); var so = frappe.meta.get_docfield("Project", "sales_order");
so.get_route_options_for_new_doc = function (field) { so.get_route_options_for_new_doc = function (field) {
if (frm.is_new()) return; if (frm.is_new()) return;
return { return {
@ -135,4 +135,4 @@ function open_form(frm, doctype, child_doctype, parentfield) {
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
}); });
} }

View File

@ -917,7 +917,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
shipping_rule: function() { shipping_rule: function() {
var me = this; var me = this;
if(this.frm.doc.shipping_rule && this.frm.doc.shipping_address) { if(this.frm.doc.shipping_rule) {
return this.frm.call({ return this.frm.call({
doc: this.frm.doc, doc: this.frm.doc,
method: "apply_shipping_rule", method: "apply_shipping_rule",

View File

@ -64,7 +64,8 @@ class ImportSupplierInvoice(Document):
"buying_price_list": self.default_buying_price_list "buying_price_list": self.default_buying_price_list
} }
if not invoices_args.get("invoice_no", ''): return if not invoices_args.get("bill_no", ''):
frappe.throw(_("Numero has not set in the XML file"))
supp_dict = get_supplier_details(file_content) supp_dict = get_supplier_details(file_content)
invoices_args["destination_code"] = get_destination_code_from_file(file_content) invoices_args["destination_code"] = get_destination_code_from_file(file_content)

View File

@ -615,8 +615,9 @@ def get_transport_details(data, doc):
data.transDocDate = frappe.utils.formatdate(doc.lr_date, 'dd/mm/yyyy') data.transDocDate = frappe.utils.formatdate(doc.lr_date, 'dd/mm/yyyy')
if doc.gst_transporter_id: if doc.gst_transporter_id:
validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID') if doc.gst_transporter_id[0:2] != "88":
data.transporterId = doc.gst_transporter_id validate_gstin_check_digit(doc.gst_transporter_id, label='GST Transporter ID')
data.transporterId = doc.gst_transporter_id
return data return data

View File

@ -29,7 +29,7 @@
"category": "Modules", "category": "Modules",
"charts": [ "charts": [
{ {
"chart_name": "Income", "chart_name": "Incoming Bills (Purchase Invoice)",
"label": "Income" "label": "Income"
} }
], ],
@ -43,7 +43,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "Selling", "label": "Selling",
"modified": "2020-05-28 13:46:08.314240", "modified": "2020-06-03 13:23:24.861706",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling", "name": "Selling",

View File

@ -8,180 +8,180 @@ from frappe import _
from frappe.utils import cint, cstr from frappe.utils import cint, cstr
def execute(filters=None): def execute(filters=None):
common_columns = [ common_columns = [
{ {
'label': _('New Customers'), 'label': _('New Customers'),
'fieldname': 'new_customers', 'fieldname': 'new_customers',
'fieldtype': 'Int', 'fieldtype': 'Int',
'default': 0, 'default': 0,
'width': 125 'width': 125
}, },
{ {
'label': _('Repeat Customers'), 'label': _('Repeat Customers'),
'fieldname': 'repeat_customers', 'fieldname': 'repeat_customers',
'fieldtype': 'Int', 'fieldtype': 'Int',
'default': 0, 'default': 0,
'width': 125 'width': 125
}, },
{ {
'label': _('Total'), 'label': _('Total'),
'fieldname': 'total', 'fieldname': 'total',
'fieldtype': 'Int', 'fieldtype': 'Int',
'default': 0, 'default': 0,
'width': 100 'width': 100
}, },
{ {
'label': _('New Customer Revenue'), 'label': _('New Customer Revenue'),
'fieldname': 'new_customer_revenue', 'fieldname': 'new_customer_revenue',
'fieldtype': 'Currency', 'fieldtype': 'Currency',
'default': 0.0, 'default': 0.0,
'width': 175 'width': 175
}, },
{ {
'label': _('Repeat Customer Revenue'), 'label': _('Repeat Customer Revenue'),
'fieldname': 'repeat_customer_revenue', 'fieldname': 'repeat_customer_revenue',
'fieldtype': 'Currency', 'fieldtype': 'Currency',
'default': 0.0, 'default': 0.0,
'width': 175 'width': 175
}, },
{ {
'label': _('Total Revenue'), 'label': _('Total Revenue'),
'fieldname': 'total_revenue', 'fieldname': 'total_revenue',
'fieldtype': 'Currency', 'fieldtype': 'Currency',
'default': 0.0, 'default': 0.0,
'width': 175 'width': 175
} }
] ]
if filters.get('view_type') == 'Monthly': if filters.get('view_type') == 'Monthly':
return get_data_by_time(filters, common_columns) return get_data_by_time(filters, common_columns)
else: else:
return get_data_by_territory(filters, common_columns) return get_data_by_territory(filters, common_columns)
def get_data_by_time(filters, common_columns): def get_data_by_time(filters, common_columns):
# key yyyy-mm # key yyyy-mm
columns = [ columns = [
{ {
'label': _('Year'), 'label': _('Year'),
'fieldname': 'year', 'fieldname': 'year',
'fieldtype': 'Data', 'fieldtype': 'Data',
'width': 100 'width': 100
}, },
{ {
'label': _('Month'), 'label': _('Month'),
'fieldname': 'month', 'fieldname': 'month',
'fieldtype': 'Data', 'fieldtype': 'Data',
'width': 100 'width': 100
}, },
] ]
columns += common_columns columns += common_columns
customers_in = get_customer_stats(filters) customers_in = get_customer_stats(filters)
# time series # time series
from_year, from_month, temp = filters.get('from_date').split('-') from_year, from_month, temp = filters.get('from_date').split('-')
to_year, to_month, temp = filters.get('to_date').split('-') to_year, to_month, temp = filters.get('to_date').split('-')
from_year, from_month, to_year, to_month = \ from_year, from_month, to_year, to_month = \
cint(from_year), cint(from_month), cint(to_year), cint(to_month) cint(from_year), cint(from_month), cint(to_year), cint(to_month)
out = [] out = []
for year in range(from_year, to_year+1): for year in range(from_year, to_year+1):
for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13): for month in range(from_month if year==from_year else 1, (to_month+1) if year==to_year else 13):
key = '{year}-{month:02d}'.format(year=year, month=month) key = '{year}-{month:02d}'.format(year=year, month=month)
data = customers_in.get(key) data = customers_in.get(key)
new = data['new'] if data else [0, 0.0] new = data['new'] if data else [0, 0.0]
repeat = data['repeat'] if data else [0, 0.0] repeat = data['repeat'] if data else [0, 0.0]
out.append({ out.append({
'year': cstr(year), 'year': cstr(year),
'month': calendar.month_name[month], 'month': calendar.month_name[month],
'new_customers': new[0], 'new_customers': new[0],
'repeat_customers': repeat[0], 'repeat_customers': repeat[0],
'total': new[0] + repeat[0], 'total': new[0] + repeat[0],
'new_customer_revenue': new[1], 'new_customer_revenue': new[1],
'repeat_customer_revenue': repeat[1], 'repeat_customer_revenue': repeat[1],
'total_revenue': new[1] + repeat[1] 'total_revenue': new[1] + repeat[1]
}) })
return columns, out return columns, out
def get_data_by_territory(filters, common_columns): def get_data_by_territory(filters, common_columns):
columns = [{ columns = [{
'label': 'Territory', 'label': 'Territory',
'fieldname': 'territory', 'fieldname': 'territory',
'fieldtype': 'Link', 'fieldtype': 'Link',
'options': 'Territory', 'options': 'Territory',
'width': 150 'width': 150
}] }]
columns += common_columns columns += common_columns
customers_in = get_customer_stats(filters, tree_view=True) customers_in = get_customer_stats(filters, tree_view=True)
territory_dict = {} territory_dict = {}
for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1): for t in frappe.db.sql('''SELECT name, lft, parent_territory, is_group FROM `tabTerritory` ORDER BY lft''', as_dict=1):
territory_dict.update({ territory_dict.update({
t.name: { t.name: {
'parent': t.parent_territory, 'parent': t.parent_territory,
'is_group': t.is_group 'is_group': t.is_group
} }
}) })
depth_map = frappe._dict() depth_map = frappe._dict()
for name, info in territory_dict.items(): for name, info in territory_dict.items():
default = depth_map.get(info['parent']) + 1 if info['parent'] else 0 default = depth_map.get(info['parent']) + 1 if info['parent'] else 0
depth_map.setdefault(name, default) depth_map.setdefault(name, default)
data = [] data = []
for name, indent in depth_map.items(): for name, indent in depth_map.items():
condition = customers_in.get(name) condition = customers_in.get(name)
new = customers_in[name]['new'] if condition else [0, 0.0] new = customers_in[name]['new'] if condition else [0, 0.0]
repeat = customers_in[name]['repeat'] if condition else [0, 0.0] repeat = customers_in[name]['repeat'] if condition else [0, 0.0]
temp = { temp = {
'territory': name, 'territory': name,
'parent_territory': territory_dict[name]['parent'], 'parent_territory': territory_dict[name]['parent'],
'indent': indent, 'indent': indent,
'new_customers': new[0], 'new_customers': new[0],
'repeat_customers': repeat[0], 'repeat_customers': repeat[0],
'total': new[0] + repeat[0], 'total': new[0] + repeat[0],
'new_customer_revenue': new[1], 'new_customer_revenue': new[1],
'repeat_customer_revenue': repeat[1], 'repeat_customer_revenue': repeat[1],
'total_revenue': new[1] + repeat[1], 'total_revenue': new[1] + repeat[1],
'bold': 0 if indent else 1 'bold': 0 if indent else 1
} }
data.append(temp) data.append(temp)
loop_data = sorted(data, key=lambda k: k['indent'], reverse=True) loop_data = sorted(data, key=lambda k: k['indent'], reverse=True)
for ld in loop_data: for ld in loop_data:
if ld['parent_territory']: if ld['parent_territory']:
parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0] parent_data = [x for x in data if x['territory'] == ld['parent_territory']][0]
for key in parent_data.keys(): for key in parent_data.keys():
if key not in ['indent', 'territory', 'parent_territory', 'bold']: if key not in ['indent', 'territory', 'parent_territory', 'bold']:
parent_data[key] += ld[key] parent_data[key] += ld[key]
return columns, data, None, None, None, 1 return columns, data, None, None, None, 1
def get_customer_stats(filters, tree_view=False): def get_customer_stats(filters, tree_view=False):
""" Calculates number of new and repeated customers. """ """ Calculates number of new and repeated customers. """
company_condition = '' company_condition = ''
if filters.get('company'): if filters.get('company'):
company_condition = ' and company=%(company)s' company_condition = ' and company=%(company)s'
customers = [] customers = []
customers_in = {} customers_in = {}
for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice` for si in frappe.db.sql('''select territory, posting_date, customer, base_grand_total from `tabSales Invoice`
where docstatus=1 and posting_date <= %(to_date)s and posting_date >= %(from_date)s where docstatus=1 and posting_date <= %(to_date)s
{company_condition} order by posting_date'''.format(company_condition=company_condition), {company_condition} order by posting_date'''.format(company_condition=company_condition),
filters, as_dict=1): filters, as_dict=1):
key = si.territory if tree_view else si.posting_date.strftime('%Y-%m') key = si.territory if tree_view else si.posting_date.strftime('%Y-%m')
customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]}) customers_in.setdefault(key, {'new': [0, 0.0], 'repeat': [0, 0.0]})
if not si.customer in customers: if not si.customer in customers:
customers_in[key]['new'][0] += 1 customers_in[key]['new'][0] += 1
customers_in[key]['new'][1] += si.base_grand_total customers_in[key]['new'][1] += si.base_grand_total
customers.append(si.customer) customers.append(si.customer)
else: else:
customers_in[key]['repeat'][0] += 1 customers_in[key]['repeat'][0] += 1
customers_in[key]['repeat'][1] += si.base_grand_total customers_in[key]['repeat'][1] += si.base_grand_total
return customers_in return customers_in

View File

@ -34,7 +34,7 @@ class ItemAttribute(Document):
if self.numeric_values: if self.numeric_values:
validate_is_incremental(self, self.name, item.value, item.name) validate_is_incremental(self, self.name, item.value, item.name)
else: else:
validate_item_attribute_value(attributes_list, self.name, item.value, item.name) validate_item_attribute_value(attributes_list, self.name, item.value, item.name, from_variant=False)
def validate_numeric(self): def validate_numeric(self):
if self.numeric_values: if self.numeric_values:

View File

@ -18,7 +18,7 @@ frappe.ui.form.on('Material Request', {
// formatter for material request item // formatter for material request item
frm.set_indicator_formatter('item_code', frm.set_indicator_formatter('item_code',
function(doc) { return (doc.qty<=doc.ordered_qty) ? "green" : "orange"; }); function(doc) { return (doc.stock_qty<=doc.ordered_qty) ? "green" : "orange"; });
frm.set_query("item_code", "items", function() { frm.set_query("item_code", "items", function() {
return { return {

View File

@ -25,7 +25,7 @@ frappe.ui.form.on("Purchase Receipt", {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Stock Entry': 'Return', 'Stock Entry': 'Return',
'Purchase Invoice': 'Invoice' 'Purchase Invoice': 'Purchase Invoice'
}; };
frm.set_query("expense_account", "items", function() { frm.set_query("expense_account", "items", function() {

View File

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

View File

@ -23,6 +23,7 @@
"image", "image",
"image_view", "image_view",
"quantity_and_rate", "quantity_and_rate",
"set_basic_rate_manually",
"qty", "qty",
"basic_rate", "basic_rate",
"basic_amount", "basic_amount",
@ -491,12 +492,21 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse",
"fieldname": "set_basic_rate_manually",
"fieldtype": "Check",
"label": "Set Basic Rate Manually",
"show_days": 1,
"show_seconds": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-23 19:19:28.539769", "modified": "2020-06-08 12:57:03.172887",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",