Merge branch 'develop' of https://github.com/frappe/erpnext into social-media-integration-feat
This commit is contained in:
commit
790b9ee8d0
@ -89,7 +89,7 @@ class Account(NestedSet):
|
||||
throw(_("Root cannot be edited."), RootNotEditable)
|
||||
|
||||
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):
|
||||
# ignore validation while creating new compnay or while syncing to child companies
|
||||
|
@ -69,6 +69,7 @@ class TestAccount(unittest.TestCase):
|
||||
acc.account_name = "Accumulated Depreciation"
|
||||
acc.parent_account = "Fixed Assets - _TC"
|
||||
acc.company = "_Test Company"
|
||||
acc.account_type = "Accumulated Depreciation"
|
||||
acc.insert()
|
||||
|
||||
doc = frappe.get_doc("Account", "Securities and Deposits - _TC")
|
||||
@ -149,7 +150,7 @@ def _make_test_records(verbose):
|
||||
|
||||
# fixed asset depreciation
|
||||
["_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 Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
|
||||
|
||||
|
@ -164,7 +164,7 @@ def build_forest(data):
|
||||
error_messages = []
|
||||
|
||||
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:
|
||||
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
|
||||
|
@ -178,7 +178,8 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
|
||||
|
||||
if pricing_rules[0].mixed_conditions and doc:
|
||||
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:
|
||||
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:
|
||||
amt = args.get('qty') * args.get("price_list_rate")
|
||||
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
|
||||
|
||||
if pr_doc.is_cumulative:
|
||||
|
@ -174,7 +174,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
|
||||
read_only: 0,
|
||||
fieldtype:'Date',
|
||||
label: __('Release Date'),
|
||||
default: me.frm.doc.release_date
|
||||
default: me.frm.doc.release_date,
|
||||
reqd: 1
|
||||
},
|
||||
{
|
||||
fieldname: 'hold_comment',
|
||||
|
@ -754,8 +754,7 @@
|
||||
{
|
||||
"fieldname": "manufacturer_part_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Manufacturer Part Number",
|
||||
"read_only": 1
|
||||
"label": "Manufacturer Part Number"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_fixed_asset",
|
||||
@ -777,7 +776,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-01 14:20:17.297284",
|
||||
"modified": "2020-04-07 18:34:35.104178",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -440,11 +440,12 @@ class SalesInvoice(SellingController):
|
||||
if pos.get("company_address"):
|
||||
self.company_address = pos.get("company_address")
|
||||
|
||||
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')
|
||||
|
||||
selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list')
|
||||
if self.customer:
|
||||
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')
|
||||
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:
|
||||
self.set('selling_price_list', selling_price_list)
|
||||
|
@ -1926,16 +1926,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
item.taxes = []
|
||||
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):
|
||||
si = frappe.new_doc("Sales Invoice")
|
||||
args = frappe._dict(args)
|
||||
|
@ -163,7 +163,8 @@ def set_price_list(party_details, party, party_type, given_price_list, pos=None)
|
||||
# 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]
|
||||
elif pos and party_type == 'Customer':
|
||||
customer_price_list = frappe.get_value('Customer', party.name, 'default_price_list')
|
||||
|
@ -365,6 +365,7 @@ def get_columns(filters):
|
||||
|
||||
columns = [
|
||||
{
|
||||
"label": _("GL Entry"),
|
||||
"fieldname": "gl_entry",
|
||||
"fieldtype": "Link",
|
||||
"options": "GL Entry",
|
||||
|
@ -11,6 +11,7 @@ from frappe.model.document import Document
|
||||
class AssetCategory(Document):
|
||||
def validate(self):
|
||||
self.validate_finance_books()
|
||||
self.validate_accounts()
|
||||
|
||||
def validate_finance_books(self):
|
||||
for d in self.finance_books:
|
||||
@ -18,6 +19,27 @@ class AssetCategory(Document):
|
||||
if cint(d.get(frappe.scrub(field)))<1:
|
||||
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()
|
||||
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"):
|
||||
|
@ -702,8 +702,7 @@
|
||||
{
|
||||
"fieldname": "manufacturer_part_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Manufacturer Part Number",
|
||||
"read_only": 1
|
||||
"label": "Manufacturer Part Number"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@ -723,7 +722,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2019-12-06 13:17:12.142799",
|
||||
"modified": "2020-04-07 18:35:17.558928",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-05-22 12:43:10",
|
||||
"doctype": "DocType",
|
||||
@ -522,8 +523,7 @@
|
||||
{
|
||||
"fieldname": "manufacturer_part_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Manufacturer Part Number",
|
||||
"read_only": 1
|
||||
"label": "Manufacturer Part Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_15",
|
||||
@ -532,7 +532,8 @@
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"modified": "2019-06-02 05:32:46.019237",
|
||||
"links": [],
|
||||
"modified": "2020-04-07 18:35:51.175947",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation Item",
|
||||
|
@ -1123,36 +1123,39 @@ def get_supplier_block_status(party_name):
|
||||
}
|
||||
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
|
||||
"""
|
||||
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
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_name = item.item_name
|
||||
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.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)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
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_name = item.item_name
|
||||
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.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_amount = 1 # Initiallize value will update in parent validation
|
||||
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"):
|
||||
new_child_flag = True
|
||||
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":
|
||||
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:
|
||||
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")):
|
||||
@ -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
|
||||
if new_child_flag:
|
||||
parent.load_from_db()
|
||||
child_item.idx = len(parent.items) + 1
|
||||
child_item.insert()
|
||||
else:
|
||||
|
@ -383,9 +383,6 @@ class StockController(AccountsController):
|
||||
# Customer Provided parts will have zero valuation rate
|
||||
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
|
||||
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,
|
||||
warehouse_account=None, company=None):
|
||||
|
@ -667,8 +667,7 @@ def get_itemised_tax_breakup_html(doc):
|
||||
itemised_tax=itemised_tax,
|
||||
itemised_taxable_amount=itemised_taxable_amount,
|
||||
tax_accounts=tax_accounts,
|
||||
conversion_rate=doc.conversion_rate,
|
||||
currency=doc.currency
|
||||
doc=doc
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -336,3 +336,27 @@ def make_opportunity_from_communication(communication, ignore_communication_link
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
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
|
19
erpnext/crm/doctype/opportunity/opportunity_calendar.js
Normal file
19
erpnext/crm/doctype/opportunity/opportunity_calendar.js
Normal 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'
|
||||
}
|
@ -2,15 +2,40 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
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) {
|
||||
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);
|
||||
if (data.count == data.total) {
|
||||
window.setTimeout(title => frm.dashboard.hide_progress(title), 1500, data.title);
|
||||
let error_occurred = data.count === -1;
|
||||
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);
|
||||
}
|
||||
});
|
||||
},
|
||||
refresh: function(frm) {
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.master_data && !frm.doc.is_master_data_imported) {
|
||||
if (frm.doc.is_master_data_processed) {
|
||||
if (frm.doc.status != "Importing Master Data") {
|
||||
@ -34,17 +59,17 @@ frappe.ui.form.on('Tally Migration', {
|
||||
}
|
||||
}
|
||||
},
|
||||
add_button: function(frm, label, method) {
|
||||
add_button: function (frm, label, method) {
|
||||
frm.add_custom_button(
|
||||
label,
|
||||
() => frm.call({
|
||||
doc: frm.doc,
|
||||
method: method,
|
||||
freeze: true,
|
||||
callback: () => {
|
||||
frm.remove_custom_button(label);
|
||||
}
|
||||
})
|
||||
() => {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: method,
|
||||
freeze: true
|
||||
});
|
||||
frm.reload_doc();
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"beta": 1,
|
||||
"creation": "2019-02-01 14:27:09.485238",
|
||||
"doctype": "DocType",
|
||||
@ -14,6 +15,7 @@
|
||||
"tally_debtors_account",
|
||||
"company_section",
|
||||
"tally_company",
|
||||
"default_uom",
|
||||
"column_break_8",
|
||||
"erpnext_company",
|
||||
"processed_files_section",
|
||||
@ -43,6 +45,7 @@
|
||||
"label": "Status"
|
||||
},
|
||||
{
|
||||
"description": "Data exported from Tally that consists of the Chart of Accounts, Customers, Suppliers, Addresses, Items and UOMs",
|
||||
"fieldname": "master_data",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
@ -50,6 +53,7 @@
|
||||
},
|
||||
{
|
||||
"default": "Sundry Creditors",
|
||||
"description": "Creditors Account set in Tally",
|
||||
"fieldname": "tally_creditors_account",
|
||||
"fieldtype": "Data",
|
||||
"label": "Tally Creditors Account",
|
||||
@ -61,6 +65,7 @@
|
||||
},
|
||||
{
|
||||
"default": "Sundry Debtors",
|
||||
"description": "Debtors Account set in Tally",
|
||||
"fieldname": "tally_debtors_account",
|
||||
"fieldtype": "Data",
|
||||
"label": "Tally Debtors Account",
|
||||
@ -72,6 +77,7 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"description": "Company Name as per Imported Tally Data",
|
||||
"fieldname": "tally_company",
|
||||
"fieldtype": "Data",
|
||||
"label": "Tally Company",
|
||||
@ -82,9 +88,11 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Your Company set in ERPNext",
|
||||
"fieldname": "erpnext_company",
|
||||
"fieldtype": "Data",
|
||||
"label": "ERPNext Company"
|
||||
"label": "ERPNext Company",
|
||||
"read_only_depends_on": "eval:doc.is_master_data_processed == 1"
|
||||
},
|
||||
{
|
||||
"fieldname": "processed_files_section",
|
||||
@ -155,24 +163,28 @@
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_master_data_processed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Master Data Processed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_day_book_data_processed",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Day Book Data Processed",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_day_book_data_imported",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Day Book Data Imported",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_master_data_imported",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Master Data Imported",
|
||||
@ -188,13 +200,23 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Day Book Data exported from Tally that consists of all historic transactions",
|
||||
"fieldname": "day_book_data",
|
||||
"fieldtype": "Attach",
|
||||
"in_list_view": 1,
|
||||
"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",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "Tally Migration",
|
||||
|
@ -4,20 +4,23 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from decimal import Decimal
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
import zipfile
|
||||
from decimal import Decimal
|
||||
|
||||
from bs4 import BeautifulSoup as bs
|
||||
|
||||
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.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import getseries, revert_series_if_last
|
||||
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"
|
||||
VOUCHER_CHUNK_SIZE = 500
|
||||
@ -39,13 +42,15 @@ class TallyMigration(Document):
|
||||
return string
|
||||
|
||||
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:
|
||||
encoded_content = zf.read(zf.namelist()[0])
|
||||
try:
|
||||
content = encoded_content.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
content = encoded_content.decode("utf-16")
|
||||
if zipfile.is_zipfile(master_file_path):
|
||||
with zipfile.ZipFile(master_file_path) as zf:
|
||||
encoded_content = zf.read(zf.namelist()[0])
|
||||
try:
|
||||
content = encoded_content.decode("utf-8-sig")
|
||||
except UnicodeDecodeError:
|
||||
content = encoded_content.decode("utf-16")
|
||||
|
||||
master = bs(sanitize(emptify(content)), "xml")
|
||||
collection = master.BODY.IMPORTDATA.REQUESTDATA
|
||||
@ -58,13 +63,14 @@ class TallyMigration(Document):
|
||||
"file_name": key + ".json",
|
||||
"attached_to_doctype": self.doctype,
|
||||
"attached_to_name": self.name,
|
||||
"content": json.dumps(value)
|
||||
"content": json.dumps(value),
|
||||
"is_private": True
|
||||
}).insert()
|
||||
setattr(self, key, f.file_url)
|
||||
|
||||
def _process_master_data(self):
|
||||
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):
|
||||
root_type_map = {
|
||||
@ -97,17 +103,17 @@ class TallyMigration(Document):
|
||||
# If Ledger doesn't have PARENT field then don't create Account
|
||||
# For example "Profit & Loss A/c"
|
||||
if account.PARENT:
|
||||
yield account.PARENT.string, account["NAME"], 0
|
||||
yield account.PARENT.string.strip(), account["NAME"], 0
|
||||
|
||||
def get_parent(account):
|
||||
if account.PARENT:
|
||||
return account.PARENT.string
|
||||
return account.PARENT.string.strip()
|
||||
return {
|
||||
("Yes", "No"): "Application of Funds (Assets)",
|
||||
("Yes", "Yes"): "Expenses",
|
||||
("No", "Yes"): "Income",
|
||||
("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):
|
||||
children, parents = {}, {}
|
||||
@ -145,38 +151,38 @@ class TallyMigration(Document):
|
||||
parties, addresses = [], []
|
||||
for account in collection.find_all("LEDGER"):
|
||||
party_type = None
|
||||
if account.NAME.string in customers:
|
||||
if account.NAME.string.strip() in customers:
|
||||
party_type = "Customer"
|
||||
parties.append({
|
||||
"doctype": party_type,
|
||||
"customer_name": account.NAME.string,
|
||||
"tax_id": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None,
|
||||
"customer_name": account.NAME.string.strip(),
|
||||
"tax_id": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None,
|
||||
"customer_group": "All Customer Groups",
|
||||
"territory": "All Territories",
|
||||
"customer_type": "Individual",
|
||||
})
|
||||
elif account.NAME.string in suppliers:
|
||||
elif account.NAME.string.strip() in suppliers:
|
||||
party_type = "Supplier"
|
||||
parties.append({
|
||||
"doctype": party_type,
|
||||
"supplier_name": account.NAME.string,
|
||||
"pan": account.INCOMETAXNUMBER.string if account.INCOMETAXNUMBER else None,
|
||||
"supplier_name": account.NAME.string.strip(),
|
||||
"pan": account.INCOMETAXNUMBER.string.strip() if account.INCOMETAXNUMBER else None,
|
||||
"supplier_group": "All Supplier Groups",
|
||||
"supplier_type": "Individual",
|
||||
})
|
||||
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({
|
||||
"doctype": "Address",
|
||||
"address_line1": address[:140].strip(),
|
||||
"address_line2": address[140:].strip(),
|
||||
"country": account.COUNTRYNAME.string if account.COUNTRYNAME else None,
|
||||
"state": account.LEDSTATENAME.string if account.LEDSTATENAME else None,
|
||||
"gst_state": account.LEDSTATENAME.string if account.LEDSTATENAME else None,
|
||||
"pin_code": account.PINCODE.string if account.PINCODE else None,
|
||||
"mobile": account.LEDGERPHONE.string if account.LEDGERPHONE else None,
|
||||
"phone": account.LEDGERPHONE.string if account.LEDGERPHONE else None,
|
||||
"gstin": account.PARTYGSTIN.string if account.PARTYGSTIN else None,
|
||||
"country": account.COUNTRYNAME.string.strip() if account.COUNTRYNAME else None,
|
||||
"state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
|
||||
"gst_state": account.LEDSTATENAME.string.strip() if account.LEDSTATENAME else None,
|
||||
"pin_code": account.PINCODE.string.strip() if account.PINCODE else None,
|
||||
"mobile": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
|
||||
"phone": account.LEDGERPHONE.string.strip() if account.LEDGERPHONE else None,
|
||||
"gstin": account.PARTYGSTIN.string.strip() if account.PARTYGSTIN else None,
|
||||
"links": [{"link_doctype": party_type, "link_name": account["NAME"]}],
|
||||
})
|
||||
return parties, addresses
|
||||
@ -184,41 +190,50 @@ class TallyMigration(Document):
|
||||
def get_stock_items_uoms(collection):
|
||||
uoms = []
|
||||
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 = []
|
||||
for item in collection.find_all("STOCKITEM"):
|
||||
stock_uom = item.BASEUNITS.string.strip() if item.BASEUNITS else self.default_uom
|
||||
items.append({
|
||||
"doctype": "Item",
|
||||
"item_code" : item.NAME.string,
|
||||
"stock_uom": item.BASEUNITS.string,
|
||||
"item_code" : item.NAME.string.strip(),
|
||||
"stock_uom": stock_uom.strip(),
|
||||
"is_stock_item": 0,
|
||||
"item_group": "All Item Groups",
|
||||
"item_defaults": [{"company": self.erpnext_company}]
|
||||
})
|
||||
|
||||
return items, uoms
|
||||
|
||||
try:
|
||||
self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5)
|
||||
collection = self.get_collection(self.master_data)
|
||||
company = get_company_name(collection)
|
||||
self.tally_company = company
|
||||
self.erpnext_company = company
|
||||
|
||||
self.publish("Process Master Data", _("Reading Uploaded File"), 1, 5)
|
||||
collection = self.get_collection(self.master_data)
|
||||
self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5)
|
||||
chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection)
|
||||
|
||||
company = get_company_name(collection)
|
||||
self.tally_company = company
|
||||
self.erpnext_company = company
|
||||
self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5)
|
||||
parties, addresses = get_parties_addresses(collection, customers, suppliers)
|
||||
|
||||
self.publish("Process Master Data", _("Processing Chart of Accounts and Parties"), 2, 5)
|
||||
chart_of_accounts, customers, suppliers = get_coa_customers_suppliers(collection)
|
||||
self.publish("Process Master Data", _("Processing Party Addresses"), 3, 5)
|
||||
parties, addresses = get_parties_addresses(collection, customers, suppliers)
|
||||
self.publish("Process Master Data", _("Processing Items and UOMs"), 4, 5)
|
||||
items, uoms = get_stock_items_uoms(collection)
|
||||
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", _("Processing Items and UOMs"), 4, 5)
|
||||
items, uoms = get_stock_items_uoms(collection)
|
||||
data = {"chart_of_accounts": chart_of_accounts, "parties": parties, "addresses": addresses, "items": items, "uoms": uoms}
|
||||
|
||||
self.dump_processed_data(data)
|
||||
self.is_master_data_processed = 1
|
||||
self.status = ""
|
||||
self.save()
|
||||
self.publish("Process Master Data", _("Done"), 5, 5)
|
||||
self.dump_processed_data(data)
|
||||
|
||||
self.is_master_data_processed = 1
|
||||
|
||||
except:
|
||||
self.publish("Process Master Data", _("Process Failed"), -1, 5)
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def publish(self, title, message, count, total):
|
||||
frappe.publish_realtime("tally_migration_progress_update", {"title": title, "message": message, "count": count, "total": total})
|
||||
@ -256,7 +271,6 @@ class TallyMigration(Document):
|
||||
except:
|
||||
self.log(address)
|
||||
|
||||
|
||||
def create_items_uoms(items_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()):
|
||||
@ -273,25 +287,35 @@ class TallyMigration(Document):
|
||||
except:
|
||||
self.log(item)
|
||||
|
||||
self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4)
|
||||
create_company_and_coa(self.chart_of_accounts)
|
||||
self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4)
|
||||
create_parties_and_addresses(self.parties, self.addresses)
|
||||
self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4)
|
||||
create_items_uoms(self.items, self.uoms)
|
||||
self.publish("Import Master Data", _("Done"), 4, 4)
|
||||
self.status = ""
|
||||
self.is_master_data_imported = 1
|
||||
self.save()
|
||||
try:
|
||||
self.publish("Import Master Data", _("Creating Company and Importing Chart of Accounts"), 1, 4)
|
||||
create_company_and_coa(self.chart_of_accounts)
|
||||
|
||||
self.publish("Import Master Data", _("Importing Parties and Addresses"), 2, 4)
|
||||
create_parties_and_addresses(self.parties, self.addresses)
|
||||
|
||||
self.publish("Import Master Data", _("Importing Items and UOMs"), 3, 4)
|
||||
create_items_uoms(self.items, self.uoms)
|
||||
|
||||
self.publish("Import Master Data", _("Done"), 4, 4)
|
||||
|
||||
self.is_master_data_imported = 1
|
||||
|
||||
except:
|
||||
self.publish("Import Master Data", _("Process Failed"), -1, 5)
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def _process_day_book_data(self):
|
||||
def get_vouchers(collection):
|
||||
vouchers = []
|
||||
for voucher in collection.find_all("VOUCHER"):
|
||||
if voucher.ISCANCELLED.string == "Yes":
|
||||
if voucher.ISCANCELLED.string.strip() == "Yes":
|
||||
continue
|
||||
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
|
||||
else:
|
||||
function = voucher_to_journal_entry
|
||||
@ -307,15 +331,15 @@ class TallyMigration(Document):
|
||||
accounts = []
|
||||
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST")
|
||||
for entry in ledger_entries:
|
||||
account = {"account": encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company), "cost_center": self.default_cost_center}
|
||||
if entry.ISPARTYLEDGER.string == "Yes":
|
||||
party_details = get_party(entry.LEDGERNAME.string)
|
||||
account = {"account": encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company), "cost_center": self.default_cost_center}
|
||||
if entry.ISPARTYLEDGER.string.strip() == "Yes":
|
||||
party_details = get_party(entry.LEDGERNAME.string.strip())
|
||||
if party_details:
|
||||
party_type, party_account = party_details
|
||||
account["party_type"] = party_type
|
||||
account["account"] = party_account
|
||||
account["party"] = entry.LEDGERNAME.string
|
||||
amount = Decimal(entry.AMOUNT.string)
|
||||
account["party"] = entry.LEDGERNAME.string.strip()
|
||||
amount = Decimal(entry.AMOUNT.string.strip())
|
||||
if amount > 0:
|
||||
account["credit_in_account_currency"] = str(abs(amount))
|
||||
else:
|
||||
@ -324,21 +348,21 @@ class TallyMigration(Document):
|
||||
|
||||
journal_entry = {
|
||||
"doctype": "Journal Entry",
|
||||
"tally_guid": voucher.GUID.string,
|
||||
"posting_date": voucher.DATE.string,
|
||||
"tally_guid": voucher.GUID.string.strip(),
|
||||
"posting_date": voucher.DATE.string.strip(),
|
||||
"company": self.erpnext_company,
|
||||
"accounts": accounts,
|
||||
}
|
||||
return journal_entry
|
||||
|
||||
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"
|
||||
party_field = "customer"
|
||||
account_field = "debit_to"
|
||||
account_name = encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
|
||||
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"
|
||||
party_field = "supplier"
|
||||
account_field = "credit_to"
|
||||
@ -351,10 +375,10 @@ class TallyMigration(Document):
|
||||
|
||||
invoice = {
|
||||
"doctype": doctype,
|
||||
party_field: voucher.PARTYNAME.string,
|
||||
"tally_guid": voucher.GUID.string,
|
||||
"posting_date": voucher.DATE.string,
|
||||
"due_date": voucher.DATE.string,
|
||||
party_field: voucher.PARTYNAME.string.strip(),
|
||||
"tally_guid": voucher.GUID.string.strip(),
|
||||
"posting_date": voucher.DATE.string.strip(),
|
||||
"due_date": voucher.DATE.string.strip(),
|
||||
"items": get_voucher_items(voucher, doctype),
|
||||
"taxes": get_voucher_taxes(voucher),
|
||||
account_field: account_name,
|
||||
@ -375,15 +399,15 @@ class TallyMigration(Document):
|
||||
for entry in inventory_entries:
|
||||
qty, uom = entry.ACTUALQTY.string.strip().split()
|
||||
items.append({
|
||||
"item_code": entry.STOCKITEMNAME.string,
|
||||
"description": entry.STOCKITEMNAME.string,
|
||||
"item_code": entry.STOCKITEMNAME.string.strip(),
|
||||
"description": entry.STOCKITEMNAME.string.strip(),
|
||||
"qty": qty.strip(),
|
||||
"uom": uom.strip(),
|
||||
"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,
|
||||
"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
|
||||
|
||||
@ -391,13 +415,13 @@ class TallyMigration(Document):
|
||||
ledger_entries = voucher.find_all("ALLLEDGERENTRIES.LIST") + voucher.find_all("LEDGERENTRIES.LIST")
|
||||
taxes = []
|
||||
for entry in ledger_entries:
|
||||
if entry.ISPARTYLEDGER.string == "No":
|
||||
tax_account = encode_company_abbr(entry.LEDGERNAME.string, self.erpnext_company)
|
||||
if entry.ISPARTYLEDGER.string.strip() == "No":
|
||||
tax_account = encode_company_abbr(entry.LEDGERNAME.string.strip(), self.erpnext_company)
|
||||
taxes.append({
|
||||
"charge_type": "Actual",
|
||||
"account_head": tax_account,
|
||||
"description": tax_account,
|
||||
"tax_amount": entry.AMOUNT.string,
|
||||
"tax_amount": entry.AMOUNT.string.strip(),
|
||||
"cost_center": self.default_cost_center,
|
||||
})
|
||||
return taxes
|
||||
@ -408,15 +432,24 @@ class TallyMigration(Document):
|
||||
elif frappe.db.exists({"doctype": "Customer", "customer_name": party}):
|
||||
return "Customer", encode_company_abbr(self.tally_debtors_account, self.erpnext_company)
|
||||
|
||||
self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3)
|
||||
collection = self.get_collection(self.day_book_data)
|
||||
self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3)
|
||||
vouchers = get_vouchers(collection)
|
||||
self.publish("Process Day Book Data", _("Done"), 3, 3)
|
||||
self.dump_processed_data({"vouchers": vouchers})
|
||||
self.status = ""
|
||||
self.is_day_book_data_processed = 1
|
||||
self.save()
|
||||
try:
|
||||
self.publish("Process Day Book Data", _("Reading Uploaded File"), 1, 3)
|
||||
collection = self.get_collection(self.day_book_data)
|
||||
|
||||
self.publish("Process Day Book Data", _("Processing Vouchers"), 2, 3)
|
||||
vouchers = get_vouchers(collection)
|
||||
|
||||
self.publish("Process Day Book Data", _("Done"), 3, 3)
|
||||
self.dump_processed_data({"vouchers": vouchers})
|
||||
|
||||
self.is_day_book_data_processed = 1
|
||||
|
||||
except:
|
||||
self.publish("Process Day Book Data", _("Process Failed"), -1, 5)
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def _import_day_book_data(self):
|
||||
def create_fiscal_years(vouchers):
|
||||
@ -454,23 +487,31 @@ class TallyMigration(Document):
|
||||
"currency": "INR"
|
||||
}).insert()
|
||||
|
||||
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("Company", self.erpnext_company, "round_off_account", self.round_off_account)
|
||||
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_debtors_account, self.erpnext_company), "account_type", "Receivable")
|
||||
frappe.db.set_value("Company", self.erpnext_company, "round_off_account", self.round_off_account)
|
||||
|
||||
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
|
||||
vouchers = json.loads(vouchers_file.get_content())
|
||||
vouchers_file = frappe.get_doc("File", {"file_url": self.vouchers})
|
||||
vouchers = json.loads(vouchers_file.get_content())
|
||||
|
||||
create_fiscal_years(vouchers)
|
||||
create_price_list()
|
||||
create_custom_fields(["Journal Entry", "Purchase Invoice", "Sales Invoice"])
|
||||
create_fiscal_years(vouchers)
|
||||
create_price_list()
|
||||
create_custom_fields(["Journal Entry", "Purchase Invoice", "Sales Invoice"])
|
||||
|
||||
total = len(vouchers)
|
||||
is_last = False
|
||||
for index in range(0, total, VOUCHER_CHUNK_SIZE):
|
||||
if index + VOUCHER_CHUNK_SIZE >= total:
|
||||
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)
|
||||
total = len(vouchers)
|
||||
is_last = False
|
||||
|
||||
for index in range(0, total, VOUCHER_CHUNK_SIZE):
|
||||
if index + VOUCHER_CHUNK_SIZE >= total:
|
||||
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)
|
||||
|
||||
except:
|
||||
self.log()
|
||||
|
||||
finally:
|
||||
self.set_status()
|
||||
|
||||
def _import_vouchers(self, start, total, is_last=False):
|
||||
frappe.flags.in_migrate = True
|
||||
@ -494,25 +535,26 @@ class TallyMigration(Document):
|
||||
frappe.flags.in_migrate = False
|
||||
|
||||
def process_master_data(self):
|
||||
self.status = "Processing Master Data"
|
||||
self.save()
|
||||
self.set_status("Processing Master Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
|
||||
|
||||
def import_master_data(self):
|
||||
self.status = "Importing Master Data"
|
||||
self.save()
|
||||
self.set_status("Importing Master Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
|
||||
|
||||
def process_day_book_data(self):
|
||||
self.status = "Processing Day Book Data"
|
||||
self.save()
|
||||
self.set_status("Processing Day Book Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
|
||||
|
||||
def import_day_book_data(self):
|
||||
self.status = "Importing Day Book Data"
|
||||
self.save()
|
||||
self.set_status("Importing Day Book Data")
|
||||
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
|
||||
|
||||
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)
|
||||
|
||||
def set_status(self, status=""):
|
||||
self.status = status
|
||||
self.save()
|
||||
|
@ -348,6 +348,8 @@ get_site_info = 'erpnext.utilities.get_site_info'
|
||||
|
||||
payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account"
|
||||
|
||||
communication_doctypes = ["Customer", "Supplier"]
|
||||
|
||||
regional_overrides = {
|
||||
'France': {
|
||||
'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method'
|
||||
|
@ -580,19 +580,22 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||
return leave_days
|
||||
|
||||
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'])
|
||||
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):
|
||||
''' Returns leave entries between from_date and to_date '''
|
||||
''' Returns leave entries between from_date and to_date. '''
|
||||
return frappe.db.sql("""
|
||||
SELECT
|
||||
employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type,
|
||||
is_carry_forward, is_expired
|
||||
FROM `tabLeave Ledger Entry`
|
||||
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
|
||||
OR to_date between %(from_date)s AND %(to_date)s
|
||||
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
|
||||
|
@ -8,7 +8,6 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate, nowdate, flt
|
||||
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.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
|
||||
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
|
||||
|
@ -141,6 +141,7 @@ def expire_allocation(allocation, expiry_date=None):
|
||||
leaves = get_remaining_leaves(allocation)
|
||||
expiry_date = expiry_date if expiry_date else allocation.to_date
|
||||
|
||||
# allows expired leaves entry to be created/reverted
|
||||
if leaves:
|
||||
args = dict(
|
||||
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
|
||||
leaves_taken = get_leaves_for_period(allocation.employee, allocation.leave_type, allocation.from_date, allocation.to_date)
|
||||
leaves = flt(allocation.leaves) + flt(leaves_taken)
|
||||
|
||||
# allow expired leaves entry to be created
|
||||
if leaves > 0:
|
||||
args = frappe._dict(
|
||||
transaction_name=allocation.name,
|
||||
|
@ -776,22 +776,16 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
for payment in self.get('loans'):
|
||||
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_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):
|
||||
|
||||
|
@ -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.save()
|
||||
repayment_entry.submit()
|
||||
|
||||
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))
|
||||
|
||||
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):
|
||||
pledges = []
|
||||
@ -189,15 +195,19 @@ class TestLoan(unittest.TestCase):
|
||||
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),
|
||||
"Loan Closure", 13315.0681)
|
||||
repayment_entry.save()
|
||||
"Loan Closure", flt(loan.loan_amount + accrued_interest_amount))
|
||||
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)
|
||||
|
||||
repayment_entry.submit()
|
||||
loan.load_from_db()
|
||||
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())
|
||||
|
||||
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.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(repayment_entry.payable_principal_amount, 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)
|
||||
self.assertEquals(amounts[0], 11250.00)
|
||||
self.assertEquals(amounts[1], 78303.00)
|
||||
|
||||
def test_security_shortfall(self):
|
||||
pledges = []
|
||||
@ -294,7 +262,7 @@ class TestLoan(unittest.TestCase):
|
||||
|
||||
make_loan_disbursement_entry(loan.name, loan.loan_amount)
|
||||
|
||||
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 100
|
||||
frappe.db.sql("""UPDATE `tabLoan Security Price` SET loan_security_price = 100
|
||||
where loan_security='Test Security 2'""")
|
||||
|
||||
create_process_loan_security_shortfall()
|
||||
|
@ -58,18 +58,16 @@ class TestLoanDisbursement(unittest.TestCase):
|
||||
|
||||
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),
|
||||
"Regular Payment", 611095.89)
|
||||
repayment_entry.submit()
|
||||
|
||||
repayment_entry.submit()
|
||||
loan.reload()
|
||||
|
||||
# After repayment loan disbursement entry should go through
|
||||
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))
|
||||
|
@ -15,12 +15,13 @@
|
||||
"company",
|
||||
"posting_date",
|
||||
"is_term_loan",
|
||||
"is_paid",
|
||||
"section_break_7",
|
||||
"pending_principal_amount",
|
||||
"payable_principal_amount",
|
||||
"paid_principal_amount",
|
||||
"column_break_14",
|
||||
"interest_amount",
|
||||
"paid_interest_amount",
|
||||
"section_break_15",
|
||||
"process_loan_interest_accrual",
|
||||
"repayment_schedule_name",
|
||||
@ -101,13 +102,6 @@
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_paid",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Paid",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "loan.is_term_loan",
|
||||
@ -143,12 +137,24 @@
|
||||
"hidden": 1,
|
||||
"label": "Repayment Schedule Name",
|
||||
"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,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-10 18:31:02.369857",
|
||||
"modified": "2020-04-16 11:24:23.258404",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Interest Accrual",
|
||||
|
@ -30,9 +30,8 @@
|
||||
"reference_number",
|
||||
"column_break_21",
|
||||
"reference_date",
|
||||
"paid_accrual_entries",
|
||||
"partial_paid_entry",
|
||||
"principal_amount_paid",
|
||||
"repayment_details",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
@ -155,13 +154,6 @@
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "paid_accrual_entries",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Paid Accrual Entries",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "against_loan.is_term_loan",
|
||||
@ -197,13 +189,6 @@
|
||||
"fieldname": "column_break_21",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "partial_paid_entry",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 1,
|
||||
"label": "Partial Paid Entry",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0.0",
|
||||
"fieldname": "principal_amount_paid",
|
||||
@ -225,11 +210,18 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Due Date",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "repayment_details",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Repayment Details",
|
||||
"options": "Loan Repayment Detail"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-26 06:18:54.934538",
|
||||
"modified": "2020-04-16 18:14:45.166754",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Repayment",
|
||||
@ -264,7 +256,6 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
|
@ -19,11 +19,11 @@ class LoanRepayment(AccountsController):
|
||||
def validate(self):
|
||||
amounts = calculate_amounts(self.against_loan, self.posting_date, self.payment_type)
|
||||
self.set_missing_values(amounts)
|
||||
|
||||
def before_submit(self):
|
||||
self.mark_as_paid()
|
||||
self.validate_amount()
|
||||
self.allocate_amounts(amounts['pending_accrual_entries'])
|
||||
|
||||
def on_submit(self):
|
||||
self.update_paid_amount()
|
||||
self.make_gl_entries()
|
||||
|
||||
def on_cancel(self):
|
||||
@ -38,32 +38,25 @@ class LoanRepayment(AccountsController):
|
||||
self.cost_center = erpnext.get_default_cost_center(self.company)
|
||||
|
||||
if not self.interest_payable:
|
||||
self.interest_payable = amounts['interest_amount']
|
||||
self.interest_payable = flt(amounts['interest_amount'], 2)
|
||||
|
||||
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:
|
||||
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:
|
||||
self.payable_principal_amount = amounts['payable_principal_amount']
|
||||
self.payable_principal_amount = flt(amounts['payable_principal_amount'], 2)
|
||||
|
||||
if not self.payable_amount:
|
||||
self.payable_amount = amounts['payable_amount']
|
||||
|
||||
if amounts.get('paid_accrual_entries'):
|
||||
self.paid_accrual_entries = frappe.as_json(amounts.get('paid_accrual_entries'))
|
||||
self.payable_amount = flt(amounts['payable_amount'], 2)
|
||||
|
||||
if amounts.get('due_date'):
|
||||
self.due_date = amounts.get('due_date')
|
||||
|
||||
def mark_as_paid(self):
|
||||
paid_entries = []
|
||||
paid_amount = self.amount_paid
|
||||
interest_paid = paid_amount
|
||||
|
||||
if not paid_amount:
|
||||
def validate_amount(self):
|
||||
if not self.amount_paid:
|
||||
frappe.throw(_("Amount paid cannot be zero"))
|
||||
|
||||
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)
|
||||
frappe.throw(msg)
|
||||
|
||||
def update_paid_amount(self):
|
||||
loan = frappe.get_doc("Loan", self.against_loan)
|
||||
|
||||
if self.paid_accrual_entries:
|
||||
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`
|
||||
SET is_paid = 1 where name in (%s)""" #nosec
|
||||
% ", ".join(['%s']*len(paid_entries)), tuple(paid_entries))
|
||||
for payment in self.repayment_details:
|
||||
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
||||
SET paid_principal_amount = `paid_principal_amount` + %s,
|
||||
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):
|
||||
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)
|
||||
|
||||
def mark_as_unpaid(self):
|
||||
|
||||
loan = frappe.get_doc("Loan", self.against_loan)
|
||||
|
||||
if self.paid_accrual_entries:
|
||||
paid_accrual_entries = json.loads(self.paid_accrual_entries)
|
||||
|
||||
if self.paid_accrual_entries:
|
||||
frappe.db.sql("""UPDATE `tabLoan Interest Accrual`
|
||||
SET is_paid = 0 where name in (%s)""" #nosec
|
||||
% ", ".join(['%s']*len(paid_accrual_entries)), tuple(paid_accrual_entries))
|
||||
|
||||
if self.partial_paid_entry:
|
||||
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"])
|
||||
for payment in self.repayment_details:
|
||||
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
||||
SET paid_principal_amount = `paid_principal_amount` - %s,
|
||||
paid_interest_amount = `paid_interest_amount` - %s
|
||||
WHERE name = %s""",
|
||||
(payment.paid_principal_amount, payment.paid_interest_amount, payment.loan_interest_accrual))
|
||||
|
||||
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
|
||||
WHERE name = %s """, (loan.total_amount_paid - self.amount_paid,
|
||||
@ -139,6 +103,38 @@ class LoanRepayment(AccountsController):
|
||||
if loan.status == "Loan Closure Requested":
|
||||
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):
|
||||
gle_map = []
|
||||
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,
|
||||
"applicant": applicant,
|
||||
"penalty_amount": penalty_amount,
|
||||
"interst_payable": interest_payable,
|
||||
"interest_payable": interest_payable,
|
||||
"payable_principal_amount": payable_principal_amount,
|
||||
"amount_paid": amount_paid,
|
||||
"loan_type": loan_type
|
||||
@ -232,15 +228,22 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
|
||||
return lr
|
||||
|
||||
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
|
||||
# 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
|
||||
payable_principal_amount += entry.payable_principal_amount
|
||||
|
||||
pending_accrual_entries.setdefault(entry.name,
|
||||
flt(entry.interest_amount) + flt(entry.payable_principal_amount))
|
||||
pending_accrual_entries.setdefault(entry.name, {
|
||||
'interest_amount': flt(entry.interest_amount),
|
||||
'payable_principal_amount': flt(entry.payable_principal_amount)
|
||||
})
|
||||
|
||||
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["penalty_amount"] = 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:
|
||||
amounts["due_date"] = final_due_date
|
||||
|
@ -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
|
||||
}
|
@ -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
|
@ -119,6 +119,7 @@
|
||||
"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",
|
||||
"fieldtype": "Int",
|
||||
"label": "Grace Period in Days"
|
||||
@ -142,7 +143,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-03 05:03:00.334813",
|
||||
"modified": "2020-04-15 00:24:43.259963",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Type",
|
||||
|
@ -28,7 +28,6 @@
|
||||
{
|
||||
"fieldname": "loan_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Loan Account",
|
||||
"options": "Account",
|
||||
"read_only": 1
|
||||
@ -50,21 +49,23 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Principal Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "interest_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Interest Amount",
|
||||
"options": "Company:company:default_currency"
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_payment",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Payment",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "loan_repayment_entry",
|
||||
@ -84,7 +85,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-09 20:01:53.546364",
|
||||
"modified": "2020-04-16 13:17:04.798335",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Salary Slip Loan",
|
||||
|
@ -82,7 +82,9 @@ frappe.ui.form.on('Job Card', {
|
||||
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) {
|
||||
@ -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) {
|
||||
if(frm.doc.__islocal)
|
||||
return;
|
||||
@ -205,5 +225,10 @@ frappe.ui.form.on('Job Card', {
|
||||
frappe.ui.form.on('Job Card Time Log', {
|
||||
completed_qty: function(frm) {
|
||||
frm.events.set_total_completed_qty(frm);
|
||||
},
|
||||
|
||||
to_time: function(frm) {
|
||||
frm.set_value('job_started', 0);
|
||||
frm.set_value('started_time', '');
|
||||
}
|
||||
})
|
@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.document import Document
|
||||
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
|
||||
|
||||
@ -189,11 +189,15 @@ class JobCard(Document):
|
||||
|
||||
def validate_job_card(self):
|
||||
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:
|
||||
frappe.throw(_("The total completed qty({0}) must be equal to qty to manufacture({1})"
|
||||
.format(frappe.bold(self.total_completed_qty),frappe.bold(self.for_quantity))))
|
||||
total_completed_qty = frappe.bold(_("Total Completed Qty"))
|
||||
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):
|
||||
if not self.work_order:
|
||||
|
@ -240,6 +240,8 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
}, __("Job Card"), __("Create"));
|
||||
|
||||
dialog.fields_dict["operations"].grid.wrapper.find('.grid-add-row').hide();
|
||||
|
||||
var pending_qty = 0;
|
||||
frm.doc.operations.forEach(data => {
|
||||
if(data.completed_qty != frm.doc.qty) {
|
||||
|
@ -100,7 +100,7 @@ def execute():
|
||||
|
||||
for entry in encounter_details:
|
||||
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:
|
||||
if not frappe.db.exists('Complaint', symptom):
|
||||
frappe.get_doc({
|
||||
@ -112,7 +112,7 @@ def execute():
|
||||
})
|
||||
row.db_update()
|
||||
|
||||
diagnosis = entry.diagnosis.split('\n')
|
||||
diagnosis = entry.diagnosis.split('\n') if entry.diagnosis else []
|
||||
for d in diagnosis:
|
||||
if not frappe.db.exists('Diagnosis', d):
|
||||
frappe.get_doc({
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:project_name",
|
||||
@ -435,16 +436,18 @@
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"fieldtype": "Text",
|
||||
"label": "Message"
|
||||
"label": "Message",
|
||||
"mandatory_depends_on": "collect_progress"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-puzzle-piece",
|
||||
"idx": 29,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2019-09-24 15:02:50.208301",
|
||||
"modified": "2020-04-08 22:11:14.552615",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
|
@ -379,7 +379,31 @@ 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);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cur_frm.add_fetch('project', 'cost_center', 'cost_center');
|
||||
|
@ -551,6 +551,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
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) => {
|
||||
me.frm.script_manager.trigger('qty', item.doctype, item.name);
|
||||
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.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,20 +1404,22 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
|
||||
apply_rule_on_other_items: function(args) {
|
||||
const me = this;
|
||||
const fields = ["discount_percentage", "discount_amount", "rate"];
|
||||
const fields = ["discount_percentage", "pricing_rules", "discount_amount", "rate"];
|
||||
|
||||
for(var k in args) {
|
||||
let data = args[k];
|
||||
|
||||
me.frm.doc.items.forEach(d => {
|
||||
if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) {
|
||||
for(var k in data) {
|
||||
if (in_list(fields, k)) {
|
||||
frappe.model.set_value(d.doctype, d.name, k, data[k]);
|
||||
if (data && data.apply_rule_on_other_items) {
|
||||
me.frm.doc.items.forEach(d => {
|
||||
if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) {
|
||||
for(var k in data) {
|
||||
if (in_list(fields, k) && data[k]) {
|
||||
frappe.model.set_value(d.doctype, d.name, k, data[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -436,6 +436,44 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
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;
|
||||
this.data = [];
|
||||
const fields = [{
|
||||
fieldtype:'Data',
|
||||
fieldname:"docname",
|
||||
read_only: 1,
|
||||
hidden: 1,
|
||||
}, {
|
||||
fieldtype:'Link',
|
||||
fieldname:"item_code",
|
||||
options: 'Item',
|
||||
in_list_view: 1,
|
||||
read_only: 0,
|
||||
disabled: 0,
|
||||
label: __('Item Code')
|
||||
}, {
|
||||
fieldtype:'Float',
|
||||
fieldname:"qty",
|
||||
default: 0,
|
||||
read_only: 0,
|
||||
in_list_view: 1,
|
||||
label: __('Qty')
|
||||
}, {
|
||||
fieldtype:'Currency',
|
||||
fieldname:"rate",
|
||||
default: 0,
|
||||
read_only: 0,
|
||||
in_list_view: 1,
|
||||
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: [
|
||||
@ -450,34 +488,7 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
get_data: () => {
|
||||
return this.data;
|
||||
},
|
||||
fields: [{
|
||||
fieldtype:'Data',
|
||||
fieldname:"docname",
|
||||
read_only: 1,
|
||||
hidden: 1,
|
||||
}, {
|
||||
fieldtype:'Link',
|
||||
fieldname:"item_code",
|
||||
options: 'Item',
|
||||
in_list_view: 1,
|
||||
read_only: 0,
|
||||
disabled: 0,
|
||||
label: __('Item Code')
|
||||
}, {
|
||||
fieldtype:'Float',
|
||||
fieldname:"qty",
|
||||
default: 0,
|
||||
read_only: 0,
|
||||
in_list_view: 1,
|
||||
label: __('Qty')
|
||||
}, {
|
||||
fieldtype:'Currency',
|
||||
fieldname:"rate",
|
||||
default: 0,
|
||||
read_only: 0,
|
||||
in_list_view: 1,
|
||||
label: __('Rate')
|
||||
}]
|
||||
fields: fields
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
@ -506,6 +517,8 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
"docname": d.name,
|
||||
"name": d.name,
|
||||
"item_code": d.item_code,
|
||||
"delivery_date": d.delivery_date,
|
||||
"schedule_date": d.schedule_date,
|
||||
"qty": d.qty,
|
||||
"rate": d.rate,
|
||||
});
|
||||
|
@ -150,7 +150,7 @@ class Customer(TransactionBase):
|
||||
contact.save()
|
||||
|
||||
else:
|
||||
lead.lead_name = lead.lead_name.split(" ")
|
||||
lead.lead_name = lead.lead_name.lstrip().split(" ")
|
||||
lead.first_name = lead.lead_name[0]
|
||||
lead.last_name = " ".join(lead.lead_name[1:])
|
||||
|
||||
|
@ -1176,7 +1176,7 @@ class POSCart {
|
||||
|
||||
return `
|
||||
<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">
|
||||
${item.item_name}
|
||||
</div>
|
||||
|
@ -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'])
|
||||
# locate function is used to sort by closest match from the beginning of the value
|
||||
|
||||
|
||||
result = []
|
||||
|
||||
items_data = frappe.db.sql(""" SELECT name as item_code,
|
||||
item_name, image as item_image, idx as idx,is_stock_item
|
||||
items_data = frappe.db.sql("""
|
||||
SELECT
|
||||
name AS item_code,
|
||||
item_name,
|
||||
stock_uom,
|
||||
image AS item_image,
|
||||
idx AS idx,
|
||||
is_stock_item
|
||||
FROM
|
||||
`tabItem`
|
||||
WHERE
|
||||
disabled = 0 and has_variants = 0 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}"""
|
||||
disabled = 0
|
||||
AND has_variants = 0
|
||||
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(
|
||||
start=start, page_length=page_length,
|
||||
lft=lft, rgt=rgt,
|
||||
start=start,
|
||||
page_length=page_length,
|
||||
lft=lft,
|
||||
rgt=rgt,
|
||||
condition=condition
|
||||
), as_dict=1)
|
||||
|
||||
|
@ -228,9 +228,15 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
|
||||
warehouse: function(doc, cdt, cdn) {
|
||||
var me = this;
|
||||
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) {
|
||||
item.serial_no = null;
|
||||
}
|
||||
|
||||
var has_batch_no;
|
||||
frappe.db.get_value('Item', {'item_code': item.item_code}, 'has_batch_no', (r) => {
|
||||
has_batch_no = r && r.has_batch_no;
|
||||
|
@ -434,15 +434,6 @@ class TestDeliveryNote(unittest.TestCase):
|
||||
update_delivery_note_status(dn.name, "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):
|
||||
# SO -> DN -> SI
|
||||
so = make_sales_order()
|
||||
|
@ -114,6 +114,8 @@
|
||||
"is_sub_contracted_item",
|
||||
"column_break_74",
|
||||
"customer_code",
|
||||
"default_item_manufacturer",
|
||||
"default_manufacturer_part_no",
|
||||
"website_section",
|
||||
"show_in_website",
|
||||
"show_variant_in_website",
|
||||
@ -1038,6 +1040,18 @@
|
||||
"fieldname": "auto_create_assets",
|
||||
"fieldtype": "Check",
|
||||
"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,
|
||||
@ -1046,7 +1060,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"max_attachments": 1,
|
||||
"modified": "2020-03-24 16:14:36.950677",
|
||||
"modified": "2020-04-07 15:56:06.195722",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2019-06-02 04:41:37.332911",
|
||||
"doctype": "DocType",
|
||||
@ -10,7 +11,8 @@
|
||||
"manufacturer_part_no",
|
||||
"column_break_3",
|
||||
"item_name",
|
||||
"description"
|
||||
"description",
|
||||
"is_default"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -52,9 +54,17 @@
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description",
|
||||
"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",
|
||||
"module": "Stock",
|
||||
"name": "Item Manufacturer",
|
||||
|
@ -11,6 +11,10 @@ from frappe.model.document import Document
|
||||
class ItemManufacturer(Document):
|
||||
def validate(self):
|
||||
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):
|
||||
if self.is_new():
|
||||
@ -24,6 +28,40 @@ class ItemManufacturer(Document):
|
||||
frappe.throw(_("Duplicate entry against the item code {0} and manufacturer {1}")
|
||||
.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()
|
||||
def get_item_manufacturer_part_no(item_code, manufacturer):
|
||||
return frappe.db.get_value("Item Manufacturer",
|
||||
|
@ -175,12 +175,11 @@ class MaterialRequest(BuyingController):
|
||||
|
||||
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({
|
||||
"target_dt": "Material Request Item",
|
||||
"target_parent_dt": self.doctype,
|
||||
"target_parent_field": "per_ordered",
|
||||
"target_ref_field": target_ref_field,
|
||||
"target_ref_field": "stock_qty",
|
||||
"target_field": "ordered_qty",
|
||||
"name": self.name,
|
||||
}, 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")
|
||||
|
||||
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}):
|
||||
wo_order = frappe.new_doc("Work Order")
|
||||
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))
|
||||
|
||||
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
|
||||
|
||||
|
@ -7,7 +7,8 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe, unittest, erpnext
|
||||
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
|
||||
|
||||
class TestMaterialRequest(unittest.TestCase):
|
||||
@ -15,8 +16,6 @@ class TestMaterialRequest(unittest.TestCase):
|
||||
erpnext.set_perpetual_inventory(0)
|
||||
|
||||
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()
|
||||
|
||||
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")))
|
||||
|
||||
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()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_supplier_quotation, mr.name)
|
||||
@ -45,12 +42,9 @@ class TestMaterialRequest(unittest.TestCase):
|
||||
|
||||
|
||||
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()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, make_stock_entry,
|
||||
mr.name)
|
||||
self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)
|
||||
|
||||
mr = frappe.get_doc("Material Request", mr.name)
|
||||
mr.material_request_type = "Material Transfer"
|
||||
@ -62,40 +56,40 @@ class TestMaterialRequest(unittest.TestCase):
|
||||
|
||||
def _insert_stock_entry(self, qty1, qty2, warehouse = None ):
|
||||
se = frappe.get_doc({
|
||||
"company": "_Test Company",
|
||||
"doctype": "Stock Entry",
|
||||
"posting_date": "2013-03-01",
|
||||
"posting_time": "00:00:00",
|
||||
"purpose": "Material Receipt",
|
||||
"items": [
|
||||
{
|
||||
"conversion_factor": 1.0,
|
||||
"doctype": "Stock Entry Detail",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"parentfield": "items",
|
||||
"basic_rate": 100,
|
||||
"qty": qty1,
|
||||
"stock_uom": "_Test UOM 1",
|
||||
"transfer_qty": qty1,
|
||||
"uom": "_Test UOM 1",
|
||||
"t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"conversion_factor": 1.0,
|
||||
"doctype": "Stock Entry Detail",
|
||||
"item_code": "_Test Item Home Desktop 200",
|
||||
"parentfield": "items",
|
||||
"basic_rate": 100,
|
||||
"qty": qty2,
|
||||
"stock_uom": "_Test UOM 1",
|
||||
"transfer_qty": qty2,
|
||||
"uom": "_Test UOM 1",
|
||||
"t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
]
|
||||
})
|
||||
"company": "_Test Company",
|
||||
"doctype": "Stock Entry",
|
||||
"posting_date": "2013-03-01",
|
||||
"posting_time": "00:00:00",
|
||||
"purpose": "Material Receipt",
|
||||
"items": [
|
||||
{
|
||||
"conversion_factor": 1.0,
|
||||
"doctype": "Stock Entry Detail",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"parentfield": "items",
|
||||
"basic_rate": 100,
|
||||
"qty": qty1,
|
||||
"stock_uom": "_Test UOM 1",
|
||||
"transfer_qty": qty1,
|
||||
"uom": "_Test UOM 1",
|
||||
"t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
},
|
||||
{
|
||||
"conversion_factor": 1.0,
|
||||
"doctype": "Stock Entry Detail",
|
||||
"item_code": "_Test Item Home Desktop 200",
|
||||
"parentfield": "items",
|
||||
"basic_rate": 100,
|
||||
"qty": qty2,
|
||||
"stock_uom": "_Test UOM 1",
|
||||
"transfer_qty": qty2,
|
||||
"uom": "_Test UOM 1",
|
||||
"t_warehouse": warehouse or "_Test Warehouse 1 - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC"
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
se.set_stock_entry_type()
|
||||
se.insert()
|
||||
@ -198,14 +192,7 @@ class TestMaterialRequest(unittest.TestCase):
|
||||
mr.insert()
|
||||
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
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
po_doc = make_purchase_order(mr.name)
|
||||
po_doc.supplier = "_Test Supplier"
|
||||
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_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_item2, existing_requested_qty_item2 - 3.0)
|
||||
|
||||
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
|
||||
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 54.0)
|
||||
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 3.0)
|
||||
|
||||
# map a stock entry
|
||||
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_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_item2, existing_requested_qty_item2 - 1.5)
|
||||
self.assertEqual(current_requested_qty_item1, existing_requested_qty_item1 + 27.0)
|
||||
self.assertEqual(current_requested_qty_item2, existing_requested_qty_item2 + 1.5)
|
||||
|
||||
# check if per complete is as expected for Stock Entry cancelled
|
||||
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_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_item2, existing_requested_qty_item2 - 3.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)
|
||||
|
||||
def test_completed_qty_for_over_transfer(self):
|
||||
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.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
|
||||
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
|
||||
|
||||
se_doc = make_stock_entry(mr.name)
|
||||
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_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_item2, existing_requested_qty_item2 - 3.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)
|
||||
|
||||
def test_incorrect_mapping_of_stock_entry(self):
|
||||
# submit material request of type Transfer
|
||||
@ -435,9 +413,6 @@ class TestMaterialRequest(unittest.TestCase):
|
||||
mr.insert()
|
||||
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.update({
|
||||
"posting_date": "2013-03-01",
|
||||
@ -468,8 +443,6 @@ class TestMaterialRequest(unittest.TestCase):
|
||||
mr.insert()
|
||||
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)
|
||||
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"))
|
||||
|
||||
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()
|
||||
|
||||
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",
|
||||
"warehouse": "_Test Warehouse - _TC"}, "indented_qty"))
|
||||
|
||||
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
|
||||
|
||||
existing_requested_qty = _get_requested_qty()
|
||||
|
||||
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]
|
||||
self.assertEqual(requested_qty, new_requested_qty)
|
||||
|
||||
def test_multi_uom_for_purchase(self):
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
def test_requested_qty_multi_uom(self):
|
||||
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.material_request_type = 'Purchase'
|
||||
item = mr.items[0]
|
||||
@ -607,7 +604,6 @@ class TestMaterialRequest(unittest.TestCase):
|
||||
self.assertEqual(mr.per_ordered, 100)
|
||||
|
||||
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)
|
||||
existing_requested_qty = self._get_requested_qty("_Test Customer", "_Test Warehouse - _TC")
|
||||
|
||||
@ -633,6 +629,8 @@ def make_material_request(**args):
|
||||
mr.append("items", {
|
||||
"item_code": args.item_code or "_Test Item",
|
||||
"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(),
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"cost_center": args.cost_center or "_Test Cost Center - _TC"
|
||||
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "hash",
|
||||
"creation": "2013-02-22 01:28:02",
|
||||
"doctype": "DocType",
|
||||
@ -374,7 +373,10 @@
|
||||
{
|
||||
"fieldname": "received_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Received Quantity"
|
||||
"label": "Received Quantity",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@ -404,14 +406,13 @@
|
||||
{
|
||||
"fieldname": "manufacturer_part_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Manufacturer Part Number",
|
||||
"read_only": 1
|
||||
"label": "Manufacturer Part Number"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-02-25 03:09:10.698967",
|
||||
"modified": "2020-04-16 09:00:00.992835",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Material Request Item",
|
||||
|
@ -356,8 +356,8 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
'accounts': [{
|
||||
'company_name': '_Test Company',
|
||||
'fixed_asset_account': '_Test Fixed Asset - _TC',
|
||||
'accumulated_depreciation_account': 'Depreciation - _TC',
|
||||
'depreciation_expense_account': 'Depreciation - _TC'
|
||||
'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
|
||||
'depreciation_expense_account': '_Test Depreciation - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
|
@ -801,8 +801,7 @@
|
||||
{
|
||||
"fieldname": "manufacturer_part_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Manufacturer Part Number",
|
||||
"read_only": 1
|
||||
"label": "Manufacturer Part Number"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_fixed_asset",
|
||||
@ -832,7 +831,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-03-11 14:19:48.799370",
|
||||
"modified": "2020-04-07 18:38:21.141558",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt Item",
|
||||
|
@ -220,8 +220,8 @@ frappe.ui.form.on('Stock Entry', {
|
||||
},
|
||||
get_query_filters: {
|
||||
docstatus: 1,
|
||||
material_request_type: "Material Transfer",
|
||||
status: ['!=', 'Transferred']
|
||||
material_request_type: ["in", ["Material Transfer", "Material Issue"]],
|
||||
status: ["not in", ["Transferred", "Issued"]]
|
||||
}
|
||||
})
|
||||
}, __("Get items from"));
|
||||
|
@ -341,6 +341,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
else:
|
||||
out["manufacturer_part_no"] = 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'
|
||||
meta = frappe.get_meta(child_doctype)
|
||||
|
@ -113,24 +113,30 @@ def get_reserved_qty(item_code, warehouse):
|
||||
return flt(reserved_qty[0][0]) if reserved_qty else 0
|
||||
|
||||
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)
|
||||
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
|
||||
where mr_item.item_code=%s and mr_item.warehouse=%s
|
||||
and mr.material_request_type in ('Purchase', 'Manufacture')
|
||||
and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name
|
||||
and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse))
|
||||
|
||||
outward_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
|
||||
where mr_item.item_code=%s and mr_item.warehouse=%s
|
||||
and mr.material_request_type in ('Material Issue', 'Material Transfer')
|
||||
and mr_item.qty > mr_item.ordered_qty and mr_item.parent=mr.name
|
||||
and mr.status!='Stopped' and mr.docstatus=1""", (item_code, warehouse))
|
||||
and mr.material_request_type in ('Purchase', 'Manufacture', 'Customer Provided', 'Material Transfer')
|
||||
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))
|
||||
inward_qty = flt(inward_qty[0][0]) if inward_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
|
||||
indented_qty = inward_qty - outward_qty
|
||||
outward_qty = frappe.db.sql("""
|
||||
select sum(mr_item.stock_qty - mr_item.ordered_qty)
|
||||
from `tabMaterial Request Item` mr_item, `tabMaterial Request` mr
|
||||
where mr_item.item_code=%s and mr_item.warehouse=%s
|
||||
and mr.material_request_type = 'Material Issue'
|
||||
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))
|
||||
outward_qty = flt(outward_qty[0][0]) if outward_qty else 0
|
||||
|
||||
return indented_qty
|
||||
requested_qty = inward_qty - outward_qty
|
||||
|
||||
return requested_qty
|
||||
|
||||
def get_ordered_qty(item_code, warehouse):
|
||||
ordered_qty = frappe.db.sql("""
|
||||
|
@ -16,7 +16,11 @@
|
||||
<tr>
|
||||
<td>{{ item }}</td>
|
||||
<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>
|
||||
{% for tax_account in tax_accounts %}
|
||||
{% set tax_details = taxes.get(tax_account) %}
|
||||
@ -25,7 +29,11 @@
|
||||
{% if tax_details.tax_rate or not tax_details.tax_amount %}
|
||||
({{ tax_details.tax_rate }}%)
|
||||
{% 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>
|
||||
{% else %}
|
||||
<td></td>
|
||||
|
Loading…
x
Reference in New Issue
Block a user