Merge branch 'develop' of https://github.com/frappe/erpnext into social-media-integration-feat

This commit is contained in:
Anupam K 2020-04-18 01:51:47 +05:30
commit 790b9ee8d0
61 changed files with 890 additions and 548 deletions

View File

@ -89,7 +89,7 @@ class Account(NestedSet):
throw(_("Root cannot be edited."), RootNotEditable) throw(_("Root cannot be edited."), RootNotEditable)
if not self.parent_account and not self.is_group: if not self.parent_account and not self.is_group:
frappe.throw(_("Root Account must be a group")) frappe.throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
def validate_root_company_and_sync_account_to_children(self): def validate_root_company_and_sync_account_to_children(self):
# ignore validation while creating new compnay or while syncing to child companies # ignore validation while creating new compnay or while syncing to child companies

View File

@ -69,6 +69,7 @@ class TestAccount(unittest.TestCase):
acc.account_name = "Accumulated Depreciation" acc.account_name = "Accumulated Depreciation"
acc.parent_account = "Fixed Assets - _TC" acc.parent_account = "Fixed Assets - _TC"
acc.company = "_Test Company" acc.company = "_Test Company"
acc.account_type = "Accumulated Depreciation"
acc.insert() acc.insert()
doc = frappe.get_doc("Account", "Securities and Deposits - _TC") doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
@ -149,7 +150,7 @@ def _make_test_records(verbose):
# fixed asset depreciation # fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, None, None], ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None], ["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],

View File

@ -164,7 +164,7 @@ def build_forest(data):
error_messages = [] error_messages = []
for i in data: for i in data:
account_name, _, account_number, is_group, account_type, root_type = i account_name, dummy, account_number, is_group, account_type, root_type = i
if not account_name: if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no)) error_messages.append("Row {0}: Please enter Account Name".format(line_no))

View File

@ -178,7 +178,8 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
if pricing_rules[0].mixed_conditions and doc: if pricing_rules[0].mixed_conditions and doc:
stock_qty, amount, items = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args) stock_qty, amount, items = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args)
pricing_rules[0].apply_rule_on_other_items = items for pricing_rule_args in pricing_rules:
pricing_rule_args.apply_rule_on_other_items = items
elif pricing_rules[0].is_cumulative: elif pricing_rules[0].is_cumulative:
items = [args.get(frappe.scrub(pr_doc.get('apply_on')))] items = [args.get(frappe.scrub(pr_doc.get('apply_on')))]
@ -329,9 +330,9 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
if pr_doc.mixed_conditions: if pr_doc.mixed_conditions:
amt = args.get('qty') * args.get("price_list_rate") amt = args.get('qty') * args.get("price_list_rate")
if args.get("item_code") != row.get("item_code"): if args.get("item_code") != row.get("item_code"):
amt = row.get('qty') * row.get("price_list_rate") amt = row.get('qty') * (row.get("price_list_rate") or args.get("rate"))
sum_qty += row.get("stock_qty") or args.get("stock_qty") sum_qty += row.get("stock_qty") or args.get("stock_qty") or args.get("qty")
sum_amt += amt sum_amt += amt
if pr_doc.is_cumulative: if pr_doc.is_cumulative:

View File

@ -174,7 +174,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
read_only: 0, read_only: 0,
fieldtype:'Date', fieldtype:'Date',
label: __('Release Date'), label: __('Release Date'),
default: me.frm.doc.release_date default: me.frm.doc.release_date,
reqd: 1
}, },
{ {
fieldname: 'hold_comment', fieldname: 'hold_comment',

View File

@ -754,8 +754,7 @@
{ {
"fieldname": "manufacturer_part_no", "fieldname": "manufacturer_part_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number", "label": "Manufacturer Part Number"
"read_only": 1
}, },
{ {
"depends_on": "is_fixed_asset", "depends_on": "is_fixed_asset",
@ -777,7 +776,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-01 14:20:17.297284", "modified": "2020-04-07 18:34:35.104178",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Item", "name": "Purchase Invoice Item",

View File

@ -440,11 +440,12 @@ class SalesInvoice(SellingController):
if pos.get("company_address"): if pos.get("company_address"):
self.company_address = pos.get("company_address") self.company_address = pos.get("company_address")
if self.customer:
customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list') customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list')
selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
else:
selling_price_list = pos.get('selling_price_list')
if selling_price_list: if selling_price_list:
self.set('selling_price_list', selling_price_list) self.set('selling_price_list', selling_price_list)

View File

@ -1926,16 +1926,6 @@ class TestSalesInvoice(unittest.TestCase):
item.taxes = [] item.taxes = []
item.save() item.save()
def test_customer_provided_parts_si(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
si = create_sales_invoice(item_code='CUST-0987', rate=0)
self.assertEqual(si.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(si.get("items")[0].amount, 0)
# test if Sales Invoice with rate is allowed
si2 = create_sales_invoice(item_code='CUST-0987', do_not_save=True)
self.assertRaises(frappe.ValidationError, si2.save)
def create_sales_invoice(**args): def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice") si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -163,7 +163,8 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None)
# price list # price list
price_list = get_permitted_documents('Price List') price_list = get_permitted_documents('Price List')
if price_list: # if there is only one permitted document based on user permissions, set it
if price_list and len(price_list) == 1:
price_list = price_list[0] price_list = price_list[0]
elif pos and party_type == 'Customer': elif pos and party_type == 'Customer':
customer_price_list = frappe.get_value('Customer', party.name, 'default_price_list') customer_price_list = frappe.get_value('Customer', party.name, 'default_price_list')

View File

@ -365,6 +365,7 @@ def get_columns(filters):
columns = [ columns = [
{ {
"label": _("GL Entry"),
"fieldname": "gl_entry", "fieldname": "gl_entry",
"fieldtype": "Link", "fieldtype": "Link",
"options": "GL Entry", "options": "GL Entry",

View File

@ -11,6 +11,7 @@ from frappe.model.document import Document
class AssetCategory(Document): class AssetCategory(Document):
def validate(self): def validate(self):
self.validate_finance_books() self.validate_finance_books()
self.validate_accounts()
def validate_finance_books(self): def validate_finance_books(self):
for d in self.finance_books: for d in self.finance_books:
@ -18,6 +19,27 @@ class AssetCategory(Document):
if cint(d.get(frappe.scrub(field)))<1: if cint(d.get(frappe.scrub(field)))<1:
frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError)
def validate_accounts(self):
account_type_map = {
'fixed_asset_account': { 'account_type': 'Fixed Asset' },
'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
'depreciation_expense_account': { 'root_type': 'Expense' },
'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
}
for d in self.accounts:
for fieldname in account_type_map.keys():
if d.get(fieldname):
selected_account = d.get(fieldname)
key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
expected_key_type = account_type_map[fieldname][key_to_match]
if selected_key_type != expected_key_type:
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
title=_("Invalid Account"))
@frappe.whitelist() @frappe.whitelist()
def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None): def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):
if item and frappe.db.get_value("Item", item, "is_fixed_asset"): if item and frappe.db.get_value("Item", item, "is_fixed_asset"):

View File

@ -702,8 +702,7 @@
{ {
"fieldname": "manufacturer_part_no", "fieldname": "manufacturer_part_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number", "label": "Manufacturer Part Number"
"read_only": 1
}, },
{ {
"default": "0", "default": "0",
@ -723,7 +722,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2019-12-06 13:17:12.142799", "modified": "2020-04-07 18:35:17.558928",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-05-22 12:43:10", "creation": "2013-05-22 12:43:10",
"doctype": "DocType", "doctype": "DocType",
@ -522,8 +523,7 @@
{ {
"fieldname": "manufacturer_part_no", "fieldname": "manufacturer_part_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number", "label": "Manufacturer Part Number"
"read_only": 1
}, },
{ {
"fieldname": "column_break_15", "fieldname": "column_break_15",
@ -532,7 +532,8 @@
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"modified": "2019-06-02 05:32:46.019237", "links": [],
"modified": "2020-04-07 18:35:51.175947",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation Item", "name": "Supplier Quotation Item",

View File

@ -1123,36 +1123,39 @@ def get_supplier_block_status(party_name):
} }
return info return info
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
""" """
Returns a Sales Order Item child item containing the default values Returns a Sales Order Item child item containing the default values
""" """
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname) child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname)
item = frappe.get_doc("Item", item_code) item = frappe.get_doc("Item", trans_item.get('item_code'))
child_item.item_code = item.item_code child_item.item_code = item.item_code
child_item.item_name = item.item_name child_item.item_name = item.item_name
child_item.description = item.description child_item.description = item.description
child_item.reqd_by_date = p_doc.delivery_date child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
child_item.uom = item.stock_uom child_item.uom = item.stock_uom
child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
.format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
return child_item return child_item
def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_code): def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
""" """
Returns a Purchase Order Item child item containing the default values Returns a Purchase Order Item child item containing the default values
""" """
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name) p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname) child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname)
item = frappe.get_doc("Item", item_code) item = frappe.get_doc("Item", trans_item.get('item_code'))
child_item.item_code = item.item_code child_item.item_code = item.item_code
child_item.item_name = item.item_name child_item.item_name = item.item_name
child_item.description = item.description child_item.description = item.description
child_item.schedule_date = p_doc.schedule_date child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date
child_item.uom = item.stock_uom child_item.uom = item.stock_uom
child_item.conversion_factor = get_conversion_factor(item_code, item.stock_uom).get("conversion_factor") or 1.0 child_item.conversion_factor = get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation
return child_item return child_item
@ -1196,9 +1199,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
if not d.get("docname"): if not d.get("docname"):
new_child_flag = True new_child_flag = True
if parent_doctype == "Sales Order": if parent_doctype == "Sales Order":
child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) child_item = set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, d)
if parent_doctype == "Purchase Order": if parent_doctype == "Purchase Order":
child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d.get("item_code")) child_item = set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, d)
else: else:
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname")) child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")): if flt(child_item.get("rate")) == flt(d.get("rate")) and flt(child_item.get("qty")) == flt(d.get("qty")):
@ -1243,6 +1246,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
child_item.flags.ignore_validate_update_after_submit = True child_item.flags.ignore_validate_update_after_submit = True
if new_child_flag: if new_child_flag:
parent.load_from_db()
child_item.idx = len(parent.items) + 1 child_item.idx = len(parent.items) + 1
child_item.insert() child_item.insert()
else: else:

View File

@ -383,9 +383,6 @@ class StockController(AccountsController):
# Customer Provided parts will have zero valuation rate # Customer Provided parts will have zero valuation rate
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'): if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1 d.allow_zero_valuation_rate = 1
if d.parenttype in ["Delivery Note", "Sales Invoice"] and d.rate:
frappe.throw(_("Row #{0}: {1} cannot have {2} as it is a Customer Provided Item")
.format(d.idx, frappe.bold(d.item_code), frappe.bold("Rate")))
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None, def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=None): warehouse_account=None, company=None):

View File

@ -667,8 +667,7 @@ def get_itemised_tax_breakup_html(doc):
itemised_tax=itemised_tax, itemised_tax=itemised_tax,
itemised_taxable_amount=itemised_taxable_amount, itemised_taxable_amount=itemised_taxable_amount,
tax_accounts=tax_accounts, tax_accounts=tax_accounts,
conversion_rate=doc.conversion_rate, doc=doc
currency=doc.currency
) )
) )

View File

@ -336,3 +336,27 @@ def make_opportunity_from_communication(communication, ignore_communication_link
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links) link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
return opportunity.name return opportunity.name
@frappe.whitelist()
def get_events(start, end, filters=None):
"""Returns events for Gantt / Calendar view rendering.
:param start: Start date-time.
:param end: End date-time.
:param filters: Filters (JSON).
"""
from frappe.desk.calendar import get_event_conditions
conditions = get_event_conditions("Opportunity", filters)
data = frappe.db.sql("""
select
distinct `tabOpportunity`.name, `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_amount,
`tabOpportunity`.title, `tabOpportunity`.contact_date
from
`tabOpportunity`
where
(`tabOpportunity`.contact_date between %(start)s and %(end)s)
{conditions}
""".format(conditions=conditions), {
"start": start,
"end": end
}, as_dict=True, update={"allDay": 0})
return data

View File

@ -0,0 +1,19 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.views.calendar["Opportunity"] = {
field_map: {
"start": "contact_date",
"end": "contact_date",
"id": "name",
"title": "customer_name",
"allDay": "allDay"
},
options: {
header: {
left: 'prev,next today',
center: 'title',
right: 'month'
}
},
get_events_method: 'erpnext.crm.doctype.opportunity.opportunity.get_events'
}

View File

@ -3,10 +3,35 @@
frappe.ui.form.on('Tally Migration', { frappe.ui.form.on('Tally Migration', {
onload: function (frm) { onload: function (frm) {
let reload_status = true;
frappe.realtime.on("tally_migration_progress_update", function (data) { frappe.realtime.on("tally_migration_progress_update", function (data) {
if (reload_status) {
frappe.model.with_doc(frm.doc.doctype, frm.doc.name, () => {
frm.refresh_header();
});
reload_status = false;
}
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message); frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
if (data.count == data.total) { let error_occurred = data.count === -1;
window.setTimeout(title => frm.dashboard.hide_progress(title), 1500, data.title); if (data.count == data.total || error_occurred) {
window.setTimeout((title) => {
frm.dashboard.hide_progress(title);
frm.reload_doc();
if (error_occurred) {
frappe.msgprint({
message: __("An error has occurred during {0}. Check {1} for more details",
[
repl("<a href='#Form/Tally Migration/%(tally_document)s' class='variant-click'>%(tally_document)s</a>", {
tally_document: frm.docname
}),
"<a href='#List/Error Log' class='variant-click'>Error Log</a>"
]
),
title: __("Tally Migration Error"),
indicator: "red"
});
}
}, 2000, data.title);
} }
}); });
}, },
@ -37,14 +62,14 @@ frappe.ui.form.on('Tally Migration', {
add_button: function (frm, label, method) { add_button: function (frm, label, method) {
frm.add_custom_button( frm.add_custom_button(
label, label,
() => frm.call({ () => {
frm.call({
doc: frm.doc, doc: frm.doc,
method: method, method: method,
freeze: true, freeze: true
callback: () => { });
frm.remove_custom_button(label); frm.reload_doc();
} }
})
); );
} }
}); });

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"beta": 1, "beta": 1,
"creation": "2019-02-01 14:27:09.485238", "creation": "2019-02-01 14:27:09.485238",
"doctype": "DocType", "doctype": "DocType",
@ -14,6 +15,7 @@
"tally_debtors_account", "tally_debtors_account",
"company_section", "company_section",
"tally_company", "tally_company",
"default_uom",
"column_break_8", "column_break_8",
"erpnext_company", "erpnext_company",
"processed_files_section", "processed_files_section",
@ -43,6 +45,7 @@
"label": "Status" "label": "Status"
}, },
{ {
"description": "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs",
"fieldname": "master_data", "fieldname": "master_data",
"fieldtype": "Attach", "fieldtype": "Attach",
"in_list_view": 1, "in_list_view": 1,
@ -50,6 +53,7 @@
}, },
{ {
"default": "Sundry Creditors", "default": "Sundry Creditors",
"description": "Creditors Account set in Tally",
"fieldname": "tally_creditors_account", "fieldname": "tally_creditors_account",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Tally Creditors Account", "label": "Tally Creditors Account",
@ -61,6 +65,7 @@
}, },
{ {
"default": "Sundry Debtors", "default": "Sundry Debtors",
"description": "Debtors Account set in Tally",
"fieldname": "tally_debtors_account", "fieldname": "tally_debtors_account",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Tally Debtors Account", "label": "Tally Debtors Account",
@ -72,6 +77,7 @@
"fieldtype": "Section Break" "fieldtype": "Section Break"
}, },
{ {
"description": "Company Name as per Imported Tally Data",
"fieldname": "tally_company", "fieldname": "tally_company",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Tally Company", "label": "Tally Company",
@ -82,9 +88,11 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "Your Company set in ERPNext",
"fieldname": "erpnext_company", "fieldname": "erpnext_company",
"fieldtype": "Data", "fieldtype": "Data",
"label": "ERPNext Company" "label": "ERPNext Company",
"read_only_depends_on": "eval:doc.is_master_data_processed == 1"
}, },
{ {
"fieldname": "processed_files_section", "fieldname": "processed_files_section",
@ -155,24 +163,28 @@
"options": "Cost Center" "options": "Cost Center"
}, },
{ {
"default": "0",
"fieldname": "is_master_data_processed", "fieldname": "is_master_data_processed",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Master Data Processed", "label": "Is Master Data Processed",
"read_only": 1 "read_only": 1
}, },
{ {
"default": "0",
"fieldname": "is_day_book_data_processed", "fieldname": "is_day_book_data_processed",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Day Book Data Processed", "label": "Is Day Book Data Processed",
"read_only": 1 "read_only": 1
}, },
{ {
"default": "0",
"fieldname": "is_day_book_data_imported", "fieldname": "is_day_book_data_imported",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Day Book Data Imported", "label": "Is Day Book Data Imported",
"read_only": 1 "read_only": 1
}, },
{ {
"default": "0",
"fieldname": "is_master_data_imported", "fieldname": "is_master_data_imported",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Master Data Imported", "label": "Is Master Data Imported",
@ -188,13 +200,23 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"description": "Day Book Data exported from Tally that consists of all historic transactions",
"fieldname": "day_book_data", "fieldname": "day_book_data",
"fieldtype": "Attach", "fieldtype": "Attach",
"in_list_view": 1, "in_list_view": 1,
"label": "Day Book Data" "label": "Day Book Data"
},
{
"default": "Unit",
"description": "UOM in case unspecified in imported data",
"fieldname": "default_uom",
"fieldtype": "Link",
"label": "Default UOM",
"options": "UOM"
} }
], ],
"modified": "2019-04-29 05:46:54.394967", "links": [],
"modified": "2020-04-16 13:03:28.894919",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Tally Migration", "name": "Tally Migration",

View File

@ -4,20 +4,23 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from decimal import Decimal
import json import json
import re import re
import traceback import traceback
import zipfile import zipfile
from decimal import Decimal
from bs4 import BeautifulSoup as bs
import frappe import frappe
from erpnext import encode_company_abbr
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
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
from bs4 import BeautifulSoup as bs
from erpnext import encode_company_abbr
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
PRIMARY_ACCOUNT = "Primary" PRIMARY_ACCOUNT = "Primary"
VOUCHER_CHUNK_SIZE = 500 VOUCHER_CHUNK_SIZE = 500
@ -39,8 +42,10 @@ class TallyMigration(Document):
return string return string
master_file = frappe.get_doc("File", {"file_url": data_file}) master_file = frappe.get_doc("File", {"file_url": data_file})
master_file_path = master_file.get_full_path()
with zipfile.ZipFile(master_file.get_full_path()) as zf: if zipfile.is_zipfile(master_file_path):
with zipfile.ZipFile(master_file_path) as zf:
encoded_content = zf.read(zf.namelist()[0]) encoded_content = zf.read(zf.namelist()[0])
try: try:
content = encoded_content.decode("utf-8-sig") content = encoded_content.decode("utf-8-sig")
@ -58,13 +63,14 @@ class TallyMigration(Document):
"file_name": key + ".json", "file_name": key + ".json",
"attached_to_doctype": self.doctype, "attached_to_doctype": self.doctype,
"attached_to_name": self.name, "attached_to_name": self.name,
"content": json.dumps(value) "content": json.dumps(value),
"is_private": True
}).insert() }).insert()
setattr(self, key, f.file_url) setattr(self, key, f.file_url)
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 return collection.find_all("REMOTECMPINFO.LIST")[0].REMOTECMPNAME.string.strip()
def get_coa_customers_suppliers(collection): def get_coa_customers_suppliers(collection):
root_type_map = { root_type_map = {
@ -97,17 +103,17 @@ class TallyMigration(Document):
# If Ledger doesn't have PARENT field then don't create Account # If Ledger doesn't have PARENT field then don't create Account
# For example "Profit & Loss A/c" # For example "Profit & Loss A/c"
if account.PARENT: if account.PARENT:
yield account.PARENT.string, account["NAME"], 0 yield account.PARENT.string.strip(), account["NAME"], 0
def get_parent(account): def get_parent(account):
if account.PARENT: if account.PARENT:
return account.PARENT.string return account.PARENT.string.strip()
return { return {
("Yes", "No"): "Application of Funds (Assets)", ("Yes", "No"): "Application of Funds (Assets)",
("Yes", "Yes"): "Expenses", ("Yes", "Yes"): "Expenses",
("No", "Yes"): "Income", ("No", "Yes"): "Income",
("No", "No"): "Source of Funds (Liabilities)", ("No", "No"): "Source of Funds (Liabilities)",
}[(account.ISDEEMEDPOSITIVE.string, account.ISREVENUE.string)] }[(account.ISDEEMEDPOSITIVE.string.strip(), account.ISREVENUE.string.strip())]
def get_children_and_parent_dict(accounts): def get_children_and_parent_dict(accounts):
children, parents = {}, {} children, parents = {}, {}
@ -145,38 +151,38 @@ 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
if account.NAME.string in customers: if account.NAME.string.strip() in customers:
party_type = "Customer" party_type = "Customer"
parties.append({ parties.append({
"doctype": party_type, "doctype": party_type,
"customer_name": account.NAME.string, "customer_name": account.NAME.string.strip(),
"tax_id": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None, "tax_id": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None,
"customer_group": "All Customer Groups", "customer_group": "All Customer Groups",
"territory": "All Territories", "territory": "All Territories",
"customer_type": "Individual", "customer_type": "Individual",
}) })
elif account.NAME.string in suppliers: elif account.NAME.string.strip() in suppliers:
party_type = "Supplier" party_type = "Supplier"
parties.append({ parties.append({
"doctype": party_type, "doctype": party_type,
"supplier_name": account.NAME.string, "supplier_name": account.NAME.string.strip(),
"pan": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None, "pan": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None,
"supplier_group": "All Supplier Groups", "supplier_group": "All Supplier Groups",
"supplier_type": "Individual", "supplier_type": "Individual",
}) })
if party_type: if party_type:
address = "\n".join([a.string for a in account.find_all("ADDRESS")]) address = "\n".join([a.string.strip() for a in account.find_all("ADDRESS")])
addresses.append({ addresses.append({
"doctype": "Address", "doctype": "Address",
"address_line1": address[:140].strip(), "address_line1": address[:140].strip(),
"address_line2": address[140:].strip(), "address_line2": address[140:].strip(),
"country": account.COUNTRYNAME.string if account.COUNTRYNAME else None, "country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None,
"state": account.LEDSTATENAME.string if account.LEDSTATENAME else None, "state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
"gst_state": account.LEDSTATENAME.string if account.LEDSTATENAME else None, "gst_state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
"pin_code": account.PINCODE.string if account.PINCODE else None, "pin_code": account.PINCODE.string.strip() if account.PINCODE else None,
"mobile": account.LEDGERPHONE.string if account.LEDGERPHONE else None, "mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
"phone": account.LEDGERPHONE.string if account.LEDGERPHONE else None, "phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
"gstin": account.PARTYGSTIN.string 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": [{"link_doctype": party_type, "link_name": account["NAME"]}],
}) })
return parties, addresses return parties, addresses
@ -184,41 +190,50 @@ class TallyMigration(Document):
def get_stock_items_uoms(collection): def get_stock_items_uoms(collection):
uoms = [] uoms = []
for uom in collection.find_all("UNIT"): for uom in collection.find_all("UNIT"):
uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string}) uoms.append({"doctype": "UOM", "uom_name": uom.NAME.string.strip()})
items = [] items = []
for item in collection.find_all("STOCKITEM"): for item in collection.find_all("STOCKITEM"):
stock_uom = item.BASEUNITS.string.strip() if item.BASEUNITS else self.default_uom
items.append({ items.append({
"doctype": "Item", "doctype": "Item",
"item_code" : item.NAME.string, "item_code" : item.NAME.string.strip(),
"stock_uom": item.BASEUNITS.string, "stock_uom": stock_uom.strip(),
"is_stock_item": 0, "is_stock_item": 0,
"item_group": "All Item Groups", "item_group": "All Item Groups",
"item_defaults": [{"company": self.erpnext_company}] "item_defaults": [{"company": self.erpnext_company}]
}) })
return items, uoms return items, uoms
try:
self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5) self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5)
collection = self.get_collection(self.master_data) collection = self.get_collection(self.master_data)
company = get_company_name(collection) company = get_company_name(collection)
self.tally_company = company self.tally_company = company
self.erpnext_company = company self.erpnext_company = company
self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5) self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5)
chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection) chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection)
self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5) self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5)
parties, addresses = get_parties_addresses(collection, customers, suppliers) parties, addresses = get_parties_addresses(collection, customers, suppliers)
self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5) self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5)
items, uoms = get_stock_items_uoms(collection) items, uoms = get_stock_items_uoms(collection)
data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms} data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms}
self.publish("Process Master Data", _("Done"), 5, 5)
self.publish("Process Master Data", _("Done"), 5, 5)
self.dump_processed_data(data) self.dump_processed_data(data)
self.is_master_data_processed = 1 self.is_master_data_processed = 1
self.status = ""
self.save() except:
self.publish("Process Master Data", _("Process Failed"), -1, 5)
self.log()
finally:
self.set_status()
def publish(self, title, message, count, total): def publish(self, title, message, count, total):
frappe.publish_realtime("tally_migration_progress_update", {"title": title, "message": message, "count": count, "total": total}) frappe.publish_realtime("tally_migration_progress_update", {"title": title, "message": message, "count": count, "total": total})
@ -256,7 +271,6 @@ class TallyMigration(Document):
except: except:
self.log(address) 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()):
@ -273,25 +287,35 @@ class TallyMigration(Document):
except: except:
self.log(item) self.log(item)
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)
create_company_and_coa(self.chart_of_accounts) create_company_and_coa(self.chart_of_accounts)
self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4) self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4)
create_parties_and_addresses(self.parties, self.addresses) create_parties_and_addresses(self.parties, self.addresses)
self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4) self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4)
create_items_uoms(self.items, self.uoms) create_items_uoms(self.items, self.uoms)
self.publish("Import Master Data", _("Done"), 4, 4) self.publish("Import Master Data", _("Done"), 4, 4)
self.status = ""
self.is_master_data_imported = 1 self.is_master_data_imported = 1
self.save()
except:
self.publish("Import Master Data", _("Process Failed"), -1, 5)
self.log()
finally:
self.set_status()
def _process_day_book_data(self): def _process_day_book_data(self):
def get_vouchers(collection): def get_vouchers(collection):
vouchers = [] vouchers = []
for voucher in collection.find_all("VOUCHER"): for voucher in collection.find_all("VOUCHER"):
if voucher.ISCANCELLED.string == "Yes": if voucher.ISCANCELLED.string.strip() == "Yes":
continue continue
inventory_entries = voucher.find_all("INVENTORYENTRIES.LIST") + voucher.find_all("ALLINVENTORYENTRIES.LIST") + voucher.find_all("INVENTORYENTRIESIN.LIST") + voucher.find_all("INVENTORYENTRIESOUT.LIST") inventory_entries = voucher.find_all("INVENTORYENTRIES.LIST") + voucher.find_all("ALLINVENTORYENTRIES.LIST") + voucher.find_all("INVENTORYENTRIESIN.LIST") + voucher.find_all("INVENTORYENTRIESOUT.LIST")
if voucher.VOUCHERTYPENAME.string not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries: if voucher.VOUCHERTYPENAME.string.strip() not in ["Journal", "Receipt", "Payment", "Contra"] and inventory_entries:
function = voucher_to_invoice function = voucher_to_invoice
else: else:
function = voucher_to_journal_entry function = voucher_to_journal_entry
@ -307,15 +331,15 @@ class TallyMigration(Document):
accounts = [] accounts = []
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST") ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST")
for entry in ledger_entries: for entry in ledger_entries:
account = {"account": encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company), "cost_center": self.default_cost_center} account = {"account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company), "cost_center": self.default_cost_center}
if entry.ISPARTYLEDGER.string == "Yes": if entry.ISPARTYLEDGER.string.strip() == "Yes":
party_details = get_party(entry.LEDGERNAME.string) party_details = get_party(entry.LEDGERNAME.string.strip())
if party_details: if party_details:
party_type, party_account = party_details party_type, party_account = party_details
account["party_type"] = party_type account["party_type"] = party_type
account["account"] = party_account account["account"] = party_account
account["party"] = entry.LEDGERNAME.string account["party"] = entry.LEDGERNAME.string.strip()
amount = Decimal(entry.AMOUNT.string) amount = Decimal(entry.AMOUNT.string.strip())
if amount > 0: if amount > 0:
account["credit_in_account_currency"] = str(abs(amount)) account["credit_in_account_currency"] = str(abs(amount))
else: else:
@ -324,21 +348,21 @@ class TallyMigration(Document):
journal_entry = { journal_entry = {
"doctype": "Journal Entry", "doctype": "Journal Entry",
"tally_guid": voucher.GUID.string, "tally_guid": voucher.GUID.string.strip(),
"posting_date": voucher.DATE.string, "posting_date": voucher.DATE.string.strip(),
"company": self.erpnext_company, "company": self.erpnext_company,
"accounts": accounts, "accounts": accounts,
} }
return journal_entry return journal_entry
def voucher_to_invoice(voucher): def voucher_to_invoice(voucher):
if voucher.VOUCHERTYPENAME.string in ["Sales", "Credit Note"]: if voucher.VOUCHERTYPENAME.string.strip() in ["Sales", "Credit Note"]:
doctype = "Sales Invoice" doctype = "Sales Invoice"
party_field = "customer" party_field = "customer"
account_field = "debit_to" account_field = "debit_to"
account_name = encode_company_abbr(self.tally_debtors_account, self.erpnext_company) account_name = encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
price_list_field = "selling_price_list" price_list_field = "selling_price_list"
elif voucher.VOUCHERTYPENAME.string in ["Purchase", "Debit Note"]: elif voucher.VOUCHERTYPENAME.string.strip() in ["Purchase", "Debit Note"]:
doctype = "Purchase Invoice" doctype = "Purchase Invoice"
party_field = "supplier" party_field = "supplier"
account_field = "credit_to" account_field = "credit_to"
@ -351,10 +375,10 @@ class TallyMigration(Document):
invoice = { invoice = {
"doctype": doctype, "doctype": doctype,
party_field: voucher.PARTYNAME.string, party_field: voucher.PARTYNAME.string.strip(),
"tally_guid": voucher.GUID.string, "tally_guid": voucher.GUID.string.strip(),
"posting_date": voucher.DATE.string, "posting_date": voucher.DATE.string.strip(),
"due_date": voucher.DATE.string, "due_date": voucher.DATE.string.strip(),
"items": get_voucher_items(voucher, doctype), "items": get_voucher_items(voucher, doctype),
"taxes": get_voucher_taxes(voucher), "taxes": get_voucher_taxes(voucher),
account_field: account_name, account_field: account_name,
@ -375,15 +399,15 @@ class TallyMigration(Document):
for entry in inventory_entries: for entry in inventory_entries:
qty, uom = entry.ACTUALQTY.string.strip().split() qty, uom = entry.ACTUALQTY.string.strip().split()
items.append({ items.append({
"item_code": entry.STOCKITEMNAME.string, "item_code": entry.STOCKITEMNAME.string.strip(),
"description": entry.STOCKITEMNAME.string, "description": entry.STOCKITEMNAME.string.strip(),
"qty": qty.strip(), "qty": qty.strip(),
"uom": uom.strip(), "uom": uom.strip(),
"conversion_factor": 1, "conversion_factor": 1,
"price_list_rate": entry.RATE.string.split("/")[0], "price_list_rate": entry.RATE.string.strip().split("/")[0],
"cost_center": self.default_cost_center, "cost_center": self.default_cost_center,
"warehouse": self.default_warehouse, "warehouse": self.default_warehouse,
account_field: encode_company_abbr(entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string, self.erpnext_company), account_field: encode_company_abbr(entry.find_all("ACCOUNTINGALLOCATIONS.LIST")[0].LEDGERNAME.string.strip(), self.erpnext_company),
}) })
return items return items
@ -391,13 +415,13 @@ class TallyMigration(Document):
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST") ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST")
taxes = [] taxes = []
for entry in ledger_entries: for entry in ledger_entries:
if entry.ISPARTYLEDGER.string == "No": if entry.ISPARTYLEDGER.string.strip() == "No":
tax_account = encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company) tax_account = encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company)
taxes.append({ taxes.append({
"charge_type": "Actual", "charge_type": "Actual",
"account_head": tax_account, "account_head": tax_account,
"description": tax_account, "description": tax_account,
"tax_amount": entry.AMOUNT.string, "tax_amount": entry.AMOUNT.string.strip(),
"cost_center": self.default_cost_center, "cost_center": self.default_cost_center,
}) })
return taxes return taxes
@ -408,15 +432,24 @@ class TallyMigration(Document):
elif frappe.db.exists({"doctype": "Customer", "customer_name": party}): elif frappe.db.exists({"doctype": "Customer", "customer_name": party}):
return "Customer", encode_company_abbr(self.tally_debtors_account, self.erpnext_company) return "Customer", encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
try:
self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3) self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3)
collection = self.get_collection(self.day_book_data) collection = self.get_collection(self.day_book_data)
self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3) self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3)
vouchers = get_vouchers(collection) vouchers = get_vouchers(collection)
self.publish("Process Day Book Data", _("Done"), 3, 3) self.publish("Process Day Book Data", _("Done"), 3, 3)
self.dump_processed_data({"vouchers": vouchers}) self.dump_processed_data({"vouchers": vouchers})
self.status = ""
self.is_day_book_data_processed = 1 self.is_day_book_data_processed = 1
self.save()
except:
self.publish("Process Day Book Data", _("Process Failed"), -1, 5)
self.log()
finally:
self.set_status()
def _import_day_book_data(self): def _import_day_book_data(self):
def create_fiscal_years(vouchers): def create_fiscal_years(vouchers):
@ -454,6 +487,7 @@ class TallyMigration(Document):
"currency": "INR" "currency": "INR"
}).insert() }).insert()
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.round_off_account)
@ -467,11 +501,18 @@ class TallyMigration(Document):
total = len(vouchers) total = len(vouchers)
is_last = False is_last = False
for index in range(0, total, VOUCHER_CHUNK_SIZE): for index in range(0, total, VOUCHER_CHUNK_SIZE):
if index + VOUCHER_CHUNK_SIZE >= total: if index + VOUCHER_CHUNK_SIZE >= total:
is_last = True is_last = True
frappe.enqueue_doc(self.doctype, self.name, "_import_vouchers", queue="long", timeout=3600, start=index+1, total=total, is_last=is_last) frappe.enqueue_doc(self.doctype, self.name, "_import_vouchers", queue="long", timeout=3600, start=index+1, total=total, is_last=is_last)
except:
self.log()
finally:
self.set_status()
def _import_vouchers(self, start, total, is_last=False): def _import_vouchers(self, start, total, is_last=False):
frappe.flags.in_migrate = True frappe.flags.in_migrate = True
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers}) vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
@ -494,25 +535,26 @@ class TallyMigration(Document):
frappe.flags.in_migrate = False frappe.flags.in_migrate = False
def process_master_data(self): def process_master_data(self):
self.status = "Processing Master Data" self.set_status("Processing Master Data")
self.save()
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
def import_master_data(self): def import_master_data(self):
self.status = "Importing Master Data" self.set_status("Importing Master Data")
self.save()
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
def process_day_book_data(self): def process_day_book_data(self):
self.status = "Processing Day Book Data" self.set_status("Processing Day Book Data")
self.save()
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600) frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
def import_day_book_data(self): def import_day_book_data(self):
self.status = "Importing Day Book Data" self.set_status("Importing Day Book Data")
self.save()
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):
message = "\n".join(["Data", json.dumps(data, default=str, indent=4), "Exception", traceback.format_exc()]) data = data or self.status
message = "\n".join(["Data:", json.dumps(data, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
return frappe.log_error(title="Tally Migration Error", message=message) return frappe.log_error(title="Tally Migration Error", message=message)
def set_status(self, status=""):
self.status = status
self.save()

View File

@ -348,6 +348,8 @@ get_site_info = 'erpnext.utilities.get_site_info'
payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account" payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account"
communication_doctypes = ["Customer", "Supplier"]
regional_overrides = { regional_overrides = {
'France': { 'France': {
'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method' 'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method'

View File

@ -580,19 +580,22 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date):
return leave_days return leave_days
def skip_expiry_leaves(leave_entry, date): def skip_expiry_leaves(leave_entry, date):
''' Checks whether the expired leaves coincide with the to_date of leave balance check ''' ''' Checks whether the expired leaves coincide with the to_date of leave balance check.
This allows backdated leave entry creation for non carry forwarded allocation '''
end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date']) end_date = frappe.db.get_value("Leave Allocation", {'name': leave_entry.transaction_name}, ['to_date'])
return True if end_date == date and not leave_entry.is_carry_forward else False return True if end_date == date and not leave_entry.is_carry_forward else False
def get_leave_entries(employee, leave_type, from_date, to_date): def get_leave_entries(employee, leave_type, from_date, to_date):
''' Returns leave entries between from_date and to_date ''' ''' Returns leave entries between from_date and to_date. '''
return frappe.db.sql(""" return frappe.db.sql("""
SELECT SELECT
employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type,
is_carry_forward, is_expired is_carry_forward, is_expired
FROM `tabLeave Ledger Entry` FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1 AND leaves<0 AND docstatus=1
AND (leaves<0
OR is_expired=1)
AND (from_date between %(from_date)s AND %(to_date)s AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s)) OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@ -8,7 +8,6 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate, nowdate, flt from frappe.utils import getdate, nowdate, flt
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.hr.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves

View File

@ -141,6 +141,7 @@ def expire_allocation(allocation, expiry_date=None):
leaves = get_remaining_leaves(allocation) leaves = get_remaining_leaves(allocation)
expiry_date = expiry_date if expiry_date else allocation.to_date expiry_date = expiry_date if expiry_date else allocation.to_date
# allows expired leaves entry to be created/reverted
if leaves: if leaves:
args = dict( args = dict(
leaves=flt(leaves) * -1, leaves=flt(leaves) * -1,
@ -160,6 +161,8 @@ def expire_carried_forward_allocation(allocation):
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date) leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date)
leaves = flt(allocation.leaves) + flt(leaves_taken) leaves = flt(allocation.leaves) + flt(leaves_taken)
# allow expired leaves entry to be created
if leaves > 0: if leaves > 0:
args = frappe._dict( args = frappe._dict(
transaction_name=allocation.name, transaction_name=allocation.name,

View File

@ -776,22 +776,16 @@ class SalarySlip(TransactionBase):
for payment in self.get('loans'): for payment in self.get('loans'):
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
total_amount = amounts['interest_amount'] + amounts['payable_principal_amount']
if payment.total_payment > total_amount:
frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2}
against loan {3}""").format(payment.idx, frappe.bold(payment.total_payment),
frappe.bold(total_amount), frappe.bold(payment.loan)))
if payment.interest_amount > amounts['interest_amount']:
frappe.throw(_("""Row {0}: Paid Interest amount {1} is greater than pending interest amount {2}
against loan {3}""").format(payment.idx, frappe.bold(payment.interest_amount),
frappe.bold(amounts['interest_amount']), frappe.bold(payment.loan)))
if payment.principal_amount > amounts['payable_principal_amount']:
frappe.throw(_("""Row {0}: Paid Principal amount {1} is greater than pending principal amount {2}
against loan {3}""").format(payment.idx, frappe.bold(payment.principal_amount),
frappe.bold(amounts['payable_principal_amount']), frappe.bold(payment.loan)))
payment.total_payment = payment.interest_amount + payment.principal_amount
self.total_interest_amount += payment.interest_amount self.total_interest_amount += payment.interest_amount
self.total_principal_amount += payment.principal_amount self.total_principal_amount += payment.principal_amount
self.total_loan_repayment = self.total_interest_amount + self.total_principal_amount self.total_loan_repayment += payment.total_payment
def get_loan_details(self): def get_loan_details(self):

View File

@ -149,13 +149,19 @@ class TestLoan(unittest.TestCase):
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68) repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 10), "Regular Payment", 111118.68)
repayment_entry.save() repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year)) penalty_amount = (accrued_interest_amount * 5 * 25) / (100 * days_in_year(get_datetime(first_date).year))
self.assertEquals(flt(repayment_entry.interest_payable, 2), flt(accrued_interest_amount, 2))
self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2)) self.assertEquals(flt(repayment_entry.penalty_amount, 2), flt(penalty_amount, 2))
repayment_entry.submit() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
loan.load_from_db()
self.assertEquals(amounts[0], repayment_entry.interest_payable)
self.assertEquals(flt(loan.total_principal_paid, 2), flt(repayment_entry.amount_paid -
penalty_amount - amounts[0], 2))
def test_loan_closure_repayment(self): def test_loan_closure_repayment(self):
pledges = [] pledges = []
@ -189,15 +195,19 @@ class TestLoan(unittest.TestCase):
process_loan_interest_accrual_for_demand_loans(posting_date = last_date) process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5),
"Loan Closure", 13315.0681) "Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
repayment_entry.save() repayment_entry.submit()
repayment_entry.amount_paid = repayment_entry.payable_amount amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
self.assertEquals(flt(repayment_entry.interest_payable, 3), flt(accrued_interest_amount, 3)) unaccrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * 6) \
/ (days_in_year(get_datetime(first_date).year) * 100)
self.assertEquals(flt(amounts[0] + unaccrued_interest_amount, 3),
flt(accrued_interest_amount, 3))
self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0) self.assertEquals(flt(repayment_entry.penalty_amount, 5), 0)
repayment_entry.submit()
loan.load_from_db() loan.load_from_db()
self.assertEquals(loan.status, "Loan Closure Requested") self.assertEquals(loan.status, "Loan Closure Requested")
@ -227,57 +237,15 @@ class TestLoan(unittest.TestCase):
process_loan_interest_accrual_for_term_loans(posting_date=nowdate()) process_loan_interest_accrual_for_term_loans(posting_date=nowdate())
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(get_last_day(nowdate()), 5), repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(get_last_day(nowdate()), 5),
"Regular Payment", 89768.7534247) "Regular Payment", 89768.75)
repayment_entry.save()
repayment_entry.submit() repayment_entry.submit()
repayment_entry.load_from_db() amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount',
'paid_principal_amount'])
self.assertEquals(repayment_entry.interest_payable, 11250.00) self.assertEquals(amounts[0], 11250.00)
self.assertEquals(repayment_entry.payable_principal_amount, 78303.00) self.assertEquals(amounts[1], 78303.00)
def test_partial_loan_repayment(self):
pledges = []
pledges.append({
"loan_security": "Test Security 1",
"qty": 4000.00,
"haircut": 50
})
loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges)
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name,
posting_date=get_first_day(nowdate()))
loan.submit()
self.assertEquals(loan.loan_amount, 1000000)
first_date = '2019-10-01'
last_date = '2019-10-30'
no_of_days = date_diff(last_date, first_date) + 1
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime().year) * 100)
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
process_loan_interest_accrual_for_demand_loans(posting_date = add_days(first_date, 15))
process_loan_interest_accrual_for_demand_loans(posting_date = add_days(first_date, 30))
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 1), "Regular Payment", 6500)
repayment_entry.save()
repayment_entry.submit()
penalty_amount = (accrued_interest_amount * 4 * 25) / (100 * days_in_year(get_datetime(first_date).year))
lia1 = frappe.get_value("Loan Interest Accrual", {"loan": loan.name, "is_paid": 1}, 'name')
lia2 = frappe.get_value("Loan Interest Accrual", {"loan": loan.name, "is_paid": 0}, 'name')
self.assertTrue(lia1)
self.assertTrue(lia2)
def test_security_shortfall(self): def test_security_shortfall(self):
pledges = [] pledges = []

View File

@ -58,18 +58,16 @@ class TestLoanDisbursement(unittest.TestCase):
process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1)) process_loan_interest_accrual_for_demand_loans(posting_date=add_days(last_date, 1))
# Paid 511095.89 amount includes 5,00,000 principal amount and 11095.89 interest amount # Should not be able to create loan disbursement entry before repayment
self.assertRaises(frappe.ValidationError, make_loan_disbursement_entry, loan.name,
500000, first_date)
repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5), repayment_entry = create_repayment_entry(loan.name, self.applicant, add_days(get_last_day(nowdate()), 5),
"Regular Payment", 611095.89) "Regular Payment", 611095.89)
repayment_entry.submit()
repayment_entry.submit()
loan.reload() loan.reload()
# After repayment loan disbursement entry should go through
make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16)) make_loan_disbursement_entry(loan.name, 500000, disbursement_date=add_days(last_date, 16))
total_principal_paid = loan.total_principal_paid
loan.reload()
# Loan Topup will result in decreasing the Total Principal Paid
self.assertEqual(flt(loan.total_principal_paid, 2), flt(total_principal_paid - 500000, 2))

View File

@ -15,12 +15,13 @@
"company", "company",
"posting_date", "posting_date",
"is_term_loan", "is_term_loan",
"is_paid",
"section_break_7", "section_break_7",
"pending_principal_amount", "pending_principal_amount",
"payable_principal_amount", "payable_principal_amount",
"paid_principal_amount",
"column_break_14", "column_break_14",
"interest_amount", "interest_amount",
"paid_interest_amount",
"section_break_15", "section_break_15",
"process_loan_interest_accrual", "process_loan_interest_accrual",
"repayment_schedule_name", "repayment_schedule_name",
@ -101,13 +102,6 @@
"label": "Company", "label": "Company",
"options": "Company" "options": "Company"
}, },
{
"default": "0",
"fieldname": "is_paid",
"fieldtype": "Check",
"label": "Is Paid",
"read_only": 1
},
{ {
"default": "0", "default": "0",
"fetch_from": "loan.is_term_loan", "fetch_from": "loan.is_term_loan",
@ -143,12 +137,24 @@
"hidden": 1, "hidden": 1,
"label": "Repayment Schedule Name", "label": "Repayment Schedule Name",
"read_only": 1 "read_only": 1
},
{
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "paid_interest_amount",
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
} }
], ],
"in_create": 1, "in_create": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-04-10 18:31:02.369857", "modified": "2020-04-16 11:24:23.258404",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Interest Accrual", "name": "Loan Interest Accrual",

View File

@ -30,9 +30,8 @@
"reference_number", "reference_number",
"column_break_21", "column_break_21",
"reference_date", "reference_date",
"paid_accrual_entries",
"partial_paid_entry",
"principal_amount_paid", "principal_amount_paid",
"repayment_details",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -155,13 +154,6 @@
"options": "Company:company:default_currency", "options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "paid_accrual_entries",
"fieldtype": "Text",
"hidden": 1,
"label": "Paid Accrual Entries",
"read_only": 1
},
{ {
"default": "0", "default": "0",
"fetch_from": "against_loan.is_term_loan", "fetch_from": "against_loan.is_term_loan",
@ -197,13 +189,6 @@
"fieldname": "column_break_21", "fieldname": "column_break_21",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "partial_paid_entry",
"fieldtype": "Text",
"hidden": 1,
"label": "Partial Paid Entry",
"read_only": 1
},
{ {
"default": "0.0", "default": "0.0",
"fieldname": "principal_amount_paid", "fieldname": "principal_amount_paid",
@ -225,11 +210,18 @@
"fieldtype": "Date", "fieldtype": "Date",
"label": "Due Date", "label": "Due Date",
"read_only": 1 "read_only": 1
},
{
"fieldname": "repayment_details",
"fieldtype": "Table",
"hidden": 1,
"label": "Repayment Details",
"options": "Loan Repayment Detail"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-02-26 06:18:54.934538", "modified": "2020-04-16 18:14:45.166754",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Repayment", "name": "Loan Repayment",
@ -264,7 +256,6 @@
"write": 1 "write": 1
} }
], ],
"quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1

View File

@ -19,11 +19,11 @@ class LoanRepayment(AccountsController):
def validate(self): def validate(self):
amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type) amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type)
self.set_missing_values(amounts) self.set_missing_values(amounts)
self.validate_amount()
def before_submit(self): self.allocate_amounts(amounts['pending_accrual_entries'])
self.mark_as_paid()
def on_submit(self): def on_submit(self):
self.update_paid_amount()
self.make_gl_entries() self.make_gl_entries()
def on_cancel(self): def on_cancel(self):
@ -38,32 +38,25 @@ 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 = amounts['interest_amount'] self.interest_payable = flt(amounts['interest_amount'], 2)
if not self.penalty_amount: if not self.penalty_amount:
self.penalty_amount = amounts['penalty_amount'] self.penalty_amount = flt(amounts['penalty_amount'], 2)
if not self.pending_principal_amount: if not self.pending_principal_amount:
self.pending_principal_amount = amounts['pending_principal_amount'] self.pending_principal_amount = flt(amounts['pending_principal_amount'], 2)
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 = amounts['payable_principal_amount'] self.payable_principal_amount = flt(amounts['payable_principal_amount'], 2)
if not self.payable_amount: if not self.payable_amount:
self.payable_amount = amounts['payable_amount'] self.payable_amount = flt(amounts['payable_amount'], 2)
if amounts.get('paid_accrual_entries'):
self.paid_accrual_entries = frappe.as_json(amounts.get('paid_accrual_entries'))
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 mark_as_paid(self): def validate_amount(self):
paid_entries = [] if not self.amount_paid:
paid_amount = self.amount_paid
interest_paid = paid_amount
if not paid_amount:
frappe.throw(_("Amount paid cannot be zero")) frappe.throw(_("Amount paid cannot be zero"))
if self.amount_paid < self.penalty_amount: if self.amount_paid < self.penalty_amount:
@ -74,37 +67,15 @@ class LoanRepayment(AccountsController):
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):
loan = frappe.get_doc("Loan", self.against_loan) loan = frappe.get_doc("Loan", self.against_loan)
if self.paid_accrual_entries: for payment in self.repayment_details:
paid_accrual_entries = json.loads(self.paid_accrual_entries)
if paid_amount - self.penalty_amount > 0 and self.paid_accrual_entries:
interest_paid = paid_amount - self.penalty_amount
for lia, interest_amount in iteritems(paid_accrual_entries):
if interest_amount <= interest_paid:
paid_entries.append(lia)
interest_paid -= interest_amount
elif interest_paid:
self.partial_paid_entry = frappe.as_json({"name": lia, "interest_amount": interest_amount})
frappe.db.set_value("Loan Interest Accrual", lia, "interest_amount",
interest_amount - interest_paid)
interest_paid = 0
if paid_entries:
self.paid_accrual_entries = frappe.as_json(paid_entries)
else:
self.paid_accrual_entries = ""
if interest_paid:
self.principal_amount_paid = interest_paid
if paid_entries:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET is_paid = 1 where name in (%s)""" #nosec SET paid_principal_amount = `paid_principal_amount` + %s,
% ", ".join(['%s']*len(paid_entries)), tuple(paid_entries)) paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
(flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), 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, 2) >= flt(loan.total_payment, 2):
frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested")
@ -116,21 +87,14 @@ class LoanRepayment(AccountsController):
update_shortfall_status(self.against_loan, self.principal_amount_paid) update_shortfall_status(self.against_loan, self.principal_amount_paid)
def mark_as_unpaid(self): def mark_as_unpaid(self):
loan = frappe.get_doc("Loan", self.against_loan) loan = frappe.get_doc("Loan", self.against_loan)
if self.paid_accrual_entries: for payment in self.repayment_details:
paid_accrual_entries = json.loads(self.paid_accrual_entries)
if self.paid_accrual_entries:
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual` frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
SET is_paid = 0 where name in (%s)""" #nosec SET paid_principal_amount = `paid_principal_amount` - %s,
% ", ".join(['%s']*len(paid_accrual_entries)), tuple(paid_accrual_entries)) paid_interest_amount = `paid_interest_amount` - %s
WHERE name = %s""",
if self.partial_paid_entry: (payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
partial_paid_entry = json.loads(self.partial_paid_entry)
frappe.db.set_value("Loan Interest Accrual", partial_paid_entry["name"], "interest_amount",
partial_paid_entry["interest_amount"])
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid, WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
@ -139,6 +103,38 @@ class LoanRepayment(AccountsController):
if loan.status == "Loan Closure Requested": if loan.status == "Loan Closure Requested":
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed") frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
def allocate_amounts(self, paid_entries):
self.set('repayment_details', [])
self.principal_amount_paid = 0
if self.amount_paid - self.penalty_amount > 0 and paid_entries:
interest_paid = self.amount_paid - self.penalty_amount
for lia, amounts in iteritems(paid_entries):
if amounts['interest_amount'] + amounts['payable_principal_amount'] <= interest_paid:
interest_amount = amounts['interest_amount']
paid_principal = amounts['payable_principal_amount']
self.principal_amount_paid += paid_principal
interest_paid -= (interest_amount + paid_principal)
elif interest_paid:
if interest_paid >= amounts['interest_amount']:
interest_amount = amounts['interest_amount']
paid_principal = interest_paid - interest_amount
self.principal_amount_paid += paid_principal
interest_paid = 0
else:
interest_amount = interest_paid
interest_paid = 0
paid_principal=0
self.append('repayment_details', {
'loan_interest_accrual': lia,
'paid_interest_amount': interest_amount,
'paid_principal_amount': paid_principal
})
if interest_paid:
self.principal_amount_paid += interest_paid
def make_gl_entries(self, cancel=0, adv_adj=0): def make_gl_entries(self, cancel=0, adv_adj=0):
gle_map = [] gle_map = []
loan_details = frappe.get_doc("Loan", self.against_loan) loan_details = frappe.get_doc("Loan", self.against_loan)
@ -223,7 +219,7 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
"posting_date": posting_date, "posting_date": posting_date,
"applicant": applicant, "applicant": applicant,
"penalty_amount": penalty_amount, "penalty_amount": penalty_amount,
"interst_payable": interest_payable, "interest_payable": interest_payable,
"payable_principal_amount": payable_principal_amount, "payable_principal_amount": payable_principal_amount,
"amount_paid": amount_paid, "amount_paid": amount_paid,
"loan_type": loan_type "loan_type": loan_type
@ -232,15 +228,22 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
return lr return lr
def get_accrued_interest_entries(against_loan): def get_accrued_interest_entries(against_loan):
accrued_interest_entries = frappe.get_all("Loan Interest Accrual",
fields=["name", "interest_amount", "posting_date", "payable_principal_amount"],
filters = {
"loan": against_loan,
"is_paid": 0,
"docstatus": 1
}, order_by="posting_date")
return accrued_interest_entries unpaid_accrued_entries = frappe.db.sql(
"""
SELECT name, posting_date, interest_amount - paid_interest_amount as interest_amount,
payable_principal_amount - paid_principal_amount as payable_principal_amount
FROM
`tabLoan Interest Accrual`
WHERE
loan = %s
AND (interest_amount - paid_interest_amount > 0 OR
payable_principal_amount - paid_principal_amount > 0)
AND
docstatus = 1
""", (against_loan), as_dict=1)
return unpaid_accrued_entries
# This function returns the amounts that are payable at the time of loan repayment based on posting date # This function returns the amounts that are payable at the time of loan repayment based on posting date
# 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
@ -273,8 +276,10 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
total_pending_interest += entry.interest_amount total_pending_interest += entry.interest_amount
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, {
flt(entry.interest_amount) + flt(entry.payable_principal_amount)) 'interest_amount': flt(entry.interest_amount),
'payable_principal_amount': flt(entry.payable_principal_amount)
})
final_due_date = due_date final_due_date = due_date
@ -291,7 +296,7 @@ def get_amounts(amounts, against_loan, posting_date, payment_type):
amounts["interest_amount"] = total_pending_interest amounts["interest_amount"] = total_pending_interest
amounts["penalty_amount"] = penalty_amount amounts["penalty_amount"] = penalty_amount
amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount amounts["payable_amount"] = payable_principal_amount + total_pending_interest + penalty_amount
amounts["paid_accrual_entries"] = pending_accrual_entries amounts["pending_accrual_entries"] = pending_accrual_entries
if final_due_date: if final_due_date:
amounts["due_date"] = final_due_date amounts["due_date"] = final_due_date

View File

@ -0,0 +1,43 @@
{
"actions": [],
"creation": "2020-04-15 18:31:54.026923",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"loan_interest_accrual",
"paid_principal_amount",
"paid_interest_amount"
],
"fields": [
{
"fieldname": "loan_interest_accrual",
"fieldtype": "Link",
"label": "Loan Interest Accrual",
"options": "Loan Interest Accrual"
},
{
"fieldname": "paid_principal_amount",
"fieldtype": "Currency",
"label": "Paid Principal Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "paid_interest_amount",
"fieldtype": "Currency",
"label": "Paid Interest Amount",
"options": "Company:company:default_currency"
}
],
"istable": 1,
"links": [],
"modified": "2020-04-15 21:50:03.837019",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Repayment Detail",
"owner": "Administrator",
"permissions": [],
"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 LoanRepaymentDetail(Document):
pass

View File

@ -119,6 +119,7 @@
"label": "Penalty Interest Rate (%) Per Day" "label": "Penalty Interest Rate (%) Per Day"
}, },
{ {
"description": "No. of days from due date until which penalty won't be charged in case of delay in loan repayment",
"fieldname": "grace_period_in_days", "fieldname": "grace_period_in_days",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Grace Period in Days" "label": "Grace Period in Days"
@ -142,7 +143,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-02-03 05:03:00.334813", "modified": "2020-04-15 00:24:43.259963",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan Type", "name": "Loan Type",

View File

@ -28,7 +28,6 @@
{ {
"fieldname": "loan_account", "fieldname": "loan_account",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Loan Account", "label": "Loan Account",
"options": "Account", "options": "Account",
"read_only": 1 "read_only": 1
@ -50,21 +49,23 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Principal Amount", "label": "Principal Amount",
"options": "Company:company:default_currency" "options": "Company:company:default_currency",
"read_only": 1
}, },
{ {
"fieldname": "interest_amount", "fieldname": "interest_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Interest Amount", "label": "Interest Amount",
"options": "Company:company:default_currency" "options": "Company:company:default_currency",
"read_only": 1
}, },
{ {
"fieldname": "total_payment", "fieldname": "total_payment",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1,
"label": "Total Payment", "label": "Total Payment",
"options": "Company:company:default_currency", "options": "Company:company:default_currency"
"read_only": 1
}, },
{ {
"fieldname": "loan_repayment_entry", "fieldname": "loan_repayment_entry",
@ -84,7 +85,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-04-09 20:01:53.546364", "modified": "2020-04-16 13:17:04.798335",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Salary Slip Loan", "name": "Salary Slip Loan",

View File

@ -82,7 +82,9 @@ frappe.ui.form.on('Job Card', {
frm.set_value('current_time' , 0); frm.set_value('current_time' , 0);
} }
frm.save(); frm.save("Save", () => {}, "", () => {
frm.doc.time_logs.pop(-1);
});
}, },
complete_job: function(frm, completed_time, completed_qty) { complete_job: function(frm, completed_time, completed_qty) {
@ -105,6 +107,24 @@ frappe.ui.form.on('Job Card', {
}); });
}, },
validate: function(frm) {
if ((!frm.doc.time_logs || !frm.doc.time_logs.length) && frm.doc.started_time) {
frm.trigger("reset_timer");
}
},
employee: function(frm) {
if (frm.doc.job_started && !frm.doc.current_time) {
frm.trigger("reset_timer");
}
},
reset_timer: function(frm) {
frm.set_value('started_time' , '');
frm.set_value('job_started', 0);
frm.set_value('current_time' , 0);
},
make_dashboard: function(frm) { make_dashboard: function(frm) {
if(frm.doc.__islocal) if(frm.doc.__islocal)
return; return;
@ -205,5 +225,10 @@ frappe.ui.form.on('Job Card', {
frappe.ui.form.on('Job Card Time Log', { frappe.ui.form.on('Job Card Time Log', {
completed_qty: function(frm) { completed_qty: function(frm) {
frm.events.set_total_completed_qty(frm); frm.events.set_total_completed_qty(frm);
},
to_time: function(frm) {
frm.set_value('job_started', 0);
frm.set_value('started_time', '');
} }
}) })

View File

@ -9,7 +9,7 @@ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate,
get_time, add_to_date, time_diff, add_days, get_datetime_str) get_time, add_to_date, time_diff, add_days, get_datetime_str, get_link_to_form)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
@ -189,11 +189,15 @@ class JobCard(Document):
def validate_job_card(self): def validate_job_card(self):
if not self.time_logs: if not self.time_logs:
frappe.throw(_("Time logs are required for job card {0}").format(self.name)) frappe.throw(_("Time logs are required for {0} {1}")
.format(frappe.bold("Job Card"), get_link_to_form("Job Card", self.name)))
if self.for_quantity and self.total_completed_qty != self.for_quantity: if self.for_quantity and self.total_completed_qty != self.for_quantity:
frappe.throw(_("The total completed qty({0}) must be equal to qty to manufacture({1})" total_completed_qty = frappe.bold(_("Total Completed Qty"))
.format(frappe.bold(self.total_completed_qty),frappe.bold(self.for_quantity)))) qty_to_manufacture = frappe.bold(_("Qty to Manufacture"))
frappe.throw(_("The {0} ({1}) must be equal to {2} ({3})"
.format(total_completed_qty, frappe.bold(self.total_completed_qty), qty_to_manufacture,frappe.bold(self.for_quantity))))
def update_work_order(self): def update_work_order(self):
if not self.work_order: if not self.work_order:

View File

@ -240,6 +240,8 @@ frappe.ui.form.on("Work Order", {
}); });
}, __("Job Card"), __("Create")); }, __("Job Card"), __("Create"));
dialog.fields_dict["operations"].grid.wrapper.find('.grid-add-row').hide();
var pending_qty = 0; var pending_qty = 0;
frm.doc.operations.forEach(data => { frm.doc.operations.forEach(data => {
if(data.completed_qty != frm.doc.qty) { if(data.completed_qty != frm.doc.qty) {

View File

@ -100,7 +100,7 @@ def execute():
for entry in encounter_details: for entry in encounter_details:
doc = frappe.get_doc('Patient Encounter', entry.name) doc = frappe.get_doc('Patient Encounter', entry.name)
symptoms = entry.symptoms.split('\n') symptoms = entry.symptoms.split('\n') if entry.symptoms else []
for symptom in symptoms: for symptom in symptoms:
if not frappe.db.exists('Complaint', symptom): if not frappe.db.exists('Complaint', symptom):
frappe.get_doc({ frappe.get_doc({
@ -112,7 +112,7 @@ def execute():
}) })
row.db_update() row.db_update()
diagnosis = entry.diagnosis.split('\n') diagnosis = entry.diagnosis.split('\n') if entry.diagnosis else []
for d in diagnosis: for d in diagnosis:
if not frappe.db.exists('Diagnosis', d): if not frappe.db.exists('Diagnosis', d):
frappe.get_doc({ frappe.get_doc({

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:project_name", "autoname": "field:project_name",
@ -435,16 +436,18 @@
}, },
{ {
"depends_on": "collect_progress", "depends_on": "collect_progress",
"description": "Message will sent to users to get their status on the project", "description": "Message will be sent to the users to get their status on the Project",
"fieldname": "message", "fieldname": "message",
"fieldtype": "Text", "fieldtype": "Text",
"label": "Message" "label": "Message",
"mandatory_depends_on": "collect_progress"
} }
], ],
"icon": "fa fa-puzzle-piece", "icon": "fa fa-puzzle-piece",
"idx": 29, "idx": 29,
"links": [],
"max_attachments": 4, "max_attachments": 4,
"modified": "2019-09-24 15:02:50.208301", "modified": "2020-04-08 22:11:14.552615",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project", "name": "Project",

View File

@ -379,6 +379,30 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
} }
}); });
} }
},
manufacturer_part_no: function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
if (row.manufacturer_part_no) {
frappe.model.get_value('Item Manufacturer',
{
'item_code': row.item_code,
'manufacturer': row.manufacturer,
'manufacturer_part_no': row.manufacturer_part_no
},
'name',
function(data) {
if (!data) {
let msg = {
message: __("Manufacturer Part Number <b>{0}</b> is invalid", [row.manufacturer_part_no]),
title: __("Invalid Part Number")
}
frappe.throw(msg);
}
});
}
} }
}); });

View File

@ -551,6 +551,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(!d[k]) d[k] = v; if(!d[k]) d[k] = v;
}); });
if (d.has_batch_no && d.has_serial_no) {
d.batch_no = undefined;
}
erpnext.show_serial_batch_selector(me.frm, d, (item) => { erpnext.show_serial_batch_selector(me.frm, d, (item) => {
me.frm.script_manager.trigger('qty', item.doctype, item.name); me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse) if (!me.frm.doc.set_warehouse)
@ -559,7 +563,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}, },
() => me.conversion_factor(doc, cdt, cdn, true), () => me.conversion_factor(doc, cdt, cdn, true),
() => me.remove_pricing_rule(item) () => me.remove_pricing_rule(item),
() => {
if (item.apply_rule_on_other_items) {
let key = item.name;
me.apply_rule_on_other_items({key: item});
}
}
]); ]);
} }
} }
@ -1394,21 +1404,23 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
apply_rule_on_other_items: function(args) { apply_rule_on_other_items: function(args) {
const me = this; const me = this;
const fields = ["discount_percentage", "discount_amount", "rate"]; const fields = ["discount_percentage", "pricing_rules", "discount_amount", "rate"];
for(var k in args) { for(var k in args) {
let data = args[k]; let data = args[k];
if (data && data.apply_rule_on_other_items) {
me.frm.doc.items.forEach(d => { me.frm.doc.items.forEach(d => {
if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) {
for(var k in data) { for(var k in data) {
if (in_list(fields, k)) { if (in_list(fields, k) && data[k]) {
frappe.model.set_value(d.doctype, d.name, k, data[k]); frappe.model.set_value(d.doctype, d.name, k, data[k]);
} }
} }
} }
}); });
} }
}
}, },
apply_product_discount: function(free_item_data) { apply_product_discount: function(free_item_data) {

View File

@ -436,21 +436,7 @@ erpnext.utils.update_child_items = function(opts) {
const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row; const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname; const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
this.data = []; this.data = [];
const dialog = new frappe.ui.Dialog({ const fields = [{
title: __("Update Items"),
fields: [
{
fieldname: "trans_items",
fieldtype: "Table",
label: "Items",
cannot_add_rows: cannot_add_row,
in_place_edit: true,
reqd: 1,
data: this.data,
get_data: () => {
return this.data;
},
fields: [{
fieldtype:'Data', fieldtype:'Data',
fieldname:"docname", fieldname:"docname",
read_only: 1, read_only: 1,
@ -477,7 +463,32 @@ erpnext.utils.update_child_items = function(opts) {
read_only: 0, read_only: 0,
in_list_view: 1, in_list_view: 1,
label: __('Rate') label: __('Rate')
}] }];
if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) {
fields.splice(2, 0, {
fieldtype: 'Date',
fieldname: frm.doc.doctype == 'Sales Order' ? "delivery_date" : "schedule_date",
in_list_view: 1,
label: frm.doc.doctype == 'Sales Order' ? __("Delivery Date") : __("Reqd by date")
})
}
const dialog = new frappe.ui.Dialog({
title: __("Update Items"),
fields: [
{
fieldname: "trans_items",
fieldtype: "Table",
label: "Items",
cannot_add_rows: cannot_add_row,
in_place_edit: true,
reqd: 1,
data: this.data,
get_data: () => {
return this.data;
},
fields: fields
}, },
], ],
primary_action: function() { primary_action: function() {
@ -506,6 +517,8 @@ erpnext.utils.update_child_items = function(opts) {
"docname": d.name, "docname": d.name,
"name": d.name, "name": d.name,
"item_code": d.item_code, "item_code": d.item_code,
"delivery_date": d.delivery_date,
"schedule_date": d.schedule_date,
"qty": d.qty, "qty": d.qty,
"rate": d.rate, "rate": d.rate,
}); });

View File

@ -150,7 +150,7 @@ class Customer(TransactionBase):
contact.save() contact.save()
else: else:
lead.lead_name = lead.lead_name.split(" ") lead.lead_name = lead.lead_name.lstrip().split(" ")
lead.first_name = lead.lead_name[0] lead.first_name = lead.lead_name[0]
lead.last_name = " ".join(lead.lead_name[1:]) lead.last_name = " ".join(lead.lead_name[1:])

View File

@ -1176,7 +1176,7 @@ class POSCart {
return ` return `
<div class="list-item indicator ${indicator_class}" data-item-code="${escape(item.item_code)}" <div class="list-item indicator ${indicator_class}" data-item-code="${escape(item.item_code)}"
data-batch-no="${batch_no}" title="Item: ${item.item_name} Available Qty: ${item.actual_qty}"> data-batch-no="${batch_no}" title="Item: ${item.item_name} Available Qty: ${item.actual_qty} ${item.stock_uom}">
<div class="item-name list-item__content list-item__content--flex-1.5 ellipsis"> <div class="item-name list-item__content list-item__content--flex-1.5 ellipsis">
${item.item_name} ${item.item_name}
</div> </div>

View File

@ -37,20 +37,33 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt']) lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt'])
# locate function is used to sort by closest match from the beginning of the value # locate function is used to sort by closest match from the beginning of the value
result = [] result = []
items_data = frappe.db.sql(""" SELECT name as item_code, items_data = frappe.db.sql("""
item_name, image as item_image, idx as idx,is_stock_item SELECT
name AS item_code,
item_name,
stock_uom,
image AS item_image,
idx AS idx,
is_stock_item
FROM FROM
`tabItem` `tabItem`
WHERE WHERE
disabled = 0 and has_variants = 0 and is_sales_item = 1 disabled = 0
and item_group in (select name from `tabItem Group` where lft >= {lft} and rgt <= {rgt}) AND has_variants = 0
and {condition} order by idx desc limit {start}, {page_length}""" AND is_sales_item = 1
AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
AND {condition}
ORDER BY
idx desc
LIMIT
{start}, {page_length}"""
.format( .format(
start=start, page_length=page_length, start=start,
lft=lft, rgt=rgt, page_length=page_length,
lft=lft,
rgt=rgt,
condition=condition condition=condition
), as_dict=1) ), as_dict=1)

View File

@ -228,9 +228,15 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
warehouse: function(doc, cdt, cdn) { warehouse: function(doc, cdt, cdn) {
var me = this; var me = this;
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if (item.serial_no && item.qty === item.serial_no.split(`\n`).length) {
return;
}
if (item.serial_no && !item.batch_no) { if (item.serial_no && !item.batch_no) {
item.serial_no = null; item.serial_no = null;
} }
var has_batch_no; var has_batch_no;
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => { frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
has_batch_no = r && r.has_batch_no; has_batch_no = r && r.has_batch_no;

View File

@ -434,15 +434,6 @@ class TestDeliveryNote(unittest.TestCase):
update_delivery_note_status(dn.name, "Closed") update_delivery_note_status(dn.name, "Closed")
self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed") self.assertEqual(frappe.db.get_value("Delivery Note", dn.name, "Status"), "Closed")
def test_customer_provided_parts_dn(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
dn = create_delivery_note(item_code='CUST-0987', rate=0)
self.assertEqual(dn.get("items")[0].allow_zero_valuation_rate, 1)
# test if Delivery Note with rate is allowed against Customer Provided Item
dn2 = create_delivery_note(item_code='CUST-0987', do_not_save=True)
self.assertRaises(frappe.ValidationError, dn2.save)
def test_dn_billing_status_case1(self): def test_dn_billing_status_case1(self):
# SO -> DN -> SI # SO -> DN -> SI
so = make_sales_order() so = make_sales_order()

View File

@ -114,6 +114,8 @@
"is_sub_contracted_item", "is_sub_contracted_item",
"column_break_74", "column_break_74",
"customer_code", "customer_code",
"default_item_manufacturer",
"default_manufacturer_part_no",
"website_section", "website_section",
"show_in_website", "show_in_website",
"show_variant_in_website", "show_variant_in_website",
@ -1038,6 +1040,18 @@
"fieldname": "auto_create_assets", "fieldname": "auto_create_assets",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Auto Create Assets on Purchase" "label": "Auto Create Assets on Purchase"
},
{
"fieldname": "default_item_manufacturer",
"fieldtype": "Data",
"label": "Default Item Manufacturer",
"read_only": 1
},
{
"fieldname": "default_manufacturer_part_no",
"fieldtype": "Data",
"label": "Default Manufacturer Part No",
"read_only": 1
} }
], ],
"has_web_view": 1, "has_web_view": 1,
@ -1046,7 +1060,7 @@
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"max_attachments": 1, "max_attachments": 1,
"modified": "2020-03-24 16:14:36.950677", "modified": "2020-04-07 15:56:06.195722",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"creation": "2019-06-02 04:41:37.332911", "creation": "2019-06-02 04:41:37.332911",
"doctype": "DocType", "doctype": "DocType",
@ -10,7 +11,8 @@
"manufacturer_part_no", "manufacturer_part_no",
"column_break_3", "column_break_3",
"item_name", "item_name",
"description" "description",
"is_default"
], ],
"fields": [ "fields": [
{ {
@ -52,9 +54,17 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Description", "label": "Description",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Default"
} }
], ],
"modified": "2019-06-06 19:07:31.175919", "links": [],
"modified": "2020-04-07 20:25:55.507905",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Manufacturer", "name": "Item Manufacturer",

View File

@ -11,6 +11,10 @@ from frappe.model.document import Document
class ItemManufacturer(Document): class ItemManufacturer(Document):
def validate(self): def validate(self):
self.validate_duplicate_entry() self.validate_duplicate_entry()
self.manage_default_item_manufacturer()
def on_trash(self):
self.manage_default_item_manufacturer(delete=True)
def validate_duplicate_entry(self): def validate_duplicate_entry(self):
if self.is_new(): if self.is_new():
@ -24,6 +28,40 @@ class ItemManufacturer(Document):
frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}") frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}")
.format(self.item_code, self.manufacturer)) .format(self.item_code, self.manufacturer))
def manage_default_item_manufacturer(self, delete=False):
from frappe.model.utils import set_default
item = frappe.get_doc("Item", self.item_code)
default_manufacturer = item.default_item_manufacturer
default_part_no = item.default_manufacturer_part_no
if not self.is_default:
# if unchecked and default in Item master, clear it.
if default_manufacturer == self.manufacturer and default_part_no == self.manufacturer_part_no:
frappe.db.set_value("Item", item.name,
{
"default_item_manufacturer": None,
"default_manufacturer_part_no": None
})
elif self.is_default:
set_default(self, "item_code")
manufacturer, manufacturer_part_no = default_manufacturer, default_part_no
if delete:
manufacturer, manufacturer_part_no = None, None
elif (default_manufacturer != self.manufacturer) or \
(default_manufacturer == self.manufacturer and default_part_no != self.manufacturer_part_no):
manufacturer = self.manufacturer
manufacturer_part_no = self.manufacturer_part_no
frappe.db.set_value("Item", item.name,
{
"default_item_manufacturer": manufacturer,
"default_manufacturer_part_no": manufacturer_part_no
})
@frappe.whitelist() @frappe.whitelist()
def get_item_manufacturer_part_no(item_code, manufacturer): def get_item_manufacturer_part_no(item_code, manufacturer):
return frappe.db.get_value("Item Manufacturer", return frappe.db.get_value("Item Manufacturer",

View File

@ -175,12 +175,11 @@ class MaterialRequest(BuyingController):
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty) frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
target_ref_field = 'qty' if self.material_request_type == "Manufacture" else 'stock_qty'
self._update_percent_field({ self._update_percent_field({
"target_dt": "Material Request Item", "target_dt": "Material Request Item",
"target_parent_dt": self.doctype, "target_parent_dt": self.doctype,
"target_parent_field": "per_ordered", "target_parent_field": "per_ordered",
"target_ref_field": target_ref_field, "target_ref_field": "stock_qty",
"target_field": "ordered_qty", "target_field": "ordered_qty",
"name": self.name, "name": self.name,
}, update_modified) }, update_modified)
@ -499,7 +498,7 @@ def raise_work_orders(material_request):
default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse") default_wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", "default_wip_warehouse")
for d in mr.items: for d in mr.items:
if (d.qty - d.ordered_qty) >0: if (d.stock_qty - d.ordered_qty) > 0:
if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}): if frappe.db.exists("BOM", {"item": d.item_code, "is_default": 1}):
wo_order = frappe.new_doc("Work Order") wo_order = frappe.new_doc("Work Order")
wo_order.update({ wo_order.update({
@ -531,7 +530,7 @@ def raise_work_orders(material_request):
msgprint(_("The following Work Orders were created:") + '\n' + new_line_sep(message)) msgprint(_("The following Work Orders were created:") + '\n' + new_line_sep(message))
if errors: if errors:
frappe.throw(_("Productions Orders cannot be raised for:") + '\n' + new_line_sep(errors)) frappe.throw(_("Work Order cannot be created for following reason:") + '\n' + new_line_sep(errors))
return work_orders return work_orders

View File

@ -7,7 +7,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, unittest, erpnext import frappe, unittest, erpnext
from frappe.utils import flt, today from frappe.utils import flt, today
from erpnext.stock.doctype.material_request.material_request import raise_work_orders from erpnext.stock.doctype.material_request.material_request \
import raise_work_orders, make_stock_entry, make_purchase_order, make_supplier_quotation
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase): class TestMaterialRequest(unittest.TestCase):
@ -15,8 +16,6 @@ class TestMaterialRequest(unittest.TestCase):
erpnext.set_perpetual_inventory(0) erpnext.set_perpetual_inventory(0)
def test_make_purchase_order(self): def test_make_purchase_order(self):
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
mr = frappe.copy_doc(test_records[0]).insert() mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_purchase_order, self.assertRaises(frappe.ValidationError, make_purchase_order,
@ -30,8 +29,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEqual(len(po.get("items")), len(mr.get("items"))) self.assertEqual(len(po.get("items")), len(mr.get("items")))
def test_make_supplier_quotation(self): def test_make_supplier_quotation(self):
from erpnext.stock.doctype.material_request.material_request import make_supplier_quotation
mr = frappe.copy_doc(test_records[0]).insert() mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_supplier_quotation, mr.name) self.assertRaises(frappe.ValidationError, make_supplier_quotation, mr.name)
@ -45,12 +42,9 @@ class TestMaterialRequest(unittest.TestCase):
def test_make_stock_entry(self): def test_make_stock_entry(self):
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
mr = frappe.copy_doc(test_records[0]).insert() mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_stock_entry, self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)
mr.name)
mr = frappe.get_doc("Material Request", mr.name) mr = frappe.get_doc("Material Request", mr.name)
mr.material_request_type = "Material Transfer" mr.material_request_type = "Material Transfer"
@ -198,14 +192,7 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert() mr.insert()
mr.submit() mr.submit()
# check if per complete is None
mr.load_from_db()
self.assertEqual(mr.per_ordered, 0)
self.assertEqual(mr.get("items")[0].ordered_qty, 0)
self.assertEqual(mr.get("items")[1].ordered_qty, 0)
# map a purchase order # map a purchase order
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
po_doc = make_purchase_order(mr.name) po_doc = make_purchase_order(mr.name)
po_doc.supplier = "_Test Supplier" po_doc.supplier = "_Test Supplier"
po_doc.transaction_date = "2013-07-07" po_doc.transaction_date = "2013-07-07"
@ -276,10 +263,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
# map a stock entry # map a stock entry
se_doc = make_stock_entry(mr.name) se_doc = make_stock_entry(mr.name)
@ -331,8 +316,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 27.0) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 1.5) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
# check if per complete is as expected for Stock Entry cancelled # check if per complete is as expected for Stock Entry cancelled
se.cancel() se.cancel()
@ -344,8 +329,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_completed_qty_for_over_transfer(self): def test_completed_qty_for_over_transfer(self):
existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") existing_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
@ -357,14 +342,7 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert() mr.insert()
mr.submit() mr.submit()
# check if per complete is None
mr.load_from_db()
self.assertEqual(mr.per_ordered, 0)
self.assertEqual(mr.get("items")[0].ordered_qty, 0)
self.assertEqual(mr.get("items")[1].ordered_qty, 0)
# map a stock entry # map a stock entry
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
se_doc = make_stock_entry(mr.name) se_doc = make_stock_entry(mr.name)
se_doc.update({ se_doc.update({
@ -425,8 +403,8 @@ class TestMaterialRequest(unittest.TestCase):
current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC") current_requested_qty_item1 = self._get_requested_qty("_Test Item Home Desktop 100", "_Test Warehouse - _TC")
current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC") current_requested_qty_item2 = self._get_requested_qty("_Test Item Home Desktop 200", "_Test Warehouse - _TC")
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 - 54.0) self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 - 3.0) self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
def test_incorrect_mapping_of_stock_entry(self): def test_incorrect_mapping_of_stock_entry(self):
# submit material request of type Transfer # submit material request of type Transfer
@ -435,9 +413,6 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert() mr.insert()
mr.submit() mr.submit()
# map a stock entry
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
se_doc = make_stock_entry(mr.name) se_doc = make_stock_entry(mr.name)
se_doc.update({ se_doc.update({
"posting_date": "2013-03-01", "posting_date": "2013-03-01",
@ -468,8 +443,6 @@ class TestMaterialRequest(unittest.TestCase):
mr.insert() mr.insert()
mr.submit() mr.submit()
# map a stock entry
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
se_doc = make_stock_entry(mr.name) se_doc = make_stock_entry(mr.name)
self.assertEqual(se_doc.get("items")[0].s_warehouse, "_Test Warehouse - _TC") self.assertEqual(se_doc.get("items")[0].s_warehouse, "_Test Warehouse - _TC")
@ -483,8 +456,6 @@ class TestMaterialRequest(unittest.TestCase):
return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty")) return flt(frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, "indented_qty"))
def test_make_stock_entry_for_material_issue(self): def test_make_stock_entry_for_material_issue(self):
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
mr = frappe.copy_doc(test_records[0]).insert() mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_stock_entry, self.assertRaises(frappe.ValidationError, make_stock_entry,
@ -503,8 +474,6 @@ class TestMaterialRequest(unittest.TestCase):
return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100", return flt(frappe.db.get_value("Bin", {"item_code": "_Test Item Home Desktop 100",
"warehouse": "_Test Warehouse - _TC"}, "indented_qty")) "warehouse": "_Test Warehouse - _TC"}, "indented_qty"))
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
existing_requested_qty = _get_requested_qty() existing_requested_qty = _get_requested_qty()
mr = frappe.copy_doc(test_records[0]) mr = frappe.copy_doc(test_records[0])
@ -563,9 +532,37 @@ class TestMaterialRequest(unittest.TestCase):
item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0] item_code= %s and warehouse= %s """, (mr.items[0].item_code, mr.items[0].warehouse))[0][0]
self.assertEqual(requested_qty, new_requested_qty) self.assertEqual(requested_qty, new_requested_qty)
def test_multi_uom_for_purchase(self): def test_requested_qty_multi_uom(self):
from erpnext.stock.doctype.material_request.material_request import make_purchase_order existing_requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
uom="_Test UOM 1", conversion_factor=12)
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty + 120)
work_order = raise_work_orders(mr.name)
wo = frappe.get_doc("Work Order", work_order[0])
wo.qty = 50
wo.wip_warehouse = "_Test Warehouse 1 - _TC"
wo.submit()
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty + 70)
wo.cancel()
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty + 120)
mr.reload()
mr.cancel()
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty)
def test_multi_uom_for_purchase(self):
mr = frappe.copy_doc(test_records[0]) mr = frappe.copy_doc(test_records[0])
mr.material_request_type = 'Purchase' mr.material_request_type = 'Purchase'
item = mr.items[0] item = mr.items[0]
@ -607,7 +604,6 @@ class TestMaterialRequest(unittest.TestCase):
self.assertEqual(mr.per_ordered, 100) self.assertEqual(mr.per_ordered, 100)
def test_customer_provided_parts_mr(self): def test_customer_provided_parts_mr(self):
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC") existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
@ -633,6 +629,8 @@ def make_material_request(**args):
mr.append("items", { mr.append("items", {
"item_code": args.item_code or "_Test Item", "item_code": args.item_code or "_Test Item",
"qty": args.qty or 10, "qty": args.qty or 10,
"uom": args.uom or "_Test UOM",
"conversion_factor": args.conversion_factor or 1,
"schedule_date": args.schedule_date or today(), "schedule_date": args.schedule_date or today(),
"warehouse": args.warehouse or "_Test Warehouse - _TC", "warehouse": args.warehouse or "_Test Warehouse - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC" "cost_center": args.cost_center or "_Test Cost Center - _TC"

View File

@ -1,5 +1,4 @@
{ {
"actions": [],
"autoname": "hash", "autoname": "hash",
"creation": "2013-02-22 01:28:02", "creation": "2013-02-22 01:28:02",
"doctype": "DocType", "doctype": "DocType",
@ -374,7 +373,10 @@
{ {
"fieldname": "received_qty", "fieldname": "received_qty",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Received Quantity" "label": "Received Quantity",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@ -404,14 +406,13 @@
{ {
"fieldname": "manufacturer_part_no", "fieldname": "manufacturer_part_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number", "label": "Manufacturer Part Number"
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-02-25 03:09:10.698967", "modified": "2020-04-16 09:00:00.992835",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Material Request Item", "name": "Material Request Item",

View File

@ -356,8 +356,8 @@ class TestPurchaseReceipt(unittest.TestCase):
'accounts': [{ 'accounts': [{
'company_name': '_Test Company', 'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC', 'fixed_asset_account': '_Test Fixed Asset - _TC',
'accumulated_depreciation_account': 'Depreciation - _TC', 'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
'depreciation_expense_account': 'Depreciation - _TC' 'depreciation_expense_account': '_Test Depreciation - _TC'
}] }]
}).insert() }).insert()

View File

@ -801,8 +801,7 @@
{ {
"fieldname": "manufacturer_part_no", "fieldname": "manufacturer_part_no",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Manufacturer Part Number", "label": "Manufacturer Part Number"
"read_only": 1
}, },
{ {
"depends_on": "is_fixed_asset", "depends_on": "is_fixed_asset",
@ -832,7 +831,7 @@
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-11 14:19:48.799370", "modified": "2020-04-07 18:38:21.141558",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@ -220,8 +220,8 @@ frappe.ui.form.on('Stock Entry', {
}, },
get_query_filters: { get_query_filters: {
docstatus: 1, docstatus: 1,
material_request_type: "Material Transfer", material_request_type: ["in", ["Material Transfer", "Material Issue"]],
status: ['!=', 'Transferred'] status: ["not in", ["Transferred", "Issued"]]
} }
}) })
}, __("Get items from")); }, __("Get items from"));

View File

@ -341,6 +341,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
else: else:
out["manufacturer_part_no"] = None out["manufacturer_part_no"] = None
out["manufacturer"] = None out["manufacturer"] = None
else:
out["manufacturer"], out["manufacturer_part_no"] = frappe.get_value("Item", item.name,
["default_item_manufacturer", "default_manufacturer_part_no"] )
child_doctype = args.doctype + ' Item' child_doctype = args.doctype + ' Item'
meta = frappe.get_meta(child_doctype) meta = frappe.get_meta(child_doctype)

View File

@ -113,24 +113,30 @@ def get_reserved_qty(item_code, warehouse):
return flt(reserved_qty[0][0]) if reserved_qty else 0 return flt(reserved_qty[0][0]) if reserved_qty else 0
def get_indented_qty(item_code, warehouse): def get_indented_qty(item_code, warehouse):
inward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor) # Ordered Qty is always maintained in stock UOM
inward_qty = frappe.db.sql("""
select sum(mr_item.stock_qty - mr_item.ordered_qty)
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
where mr_item.item_code=%s and mr_item.warehouse=%s where mr_item.item_code=%s and mr_item.warehouse=%s
and mr.material_request_type in ('Purchase', 'Manufacture') and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer')
and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse)) and mr.status!='Stopped' and mr.docstatus=1
""", (item_code, warehouse))
inward_qty = flt(inward_qty[0][0]) if inward_qty else 0
outward_qty = frappe.db.sql("""select sum((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor) outward_qty = frappe.db.sql("""
select sum(mr_item.stock_qty - mr_item.ordered_qty)
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
where mr_item.item_code=%s and mr_item.warehouse=%s where mr_item.item_code=%s and mr_item.warehouse=%s
and mr.material_request_type in ('Material Issue', 'Material Transfer') and mr.material_request_type = 'Material Issue'
and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name and mr_item.stock_qty > mr_item.ordered_qty and mr_item.parent=mr.name
and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse)) and mr.status!='Stopped' and mr.docstatus=1
""", (item_code, warehouse))
outward_qty = flt(outward_qty[0][0]) if outward_qty else 0
inward_qty, outward_qty = flt(inward_qty[0][0]) if inward_qty else 0, flt(outward_qty[0][0]) if outward_qty else 0 requested_qty = inward_qty - outward_qty
indented_qty = inward_qty - outward_qty
return indented_qty return requested_qty
def get_ordered_qty(item_code, warehouse): def get_ordered_qty(item_code, warehouse):
ordered_qty = frappe.db.sql(""" ordered_qty = frappe.db.sql("""

View File

@ -16,7 +16,11 @@
<tr> <tr>
<td>{{ item }}</td> <td>{{ item }}</td>
<td class='text-right'> <td class='text-right'>
{{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, currency) }} {% if doc.get('is_return') %}
{{ frappe.utils.fmt_money((itemised_taxable_amount.get(item, 0))|abs, None, doc.currency) }}
{% else %}
{{ frappe.utils.fmt_money(itemised_taxable_amount.get(item, 0), None, doc.currency) }}
{% endif %}
</td> </td>
{% for tax_account in tax_accounts %} {% for tax_account in tax_accounts %}
{% set tax_details = taxes.get(tax_account) %} {% set tax_details = taxes.get(tax_account) %}
@ -25,7 +29,11 @@
{% if tax_details.tax_rate or not tax_details.tax_amount %} {% if tax_details.tax_rate or not tax_details.tax_amount %}
({{ tax_details.tax_rate }}%) ({{ tax_details.tax_rate }}%)
{% endif %} {% endif %}
{{ frappe.utils.fmt_money(tax_details.tax_amount / conversion_rate, None, currency) }} {% if doc.get('is_return') %}
{{ frappe.utils.fmt_money((tax_details.tax_amount / doc.conversion_rate)|abs, None, doc.currency) }}
{% else %}
{{ frappe.utils.fmt_money(tax_details.tax_amount / doc.conversion_rate, None, doc.currency) }}
{% endif %}
</td> </td>
{% else %} {% else %}
<td></td> <td></td>