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

This commit is contained in:
Deepesh Garg 2020-06-13 22:50:14 +05:30
commit 3a16b033b6
75 changed files with 1350 additions and 470 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,6 +14,9 @@ frappe.treeview_settings["Account"] = {
on_change: function() { on_change: function() {
var me = frappe.treeview_settings['Account'].treeview; var me = frappe.treeview_settings['Account'].treeview;
var company = me.page.fields_dict.company.get_value(); var company = me.page.fields_dict.company.get_value();
if (!company) {
frappe.throw(__("Please set a Company"));
}
frappe.call({ frappe.call({
method: "erpnext.accounts.doctype.account.account.get_root_company", method: "erpnext.accounts.doctype.account.account.get_root_company",
args: { args: {

View File

@ -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,9 +61,12 @@ 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
@ -57,6 +78,9 @@ class CostCenter(NestedSet):
return frappe.db.sql("select name from `tabCost Center` where \ return frappe.db.sql("select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2", self.name) parent_cost_center = %s and docstatus != 2", self.name)
def check_if_part_of_distributed_cost_center(self):
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
def before_rename(self, olddn, newdn, merge=False): def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided # Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr from erpnext.setup.doctype.company.company import get_name_with_abbr
@ -100,3 +124,7 @@ def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit(): if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account new_account = account_number + " - " + new_account
return new_account return new_account
def check_if_distributed_cost_center_enabled(cost_center_list):
value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
return next((True for x in value_list if x[0]), False)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1450,11 +1450,17 @@ def get_inter_company_details(doc, doctype):
parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}) parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company})
company = frappe.get_cached_value("Customer", doc.customer, "represents_company") company = frappe.get_cached_value("Customer", doc.customer, "represents_company")
if not parties:
frappe.throw(_('No Supplier found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company)))
party = get_internal_party(parties, "Supplier", doc) party = get_internal_party(parties, "Supplier", doc)
else: else:
parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}) parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company})
company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company") company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company")
if not parties:
frappe.throw(_('No Customer found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company)))
party = get_internal_party(parties, "Customer", doc) party = get_internal_party(parties, "Customer", doc)
return { return {

View File

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

View File

@ -169,6 +169,8 @@ 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)
if sub_total_row:
self.data.append(sub_total_row) self.data.append(sub_total_row)
self.data.append({}) self.data.append({})
self.update_sub_total_row(sub_total_row, 'Total') self.update_sub_total_row(sub_total_row, 'Total')
@ -232,6 +234,7 @@ 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)
if self.data:
self.data.append(self.total_row_map.get('Total')) self.data.append(self.total_row_map.get('Total'))
def append_row(self, row): def append_row(self, row):

View File

@ -29,6 +29,26 @@ 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:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
else:
DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(dimension)s
AND parent NOT IN %(dimension)s
GROUP BY parent''',{'dimension':[dimension]})
if DCC_allocation:
filters['budget_against_filter'] = [DCC_allocation[0][0]]
cam_map = get_dimension_account_month_map(filters)
dimension_items = cam_map.get(DCC_allocation[0][0])
if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
chart = get_chart_data(filters, columns, data)
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): for account, monthwise_data in iteritems(dimension_items):
row = [dimension, account] row = [dimension, account]
totals = [0, 0, 0] totals = [0, 0, 0]
@ -46,7 +66,11 @@ def execute(filters=None):
period_data[0] += last_total period_data[0] += last_total
if filters.get("show_cumulative"): 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] last_total = period_data[0] - period_data[1]
period_data[2] = period_data[0] - period_data[1] period_data[2] = period_data[0] - period_data[1]
@ -56,9 +80,8 @@ def execute(filters=None):
row += totals row += totals
data.append(row) data.append(row)
chart = get_chart_data(filters, columns, data) return data
return columns, data, None, chart
def get_columns(filters): def get_columns(filters):
columns = [ columns = [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -241,12 +241,15 @@ 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:
for tax in shipping_charge.get("tax_lines"):
taxes.append({ taxes.append({
"charge_type": _("Actual"), "charge_type": _("Actual"),
"account_head": get_tax_account_head(shipping_charge), "account_head": get_tax_account_head(tax),
"description": shipping_charge["title"], "description": tax["title"],
"tax_amount": shipping_charge["price"], "tax_amount": tax["price"],
"cost_center": shopify_settings.cost_center "cost_center": shopify_settings.cost_center
}) })

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
}, },
{ {
@ -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)
try:
coa = traverse({}, children, roots, roots, group_set) 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
try:
company = frappe.get_doc({ company = frappe.get_doc({
"doctype": "Company", "doctype": "Company",
"company_name": self.erpnext_company, "company_name": self.erpnext_company,
"default_currency": "INR", "default_currency": "INR",
"enable_perpetual_inventory": 0, "enable_perpetual_inventory": 0,
}).insert() }).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,13 +518,20 @@ 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 = {
"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) create_custom_field(doctype, df)
def create_price_list(): def create_price_list():
@ -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,6 +611,19 @@ 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):
if isinstance(data, frappe.model.document.Document):
if sys.exc_info()[1].__class__ != frappe.DuplicateEntryError:
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 data = data or self.status
message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]) 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) return frappe.log_error(title="Tally Migration Error", message=message)

View File

@ -18,7 +18,7 @@
{ {
"hidden": 0, "hidden": 0,
"label": "Leaves", "label": "Leaves",
"links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]" "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]"
}, },
{ {
"hidden": 0, "hidden": 0,
@ -93,7 +93,7 @@
"idx": 0, "idx": 0,
"is_standard": 1, "is_standard": 1,
"label": "HR", "label": "HR",
"modified": "2020-05-28 13:36:07.710600", "modified": "2020-06-10 12:41:41.695669",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR", "name": "HR",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,7 +73,7 @@ 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,

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,14 +177,16 @@ 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):
precision = cint(frappe.db.get_default("currency_precision")) or 2
loan_interest_accrual = frappe.new_doc("Loan Interest Accrual") loan_interest_accrual = frappe.new_doc("Loan Interest Accrual")
loan_interest_accrual.loan = args.loan loan_interest_accrual.loan = args.loan
loan_interest_accrual.applicant_type = args.applicant_type loan_interest_accrual.applicant_type = args.applicant_type
loan_interest_accrual.applicant = args.applicant loan_interest_accrual.applicant = args.applicant
loan_interest_accrual.interest_income_account = args.interest_income_account loan_interest_accrual.interest_income_account = args.interest_income_account
loan_interest_accrual.loan_account = args.loan_account loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, 2) loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, 2) loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate() loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name loan_interest_accrual.repayment_schedule_name = args.repayment_schedule_name

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,13 +112,14 @@ 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', { child = self.append('operations', {
"operation": d.operation, "operation": d.operation,
"workstation": d.workstation, "workstation": d.workstation,
"description": d.description, "description": d.description,
"time_in_mins": d.time_in_mins, "time_in_mins": d.time_in_mins,
"batch_size": d.batch_size "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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -680,6 +680,8 @@ erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
erpnext.patches.v12_0.retain_permission_rules_for_video_doctype erpnext.patches.v12_0.retain_permission_rules_for_video_doctype
erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22 erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22
erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
execute:frappe.reload_doc("HR", "doctype", "Employee Advance")
erpnext.patches.v12_0.move_due_advance_amount_to_pending_amount
execute:frappe.delete_doc_if_exists("Page", "appointment-analytic") execute:frappe.delete_doc_if_exists("Page", "appointment-analytic")
execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True) execute:frappe.rename_doc("Desk Page", "Getting Started", "Home", force=True)
erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price erpnext.patches.v12_0.unset_customer_supplier_based_on_type_of_item_price

View File

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

View File

@ -7,4 +7,4 @@ def execute():
for entry in doctypes: for entry in doctypes:
if frappe.db.exists('DocType', entry): if frappe.db.exists('DocType', entry):
frappe.reload_doc('Healthcare', 'doctype', entry) frappe.reload_doc('Healthcare', 'doctype', entry)
frappe.db.sql("update `tab{dt}` set company = '{company}' where ifnull(company, '') = ''".format(dt=entry, company=company)) frappe.db.sql("update `tab{dt}` set company = {company} where ifnull(company, '') = ''".format(dt=entry, company=frappe.db.escape(company)))

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 {

View File

@ -258,7 +258,12 @@ var calculate_end_time = function(frm, cdt, cdn) {
var update_billing_hours = function(frm, cdt, cdn){ var update_billing_hours = function(frm, cdt, cdn){
var child = locals[cdt][cdn]; var child = locals[cdt][cdn];
if(!child.billable) frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0); if(!child.billable) {
frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0);
} else {
// bill all hours by default
frappe.model.set_value(cdt, cdn, "billing_hours", child.hours);
}
}; };
var update_time_rates = function(frm, cdt, cdn){ var update_time_rates = function(frm, cdt, cdn){

View File

@ -552,7 +552,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if (show_batch_dialog) if (show_batch_dialog)
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"]) return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => { .then((r) => {
if(r.message.has_batch_no || r.message.has_serial_no) { if (r.message &&
(r.message.has_batch_no || r.message.has_serial_no)) {
frappe.flags.hide_serial_batch_dialog = false; frappe.flags.hide_serial_batch_dialog = false;
} }
}); });
@ -917,7 +918,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

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

@ -3,16 +3,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import json
from frappe.model.naming import set_name_by_naming_series from frappe.model.naming import set_name_by_naming_series
from frappe import _, msgprint, throw from frappe import _, msgprint
import frappe.defaults import frappe.defaults
from frappe.utils import flt, cint, cstr, today from frappe.utils import flt, cint, cstr, today, get_formatted_email
from frappe.desk.reportview import build_match_conditions, get_filters_cond from frappe.desk.reportview import build_match_conditions, get_filters_cond
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
from erpnext.accounts.party import validate_party_accounts, get_dashboard_info, get_timeline_data # keep this from erpnext.accounts.party import validate_party_accounts, get_dashboard_info, get_timeline_data # keep this
from frappe.contacts.address_and_contact import load_address_and_contact, delete_contact_and_address from frappe.contacts.address_and_contact import load_address_and_contact, delete_contact_and_address
from frappe.model.rename_doc import update_linked_doctypes from frappe.model.rename_doc import update_linked_doctypes
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.utils.user import get_users_with_role
class Customer(TransactionBase): class Customer(TransactionBase):
def get_feed(self): def get_feed(self):
@ -378,10 +381,45 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
.format(customer, customer_outstanding, credit_limit)) .format(customer, customer_outstanding, credit_limit))
# If not authorized person raise exception # If not authorized person raise exception
credit_controller = frappe.db.get_value('Accounts Settings', None, 'credit_controller') credit_controller_role = frappe.db.get_single_value('Accounts Settings', 'credit_controller')
if not credit_controller or credit_controller not in frappe.get_roles(): if not credit_controller_role or credit_controller_role not in frappe.get_roles():
throw(_("Please contact to the user who have Sales Master Manager {0} role") # form a list of emails for the credit controller users
.format(" / " + credit_controller if credit_controller else "")) credit_controller_users = get_users_with_role(credit_controller_role or "Sales Master Manager")
# form a list of emails and names to show to the user
credit_controller_users_list = [user for user in credit_controller_users if frappe.db.exists("Employee", {"prefered_email": user})]
credit_controller_users = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users_list]
if not credit_controller_users:
frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer)))
message = """Please contact any of the following users to extend the credit limits for {0}:
<br><br><ul><li>{1}</li></ul>""".format(customer, '<li>'.join(credit_controller_users))
# if the current user does not have permissions to override credit limit,
# prompt them to send out an email to the controller users
frappe.msgprint(message,
title="Notify",
raise_exception=1,
primary_action={
'label': 'Send Email',
'server_action': 'erpnext.selling.doctype.customer.customer.send_emails',
'args': {
'customer': customer,
'customer_outstanding': customer_outstanding,
'credit_limit': credit_limit,
'credit_controller_users_list': credit_controller_users_list
}
}
)
@frappe.whitelist()
def send_emails(args):
args = json.loads(args)
subject = (_("Credit limit reached for customer {0}").format(args.get('customer')))
message = (_("Credit limit has been crossed for customer {0} ({1}/{2})")
.format(args.get('customer'), args.get('customer_outstanding'), args.get('credit_limit')))
frappe.sendmail(recipients=[args.get('credit_controller_users_list')], subject=subject, message=message)
def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None): def get_customer_outstanding(customer, company, ignore_outstanding_sales_order=False, cost_center=None):
# Outstanding based on GL Entries # Outstanding based on GL Entries

View File

@ -169,7 +169,7 @@ def get_customer_stats(filters, tree_view=False):
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):

View File

@ -337,21 +337,17 @@ def set_price_list_and_rate(quotation, cart_settings):
def _set_price_list(cart_settings, quotation=None): def _set_price_list(cart_settings, quotation=None):
"""Set price list based on customer or shopping cart default""" """Set price list based on customer or shopping cart default"""
from erpnext.accounts.party import get_default_price_list from erpnext.accounts.party import get_default_price_list
party_name = quotation.get("party_name") if quotation else get_party().get("name")
# check if customer price list exists
selling_price_list = None selling_price_list = None
if quotation and quotation.get("party_name"):
selling_price_list = frappe.db.get_value('Customer', quotation.get("party_name"), 'default_price_list')
# else check for territory based price list # check if default customer price list exists
if party_name:
selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name))
# check default price list in shopping cart
if not selling_price_list: if not selling_price_list:
selling_price_list = cart_settings.price_list selling_price_list = cart_settings.price_list
party_name = quotation.get("party_name") if quotation else get_party().get("name")
if not selling_price_list and party_name:
selling_price_list = get_default_price_list(frappe.get_doc("Customer", party_name))
if quotation: if quotation:
quotation.selling_price_list = selling_price_list quotation.selling_price_list = selling_price_list

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

@ -119,11 +119,13 @@ def get_items_with_location_and_quantity(item_doc, item_location_map):
if item_location.serial_no: if item_location.serial_no:
serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)]) serial_nos = '\n'.join(item_location.serial_no[0: cint(stock_qty)])
auto_set_serial_no = frappe.db.get_single_value("Stock Settings", "automatically_set_serial_nos_based_on_fifo")
locations.append(frappe._dict({ locations.append(frappe._dict({
'qty': qty, 'qty': qty,
'stock_qty': stock_qty, 'stock_qty': stock_qty,
'warehouse': item_location.warehouse, 'warehouse': item_location.warehouse,
'serial_no': serial_nos, 'serial_no': serial_nos if auto_set_serial_no else item_doc.serial_no,
'batch_no': item_location.batch_no 'batch_no': item_location.batch_no
})) }))
@ -206,6 +208,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
sle.batch_no = batch.name sle.batch_no = batch.name
and sle.`item_code`=%(item_code)s and sle.`item_code`=%(item_code)s
and sle.`company` = %(company)s and sle.`company` = %(company)s
and batch.disabled = 0
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition} {warehouse_condition}
GROUP BY GROUP BY

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",

View File

@ -8,7 +8,7 @@ from frappe.utils import getdate, flt
def execute(filters=None): def execute(filters=None):
if not filters: filters = {} if not filters: filters = {}
float_preceision = frappe.db.get_default("float_preceision") float_precision = frappe.db.get_default("float_precision")
condition = get_condition(filters) condition = get_condition(filters)
@ -25,7 +25,7 @@ def execute(filters=None):
data = [] data = []
for item in items: for item in items:
total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name,0)) total_outgoing = flt(consumed_item_map.get(item.name, 0)) + flt(delivered_item_map.get(item.name,0))
avg_daily_outgoing = flt(total_outgoing / diff, float_preceision) avg_daily_outgoing = flt(total_outgoing / diff, float_precision)
reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock) reorder_level = (avg_daily_outgoing * flt(item.lead_time_days)) + flt(item.safety_stock)
data.append([item.name, item.item_name, item.item_group, item.brand, item.description, data.append([item.name, item.item_name, item.item_group, item.brand, item.description,

View File

@ -180,10 +180,10 @@ def get_fifo_queue(filters, sle=None):
qty_to_pop = abs(d.actual_qty) qty_to_pop = abs(d.actual_qty)
while qty_to_pop: while qty_to_pop:
batch = fifo_queue[0] if fifo_queue else [0, None] batch = fifo_queue[0] if fifo_queue else [0, None]
if 0 < batch[0] <= qty_to_pop: if 0 < flt(batch[0]) <= qty_to_pop:
# if batch qty > 0 # if batch qty > 0
# not enough or exactly same qty in current batch, clear batch # not enough or exactly same qty in current batch, clear batch
qty_to_pop -= batch[0] qty_to_pop -= flt(batch[0])
transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0)) transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0))
else: else:
# all from current batch # all from current batch

View File

@ -230,12 +230,12 @@ def get_valuation_method(item_code):
def get_fifo_rate(previous_stock_queue, qty): def get_fifo_rate(previous_stock_queue, qty):
"""get FIFO (average) Rate from Queue""" """get FIFO (average) Rate from Queue"""
if qty >= 0: if flt(qty) >= 0:
total = sum(f[0] for f in previous_stock_queue) total = sum(f[0] for f in previous_stock_queue)
return sum(flt(f[0]) * flt(f[1]) for f in previous_stock_queue) / flt(total) if total else 0.0 return sum(flt(f[0]) * flt(f[1]) for f in previous_stock_queue) / flt(total) if total else 0.0
else: else:
available_qty_for_outgoing, outgoing_cost = 0, 0 available_qty_for_outgoing, outgoing_cost = 0, 0
qty_to_pop = abs(qty) qty_to_pop = abs(flt(qty))
while qty_to_pop and previous_stock_queue: while qty_to_pop and previous_stock_queue:
batch = previous_stock_queue[0] batch = previous_stock_queue[0]
if 0 < batch[0] <= qty_to_pop: if 0 < batch[0] <= qty_to_pop: