mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-19 13:09:17 +00:00
Merge branch 'v12-pre-release' into version-12
This commit is contained in:
@@ -5,7 +5,7 @@ import frappe
|
|||||||
from erpnext.hooks import regional_overrides
|
from erpnext.hooks import regional_overrides
|
||||||
from frappe.utils import getdate
|
from frappe.utils import getdate
|
||||||
|
|
||||||
__version__ = '12.13.0'
|
__version__ = '12.14.0'
|
||||||
|
|
||||||
def get_default_company(user=None):
|
def get_default_company(user=None):
|
||||||
'''Get default company for user'''
|
'''Get default company for user'''
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
frappe.provide('erpnext.integrations');
|
||||||
|
|
||||||
frappe.ui.form.on('Bank', {
|
frappe.ui.form.on('Bank', {
|
||||||
onload: function(frm) {
|
onload: function(frm) {
|
||||||
@@ -7,6 +8,12 @@ frappe.ui.form.on('Bank', {
|
|||||||
},
|
},
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
add_fields_to_mapping_table(frm);
|
add_fields_to_mapping_table(frm);
|
||||||
|
|
||||||
|
if (frm.doc.plaid_access_token) {
|
||||||
|
frm.add_custom_button(__('Refresh Plaid Link'), () => {
|
||||||
|
new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,3 +35,79 @@ let add_fields_to_mapping_table = function (frm) {
|
|||||||
|
|
||||||
frm.fields_dict.bank_transaction_mapping.grid.refresh();
|
frm.fields_dict.bank_transaction_mapping.grid.refresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
|
||||||
|
constructor(access_token) {
|
||||||
|
this.access_token = access_token;
|
||||||
|
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
|
||||||
|
this.init_config();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init_config() {
|
||||||
|
this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env');
|
||||||
|
this.token = await this.get_link_token_for_update();
|
||||||
|
this.init_plaid();
|
||||||
|
}
|
||||||
|
|
||||||
|
async get_link_token_for_update() {
|
||||||
|
const token = frappe.xcall(
|
||||||
|
'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update',
|
||||||
|
{ access_token: this.access_token }
|
||||||
|
)
|
||||||
|
if (!token) {
|
||||||
|
frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information'));
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
init_plaid() {
|
||||||
|
const me = this;
|
||||||
|
me.loadScript(me.plaidUrl)
|
||||||
|
.then(() => {
|
||||||
|
me.onScriptLoaded(me);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
if (me.linkHandler) {
|
||||||
|
me.linkHandler.open();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
me.onScriptError(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadScript(src) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
if (document.querySelector("script[src='" + src + "']")) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = document.createElement('script');
|
||||||
|
el.type = 'text/javascript';
|
||||||
|
el.async = true;
|
||||||
|
el.src = src;
|
||||||
|
el.addEventListener('load', resolve);
|
||||||
|
el.addEventListener('error', reject);
|
||||||
|
el.addEventListener('abort', reject);
|
||||||
|
document.head.appendChild(el);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onScriptLoaded(me) {
|
||||||
|
me.linkHandler = Plaid.create({
|
||||||
|
env: me.plaid_env,
|
||||||
|
token: me.token,
|
||||||
|
onSuccess: me.plaid_success
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onScriptError(error) {
|
||||||
|
frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
|
||||||
|
console.log(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
plaid_success(token, response) {
|
||||||
|
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class CashierClosing(Document):
|
|||||||
for i in self.payments:
|
for i in self.payments:
|
||||||
total += flt(i.amount)
|
total += flt(i.amount)
|
||||||
|
|
||||||
self.net_amount = total + self.outstanding_amount + self.expense - self.custody + self.returns
|
self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
|
||||||
|
|
||||||
def validate_time(self):
|
def validate_time(self):
|
||||||
if self.from_time >= self.time:
|
if self.from_time >= self.time:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "Prompt",
|
"autoname": "Prompt",
|
||||||
"creation": "2013-05-24 12:15:51",
|
"creation": "2013-05-24 12:15:51",
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"allow_user_to_edit_discount",
|
"allow_user_to_edit_discount",
|
||||||
"allow_print_before_pay",
|
"allow_print_before_pay",
|
||||||
"display_items_in_stock",
|
"display_items_in_stock",
|
||||||
|
"hide_unavailable_items",
|
||||||
"section_break_15",
|
"section_break_15",
|
||||||
"applicable_for_users",
|
"applicable_for_users",
|
||||||
"section_break_11",
|
"section_break_11",
|
||||||
@@ -389,11 +391,18 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Tax Category",
|
"label": "Tax Category",
|
||||||
"options": "Tax Category"
|
"options": "Tax Category"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "hide_unavailable_items",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Hide Unavailable Items"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"modified": "2020-01-24 15:52:03.797701",
|
"links": [],
|
||||||
|
"modified": "2020-10-16 04:33:57.283873",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Profile",
|
"name": "POS Profile",
|
||||||
|
|||||||
@@ -1,123 +1,39 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2017-10-27 16:46:06.060930",
|
"creation": "2017-10-27 16:46:06.060930",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"default",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "default",
|
"fieldname": "default",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Default"
|
||||||
"label": "Default",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "user",
|
"fieldname": "user",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "User",
|
"label": "User",
|
||||||
"length": 0,
|
"options": "User"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "User",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"istable": 1,
|
||||||
"hide_heading": 0,
|
"links": [],
|
||||||
"hide_toolbar": 0,
|
"modified": "2020-10-16 04:33:27.594859",
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2017-11-23 17:13:16.005475",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "POS Profile User",
|
"name": "POS Profile User",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [],
|
||||||
{
|
|
||||||
"amend": 0,
|
|
||||||
"apply_user_permissions": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -49,9 +49,10 @@ class PricingRule(Document):
|
|||||||
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
if self.apply_on == apply_on and len(self.get(field) or []) < 1:
|
||||||
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
throw(_("{0} is not added in the table").format(apply_on), frappe.MandatoryError)
|
||||||
|
|
||||||
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
if self.get("applicable_for", "") is not None:
|
||||||
if tocheck and not self.get(tocheck):
|
tocheck = frappe.scrub(self.get("applicable_for", ""))
|
||||||
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
|
if tocheck and not self.get(tocheck):
|
||||||
|
throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError)
|
||||||
|
|
||||||
if self.apply_rule_on_other:
|
if self.apply_rule_on_other:
|
||||||
o_field = 'other_' + frappe.scrub(self.apply_rule_on_other)
|
o_field = 'other_' + frappe.scrub(self.apply_rule_on_other)
|
||||||
@@ -341,8 +342,14 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
|||||||
pricing_rule_rate = 0.0
|
pricing_rule_rate = 0.0
|
||||||
if pricing_rule.currency == args.currency:
|
if pricing_rule.currency == args.currency:
|
||||||
pricing_rule_rate = pricing_rule.rate
|
pricing_rule_rate = pricing_rule.rate
|
||||||
|
|
||||||
|
if pricing_rule_rate:
|
||||||
|
# Override already set price list rate (from item price)
|
||||||
|
# if pricing_rule_rate > 0
|
||||||
|
item_details.update({
|
||||||
|
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
|
||||||
|
})
|
||||||
item_details.update({
|
item_details.update({
|
||||||
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
|
|
||||||
"discount_percentage": 0.0
|
"discount_percentage": 0.0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -430,6 +430,43 @@ class TestPricingRule(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertTrue(details)
|
self.assertTrue(details)
|
||||||
|
|
||||||
|
def test_item_price_with_pricing_rule(self):
|
||||||
|
item = make_item("Water Flask")
|
||||||
|
make_item_price("Water Flask", "_Test Price List", 100)
|
||||||
|
|
||||||
|
pricing_rule_record = {
|
||||||
|
"doctype": "Pricing Rule",
|
||||||
|
"title": "_Test Water Flask Rule",
|
||||||
|
"apply_on": "Item Code",
|
||||||
|
"items": [{
|
||||||
|
"item_code": "Water Flask",
|
||||||
|
}],
|
||||||
|
"selling": 1,
|
||||||
|
"currency": "INR",
|
||||||
|
"rate_or_discount": "Rate",
|
||||||
|
"rate": 0,
|
||||||
|
"margin_type": "Percentage",
|
||||||
|
"margin_rate_or_amount": 2,
|
||||||
|
"company": "_Test Company"
|
||||||
|
}
|
||||||
|
rule = frappe.get_doc(pricing_rule_record)
|
||||||
|
rule.insert()
|
||||||
|
|
||||||
|
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
|
||||||
|
si.selling_price_list = "_Test Price List"
|
||||||
|
si.save()
|
||||||
|
|
||||||
|
# If rate in Rule is 0, give preference to Item Price if it exists
|
||||||
|
self.assertEqual(si.items[0].price_list_rate, 100)
|
||||||
|
self.assertEqual(si.items[0].margin_rate_or_amount, 2)
|
||||||
|
self.assertEqual(si.items[0].rate_with_margin, 102)
|
||||||
|
self.assertEqual(si.items[0].rate, 102)
|
||||||
|
|
||||||
|
si.delete()
|
||||||
|
rule.delete()
|
||||||
|
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
|
||||||
|
item.delete()
|
||||||
|
|
||||||
def make_pricing_rule(**args):
|
def make_pricing_rule(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
|||||||
@@ -175,6 +175,13 @@ def get_items_list(pos_profile, company):
|
|||||||
if args_list:
|
if args_list:
|
||||||
cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list)))
|
cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list)))
|
||||||
|
|
||||||
|
bin_join = bin_cond = ""
|
||||||
|
if pos_profile.get('hide_unavailable_items'):
|
||||||
|
bin_join = ",`tabBin` b"
|
||||||
|
bin_cond = "and i.item_code = b.item_code and ifnull(b.actual_qty, 0) > 0 "
|
||||||
|
if pos_profile.get('warehouse'):
|
||||||
|
bin_cond += "and b.warehouse = {}".format(frappe.db.escape(pos_profile.get('warehouse')))
|
||||||
|
|
||||||
return frappe.db.sql("""
|
return frappe.db.sql("""
|
||||||
select
|
select
|
||||||
i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
|
i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no,
|
||||||
@@ -186,11 +193,13 @@ def get_items_list(pos_profile, company):
|
|||||||
left join `tabItem Default` id on id.parent = i.name and id.company = %s
|
left join `tabItem Default` id on id.parent = i.name and id.company = %s
|
||||||
left join `tabItem Tax` it on it.parent = i.name
|
left join `tabItem Tax` it on it.parent = i.name
|
||||||
left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom
|
left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom
|
||||||
|
{bin_join}
|
||||||
where
|
where
|
||||||
i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1
|
i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1
|
||||||
{cond}
|
{cond}
|
||||||
|
{bin_cond}
|
||||||
group by i.item_code
|
group by i.item_code
|
||||||
""".format(cond=cond), tuple([company] + args_list), as_dict=1)
|
""".format(cond=cond, bin_join=bin_join, bin_cond=bin_cond), tuple([company] + args_list), as_dict=1)
|
||||||
|
|
||||||
|
|
||||||
def get_item_groups(pos_profile):
|
def get_item_groups(pos_profile):
|
||||||
|
|||||||
@@ -570,7 +570,8 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
def validate_pos(self):
|
def validate_pos(self):
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(self.grand_total) > \
|
invoice_total = self.rounded_total or self.grand_total
|
||||||
|
if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \
|
||||||
1.0/(10.0**(self.precision("grand_total") + 1.0)):
|
1.0/(10.0**(self.precision("grand_total") + 1.0)):
|
||||||
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total"))
|
||||||
|
|
||||||
@@ -1394,6 +1395,7 @@ def make_delivery_note(source_name, target_doc=None):
|
|||||||
def set_missing_values(source, target):
|
def set_missing_values(source, target):
|
||||||
target.ignore_pricing_rule = 1
|
target.ignore_pricing_rule = 1
|
||||||
target.run_method("set_missing_values")
|
target.run_method("set_missing_values")
|
||||||
|
target.run_method("set_po_nos")
|
||||||
target.run_method("calculate_taxes_and_totals")
|
target.run_method("calculate_taxes_and_totals")
|
||||||
|
|
||||||
def update_item(source_doc, target_doc, source_parent):
|
def update_item(source_doc, target_doc, source_parent):
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
subscription = frappe.new_doc('Subscription')
|
subscription = frappe.new_doc('Subscription')
|
||||||
subscription.customer = '_Test Customer'
|
subscription.customer = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start = add_days(nowdate(), -1000)
|
||||||
subscription.days_until_due = 1
|
subscription.days_until_due = 1
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
|||||||
me.page.set_indicator(__("Online"), "green")
|
me.page.set_indicator(__("Online"), "green")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function () {
|
onload: function () {
|
||||||
@@ -278,6 +278,14 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
set_pos_profile_title(pos_profile) {
|
||||||
|
this.page.set_title_sub(
|
||||||
|
`<span class="indicator blue">
|
||||||
|
<a class="text-muted" href="#Form/POS Profile/${pos_profile}">${pos_profile}</a>
|
||||||
|
</span>`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
get_data_from_server: function (callback) {
|
get_data_from_server: function (callback) {
|
||||||
var me = this;
|
var me = this;
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@@ -286,6 +294,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
|||||||
freeze_message: __("Master data syncing, it might take some time"),
|
freeze_message: __("Master data syncing, it might take some time"),
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
localStorage.setItem('doc', JSON.stringify(r.message.doc));
|
localStorage.setItem('doc', JSON.stringify(r.message.doc));
|
||||||
|
me.set_pos_profile_title(r.message.pos_profile.name);
|
||||||
me.init_master_data(r)
|
me.init_master_data(r)
|
||||||
me.set_interval_for_si_sync();
|
me.set_interval_for_si_sync();
|
||||||
me.check_internet_connection();
|
me.check_internet_connection();
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
|
|||||||
billing_address=party_address, shipping_address=shipping_address)
|
billing_address=party_address, shipping_address=shipping_address)
|
||||||
|
|
||||||
if fetch_payment_terms_template:
|
if fetch_payment_terms_template:
|
||||||
party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company)
|
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
|
||||||
|
|
||||||
if not party_details.get("currency"):
|
if not party_details.get("currency"):
|
||||||
party_details["currency"] = currency
|
party_details["currency"] = currency
|
||||||
@@ -318,7 +318,7 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None):
|
|||||||
due_date = None
|
due_date = None
|
||||||
if (bill_date or posting_date) and party:
|
if (bill_date or posting_date) and party:
|
||||||
due_date = bill_date or posting_date
|
due_date = bill_date or posting_date
|
||||||
template_name = get_pyt_term_template(party, party_type, company)
|
template_name = get_payment_terms_template(party, party_type, company)
|
||||||
|
|
||||||
if template_name:
|
if template_name:
|
||||||
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
|
due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d")
|
||||||
@@ -425,7 +425,7 @@ def set_taxes(party, party_type, posting_date, company, customer_group=None, sup
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_pyt_term_template(party_name, party_type, company=None):
|
def get_payment_terms_template(party_name, party_type, company=None):
|
||||||
if party_type not in ("Customer", "Supplier"):
|
if party_type not in ("Customer", "Supplier"):
|
||||||
return
|
return
|
||||||
template = None
|
template = None
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ class ReceivablePayableReport(object):
|
|||||||
else:
|
else:
|
||||||
# advance / unlinked payment or other adjustment
|
# advance / unlinked payment or other adjustment
|
||||||
row.paid -= gle_balance
|
row.paid -= gle_balance
|
||||||
|
if gle.cost_center:
|
||||||
|
row.cost_center = gle.cost_center
|
||||||
|
|
||||||
def update_sub_total_row(self, row, party):
|
def update_sub_total_row(self, row, party):
|
||||||
total_row = self.total_row_map.get(party)
|
total_row = self.total_row_map.get(party)
|
||||||
@@ -210,7 +212,6 @@ class ReceivablePayableReport(object):
|
|||||||
for key, row in self.voucher_balance.items():
|
for key, row in self.voucher_balance.items():
|
||||||
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
|
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
|
||||||
row.invoice_grand_total = row.invoiced
|
row.invoice_grand_total = row.invoiced
|
||||||
|
|
||||||
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
|
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
|
||||||
# non-zero oustanding, we must consider this row
|
# non-zero oustanding, we must consider this row
|
||||||
|
|
||||||
@@ -577,7 +578,7 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
self.gl_entries = frappe.db.sql("""
|
self.gl_entries = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
name, posting_date, account, party_type, party, voucher_type, voucher_no,
|
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
||||||
against_voucher_type, against_voucher, account_currency, remarks, {0}
|
against_voucher_type, against_voucher, account_currency, remarks, {0}
|
||||||
from
|
from
|
||||||
`tabGL Entry`
|
`tabGL Entry`
|
||||||
@@ -741,6 +742,7 @@ class ReceivablePayableReport(object):
|
|||||||
self.add_column(_("Customer Contact"), fieldname='customer_primary_contact',
|
self.add_column(_("Customer Contact"), fieldname='customer_primary_contact',
|
||||||
fieldtype='Link', options='Contact')
|
fieldtype='Link', options='Contact')
|
||||||
|
|
||||||
|
self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data')
|
||||||
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
|
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
|
||||||
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
|
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
|
||||||
options='voucher_type', width=180)
|
options='voucher_type', width=180)
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ def get_accounts(company, root_type):
|
|||||||
where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True)
|
where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True)
|
||||||
|
|
||||||
|
|
||||||
def filter_accounts(accounts, depth=10):
|
def filter_accounts(accounts, depth=20):
|
||||||
parent_children_map = {}
|
parent_children_map = {}
|
||||||
accounts_by_name = {}
|
accounts_by_name = {}
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
|
|
||||||
item_code: function(frm) {
|
item_code: function(frm) {
|
||||||
if(frm.doc.item_code) {
|
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
|
||||||
frm.trigger('set_finance_book');
|
frm.trigger('set_finance_book');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -323,6 +323,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
calculate_depreciation: function(frm) {
|
calculate_depreciation: function(frm) {
|
||||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||||
|
frm.trigger('set_finance_book');
|
||||||
},
|
},
|
||||||
|
|
||||||
gross_purchase_amount: function(frm) {
|
gross_purchase_amount: function(frm) {
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Depreciation Posting Date",
|
"label": "Depreciation Posting Date",
|
||||||
|
"mandatory_depends_on": "eval:parent.doctype == 'Asset'",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -86,7 +87,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-16 12:11:30.631788",
|
"modified": "2020-10-30 15:22:29.119868",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Finance Book",
|
"name": "Asset Finance Book",
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_team_members(doctype, txt, searchfield, start, page_len, filters):
|
def get_team_members(doctype, txt, searchfield, start, page_len, filters):
|
||||||
return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") })
|
return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }, "team_member")
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_maintenance_log(asset_name):
|
def get_maintenance_log(asset_name):
|
||||||
|
|||||||
@@ -855,7 +855,7 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
|
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
|
||||||
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name
|
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -864,6 +864,10 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||||
se.submit()
|
se.submit()
|
||||||
|
|
||||||
|
# Test po_detail field has value or not
|
||||||
|
for item_row in se.items:
|
||||||
|
self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name)
|
||||||
|
|
||||||
po_doc = frappe.get_doc("Purchase Order", po.name)
|
po_doc = frappe.get_doc("Purchase Order", po.name)
|
||||||
for row in po_doc.supplied_items:
|
for row in po_doc.supplied_items:
|
||||||
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
|
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
|
||||||
"autoname": "hash",
|
"autoname": "hash",
|
||||||
"creation": "2013-05-22 12:43:10",
|
"creation": "2013-05-22 12:43:10",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@@ -237,7 +236,7 @@
|
|||||||
"fieldname": "rate",
|
"fieldname": "rate",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Rate ",
|
"label": "Rate",
|
||||||
"oldfieldname": "import_rate",
|
"oldfieldname": "import_rate",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "currency"
|
"options": "currency"
|
||||||
@@ -531,9 +530,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"modified": "2020-10-19 17:16:06.731729",
|
||||||
"modified": "2020-04-07 18:35:51.175947",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier Quotation Item",
|
"name": "Supplier Quotation Item",
|
||||||
|
|||||||
36
erpnext/change_log/v12/v12_14_0.md
Normal file
36
erpnext/change_log/v12/v12_14_0.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
## ERPNext v12.14.0 Release Note
|
||||||
|
|
||||||
|
### Fixes and Enhancements
|
||||||
|
|
||||||
|
- Incorrect backflush qty in manufacture entry ([#23878](https://github.com/frappe/erpnext/pull/23878))
|
||||||
|
- Fuel expense amount of vehicle log ([#23634](https://github.com/frappe/erpnext/pull/23634))
|
||||||
|
- Extra material received against send to warehouse entry ([#23645](https://github.com/frappe/erpnext/pull/23645))
|
||||||
|
- Show accounts in financial statements upto level 20 ([#23719](https://github.com/frappe/erpnext/pull/23719))
|
||||||
|
- Remove Production Order reference from Item Validation ([#23733](https://github.com/frappe/erpnext/pull/23733))
|
||||||
|
- Place of Supply fix in Sales Invoices ([#23786](https://github.com/frappe/erpnext/pull/23786))
|
||||||
|
- Filter Leave Type based on allocation for a particular employee ([#22050](https://github.com/frappe/erpnext/pull/22050))
|
||||||
|
- Incorrect assign to in Maintenance Schedule ([#23830](https://github.com/frappe/erpnext/pull/23830))
|
||||||
|
- Manually set serial nos override with current available serial nos ([#23651](https://github.com/frappe/erpnext/pull/23651))
|
||||||
|
- SO to PO flow improvement ([#23357](https://github.com/frappe/erpnext/pull/23357))
|
||||||
|
- Payment Terms not fetched in Purchase Invoice from Purchase Receipt ([#23866](https://github.com/frappe/erpnext/pull/23866))
|
||||||
|
- Re-linking bank accounts with plaid ([#23913](https://github.com/frappe/erpnext/pull/23913))
|
||||||
|
- Incorrect outstanding amount for multicurrency with Reverse Charge ([#23863](https://github.com/frappe/erpnext/pull/23863))
|
||||||
|
- Overproduction, not allowed to transfer extra materials ([#23647](https://github.com/frappe/erpnext/pull/23647))
|
||||||
|
- Consider rounded_total in returns ([#23631](https://github.com/frappe/erpnext/pull/23631))
|
||||||
|
- Default cost center in item master not set in stock entry ([#23816](https://github.com/frappe/erpnext/pull/23816))
|
||||||
|
- Asset finance book posting date fix ([#23780](https://github.com/frappe/erpnext/pull/23780))
|
||||||
|
- Added column cost_center to receivable reports ([#23837](https://github.com/frappe/erpnext/pull/23837))
|
||||||
|
- Override field_map for job card gantt ([#23740](https://github.com/frappe/erpnext/pull/23740))
|
||||||
|
- Added filter show in website for filtering product ([#23637](https://github.com/frappe/erpnext/pull/23637))
|
||||||
|
- Serial no field is blank in stock reconciliation ([#23646](https://github.com/frappe/erpnext/pull/23646))
|
||||||
|
- Copying po no when mapping doc ([#23730](https://github.com/frappe/erpnext/pull/23730))
|
||||||
|
- Show form buttons only if permissions exist ([#23889](https://github.com/frappe/erpnext/pull/23889))
|
||||||
|
- Cannot auto unlink payments for credit/debit notes ([#23690](https://github.com/frappe/erpnext/pull/23690))
|
||||||
|
- None type error if the Pricing Rule applicable_for is None ([#23664](https://github.com/frappe/erpnext/pull/23664))
|
||||||
|
- Don't copy terms, discount and required by from SO to PO ([#23904](https://github.com/frappe/erpnext/pull/23904))
|
||||||
|
- Add Taxes if missing via Update Items ([#23705](https://github.com/frappe/erpnext/pull/23705))
|
||||||
|
- Don't overrule Item Price via Pricing Rule Rate if 0 ([#23915](https://github.com/frappe/erpnext/pull/23915))
|
||||||
|
- Show only available items in point of sale ([#23667](https://github.com/frappe/erpnext/pull/23667))
|
||||||
|
- Auto State-wise gst tax template ([#23859](https://github.com/frappe/erpnext/pull/23859))
|
||||||
|
- Stock ageing report not working ([#23924](https://github.com/frappe/erpnext/pull/23924))
|
||||||
|
- Validate duplicate packing item in Product Bundle ([#23898](https://github.com/frappe/erpnext/pull/23898))
|
||||||
@@ -601,8 +601,6 @@ class AccountsController(TransactionBase):
|
|||||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||||
|
|
||||||
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
if self.is_return: return
|
|
||||||
|
|
||||||
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
|
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
|
||||||
unlink_ref_doc_from_payment_entries(self)
|
unlink_ref_doc_from_payment_entries(self)
|
||||||
|
|
||||||
|
|||||||
@@ -292,7 +292,7 @@ class BuyingController(StockController):
|
|||||||
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
|
# backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
|
||||||
|
|
||||||
for raw_material in transferred_raw_materials + non_stock_items:
|
for raw_material in transferred_raw_materials + non_stock_items:
|
||||||
rm_item_key = (raw_material.rm_item_code, item.purchase_order)
|
rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
|
||||||
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
|
||||||
|
|
||||||
consumed_qty = raw_material_data.get('qty', 0)
|
consumed_qty = raw_material_data.get('qty', 0)
|
||||||
@@ -881,7 +881,7 @@ def get_backflushed_subcontracted_raw_materials(purchase_orders):
|
|||||||
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
|
purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
|
||||||
|
|
||||||
for data in purchase_receipt_supplied_items:
|
for data in purchase_receipt_supplied_items:
|
||||||
pr_key = (data.rm_item_code, args[0])
|
pr_key = (data.rm_item_code, data.main_item_code, args[0])
|
||||||
if pr_key not in backflushed_raw_materials_map:
|
if pr_key not in backflushed_raw_materials_map:
|
||||||
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
|
backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
|
||||||
"qty": 0.0,
|
"qty": 0.0,
|
||||||
@@ -907,7 +907,7 @@ def get_backflushed_subcontracted_raw_materials(purchase_orders):
|
|||||||
|
|
||||||
def get_supplied_items(item_code, purchase_receipt, references):
|
def get_supplied_items(item_code, purchase_receipt, references):
|
||||||
return frappe.get_all("Purchase Receipt Item Supplied",
|
return frappe.get_all("Purchase Receipt Item Supplied",
|
||||||
fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"],
|
fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
|
||||||
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
|
filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
|
||||||
|
|
||||||
def get_asset_item_details(asset_items):
|
def get_asset_item_details(asset_items):
|
||||||
|
|||||||
@@ -365,13 +365,27 @@ class SellingController(StockController):
|
|||||||
self.make_sl_entries(sl_entries)
|
self.make_sl_entries(sl_entries)
|
||||||
|
|
||||||
def set_po_nos(self):
|
def set_po_nos(self):
|
||||||
if self.doctype in ("Delivery Note", "Sales Invoice") and hasattr(self, "items"):
|
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
|
||||||
ref_fieldname = "against_sales_order" if self.doctype == "Delivery Note" else "sales_order"
|
self.set_pos_for_sales_invoice()
|
||||||
sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
|
if self.doctype == 'Delivery Note' and hasattr(self, "items"):
|
||||||
if sales_orders:
|
self.set_pos_for_delivery_note()
|
||||||
po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)})
|
|
||||||
if po_nos and po_nos[0].get('po_no'):
|
def set_pos_for_sales_invoice(self):
|
||||||
self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no])))
|
po_nos = []
|
||||||
|
self.get_po_nos('Sales Order', 'sales_order', po_nos)
|
||||||
|
self.get_po_nos('Delivery Note', 'delivery_note', po_nos)
|
||||||
|
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
|
||||||
|
|
||||||
|
def set_pos_for_delivery_note(self):
|
||||||
|
po_nos = []
|
||||||
|
self.get_po_nos('Sales Order', 'against_sales_order', po_nos)
|
||||||
|
self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos)
|
||||||
|
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
|
||||||
|
|
||||||
|
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
|
||||||
|
doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
|
||||||
|
if doc_list:
|
||||||
|
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
|
||||||
|
|
||||||
def set_gross_profit(self):
|
def set_gross_profit(self):
|
||||||
if self.doctype == "Sales Order":
|
if self.doctype == "Sales Order":
|
||||||
|
|||||||
@@ -246,22 +246,26 @@ class StatusUpdater(Document):
|
|||||||
if not args.get("second_source_extra_cond"):
|
if not args.get("second_source_extra_cond"):
|
||||||
args["second_source_extra_cond"] = ""
|
args["second_source_extra_cond"] = ""
|
||||||
|
|
||||||
args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
|
args['second_source_condition'] = frappe.db.sql(""" select ifnull((select sum(%(second_source_field)s)
|
||||||
from `tab%(second_source_dt)s`
|
from `tab%(second_source_dt)s`
|
||||||
where `%(second_join_field)s`="%(detail_id)s"
|
where `%(second_join_field)s`="%(detail_id)s"
|
||||||
and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0) """ % args
|
and (`tab%(second_source_dt)s`.docstatus=1)
|
||||||
|
%(second_source_extra_cond)s), 0) """ % args)[0][0]
|
||||||
|
|
||||||
if args['detail_id']:
|
if args['detail_id']:
|
||||||
if not args.get("extra_cond"): args["extra_cond"] = ""
|
if not args.get("extra_cond"): args["extra_cond"] = ""
|
||||||
|
|
||||||
frappe.db.sql("""update `tab%(target_dt)s`
|
args["source_dt_value"] = frappe.db.sql("""
|
||||||
set %(target_field)s = (
|
|
||||||
(select ifnull(sum(%(source_field)s), 0)
|
(select ifnull(sum(%(source_field)s), 0)
|
||||||
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
|
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
|
||||||
and (docstatus=1 %(cond)s) %(extra_cond)s)
|
and (docstatus=1 %(cond)s) %(extra_cond)s)
|
||||||
%(second_source_condition)s
|
""" % args)[0][0] or 0.0
|
||||||
)
|
|
||||||
%(update_modified)s
|
if args['second_source_condition']:
|
||||||
|
args["source_dt_value"] += flt(args['second_source_condition'])
|
||||||
|
|
||||||
|
frappe.db.sql("""update `tab%(target_dt)s`
|
||||||
|
set %(target_field)s = %(source_dt_value)s %(update_modified)s
|
||||||
where name='%(detail_id)s'""" % args)
|
where name='%(detail_id)s'""" % args)
|
||||||
|
|
||||||
def _update_percent_field_in_targets(self, args, update_modified=True):
|
def _update_percent_field_in_targets(self, args, update_modified=True):
|
||||||
|
|||||||
@@ -531,16 +531,6 @@ class calculate_taxes_and_totals(object):
|
|||||||
self._set_in_company_currency(self.doc, ['write_off_amount'])
|
self._set_in_company_currency(self.doc, ['write_off_amount'])
|
||||||
|
|
||||||
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
grand_total = self.doc.rounded_total or self.doc.grand_total
|
|
||||||
if self.doc.party_account_currency == self.doc.currency:
|
|
||||||
total_amount_to_pay = flt(grand_total - self.doc.total_advance
|
|
||||||
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
|
|
||||||
else:
|
|
||||||
total_amount_to_pay = flt(flt(grand_total *
|
|
||||||
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
|
|
||||||
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
|
|
||||||
|
|
||||||
self.doc.round_floats_in(self.doc, ["paid_amount"])
|
|
||||||
change_amount = 0
|
change_amount = 0
|
||||||
|
|
||||||
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
|
if self.doc.doctype == "Sales Invoice" and not self.doc.get('is_return'):
|
||||||
@@ -549,14 +539,10 @@ class calculate_taxes_and_totals(object):
|
|||||||
change_amount = self.doc.change_amount \
|
change_amount = self.doc.change_amount \
|
||||||
if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount
|
if self.doc.party_account_currency == self.doc.currency else self.doc.base_change_amount
|
||||||
|
|
||||||
paid_amount = self.doc.paid_amount \
|
calculate_outstanding_amount(self.doc, change_amount)
|
||||||
if self.doc.party_account_currency == self.doc.currency else self.doc.base_paid_amount
|
|
||||||
|
|
||||||
self.doc.outstanding_amount = flt(total_amount_to_pay - flt(paid_amount) + flt(change_amount),
|
|
||||||
self.doc.precision("outstanding_amount"))
|
|
||||||
|
|
||||||
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
|
if self.doc.doctype == 'Sales Invoice' and self.doc.get('is_pos') and self.doc.get('is_return'):
|
||||||
self.update_paid_amount_for_return(total_amount_to_pay)
|
self.update_paid_amount_for_return(self.doc.total_amount_to_pay)
|
||||||
|
|
||||||
def calculate_paid_amount(self):
|
def calculate_paid_amount(self):
|
||||||
|
|
||||||
@@ -751,3 +737,20 @@ def get_rounded_tax_amount(itemised_tax, precision):
|
|||||||
for taxes in itemised_tax.values():
|
for taxes in itemised_tax.values():
|
||||||
for tax_account in taxes:
|
for tax_account in taxes:
|
||||||
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision)
|
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision)
|
||||||
|
|
||||||
|
def calculate_outstanding_amount(doc, change_amount=None):
|
||||||
|
grand_total = doc.rounded_total or doc.grand_total
|
||||||
|
if doc.party_account_currency == doc.currency:
|
||||||
|
doc.total_amount_to_pay = flt(grand_total - doc.total_advance
|
||||||
|
- flt(doc.write_off_amount), doc.precision("grand_total"))
|
||||||
|
else:
|
||||||
|
doc.total_amount_to_pay = flt(flt(grand_total *
|
||||||
|
doc.conversion_rate, doc.precision("grand_total")) - doc.total_advance
|
||||||
|
- flt(doc.base_write_off_amount), doc.precision("grand_total"))
|
||||||
|
|
||||||
|
doc.round_floats_in(doc, ["paid_amount"])
|
||||||
|
paid_amount = doc.paid_amount \
|
||||||
|
if doc.party_account_currency == doc.currency else doc.base_paid_amount
|
||||||
|
|
||||||
|
doc.outstanding_amount = flt(doc.total_amount_to_pay - flt(paid_amount) + flt(change_amount),
|
||||||
|
doc.precision("outstanding_amount"))
|
||||||
|
|||||||
@@ -30,20 +30,31 @@ class PlaidConnector():
|
|||||||
access_token = response["access_token"]
|
access_token = response["access_token"]
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
def get_link_token(self):
|
def get_token_request(self, update_mode=False):
|
||||||
token_request = {
|
args = {
|
||||||
"client_name": self.client_name,
|
"client_name": self.client_name,
|
||||||
"client_id": self.settings.plaid_client_id,
|
|
||||||
"secret": self.settings.plaid_secret,
|
|
||||||
"products": self.products,
|
|
||||||
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
|
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
|
||||||
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
|
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
|
||||||
"country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
|
"country_codes": ["US", "CA", "ES", "FR", "GB", "IE", "NL"],
|
||||||
"user": {
|
"user": {
|
||||||
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
|
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if update_mode:
|
||||||
|
args["access_token"] = self.access_token
|
||||||
|
else:
|
||||||
|
args.update({
|
||||||
|
"client_id": self.settings.plaid_client_id,
|
||||||
|
"secret": self.settings.plaid_secret,
|
||||||
|
"products": self.products,
|
||||||
|
})
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
|
def get_link_token(self, update_mode=False):
|
||||||
|
token_request = self.get_token_request(update_mode)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.client.LinkToken.create(token_request)
|
response = self.client.LinkToken.create(token_request)
|
||||||
except InvalidRequestError:
|
except InvalidRequestError:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ frappe.ui.form.on('Plaid Settings', {
|
|||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
if (frm.doc.enabled) {
|
if (frm.doc.enabled) {
|
||||||
frm.add_custom_button('Link a new bank account', () => {
|
frm.add_custom_button(__('Link a new bank account'), () => {
|
||||||
new erpnext.integrations.plaidLink(frm);
|
new erpnext.integrations.plaidLink(frm);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -30,10 +30,18 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
this.product = ["auth", "transactions"];
|
this.product = ["auth", "transactions"];
|
||||||
this.plaid_env = this.frm.doc.plaid_env;
|
this.plaid_env = this.frm.doc.plaid_env;
|
||||||
this.client_name = frappe.boot.sitename;
|
this.client_name = frappe.boot.sitename;
|
||||||
this.token = await this.frm.call("get_link_token").then(resp => resp.message);
|
this.token = await this.get_link_token();
|
||||||
this.init_plaid();
|
this.init_plaid();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async get_link_token() {
|
||||||
|
const token = await this.frm.call("get_link_token").then(resp => resp.message);
|
||||||
|
if (!token) {
|
||||||
|
frappe.throw(__('Cannot retrieve link token. Check Error Log for more information'));
|
||||||
|
}
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
init_plaid() {
|
init_plaid() {
|
||||||
const me = this;
|
const me = this;
|
||||||
me.loadScript(me.plaidUrl)
|
me.loadScript(me.plaidUrl)
|
||||||
@@ -78,8 +86,8 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onScriptError(error) {
|
onScriptError(error) {
|
||||||
frappe.msgprint("There was an issue connecting to Plaid's authentication server");
|
frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
|
||||||
frappe.msgprint(error);
|
console.log(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
plaid_success(token, response) {
|
plaid_success(token, response) {
|
||||||
|
|||||||
@@ -239,3 +239,8 @@ def automatic_synchronization():
|
|||||||
bank=plaid_account.bank,
|
bank=plaid_account.bank,
|
||||||
bank_account=plaid_account.name
|
bank_account=plaid_account.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_link_token_for_update(access_token):
|
||||||
|
plaid = PlaidConnector(access_token)
|
||||||
|
return plaid.get_link_token(update_mode=True)
|
||||||
@@ -46,6 +46,7 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
|
|
||||||
make_dashboard: function(frm) {
|
make_dashboard: function(frm) {
|
||||||
var leave_details;
|
var leave_details;
|
||||||
|
let lwps;
|
||||||
if (frm.doc.employee) {
|
if (frm.doc.employee) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
|
method: "erpnext.hr.doctype.leave_application.leave_application.get_leave_details",
|
||||||
@@ -61,6 +62,7 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
if (!r.exc && r.message['leave_approver']) {
|
if (!r.exc && r.message['leave_approver']) {
|
||||||
frm.set_value('leave_approver', r.message['leave_approver']);
|
frm.set_value('leave_approver', r.message['leave_approver']);
|
||||||
}
|
}
|
||||||
|
lwps = r.message["lwps"];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$("div").remove(".form-dashboard-section");
|
$("div").remove(".form-dashboard-section");
|
||||||
@@ -70,6 +72,18 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
frm.dashboard.show();
|
frm.dashboard.show();
|
||||||
|
let allowed_leave_types = Object.keys(leave_details);
|
||||||
|
|
||||||
|
// lwps should be allowed, lwps don't have any allocation
|
||||||
|
allowed_leave_types = allowed_leave_types.concat(lwps);
|
||||||
|
|
||||||
|
frm.set_query('leave_type', function(){
|
||||||
|
return {
|
||||||
|
filters : [
|
||||||
|
['leave_type_name', 'in', allowed_leave_types]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class NotAnOptionalHoliday(frappe.ValidationError): pass
|
|||||||
|
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
class LeaveApplication(Document):
|
class LeaveApplication(Document):
|
||||||
|
|
||||||
def get_feed(self):
|
def get_feed(self):
|
||||||
return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type)
|
return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type)
|
||||||
|
|
||||||
@@ -451,9 +450,14 @@ def get_leave_details(employee, date):
|
|||||||
"pending_leaves": leaves_pending,
|
"pending_leaves": leaves_pending,
|
||||||
"remaining_leaves": remaining_leaves}
|
"remaining_leaves": remaining_leaves}
|
||||||
|
|
||||||
|
#is used in set query
|
||||||
|
lwps = frappe.get_list("Leave Type", filters = {"is_lwp": 1})
|
||||||
|
lwps = [lwp.name for lwp in lwps]
|
||||||
|
|
||||||
ret = {
|
ret = {
|
||||||
'leave_allocation': leave_allocation,
|
'leave_allocation': leave_allocation,
|
||||||
'leave_approver': get_leave_approver(employee)
|
'leave_approver': get_leave_approver(employee),
|
||||||
|
'lwps': lwps
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
{% if data %}
|
{% if not jQuery.isEmptyObject(data) %}
|
||||||
<h5 style="margin-top: 20px;"> {{ __("Allocated Leaves") }} </h5>
|
<h5 style="margin-top: 20px;"> {{ __("Allocated Leaves") }} </h5>
|
||||||
<table class="table table-bordered small">
|
<table class="table table-bordered small">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -11,7 +11,6 @@
|
|||||||
<th style="width: 16%" class="text-right">{{ __("Pending Leaves") }}</th>
|
<th style="width: 16%" class="text-right">{{ __("Pending Leaves") }}</th>
|
||||||
<th style="width: 16%" class="text-right">{{ __("Available Leaves") }}</th>
|
<th style="width: 16%" class="text-right">{{ __("Available Leaves") }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for(const [key, value] of Object.entries(data)) { %}
|
{% for(const [key, value] of Object.entries(data)) { %}
|
||||||
@@ -26,6 +25,6 @@
|
|||||||
{% } %}
|
{% } %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% } else { %}
|
{% else %}
|
||||||
<p style="margin-top: 30px;"> No Leaves have been allocated. </p>
|
<p style="margin-top: 30px;"> No Leaves have been allocated. </p>
|
||||||
{% } %}
|
{% endif %}
|
||||||
@@ -95,7 +95,11 @@ class LeaveEncashment(Document):
|
|||||||
create_leave_ledger_entry(self, args, submit)
|
create_leave_ledger_entry(self, args, submit)
|
||||||
|
|
||||||
# create reverse entry for expired leaves
|
# create reverse entry for expired leaves
|
||||||
to_date = self.get_leave_allocation().get('to_date')
|
leave_allocation = self.get_leave_allocation()
|
||||||
|
if not leave_allocation:
|
||||||
|
return
|
||||||
|
|
||||||
|
to_date = leave_allocation.get('to_date')
|
||||||
if to_date < getdate(nowdate()):
|
if to_date < getdate(nowdate()):
|
||||||
args = frappe._dict(
|
args = frappe._dict(
|
||||||
leaves=self.encashable_days,
|
leaves=self.encashable_days,
|
||||||
|
|||||||
@@ -6,18 +6,28 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
import unittest
|
import unittest
|
||||||
from frappe.utils import nowdate,flt, cstr,random_string
|
from frappe.utils import nowdate,flt, cstr,random_string
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
|
||||||
|
|
||||||
class TestVehicleLog(unittest.TestCase):
|
class TestVehicleLog(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
employee_id = frappe.db.sql("""select name from `tabEmployee` where name='testdriver@example.com'""")
|
||||||
|
self.employee_id = employee_id[0][0] if employee_id else None
|
||||||
|
|
||||||
|
if not self.employee_id:
|
||||||
|
self.employee_id = make_employee("testdriver@example.com", company="_Test Company")
|
||||||
|
|
||||||
|
self.license_plate = get_vehicle(self.employee_id)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.delete_doc("Vehicle", self.license_plate, force=1)
|
||||||
|
frappe.delete_doc("Employee", self.employee_id, force=1)
|
||||||
|
|
||||||
def test_make_vehicle_log_and_syncing_of_odometer_value(self):
|
def test_make_vehicle_log_and_syncing_of_odometer_value(self):
|
||||||
employee_id = frappe.db.sql("""select name from `tabEmployee` where status='Active' order by modified desc limit 1""")
|
|
||||||
employee_id = employee_id[0][0] if employee_id else None
|
|
||||||
|
|
||||||
license_plate = get_vehicle(employee_id)
|
|
||||||
|
|
||||||
vehicle_log = frappe.get_doc({
|
vehicle_log = frappe.get_doc({
|
||||||
"doctype": "Vehicle Log",
|
"doctype": "Vehicle Log",
|
||||||
"license_plate": cstr(license_plate),
|
"license_plate": cstr(self.license_plate),
|
||||||
"employee":employee_id,
|
"employee": self.employee_id,
|
||||||
"date":frappe.utils.nowdate(),
|
"date":frappe.utils.nowdate(),
|
||||||
"odometer":5010,
|
"odometer":5010,
|
||||||
"fuel_qty":frappe.utils.flt(50),
|
"fuel_qty":frappe.utils.flt(50),
|
||||||
@@ -27,7 +37,7 @@ class TestVehicleLog(unittest.TestCase):
|
|||||||
vehicle_log.submit()
|
vehicle_log.submit()
|
||||||
|
|
||||||
#checking value of vehicle odometer value on submit.
|
#checking value of vehicle odometer value on submit.
|
||||||
vehicle = frappe.get_doc("Vehicle", license_plate)
|
vehicle = frappe.get_doc("Vehicle", self.license_plate)
|
||||||
self.assertEqual(vehicle.last_odometer, vehicle_log.odometer)
|
self.assertEqual(vehicle.last_odometer, vehicle_log.odometer)
|
||||||
|
|
||||||
#checking value vehicle odometer on vehicle log cancellation.
|
#checking value vehicle odometer on vehicle log cancellation.
|
||||||
@@ -40,6 +50,28 @@ class TestVehicleLog(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled)
|
self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled)
|
||||||
|
|
||||||
|
vehicle_log.delete()
|
||||||
|
|
||||||
|
def test_vehicle_log_fuel_expense(self):
|
||||||
|
vehicle_log = frappe.get_doc({
|
||||||
|
"doctype": "Vehicle Log",
|
||||||
|
"license_plate": cstr(self.license_plate),
|
||||||
|
"employee": self.employee_id,
|
||||||
|
"date": frappe.utils.nowdate(),
|
||||||
|
"odometer":5010,
|
||||||
|
"fuel_qty":frappe.utils.flt(50),
|
||||||
|
"price": frappe.utils.flt(500)
|
||||||
|
})
|
||||||
|
vehicle_log.save()
|
||||||
|
vehicle_log.submit()
|
||||||
|
|
||||||
|
expense_claim = make_expense_claim(vehicle_log.name)
|
||||||
|
fuel_expense = expense_claim.expenses[0].amount
|
||||||
|
self.assertEqual(fuel_expense, 50*500)
|
||||||
|
|
||||||
|
vehicle_log.cancel()
|
||||||
|
frappe.delete_doc("Expense Claim", expense_claim.name)
|
||||||
|
frappe.delete_doc("Vehicle Log", vehicle_log.name)
|
||||||
|
|
||||||
def get_vehicle(employee_id):
|
def get_vehicle(employee_id):
|
||||||
license_plate=random_string(10).upper()
|
license_plate=random_string(10).upper()
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def make_expense_claim(docname):
|
|||||||
vehicle_log = frappe.get_doc("Vehicle Log", docname)
|
vehicle_log = frappe.get_doc("Vehicle Log", docname)
|
||||||
service_expense = sum([flt(d.expense_amount) for d in vehicle_log.service_detail])
|
service_expense = sum([flt(d.expense_amount) for d in vehicle_log.service_detail])
|
||||||
|
|
||||||
claim_amount = service_expense + flt(vehicle_log.price)
|
claim_amount = service_expense + (flt(vehicle_log.price) * flt(vehicle_log.fuel_qty) or 1)
|
||||||
if not claim_amount:
|
if not claim_amount:
|
||||||
frappe.throw(_("No additional expenses has been added"))
|
frappe.throw(_("No additional expenses has been added"))
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,17 @@ frappe.views.calendar["Job Card"] = {
|
|||||||
"allDay": "allDay",
|
"allDay": "allDay",
|
||||||
"progress": "progress"
|
"progress": "progress"
|
||||||
},
|
},
|
||||||
gantt: true,
|
gantt: {
|
||||||
|
field_map: {
|
||||||
|
"start": "started_time",
|
||||||
|
"end": "started_time",
|
||||||
|
"id": "name",
|
||||||
|
"title": "subject",
|
||||||
|
"color": "color",
|
||||||
|
"allDay": "allDay",
|
||||||
|
"progress": "progress"
|
||||||
|
}
|
||||||
|
},
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
|||||||
@@ -709,8 +709,12 @@ def get_items_for_material_requests(doc, ignore_existing_ordered_qty=None):
|
|||||||
mr_items.append(items)
|
mr_items.append(items)
|
||||||
|
|
||||||
if not mr_items:
|
if not mr_items:
|
||||||
frappe.msgprint(_("""As raw materials projected quantity is more than required quantity, there is no need to create material request.
|
to_enable = frappe.bold(_("Ignore Existing Projected Quantity"))
|
||||||
Still if you want to make material request, kindly enable <b>Ignore Existing Projected Quantity</b> checkbox"""))
|
warehouse = frappe.bold(doc.get('for_warehouse'))
|
||||||
|
message = _("As there are sufficient raw materials, Material Request is not required for Warehouse {0}.").format(warehouse) + "<br><br>"
|
||||||
|
message += _(" If you still want to proceed, please enable {0}.").format(to_enable)
|
||||||
|
|
||||||
|
frappe.msgprint(message, title=_("Note"))
|
||||||
|
|
||||||
return mr_items
|
return mr_items
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,42 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
self.assertEqual(cint(bin1_on_end_production.projected_qty),
|
self.assertEqual(cint(bin1_on_end_production.projected_qty),
|
||||||
cint(bin1_on_end_production.projected_qty))
|
cint(bin1_on_end_production.projected_qty))
|
||||||
|
|
||||||
|
def test_backflush_qty_for_overpduction_manufacture(self):
|
||||||
|
cancel_stock_entry = []
|
||||||
|
allow_overproduction("overproduction_percentage_for_work_order", 30)
|
||||||
|
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=100)
|
||||||
|
ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
|
||||||
|
target="_Test Warehouse - _TC", qty=120, basic_rate=5000.0)
|
||||||
|
ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
|
||||||
|
target="_Test Warehouse - _TC", qty=240, basic_rate=1000.0)
|
||||||
|
|
||||||
|
cancel_stock_entry.extend([ste1.name, ste2.name])
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60))
|
||||||
|
s.submit()
|
||||||
|
cancel_stock_entry.append(s.name)
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 60))
|
||||||
|
s.submit()
|
||||||
|
cancel_stock_entry.append(s.name)
|
||||||
|
|
||||||
|
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 60))
|
||||||
|
s.submit()
|
||||||
|
cancel_stock_entry.append(s.name)
|
||||||
|
|
||||||
|
s1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 50))
|
||||||
|
s1.submit()
|
||||||
|
cancel_stock_entry.append(s1.name)
|
||||||
|
|
||||||
|
self.assertEqual(s1.items[0].qty, 50)
|
||||||
|
self.assertEqual(s1.items[1].qty, 100)
|
||||||
|
cancel_stock_entry.reverse()
|
||||||
|
for ste in cancel_stock_entry:
|
||||||
|
doc = frappe.get_doc("Stock Entry", ste)
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
|
allow_overproduction("overproduction_percentage_for_work_order", 0)
|
||||||
|
|
||||||
def test_reserved_qty_for_stopped_production(self):
|
def test_reserved_qty_for_stopped_production(self):
|
||||||
test_stock_entry.make_stock_entry(item_code="_Test Item",
|
test_stock_entry.make_stock_entry(item_code="_Test Item",
|
||||||
target= self.warehouse, qty=100, basic_rate=100)
|
target= self.warehouse, qty=100, basic_rate=100)
|
||||||
@@ -371,6 +407,11 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
|
ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
|
||||||
self.assertEqual(len(ste1.items), 3)
|
self.assertEqual(len(ste1.items), 3)
|
||||||
|
|
||||||
|
def test_cost_center_for_manufacture(self):
|
||||||
|
wo_order = make_wo_order_test_record()
|
||||||
|
ste = make_stock_entry(wo_order.name, "Material Transfer for Manufacture", wo_order.qty)
|
||||||
|
self.assertEquals(ste.get("items")[0].get("cost_center"), "_Test Cost Center - _TC")
|
||||||
|
|
||||||
def test_operation_time_with_batch_size(self):
|
def test_operation_time_with_batch_size(self):
|
||||||
fg_item = "Test Batch Size Item For BOM"
|
fg_item = "Test Batch Size Item For BOM"
|
||||||
rm1 = "Test Batch Size Item RM 1 For BOM"
|
rm1 = "Test Batch Size Item RM 1 For BOM"
|
||||||
|
|||||||
@@ -635,7 +635,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart')
|
|||||||
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field')
|
execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field')
|
||||||
erpnext.patches.v12_0.add_default_dashboards
|
erpnext.patches.v12_0.add_default_dashboards
|
||||||
erpnext.patches.v12_0.remove_bank_remittance_custom_fields
|
erpnext.patches.v12_0.remove_bank_remittance_custom_fields
|
||||||
erpnext.patches.v12_0.generate_leave_ledger_entries
|
erpnext.patches.v12_0.generate_leave_ledger_entries #04-11-2020
|
||||||
erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit
|
erpnext.patches.v12_0.move_credit_limit_to_customer_credit_limit
|
||||||
erpnext.patches.v12_0.add_variant_of_in_item_attribute_table
|
erpnext.patches.v12_0.add_variant_of_in_item_attribute_table
|
||||||
erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account
|
erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ def execute():
|
|||||||
frappe.reload_doc("HR", "doctype", "Leave Ledger Entry")
|
frappe.reload_doc("HR", "doctype", "Leave Ledger Entry")
|
||||||
frappe.reload_doc("HR", "doctype", "Leave Encashment")
|
frappe.reload_doc("HR", "doctype", "Leave Encashment")
|
||||||
frappe.reload_doc("HR", "doctype", "Leave Type")
|
frappe.reload_doc("HR", "doctype", "Leave Type")
|
||||||
if frappe.db.a_row_exists("Leave Ledger Entry"):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"):
|
if not frappe.get_meta("Leave Allocation").has_field("unused_leaves"):
|
||||||
frappe.reload_doc("HR", "doctype", "Leave Allocation")
|
frappe.reload_doc("HR", "doctype", "Leave Allocation")
|
||||||
@@ -36,8 +34,7 @@ def generate_allocation_ledger_entries():
|
|||||||
|
|
||||||
for allocation in allocation_list:
|
for allocation in allocation_list:
|
||||||
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}):
|
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name}):
|
||||||
allocation.update(dict(doctype="Leave Allocation"))
|
allocation_obj = frappe.get_doc("Leave Allocation", allocation)
|
||||||
allocation_obj = frappe.get_doc(allocation)
|
|
||||||
allocation_obj.create_leave_ledger_entry()
|
allocation_obj.create_leave_ledger_entry()
|
||||||
|
|
||||||
def generate_application_leave_ledger_entries():
|
def generate_application_leave_ledger_entries():
|
||||||
@@ -46,8 +43,7 @@ def generate_application_leave_ledger_entries():
|
|||||||
|
|
||||||
for application in leave_applications:
|
for application in leave_applications:
|
||||||
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}):
|
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Application', 'transaction_name': application.name}):
|
||||||
application.update(dict(doctype="Leave Application"))
|
frappe.get_doc("Leave Application", application.name).create_leave_ledger_entry()
|
||||||
frappe.get_doc(application).create_leave_ledger_entry()
|
|
||||||
|
|
||||||
def generate_encashment_leave_ledger_entries():
|
def generate_encashment_leave_ledger_entries():
|
||||||
''' fix ledger entries for missing leave encashment transaction '''
|
''' fix ledger entries for missing leave encashment transaction '''
|
||||||
@@ -55,8 +51,7 @@ def generate_encashment_leave_ledger_entries():
|
|||||||
|
|
||||||
for encashment in leave_encashments:
|
for encashment in leave_encashments:
|
||||||
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}):
|
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}):
|
||||||
encashment.update(dict(doctype="Leave Encashment"))
|
frappe.get_doc("Leave Encashment", encashment).create_leave_ledger_entry()
|
||||||
frappe.get_doc(encashment).create_leave_ledger_entry()
|
|
||||||
|
|
||||||
def generate_expiry_allocation_ledger_entries():
|
def generate_expiry_allocation_ledger_entries():
|
||||||
''' fix ledger entries for missing leave allocation transaction '''
|
''' fix ledger entries for missing leave allocation transaction '''
|
||||||
@@ -65,24 +60,16 @@ def generate_expiry_allocation_ledger_entries():
|
|||||||
|
|
||||||
for allocation in allocation_list:
|
for allocation in allocation_list:
|
||||||
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}):
|
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Allocation', 'transaction_name': allocation.name, 'is_expired': 1}):
|
||||||
allocation.update(dict(doctype="Leave Allocation"))
|
allocation_obj = frappe.get_doc("Leave Allocation", allocation)
|
||||||
allocation_obj = frappe.get_doc(allocation)
|
|
||||||
if allocation_obj.to_date <= getdate(today()):
|
if allocation_obj.to_date <= getdate(today()):
|
||||||
expire_allocation(allocation_obj)
|
expire_allocation(allocation_obj)
|
||||||
|
|
||||||
def get_allocation_records():
|
def get_allocation_records():
|
||||||
return frappe.get_all("Leave Allocation", filters={
|
return frappe.get_all("Leave Allocation", filters={"docstatus": 1},
|
||||||
"docstatus": 1
|
fields=['name'], order_by='to_date ASC')
|
||||||
}, fields=['name', 'employee', 'leave_type', 'new_leaves_allocated',
|
|
||||||
'unused_leaves', 'from_date', 'to_date', 'carry_forward'
|
|
||||||
], order_by='to_date ASC')
|
|
||||||
|
|
||||||
def get_leaves_application_records():
|
def get_leaves_application_records():
|
||||||
return frappe.get_all("Leave Application", filters={
|
return frappe.get_all("Leave Application", filters={"docstatus": 1}, fields=['name'])
|
||||||
"docstatus": 1
|
|
||||||
}, fields=['name', 'employee', 'leave_type', 'total_leave_days', 'from_date', 'to_date'])
|
|
||||||
|
|
||||||
def get_leave_encashment_records():
|
def get_leave_encashment_records():
|
||||||
return frappe.get_all("Leave Encashment", filters={
|
return frappe.get_all("Leave Encashment", filters={"docstatus": 1}, fields=['name'])
|
||||||
"docstatus": 1
|
|
||||||
}, fields=['name', 'employee', 'leave_type', 'encashable_days', 'encashment_date'])
|
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ def get_timesheet_details(filters, timesheet_list):
|
|||||||
return timesheet_details_map
|
return timesheet_details_map
|
||||||
|
|
||||||
def get_billable_and_total_duration(activity, start_time, end_time):
|
def get_billable_and_total_duration(activity, start_time, end_time):
|
||||||
|
precision = frappe.get_precision("Timesheet Detail", "hours")
|
||||||
activity_duration = time_diff_in_hours(end_time, start_time)
|
activity_duration = time_diff_in_hours(end_time, start_time)
|
||||||
billing_duration = 0.0
|
billing_duration = 0.0
|
||||||
if activity.billable:
|
if activity.billable:
|
||||||
@@ -143,4 +144,4 @@ def get_billable_and_total_duration(activity, start_time, end_time):
|
|||||||
if activity_duration != activity.billing_hours:
|
if activity_duration != activity.billing_hours:
|
||||||
billing_duration = activity_duration * activity.billing_hours / activity.hours
|
billing_duration = activity_duration * activity.billing_hours / activity.hours
|
||||||
|
|
||||||
return flt(activity_duration, 2), flt(billing_duration, 2)
|
return flt(activity_duration, precision), flt(billing_duration, precision)
|
||||||
@@ -4,7 +4,7 @@ from frappe import _
|
|||||||
import erpnext
|
import erpnext
|
||||||
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
|
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
|
||||||
from erpnext.regional.india import states, state_numbers
|
from erpnext.regional.india import states, state_numbers
|
||||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
|
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount
|
||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||||
from erpnext.hr.utils import get_salary_assignment
|
from erpnext.hr.utils import get_salary_assignment
|
||||||
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
|
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
|
||||||
@@ -139,7 +139,7 @@ def get_place_of_supply(party_details, doctype):
|
|||||||
if not frappe.get_meta('Address').has_field('gst_state'): return
|
if not frappe.get_meta('Address').has_field('gst_state'): return
|
||||||
|
|
||||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||||
address_name = party_details.shipping_address_name or party_details.customer_address
|
address_name = party_details.customer_address or party_details.shipping_address_name
|
||||||
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
||||||
address_name = party_details.shipping_address or party_details.supplier_address
|
address_name = party_details.shipping_address or party_details.supplier_address
|
||||||
|
|
||||||
@@ -218,10 +218,9 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
|
|||||||
|
|
||||||
for tax_category in tax_categories:
|
for tax_category in tax_categories:
|
||||||
if tax_category.gst_state == number_state_mapping[state_code] or \
|
if tax_category.gst_state == number_state_mapping[state_code] or \
|
||||||
(not default_tax and not tax_category.gst_state):
|
(not default_tax and not tax_category.gst_state):
|
||||||
default_tax = frappe.db.get_value(master_doctype,
|
default_tax = frappe.db.get_value(master_doctype,
|
||||||
{'disabled': 0, 'tax_category': tax_category.name}, 'name')
|
{'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name')
|
||||||
|
|
||||||
return default_tax
|
return default_tax
|
||||||
|
|
||||||
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
|
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
|
||||||
@@ -690,16 +689,14 @@ def update_totals(gst_tax, base_gst_tax, doc):
|
|||||||
doc.grand_total -= gst_tax
|
doc.grand_total -= gst_tax
|
||||||
|
|
||||||
if doc.meta.get_field("rounded_total"):
|
if doc.meta.get_field("rounded_total"):
|
||||||
if doc.is_rounded_total_disabled():
|
if not doc.is_rounded_total_disabled():
|
||||||
doc.outstanding_amount = doc.grand_total
|
|
||||||
else:
|
|
||||||
doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
|
doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
|
||||||
doc.currency, doc.precision("rounded_total"))
|
doc.currency, doc.precision("rounded_total"))
|
||||||
|
|
||||||
doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
|
doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
|
||||||
doc.precision("rounding_adjustment"))
|
doc.precision("rounding_adjustment"))
|
||||||
|
|
||||||
doc.outstanding_amount = doc.rounded_total or doc.grand_total
|
calculate_outstanding_amount(doc)
|
||||||
|
|
||||||
doc.in_words = money_in_words(doc.grand_total, doc.currency)
|
doc.in_words = money_in_words(doc.grand_total, doc.currency)
|
||||||
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
|
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ProductBundle(Document):
|
|||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_main_item()
|
self.validate_main_item()
|
||||||
self.validate_child_items()
|
self.validate_child_items()
|
||||||
|
self.validate_duplicate_packing_item()
|
||||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||||
validate_uom_is_integer(self, "uom", "qty")
|
validate_uom_is_integer(self, "uom", "qty")
|
||||||
|
|
||||||
@@ -28,6 +29,14 @@ class ProductBundle(Document):
|
|||||||
if frappe.db.exists("Product Bundle", item.item_code):
|
if frappe.db.exists("Product Bundle", item.item_code):
|
||||||
frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code)))
|
frappe.throw(_("Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save").format(item.idx, frappe.bold(item.item_code)))
|
||||||
|
|
||||||
|
def validate_duplicate_packing_item(self):
|
||||||
|
items = []
|
||||||
|
for d in self.items:
|
||||||
|
if d.item_code not in items:
|
||||||
|
items.append(d.item_code)
|
||||||
|
else:
|
||||||
|
frappe.throw(_("The item {0} added multiple times")
|
||||||
|
.format(frappe.bold(d.item_code)), title=_("Duplicate Item Error"))
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
|
|
||||||
// sales invoice
|
// sales invoice
|
||||||
if(flt(doc.per_billed, 6) < 100) {
|
if(flt(doc.per_billed, 6) < 100) {
|
||||||
this.frm.add_custom_button(__('Invoice'), () => me.make_sales_invoice(), __('Create'));
|
this.frm.add_custom_button(__('Sales Invoice'), () => me.make_sales_invoice(), __('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// material request
|
// material request
|
||||||
@@ -542,19 +542,26 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
},
|
},
|
||||||
|
|
||||||
make_purchase_order: function(){
|
make_purchase_order: function(){
|
||||||
|
let pending_items = this.frm.doc.items.some((item) =>{
|
||||||
|
let pending_qty = flt(item.stock_qty) - flt(item.ordered_qty);
|
||||||
|
return pending_qty > 0;
|
||||||
|
})
|
||||||
|
if(!pending_items){
|
||||||
|
frappe.throw({message: __("Purchase Order already created for all Sales Order items"), title: __("Note")});
|
||||||
|
}
|
||||||
|
|
||||||
var me = this;
|
var me = this;
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("For Supplier"),
|
title: __("Select Items"),
|
||||||
fields: [
|
fields: [
|
||||||
{"fieldtype": "Link", "label": __("Supplier"), "fieldname": "supplier", "options":"Supplier",
|
{
|
||||||
"description": __("Leave the field empty to make purchase orders for all suppliers"),
|
"fieldtype": "Check",
|
||||||
"get_query": function () {
|
"label": __("Against Default Supplier"),
|
||||||
return {
|
"fieldname": "against_default_supplier",
|
||||||
query:"erpnext.selling.doctype.sales_order.sales_order.get_supplier",
|
"default": 0
|
||||||
filters: {'parent': me.frm.doc.name}
|
},
|
||||||
}
|
{
|
||||||
}},
|
fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items',
|
||||||
{fieldname: 'items_for_po', fieldtype: 'Table', label: 'Select Items',
|
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldtype:'Data',
|
fieldtype:'Data',
|
||||||
@@ -572,8 +579,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype:'Float',
|
fieldtype:'Float',
|
||||||
fieldname:'qty',
|
fieldname:'pending_qty',
|
||||||
label: __('Quantity'),
|
label: __('Pending Qty'),
|
||||||
read_only: 1,
|
read_only: 1,
|
||||||
in_list_view:1
|
in_list_view:1
|
||||||
},
|
},
|
||||||
@@ -582,60 +589,97 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
read_only:1,
|
read_only:1,
|
||||||
fieldname:'uom',
|
fieldname:'uom',
|
||||||
label: __('UOM'),
|
label: __('UOM'),
|
||||||
|
in_list_view:1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldtype:'Data',
|
||||||
|
fieldname:'supplier',
|
||||||
|
label: __('Supplier'),
|
||||||
|
read_only:1,
|
||||||
in_list_view:1
|
in_list_view:1
|
||||||
}
|
},
|
||||||
],
|
]
|
||||||
data: cur_frm.doc.items,
|
|
||||||
get_data: function() {
|
|
||||||
return cur_frm.doc.items
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
{"fieldtype": "Button", "label": __('Create Purchase Order'), "fieldname": "make_purchase_order", "cssClass": "btn-primary"},
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.fields_dict.make_purchase_order.$input.click(function() {
|
|
||||||
var args = dialog.get_values();
|
|
||||||
let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children()
|
|
||||||
if(selected_items.length == 0) {
|
|
||||||
frappe.throw({message: 'Please select Item form Table', title: __('Message'), indicator:'blue'})
|
|
||||||
}
|
|
||||||
let selected_items_list = []
|
|
||||||
for(let i in selected_items){
|
|
||||||
selected_items_list.push(selected_items[i].item_code)
|
|
||||||
}
|
|
||||||
dialog.hide();
|
|
||||||
return frappe.call({
|
|
||||||
type: "GET",
|
|
||||||
method: "erpnext.selling.doctype.sales_order.sales_order.make_purchase_order",
|
|
||||||
args: {
|
|
||||||
"source_name": me.frm.doc.name,
|
|
||||||
"for_supplier": args.supplier,
|
|
||||||
"selected_items": selected_items_list
|
|
||||||
},
|
|
||||||
freeze: true,
|
|
||||||
callback: function(r) {
|
|
||||||
if(!r.exc) {
|
|
||||||
// var args = dialog.get_values();
|
|
||||||
if (args.supplier){
|
|
||||||
var doc = frappe.model.sync(r.message);
|
|
||||||
frappe.set_route("Form", r.message.doctype, r.message.name);
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
frappe.route_options = {
|
|
||||||
"sales_order": me.frm.doc.name
|
|
||||||
}
|
|
||||||
frappe.set_route("List", "Purchase Order");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
],
|
||||||
|
primary_action_label: 'Create Purchase Order',
|
||||||
|
primary_action (args) {
|
||||||
|
if (!args) return;
|
||||||
|
|
||||||
|
let selected_items = dialog.fields_dict.items_for_po.grid.get_selected_children();
|
||||||
|
if(selected_items.length == 0) {
|
||||||
|
frappe.throw({message: 'Please select Items from the Table', title: __('Items Required'), indicator:'blue'})
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.hide();
|
||||||
|
|
||||||
|
var method = args.against_default_supplier ? "make_purchase_order_for_default_supplier" : "make_purchase_order"
|
||||||
|
return frappe.call({
|
||||||
|
method: "erpnext.selling.doctype.sales_order.sales_order." + method,
|
||||||
|
freeze: true,
|
||||||
|
freeze_message: __("Creating Purchase Order ..."),
|
||||||
|
args: {
|
||||||
|
"source_name": me.frm.doc.name,
|
||||||
|
"selected_items": selected_items
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: function(r) {
|
||||||
|
if(!r.exc) {
|
||||||
|
if (!args.against_default_supplier) {
|
||||||
|
frappe.model.sync(r.message);
|
||||||
|
frappe.set_route("Form", r.message.doctype, r.message.name);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
frappe.route_options = {
|
||||||
|
"sales_order": me.frm.doc.name
|
||||||
|
}
|
||||||
|
frappe.set_route("List", "Purchase Order");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
});
|
});
|
||||||
dialog.get_field("items_for_po").grid.only_sortable()
|
|
||||||
dialog.get_field("items_for_po").refresh()
|
dialog.fields_dict["against_default_supplier"].df.onchange = () => set_po_items_data(dialog);
|
||||||
|
|
||||||
|
function set_po_items_data (dialog) {
|
||||||
|
var against_default_supplier = dialog.get_value("against_default_supplier");
|
||||||
|
var items_for_po = dialog.get_value("items_for_po");
|
||||||
|
|
||||||
|
if (against_default_supplier) {
|
||||||
|
let items_with_supplier = items_for_po.filter((item) => item.supplier)
|
||||||
|
|
||||||
|
dialog.fields_dict["items_for_po"].df.data = items_with_supplier;
|
||||||
|
dialog.get_field("items_for_po").refresh();
|
||||||
|
} else {
|
||||||
|
let po_items = [];
|
||||||
|
me.frm.doc.items.forEach(d => {
|
||||||
|
let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor);
|
||||||
|
if (pending_qty > 0) {
|
||||||
|
po_items.push({
|
||||||
|
"doctype": "Sales Order Item",
|
||||||
|
"name": d.name,
|
||||||
|
"item_name": d.item_name,
|
||||||
|
"item_code": d.item_code,
|
||||||
|
"pending_qty": pending_qty,
|
||||||
|
"uom": d.uom,
|
||||||
|
"supplier": d.supplier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.fields_dict["items_for_po"].df.data = po_items;
|
||||||
|
dialog.get_field("items_for_po").refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set_po_items_data(dialog);
|
||||||
|
dialog.get_field("items_for_po").grid.only_sortable();
|
||||||
|
dialog.get_field("items_for_po").refresh();
|
||||||
|
dialog.wrapper.find('.grid-heading-row .grid-row-check').click();
|
||||||
dialog.show();
|
dialog.show();
|
||||||
},
|
},
|
||||||
|
|
||||||
hold_sales_order: function(){
|
hold_sales_order: function(){
|
||||||
var me = this;
|
var me = this;
|
||||||
var d = new frappe.ui.Dialog({
|
var d = new frappe.ui.Dialog({
|
||||||
|
|||||||
@@ -443,25 +443,19 @@ class SalesOrder(SellingController):
|
|||||||
for item in self.items:
|
for item in self.items:
|
||||||
if item.ensure_delivery_based_on_produced_serial_no:
|
if item.ensure_delivery_based_on_produced_serial_no:
|
||||||
if item.item_code in normal_items:
|
if item.item_code in normal_items:
|
||||||
frappe.throw(_("Cannot ensure delivery by Serial No as \
|
frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code))
|
||||||
Item {0} is added with and without Ensure Delivery by \
|
|
||||||
Serial No.").format(item.item_code))
|
|
||||||
if item.item_code not in reserved_items:
|
if item.item_code not in reserved_items:
|
||||||
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
|
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
|
||||||
frappe.throw(_("Item {0} has no Serial No. Only serilialized items \
|
frappe.throw(_("Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No").format(item.item_code))
|
||||||
can have delivery based on Serial No").format(item.item_code))
|
|
||||||
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):
|
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):
|
||||||
frappe.throw(_("No active BOM found for item {0}. Delivery by \
|
frappe.throw(_("No active BOM found for item {0}. Delivery by Serial No cannot be ensured").format(item.item_code))
|
||||||
Serial No cannot be ensured").format(item.item_code))
|
|
||||||
reserved_items.append(item.item_code)
|
reserved_items.append(item.item_code)
|
||||||
else:
|
else:
|
||||||
normal_items.append(item.item_code)
|
normal_items.append(item.item_code)
|
||||||
|
|
||||||
if not item.ensure_delivery_based_on_produced_serial_no and \
|
if not item.ensure_delivery_based_on_produced_serial_no and \
|
||||||
item.item_code in reserved_items:
|
item.item_code in reserved_items:
|
||||||
frappe.throw(_("Cannot ensure delivery by Serial No as \
|
frappe.throw(_("Cannot ensure delivery by Serial No as Item {0} is added with and without Ensure Delivery by Serial No.").format(item.item_code))
|
||||||
Item {0} is added with and without Ensure Delivery by \
|
|
||||||
Serial No.").format(item.item_code))
|
|
||||||
|
|
||||||
def get_list_context(context=None):
|
def get_list_context(context=None):
|
||||||
from erpnext.controllers.website_list_for_contact import get_list_context
|
from erpnext.controllers.website_list_for_contact import get_list_context
|
||||||
@@ -785,7 +779,9 @@ def get_events(start, end, filters=None):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_purchase_order(source_name, for_supplier=None, selected_items=[], target_doc=None):
|
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
|
||||||
|
if not selected_items: return
|
||||||
|
|
||||||
if isinstance(selected_items, string_types):
|
if isinstance(selected_items, string_types):
|
||||||
selected_items = json.loads(selected_items)
|
selected_items = json.loads(selected_items)
|
||||||
|
|
||||||
@@ -822,24 +818,21 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
|||||||
|
|
||||||
def update_item(source, target, source_parent):
|
def update_item(source, target, source_parent):
|
||||||
target.schedule_date = source.delivery_date
|
target.schedule_date = source.delivery_date
|
||||||
target.qty = flt(source.qty) - flt(source.ordered_qty)
|
target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor))
|
||||||
target.stock_qty = (flt(source.qty) - flt(source.ordered_qty)) * flt(source.conversion_factor)
|
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
||||||
target.project = source_parent.project
|
target.project = source_parent.project
|
||||||
|
|
||||||
suppliers =[]
|
suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')]
|
||||||
if for_supplier:
|
suppliers = list(set(suppliers))
|
||||||
suppliers.append(for_supplier)
|
|
||||||
else:
|
items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
|
||||||
sales_order = frappe.get_doc("Sales Order", source_name)
|
items_to_map = list(set(items_to_map))
|
||||||
for item in sales_order.items:
|
|
||||||
if item.supplier and item.supplier not in suppliers:
|
|
||||||
suppliers.append(item.supplier)
|
|
||||||
|
|
||||||
if not suppliers:
|
if not suppliers:
|
||||||
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
|
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
|
||||||
|
|
||||||
for supplier in suppliers:
|
for supplier in suppliers:
|
||||||
po =frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
||||||
if len(po) == 0:
|
if len(po) == 0:
|
||||||
doc = get_mapped_doc("Sales Order", source_name, {
|
doc = get_mapped_doc("Sales Order", source_name, {
|
||||||
"Sales Order": {
|
"Sales Order": {
|
||||||
@@ -850,7 +843,9 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
|||||||
"contact_mobile",
|
"contact_mobile",
|
||||||
"contact_email",
|
"contact_email",
|
||||||
"contact_person",
|
"contact_person",
|
||||||
"taxes_and_charges"
|
"taxes_and_charges",
|
||||||
|
"shipping_address",
|
||||||
|
"terms"
|
||||||
],
|
],
|
||||||
"validation": {
|
"validation": {
|
||||||
"docstatus": ["=", 1]
|
"docstatus": ["=", 1]
|
||||||
@@ -869,55 +864,94 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe
|
|||||||
"field_no_map": [
|
"field_no_map": [
|
||||||
"rate",
|
"rate",
|
||||||
"price_list_rate",
|
"price_list_rate",
|
||||||
"item_tax_template"
|
"item_tax_template",
|
||||||
|
"discount_percentage",
|
||||||
|
"discount_amount",
|
||||||
|
"pricing_rules"
|
||||||
],
|
],
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: doc.ordered_qty < doc.qty and doc.supplier == supplier and doc.item_code in selected_items
|
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
|
||||||
}
|
}
|
||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
if not for_supplier:
|
|
||||||
doc.insert()
|
doc.insert()
|
||||||
else:
|
else:
|
||||||
suppliers =[]
|
suppliers =[]
|
||||||
if suppliers:
|
if suppliers:
|
||||||
if not for_supplier:
|
frappe.db.commit()
|
||||||
frappe.db.commit()
|
|
||||||
return doc
|
return doc
|
||||||
else:
|
else:
|
||||||
frappe.msgprint(_("PO already created for all sales order items"))
|
frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||||
def get_supplier(doctype, txt, searchfield, start, page_len, filters):
|
if not selected_items: return
|
||||||
supp_master_name = frappe.defaults.get_user_default("supp_master_name")
|
|
||||||
if supp_master_name == "Supplier Name":
|
|
||||||
fields = ["name", "supplier_group"]
|
|
||||||
else:
|
|
||||||
fields = ["name", "supplier_name", "supplier_group"]
|
|
||||||
fields = ", ".join(fields)
|
|
||||||
|
|
||||||
return frappe.db.sql("""select {field} from `tabSupplier`
|
if isinstance(selected_items, string_types):
|
||||||
where docstatus < 2
|
selected_items = json.loads(selected_items)
|
||||||
and ({key} like %(txt)s
|
|
||||||
or supplier_name like %(txt)s)
|
items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
|
||||||
and name in (select supplier from `tabSales Order Item` where parent = %(parent)s)
|
items_to_map = list(set(items_to_map))
|
||||||
and name not in (select supplier from `tabPurchase Order` po inner join `tabPurchase Order Item` poi
|
|
||||||
on po.name=poi.parent where po.docstatus<2 and poi.sales_order=%(parent)s)
|
def set_missing_values(source, target):
|
||||||
order by
|
target.supplier = ""
|
||||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
target.apply_discount_on = ""
|
||||||
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
|
target.additional_discount_percentage = 0.0
|
||||||
name, supplier_name
|
target.discount_amount = 0.0
|
||||||
limit %(start)s, %(page_len)s """.format(**{
|
target.inter_company_order_reference = ""
|
||||||
'field': fields,
|
target.customer = ""
|
||||||
'key': frappe.db.escape(searchfield)
|
target.customer_name = ""
|
||||||
}), {
|
target.run_method("set_missing_values")
|
||||||
'txt': "%%%s%%" % txt,
|
target.run_method("calculate_taxes_and_totals")
|
||||||
'_txt': txt.replace("%", ""),
|
|
||||||
'start': start,
|
def update_item(source, target, source_parent):
|
||||||
'page_len': page_len,
|
target.schedule_date = source.delivery_date
|
||||||
'parent': filters.get('parent')
|
target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor))
|
||||||
})
|
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
|
||||||
|
target.project = source_parent.project
|
||||||
|
|
||||||
|
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
|
||||||
|
doc = get_mapped_doc("Sales Order", source_name, {
|
||||||
|
"Sales Order": {
|
||||||
|
"doctype": "Purchase Order",
|
||||||
|
"field_no_map": [
|
||||||
|
"address_display",
|
||||||
|
"contact_display",
|
||||||
|
"contact_mobile",
|
||||||
|
"contact_email",
|
||||||
|
"contact_person",
|
||||||
|
"taxes_and_charges",
|
||||||
|
"shipping_address",
|
||||||
|
"terms"
|
||||||
|
],
|
||||||
|
"validation": {
|
||||||
|
"docstatus": ["=", 1]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Sales Order Item": {
|
||||||
|
"doctype": "Purchase Order Item",
|
||||||
|
"field_map": [
|
||||||
|
["name", "sales_order_item"],
|
||||||
|
["parent", "sales_order"],
|
||||||
|
["stock_uom", "stock_uom"],
|
||||||
|
["uom", "uom"],
|
||||||
|
["conversion_factor", "conversion_factor"],
|
||||||
|
["delivery_date", "schedule_date"]
|
||||||
|
],
|
||||||
|
"field_no_map": [
|
||||||
|
"rate",
|
||||||
|
"price_list_rate",
|
||||||
|
"item_tax_template",
|
||||||
|
"discount_percentage",
|
||||||
|
"discount_amount",
|
||||||
|
"supplier",
|
||||||
|
"pricing_rules"
|
||||||
|
],
|
||||||
|
"postprocess": update_item,
|
||||||
|
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map
|
||||||
|
}
|
||||||
|
}, target_doc, set_missing_values)
|
||||||
|
return doc
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_work_orders(items, sales_order, company, project=None):
|
def make_work_orders(items, sales_order, company, project=None):
|
||||||
|
|||||||
@@ -674,12 +674,12 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
||||||
|
|
||||||
def test_drop_shipping(self):
|
def test_drop_shipping(self):
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \
|
||||||
|
update_status as so_update_status
|
||||||
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
|
from erpnext.buying.doctype.purchase_order.purchase_order import update_status
|
||||||
|
|
||||||
make_stock_entry(target="_Test Warehouse - _TC", qty=10, rate=100)
|
# make items
|
||||||
po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1})
|
po_item = make_item("_Test Item for Drop Shipping", {"is_stock_item": 1, "delivered_by_supplier": 1})
|
||||||
|
|
||||||
dn_item = make_item("_Test Regular Item", {"is_stock_item": 1})
|
dn_item = make_item("_Test Regular Item", {"is_stock_item": 1})
|
||||||
|
|
||||||
so_items = [
|
so_items = [
|
||||||
@@ -701,80 +701,61 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1:
|
if frappe.db.get_value("Item", "_Test Regular Item", "is_stock_item")==1:
|
||||||
make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=10, rate=100)
|
make_stock_entry(item="_Test Regular Item", target="_Test Warehouse - _TC", qty=2, rate=100)
|
||||||
|
|
||||||
#setuo existing qty from bin
|
#create so, po and dn
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
|
||||||
fields=["ordered_qty", "reserved_qty"])
|
|
||||||
|
|
||||||
existing_ordered_qty = bin[0].ordered_qty if bin else 0.0
|
|
||||||
existing_reserved_qty = bin[0].reserved_qty if bin else 0.0
|
|
||||||
|
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": dn_item.item_code,
|
|
||||||
"warehouse": "_Test Warehouse - _TC"}, fields=["reserved_qty"])
|
|
||||||
|
|
||||||
existing_reserved_qty_for_dn_item = bin[0].reserved_qty if bin else 0.0
|
|
||||||
|
|
||||||
#create so, po and partial dn
|
|
||||||
so = make_sales_order(item_list=so_items, do_not_submit=True)
|
so = make_sales_order(item_list=so_items, do_not_submit=True)
|
||||||
so.submit()
|
so.submit()
|
||||||
|
|
||||||
po = make_purchase_order(so.name, '_Test Supplier', selected_items=[so_items[0]['item_code']])
|
po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
|
||||||
po.submit()
|
po.submit()
|
||||||
|
|
||||||
dn = create_dn_against_so(so.name, delivered_qty=1)
|
dn = create_dn_against_so(so.name, delivered_qty=2)
|
||||||
|
|
||||||
self.assertEqual(so.customer, po.customer)
|
self.assertEqual(so.customer, po.customer)
|
||||||
self.assertEqual(po.items[0].sales_order, so.name)
|
self.assertEqual(po.items[0].sales_order, so.name)
|
||||||
self.assertEqual(po.items[0].item_code, po_item.item_code)
|
self.assertEqual(po.items[0].item_code, po_item.item_code)
|
||||||
self.assertEqual(dn.items[0].item_code, dn_item.item_code)
|
self.assertEqual(dn.items[0].item_code, dn_item.item_code)
|
||||||
|
|
||||||
#test ordered_qty and reserved_qty
|
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
|
||||||
fields=["ordered_qty", "reserved_qty"])
|
|
||||||
|
|
||||||
ordered_qty = bin[0].ordered_qty if bin else 0.0
|
|
||||||
reserved_qty = bin[0].reserved_qty if bin else 0.0
|
|
||||||
|
|
||||||
self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty)
|
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty)
|
|
||||||
|
|
||||||
reserved_qty = frappe.db.get_value("Bin",
|
|
||||||
{"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty")
|
|
||||||
|
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item + 1)
|
|
||||||
|
|
||||||
#test po_item length
|
#test po_item length
|
||||||
self.assertEqual(len(po.items), 1)
|
self.assertEqual(len(po.items), 1)
|
||||||
|
|
||||||
#test per_delivered status
|
# test ordered_qty and reserved_qty for drop ship item
|
||||||
|
bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
||||||
|
fields=["ordered_qty", "reserved_qty"])
|
||||||
|
|
||||||
|
ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0
|
||||||
|
reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0
|
||||||
|
|
||||||
|
# drop ship PO should not impact bin, test the same
|
||||||
|
self.assertEqual(abs(flt(ordered_qty)), 0)
|
||||||
|
self.assertEqual(abs(flt(reserved_qty)), 0)
|
||||||
|
|
||||||
|
# test per_delivered status
|
||||||
update_status("Delivered", po.name)
|
update_status("Delivered", po.name)
|
||||||
self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 75.00)
|
self.assertEqual(flt(frappe.db.get_value("Sales Order", so.name, "per_delivered"), 2), 100.00)
|
||||||
|
po.load_from_db()
|
||||||
|
|
||||||
#test reserved qty after complete delivery
|
# test after closing so
|
||||||
dn = create_dn_against_so(so.name, delivered_qty=1)
|
|
||||||
reserved_qty = frappe.db.get_value("Bin",
|
|
||||||
{"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty")
|
|
||||||
|
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item)
|
|
||||||
|
|
||||||
#test after closing so
|
|
||||||
so.db_set('status', "Closed")
|
so.db_set('status', "Closed")
|
||||||
so.update_reserved_qty()
|
so.update_reserved_qty()
|
||||||
|
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
# test ordered_qty and reserved_qty for drop ship item after closing so
|
||||||
|
bin_po_item = frappe.get_all("Bin", filters={"item_code": po_item.item_code, "warehouse": "_Test Warehouse - _TC"},
|
||||||
fields=["ordered_qty", "reserved_qty"])
|
fields=["ordered_qty", "reserved_qty"])
|
||||||
|
|
||||||
ordered_qty = bin[0].ordered_qty if bin else 0.0
|
ordered_qty = bin_po_item[0].ordered_qty if bin_po_item else 0.0
|
||||||
reserved_qty = bin[0].reserved_qty if bin else 0.0
|
reserved_qty = bin_po_item[0].reserved_qty if bin_po_item else 0.0
|
||||||
|
|
||||||
self.assertEqual(abs(flt(ordered_qty)), existing_ordered_qty)
|
self.assertEqual(abs(flt(ordered_qty)), 0)
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty)
|
self.assertEqual(abs(flt(reserved_qty)), 0)
|
||||||
|
|
||||||
reserved_qty = frappe.db.get_value("Bin",
|
# teardown
|
||||||
{"item_code": dn_item.item_code, "warehouse": "_Test Warehouse - _TC"}, "reserved_qty")
|
so_update_status("Draft", so.name)
|
||||||
|
dn.load_from_db()
|
||||||
self.assertEqual(abs(flt(reserved_qty)), existing_reserved_qty_for_dn_item)
|
dn.cancel()
|
||||||
|
po.cancel()
|
||||||
|
so.load_from_db()
|
||||||
|
so.cancel()
|
||||||
|
|
||||||
def test_reserved_qty_for_closing_so(self):
|
def test_reserved_qty_for_closing_so(self):
|
||||||
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
|
||||||
@@ -1050,6 +1031,7 @@ def make_sales_order(**args):
|
|||||||
so.company = args.company or "_Test Company"
|
so.company = args.company or "_Test Company"
|
||||||
so.customer = args.customer or "_Test Customer"
|
so.customer = args.customer or "_Test Customer"
|
||||||
so.currency = args.currency or "INR"
|
so.currency = args.currency or "INR"
|
||||||
|
so.po_no = args.po_no or '12345'
|
||||||
if args.selling_price_list:
|
if args.selling_price_list:
|
||||||
so.selling_price_list = args.selling_price_list
|
so.selling_price_list = args.selling_price_list
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
|||||||
display_items_in_stock = 0
|
display_items_in_stock = 0
|
||||||
|
|
||||||
if pos_profile:
|
if pos_profile:
|
||||||
warehouse, display_items_in_stock = frappe.db.get_value('POS Profile', pos_profile, ['warehouse', 'display_items_in_stock'])
|
warehouse, display_items_in_stock, hide_unavailable_items = frappe.db.get_value(
|
||||||
|
'POS Profile', pos_profile, ['warehouse', 'display_items_in_stock', 'hide_unavailable_items']
|
||||||
|
)
|
||||||
|
|
||||||
if not frappe.db.exists('Item Group', item_group):
|
if not frappe.db.exists('Item Group', item_group):
|
||||||
item_group = get_root_of('Item Group')
|
item_group = get_root_of('Item Group')
|
||||||
@@ -37,24 +39,31 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
|||||||
lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt'])
|
lft, rgt = frappe.db.get_value('Item Group', item_group, ['lft', 'rgt'])
|
||||||
# locate function is used to sort by closest match from the beginning of the value
|
# locate function is used to sort by closest match from the beginning of the value
|
||||||
|
|
||||||
|
bin_join = bin_cond = ""
|
||||||
|
if hide_unavailable_items:
|
||||||
|
bin_join = ",`tabBin` b"
|
||||||
|
bin_cond = "and i.item_code = b.item_code and ifnull(b.actual_qty, 0) > 0 "
|
||||||
|
if warehouse:
|
||||||
|
bin_cond += "and b.warehouse = {}".format(frappe.db.escape(warehouse))
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
items_data = frappe.db.sql("""
|
items_data = frappe.db.sql("""
|
||||||
SELECT
|
SELECT
|
||||||
name AS item_code,
|
i.name AS item_code,
|
||||||
item_name,
|
i.item_name,
|
||||||
stock_uom,
|
i.stock_uom,
|
||||||
image AS item_image,
|
i.image AS item_image,
|
||||||
idx AS idx,
|
i.idx AS idx,
|
||||||
is_stock_item
|
i.is_stock_item
|
||||||
FROM
|
FROM
|
||||||
`tabItem`
|
`tabItem` i {bin_join}
|
||||||
WHERE
|
WHERE
|
||||||
disabled = 0
|
disabled = 0
|
||||||
AND has_variants = 0
|
AND i.has_variants = 0
|
||||||
AND is_sales_item = 1
|
AND i.is_sales_item = 1
|
||||||
AND item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
|
AND i.item_group in (SELECT name FROM `tabItem Group` WHERE lft >= {lft} AND rgt <= {rgt})
|
||||||
AND {condition}
|
{condition} {bin_cond}
|
||||||
ORDER BY
|
ORDER BY
|
||||||
idx desc
|
idx desc
|
||||||
LIMIT
|
LIMIT
|
||||||
@@ -64,7 +73,9 @@ def get_items(start, page_length, price_list, item_group, search_value="", pos_p
|
|||||||
page_length=page_length,
|
page_length=page_length,
|
||||||
lft=lft,
|
lft=lft,
|
||||||
rgt=rgt,
|
rgt=rgt,
|
||||||
condition=condition
|
condition=condition,
|
||||||
|
bin_join=bin_join,
|
||||||
|
bin_cond=bin_cond
|
||||||
), as_dict=1)
|
), as_dict=1)
|
||||||
|
|
||||||
if items_data:
|
if items_data:
|
||||||
@@ -154,16 +165,16 @@ def search_serial_or_batch_or_barcode_number(search_value):
|
|||||||
|
|
||||||
def get_conditions(item_code, serial_no, batch_no, barcode):
|
def get_conditions(item_code, serial_no, batch_no, barcode):
|
||||||
if serial_no or batch_no or barcode:
|
if serial_no or batch_no or barcode:
|
||||||
return "name = {0}".format(frappe.db.escape(item_code))
|
return "and i.name = {0}".format(frappe.db.escape(item_code))
|
||||||
|
|
||||||
return """(name like {item_code}
|
return ("""and (i.name like {item_code} or i.item_name like {item_code})"""
|
||||||
or item_name like {item_code})""".format(item_code = frappe.db.escape('%' + item_code + '%'))
|
.format(item_code=frappe.db.escape('%' + item_code + '%')))
|
||||||
|
|
||||||
def get_item_group_condition(pos_profile):
|
def get_item_group_condition(pos_profile):
|
||||||
cond = "and 1=1"
|
cond = "and 1=1"
|
||||||
item_groups = get_item_groups(pos_profile)
|
item_groups = get_item_groups(pos_profile)
|
||||||
if item_groups:
|
if item_groups:
|
||||||
cond = "and item_group in (%s)"%(', '.join(['%s']*len(item_groups)))
|
cond = "and i.item_group in (%s)"%(', '.join(['%s']*len(item_groups)))
|
||||||
|
|
||||||
return cond % tuple(item_groups)
|
return cond % tuple(item_groups)
|
||||||
|
|
||||||
|
|||||||
@@ -71,29 +71,41 @@ frappe.ui.form.on("Company", {
|
|||||||
frm.toggle_enable("default_currency", (frm.doc.__onload &&
|
frm.toggle_enable("default_currency", (frm.doc.__onload &&
|
||||||
!frm.doc.__onload.transactions_exist));
|
!frm.doc.__onload.transactions_exist));
|
||||||
|
|
||||||
frm.add_custom_button(__('Create Tax Template'), function() {
|
if (frm.has_perm('write')) {
|
||||||
frm.trigger("make_default_tax_template");
|
frm.add_custom_button(__('Create Tax Template'), function() {
|
||||||
});
|
frm.trigger("make_default_tax_template");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (frappe.perm.has_perm("Cost Center", 0, 'read')) {
|
||||||
|
frm.add_custom_button(__('Cost Centers'), function() {
|
||||||
|
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name});
|
||||||
|
}, __("View"));
|
||||||
|
}
|
||||||
|
|
||||||
frm.add_custom_button(__('Cost Centers'), function() {
|
if (frappe.perm.has_perm("Account", 0, 'read')) {
|
||||||
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name})
|
frm.add_custom_button(__('Chart of Accounts'), function() {
|
||||||
}, __("View"));
|
frappe.set_route('Tree', 'Account', {'company': frm.doc.name});
|
||||||
|
}, __("View"));
|
||||||
|
}
|
||||||
|
|
||||||
frm.add_custom_button(__('Chart of Accounts'), function() {
|
|
||||||
frappe.set_route('Tree', 'Account', {'company': frm.doc.name})
|
|
||||||
}, __("View"));
|
|
||||||
|
|
||||||
frm.add_custom_button(__('Sales Tax Template'), function() {
|
if (frappe.perm.has_perm("Sales Taxes and Charges Template", 0, 'read')) {
|
||||||
frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name});
|
frm.add_custom_button(__('Sales Tax Template'), function() {
|
||||||
}, __("View"));
|
frappe.set_route('List', 'Sales Taxes and Charges Template', {'company': frm.doc.name});
|
||||||
|
}, __("View"));
|
||||||
|
}
|
||||||
|
|
||||||
frm.add_custom_button(__('Purchase Tax Template'), function() {
|
if (frappe.perm.has_perm("Purchase Taxes and Charges Template", 0, 'read')) {
|
||||||
frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name});
|
frm.add_custom_button(__('Purchase Tax Template'), function() {
|
||||||
}, __("View"));
|
frappe.set_route('List', 'Purchase Taxes and Charges Template', {'company': frm.doc.name});
|
||||||
|
}, __("View"));
|
||||||
|
}
|
||||||
|
|
||||||
frm.add_custom_button(__('Default Tax Template'), function() {
|
if (frm.has_perm('write')) {
|
||||||
frm.trigger("make_default_tax_template");
|
frm.add_custom_button(__('Default Tax Template'), function() {
|
||||||
}, __('Create'));
|
frm.trigger("make_default_tax_template");
|
||||||
|
}, __('Create'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
erpnext.company.set_chart_of_accounts_options(frm.doc);
|
erpnext.company.set_chart_of_accounts_options(frm.doc);
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
|
|
||||||
sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name})
|
sle = frappe.get_doc("Stock Ledger Entry", {"voucher_type": "Delivery Note", "voucher_no": dn.name})
|
||||||
|
|
||||||
self.assertEqual(sle.stock_value_difference, -1*stock_queue[0][1])
|
self.assertEqual(sle.stock_value_difference, flt(-1*stock_queue[0][1]))
|
||||||
|
|
||||||
self.assertFalse(get_gl_entries("Delivery Note", dn.name))
|
self.assertFalse(get_gl_entries("Delivery Note", dn.name))
|
||||||
|
|
||||||
@@ -442,9 +442,15 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
self.assertEqual(dn.status, "To Bill")
|
self.assertEqual(dn.status, "To Bill")
|
||||||
self.assertEqual(dn.per_billed, 0)
|
self.assertEqual(dn.per_billed, 0)
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(dn.po_no, so.po_no)
|
||||||
|
|
||||||
si = make_sales_invoice(dn.name)
|
si = make_sales_invoice(dn.name)
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(dn.po_no, si.po_no)
|
||||||
|
|
||||||
dn.load_from_db()
|
dn.load_from_db()
|
||||||
self.assertEqual(dn.get("items")[0].billed_amt, 200)
|
self.assertEqual(dn.get("items")[0].billed_amt, 200)
|
||||||
self.assertEqual(dn.per_billed, 100)
|
self.assertEqual(dn.per_billed, 100)
|
||||||
@@ -461,6 +467,9 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(so.po_no, si.po_no)
|
||||||
|
|
||||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||||
|
|
||||||
dn1 = make_delivery_note(so.name)
|
dn1 = make_delivery_note(so.name)
|
||||||
@@ -469,6 +478,9 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
dn1.get("items")[0].qty = 2
|
dn1.get("items")[0].qty = 2
|
||||||
dn1.submit()
|
dn1.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(so.po_no, dn1.po_no)
|
||||||
|
|
||||||
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
|
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
|
||||||
self.assertEqual(dn1.per_billed, 100)
|
self.assertEqual(dn1.per_billed, 100)
|
||||||
self.assertEqual(dn1.status, "Completed")
|
self.assertEqual(dn1.status, "Completed")
|
||||||
@@ -479,6 +491,9 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
dn2.get("items")[0].qty = 4
|
dn2.get("items")[0].qty = 4
|
||||||
dn2.submit()
|
dn2.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(so.po_no, dn2.po_no)
|
||||||
|
|
||||||
dn1.load_from_db()
|
dn1.load_from_db()
|
||||||
self.assertEqual(dn1.get("items")[0].billed_amt, 100)
|
self.assertEqual(dn1.get("items")[0].billed_amt, 100)
|
||||||
self.assertEqual(dn1.per_billed, 50)
|
self.assertEqual(dn1.per_billed, 50)
|
||||||
@@ -502,9 +517,15 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
dn1.get("items")[0].qty = 2
|
dn1.get("items")[0].qty = 2
|
||||||
dn1.submit()
|
dn1.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(dn1.po_no, so.po_no)
|
||||||
|
|
||||||
si1 = make_sales_invoice(dn1.name)
|
si1 = make_sales_invoice(dn1.name)
|
||||||
si1.submit()
|
si1.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(dn1.po_no, si1.po_no)
|
||||||
|
|
||||||
dn1.load_from_db()
|
dn1.load_from_db()
|
||||||
self.assertEqual(dn1.per_billed, 100)
|
self.assertEqual(dn1.per_billed, 100)
|
||||||
|
|
||||||
@@ -512,11 +533,17 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
si2.get("items")[0].qty = 4
|
si2.get("items")[0].qty = 4
|
||||||
si2.submit()
|
si2.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(si2.po_no, so.po_no)
|
||||||
|
|
||||||
dn2 = make_delivery_note(so.name)
|
dn2 = make_delivery_note(so.name)
|
||||||
dn2.posting_time = "08:00"
|
dn2.posting_time = "08:00"
|
||||||
dn2.get("items")[0].qty = 5
|
dn2.get("items")[0].qty = 5
|
||||||
dn2.submit()
|
dn2.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(dn2.po_no, so.po_no)
|
||||||
|
|
||||||
dn1.load_from_db()
|
dn1.load_from_db()
|
||||||
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
|
self.assertEqual(dn1.get("items")[0].billed_amt, 200)
|
||||||
self.assertEqual(dn1.per_billed, 100)
|
self.assertEqual(dn1.per_billed, 100)
|
||||||
@@ -536,9 +563,15 @@ class TestDeliveryNote(unittest.TestCase):
|
|||||||
si = make_sales_invoice(so.name)
|
si = make_sales_invoice(so.name)
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(so.po_no, si.po_no)
|
||||||
|
|
||||||
dn = make_delivery_note(si.name)
|
dn = make_delivery_note(si.name)
|
||||||
dn.submit()
|
dn.submit()
|
||||||
|
|
||||||
|
# Testing if Customer's Purchase Order No was rightly copied
|
||||||
|
self.assertEqual(dn.po_no, si.po_no)
|
||||||
|
|
||||||
self.assertEqual(dn.get("items")[0].billed_amt, 1000)
|
self.assertEqual(dn.get("items")[0].billed_amt, 1000)
|
||||||
self.assertEqual(dn.per_billed, 100)
|
self.assertEqual(dn.per_billed, 100)
|
||||||
self.assertEqual(dn.status, "Completed")
|
self.assertEqual(dn.status, "Completed")
|
||||||
|
|||||||
@@ -983,9 +983,7 @@ class Item(WebsiteGenerator):
|
|||||||
if self.stock_ledger_created():
|
if self.stock_ledger_created():
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}) or \
|
elif frappe.db.get_value(doctype, filters={"item_code": self.name, "docstatus": 1}):
|
||||||
frappe.db.get_value("Production Order",
|
|
||||||
filters={"production_item": self.name, "docstatus": 1}):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def validate_auto_reorder_enabled_in_stock_settings(self):
|
def validate_auto_reorder_enabled_in_stock_settings(self):
|
||||||
|
|||||||
@@ -500,6 +500,8 @@ def update_billed_amount_based_on_po(po_detail, update_modified=True):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def make_purchase_invoice(source_name, target_doc=None):
|
def make_purchase_invoice(source_name, target_doc=None):
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from erpnext.accounts.party import get_payment_terms_template
|
||||||
|
|
||||||
doc = frappe.get_doc('Purchase Receipt', source_name)
|
doc = frappe.get_doc('Purchase Receipt', source_name)
|
||||||
returned_qty_map = get_returned_qty_map(source_name)
|
returned_qty_map = get_returned_qty_map(source_name)
|
||||||
invoiced_qty_map = get_invoiced_qty_map(source_name)
|
invoiced_qty_map = get_invoiced_qty_map(source_name)
|
||||||
@@ -510,6 +512,7 @@ def make_purchase_invoice(source_name, target_doc=None):
|
|||||||
|
|
||||||
doc = frappe.get_doc(target)
|
doc = frappe.get_doc(target)
|
||||||
doc.ignore_pricing_rule = 1
|
doc.ignore_pricing_rule = 1
|
||||||
|
doc.payment_terms_template = get_payment_terms_template(source.supplier, "Supplier", source.company)
|
||||||
doc.run_method("onload")
|
doc.run_method("onload")
|
||||||
doc.run_method("set_missing_values")
|
doc.run_method("set_missing_values")
|
||||||
doc.run_method("calculate_taxes_and_totals")
|
doc.run_method("calculate_taxes_and_totals")
|
||||||
|
|||||||
@@ -20,6 +20,30 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
|
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
|
||||||
|
|
||||||
def test_make_purchase_invoice(self):
|
def test_make_purchase_invoice(self):
|
||||||
|
if not frappe.db.exists('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice'):
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'Payment Terms Template',
|
||||||
|
'template_name': '_Test Payment Terms Template For Purchase Invoice',
|
||||||
|
'allocate_payment_based_on_payment_terms': 1,
|
||||||
|
'terms': [
|
||||||
|
{
|
||||||
|
'doctype': 'Payment Terms Template Detail',
|
||||||
|
'invoice_portion': 50.00,
|
||||||
|
'credit_days_based_on': 'Day(s) after invoice date',
|
||||||
|
'credit_days': 00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'doctype': 'Payment Terms Template Detail',
|
||||||
|
'invoice_portion': 50.00,
|
||||||
|
'credit_days_based_on': 'Day(s) after invoice date',
|
||||||
|
'credit_days': 30
|
||||||
|
}]
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
template = frappe.db.get_value('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice')
|
||||||
|
old_template_in_supplier = frappe.db.get_value("Supplier", "_Test Supplier", "payment_terms")
|
||||||
|
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", template)
|
||||||
|
|
||||||
pr = make_purchase_receipt(do_not_save=True)
|
pr = make_purchase_receipt(do_not_save=True)
|
||||||
self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name)
|
self.assertRaises(frappe.ValidationError, make_purchase_invoice, pr.name)
|
||||||
pr.submit()
|
pr.submit()
|
||||||
@@ -29,10 +53,23 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
self.assertEqual(pi.doctype, "Purchase Invoice")
|
self.assertEqual(pi.doctype, "Purchase Invoice")
|
||||||
self.assertEqual(len(pi.get("items")), len(pr.get("items")))
|
self.assertEqual(len(pi.get("items")), len(pr.get("items")))
|
||||||
|
|
||||||
# modify rate
|
# test maintaining same rate throughout purchade cycle
|
||||||
pi.get("items")[0].rate = 200
|
pi.get("items")[0].rate = 200
|
||||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(pi).submit)
|
self.assertRaises(frappe.ValidationError, frappe.get_doc(pi).submit)
|
||||||
|
|
||||||
|
# test if payment terms are fetched and set in PI
|
||||||
|
self.assertEqual(pi.payment_terms_template, template)
|
||||||
|
self.assertEqual(pi.payment_schedule[0].payment_amount, flt(pi.grand_total)/2)
|
||||||
|
self.assertEqual(pi.payment_schedule[0].invoice_portion, 50)
|
||||||
|
self.assertEqual(pi.payment_schedule[1].payment_amount, flt(pi.grand_total)/2)
|
||||||
|
self.assertEqual(pi.payment_schedule[1].invoice_portion, 50)
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
pi.delete() # draft PI
|
||||||
|
pr.cancel()
|
||||||
|
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", old_template_in_supplier)
|
||||||
|
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
|
||||||
|
|
||||||
def test_purchase_receipt_no_gl_entry(self):
|
def test_purchase_receipt_no_gl_entry(self):
|
||||||
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
|
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cstr, cint, flt, comma_or, getdate, nowdate, formatdate, format_time
|
from frappe.utils import cstr, cint, flt, comma_or, getdate, nowdate, formatdate, format_time, get_link_to_form
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_incoming_rate
|
||||||
from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError, get_valuation_rate
|
from erpnext.stock.stock_ledger import get_previous_sle, NegativeStockError, get_valuation_rate
|
||||||
from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor, get_reserved_qty_for_so
|
from erpnext.stock.get_item_details import get_bin_details, get_default_cost_center, get_conversion_factor, get_reserved_qty_for_so
|
||||||
@@ -27,6 +27,7 @@ class IncorrectValuationRateError(frappe.ValidationError): pass
|
|||||||
class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass
|
class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass
|
||||||
class OperationsNotCompleteError(frappe.ValidationError): pass
|
class OperationsNotCompleteError(frappe.ValidationError): pass
|
||||||
class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass
|
class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass
|
||||||
|
class ExtraMaterialReceived(frappe.ValidationError): pass
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
|
|
||||||
@@ -35,6 +36,11 @@ form_grid_templates = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class StockEntry(StockController):
|
class StockEntry(StockController):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""To initialize the status updater."""
|
||||||
|
super(StockEntry, self).__init__(*args, **kwargs)
|
||||||
|
self.status_updater = []
|
||||||
|
|
||||||
def get_feed(self):
|
def get_feed(self):
|
||||||
return self.stock_entry_type
|
return self.stock_entry_type
|
||||||
|
|
||||||
@@ -51,7 +57,6 @@ class StockEntry(StockController):
|
|||||||
self.validate_purpose()
|
self.validate_purpose()
|
||||||
self.validate_item()
|
self.validate_item()
|
||||||
self.validate_customer_provided_item()
|
self.validate_customer_provided_item()
|
||||||
self.validate_qty()
|
|
||||||
self.set_transfer_qty()
|
self.set_transfer_qty()
|
||||||
self.validate_uom_is_integer("uom", "qty")
|
self.validate_uom_is_integer("uom", "qty")
|
||||||
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
self.validate_uom_is_integer("stock_uom", "transfer_qty")
|
||||||
@@ -122,6 +127,22 @@ class StockEntry(StockController):
|
|||||||
self.from_bom = 1
|
self.from_bom = 1
|
||||||
self.bom_no = data.bom_no
|
self.bom_no = data.bom_no
|
||||||
|
|
||||||
|
def limits_crossed_error(self, args, item, qty_or_amount):
|
||||||
|
"""To override the method limits_crossed_error which is defined in the status_updater."""
|
||||||
|
"""Raise the exception for extra material transfer against the send to warehouse."""
|
||||||
|
|
||||||
|
send_to_ste = frappe.bold(get_link_to_form("Stock Entry", self.outgoing_stock_entry))
|
||||||
|
message = _("For more details please check the send to warehouse document {0}.").format(send_to_ste)
|
||||||
|
|
||||||
|
frappe.throw(_('For the item {0}, the received quantity {1} is more than the sent quantity {2}. {3}{4}')
|
||||||
|
.format(
|
||||||
|
frappe.bold(item.get('item_code')),
|
||||||
|
frappe.bold((item[args["target_field"]])),
|
||||||
|
frappe.bold(item[args["target_ref_field"]]),
|
||||||
|
'<br>',
|
||||||
|
message
|
||||||
|
), ExtraMaterialReceived, title = _('Extra Materials Transferred'))
|
||||||
|
|
||||||
def validate_work_order_status(self):
|
def validate_work_order_status(self):
|
||||||
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||||
if pro_doc.status == 'Completed':
|
if pro_doc.status == 'Completed':
|
||||||
@@ -205,33 +226,6 @@ class StockEntry(StockController):
|
|||||||
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
|
frappe.throw(_("Row #{0}: Please specify Serial No for Item {1}").format(item.idx, item.item_code),
|
||||||
frappe.MandatoryError)
|
frappe.MandatoryError)
|
||||||
|
|
||||||
def validate_qty(self):
|
|
||||||
manufacture_purpose = ["Manufacture", "Material Consumption for Manufacture"]
|
|
||||||
|
|
||||||
if self.purpose in manufacture_purpose and self.work_order:
|
|
||||||
if not frappe.get_value('Work Order', self.work_order, 'skip_transfer'):
|
|
||||||
item_code = []
|
|
||||||
for item in self.items:
|
|
||||||
if cstr(item.t_warehouse) == '':
|
|
||||||
req_items = frappe.get_all('Work Order Item',
|
|
||||||
filters={'parent': self.work_order, 'item_code': item.item_code}, fields=["item_code"])
|
|
||||||
|
|
||||||
transferred_materials = frappe.db.sql("""
|
|
||||||
select
|
|
||||||
sum(qty) as qty
|
|
||||||
from `tabStock Entry` se,`tabStock Entry Detail` sed
|
|
||||||
where
|
|
||||||
se.name = sed.parent and se.docstatus=1 and
|
|
||||||
(se.purpose='Material Transfer for Manufacture' or se.purpose='Manufacture')
|
|
||||||
and sed.item_code=%s and se.work_order= %s and ifnull(sed.t_warehouse, '') != ''
|
|
||||||
""", (item.item_code, self.work_order), as_dict=1)
|
|
||||||
|
|
||||||
stock_qty = flt(item.qty)
|
|
||||||
trans_qty = flt(transferred_materials[0].qty)
|
|
||||||
if req_items:
|
|
||||||
if stock_qty > trans_qty:
|
|
||||||
item_code.append(item.item_code)
|
|
||||||
|
|
||||||
def validate_fg_completed_qty(self):
|
def validate_fg_completed_qty(self):
|
||||||
if self.purpose == "Manufacture" and self.work_order:
|
if self.purpose == "Manufacture" and self.work_order:
|
||||||
production_item = frappe.get_value('Work Order', self.work_order, 'production_item')
|
production_item = frappe.get_value('Work Order', self.work_order, 'production_item')
|
||||||
@@ -600,6 +594,15 @@ class StockEntry(StockController):
|
|||||||
if not row.subcontracted_item:
|
if not row.subcontracted_item:
|
||||||
frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}")
|
frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}")
|
||||||
.format(row.idx, frappe.bold(row.item_code)))
|
.format(row.idx, frappe.bold(row.item_code)))
|
||||||
|
elif not row.po_detail:
|
||||||
|
filters = {
|
||||||
|
"parent": self.purchase_order, "docstatus": 1,
|
||||||
|
"rm_item_code": row.item_code, "main_item_code": row.subcontracted_item
|
||||||
|
}
|
||||||
|
|
||||||
|
po_detail = frappe.db.get_value("Purchase Order Item Supplied", filters, "name")
|
||||||
|
if po_detail:
|
||||||
|
row.db_set("po_detail", po_detail)
|
||||||
|
|
||||||
def validate_bom(self):
|
def validate_bom(self):
|
||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
@@ -1110,7 +1113,10 @@ class StockEntry(StockController):
|
|||||||
for d in backflushed_materials.get(item.item_code):
|
for d in backflushed_materials.get(item.item_code):
|
||||||
if d.get(item.warehouse):
|
if d.get(item.warehouse):
|
||||||
if (qty > req_qty):
|
if (qty > req_qty):
|
||||||
qty-= d.get(item.warehouse)
|
qty = (qty/trans_qty) * flt(self.fg_completed_qty)
|
||||||
|
|
||||||
|
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
|
||||||
|
qty = frappe.utils.ceil(qty)
|
||||||
|
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
self.add_to_stock_entry_detail({
|
self.add_to_stock_entry_detail({
|
||||||
@@ -1191,8 +1197,6 @@ class StockEntry(StockController):
|
|||||||
return item_dict
|
return item_dict
|
||||||
|
|
||||||
def add_to_stock_entry_detail(self, item_dict, bom_no=None):
|
def add_to_stock_entry_detail(self, item_dict, bom_no=None):
|
||||||
cost_center = frappe.db.get_value("Company", self.company, 'cost_center')
|
|
||||||
|
|
||||||
for d in item_dict:
|
for d in item_dict:
|
||||||
stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
|
stock_uom = item_dict[d].get("stock_uom") or frappe.db.get_value("Item", d, "stock_uom")
|
||||||
|
|
||||||
@@ -1203,9 +1207,10 @@ class StockEntry(StockController):
|
|||||||
se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom
|
se_child.uom = item_dict[d]["uom"] if item_dict[d].get("uom") else stock_uom
|
||||||
se_child.stock_uom = stock_uom
|
se_child.stock_uom = stock_uom
|
||||||
se_child.qty = flt(item_dict[d]["qty"], se_child.precision("qty"))
|
se_child.qty = flt(item_dict[d]["qty"], se_child.precision("qty"))
|
||||||
se_child.cost_center = item_dict[d].get("cost_center") or cost_center
|
|
||||||
se_child.allow_alternative_item = item_dict[d].get("allow_alternative_item", 0)
|
se_child.allow_alternative_item = item_dict[d].get("allow_alternative_item", 0)
|
||||||
se_child.subcontracted_item = item_dict[d].get("main_item_code")
|
se_child.subcontracted_item = item_dict[d].get("main_item_code")
|
||||||
|
se_child.cost_center = (item_dict[d].get("cost_center") or
|
||||||
|
get_default_cost_center(item_dict[d], company = self.company))
|
||||||
|
|
||||||
for field in ["idx", "po_detail", "original_item",
|
for field in ["idx", "po_detail", "original_item",
|
||||||
"expense_account", "description", "item_name"]:
|
"expense_account", "description", "item_name"]:
|
||||||
@@ -1309,37 +1314,7 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def update_transferred_qty(self):
|
def update_transferred_qty(self):
|
||||||
if self.purpose == 'Receive at Warehouse':
|
if self.purpose == 'Receive at Warehouse':
|
||||||
stock_entries = {}
|
self.status_updater.append({
|
||||||
stock_entries_child_list = []
|
|
||||||
for d in self.items:
|
|
||||||
if not (d.against_stock_entry and d.ste_detail):
|
|
||||||
continue
|
|
||||||
|
|
||||||
stock_entries_child_list.append(d.ste_detail)
|
|
||||||
transferred_qty = frappe.get_all("Stock Entry Detail", fields = ["sum(qty) as qty"],
|
|
||||||
filters = { 'against_stock_entry': d.against_stock_entry,
|
|
||||||
'ste_detail': d.ste_detail,'docstatus': 1})
|
|
||||||
|
|
||||||
stock_entries[(d.against_stock_entry, d.ste_detail)] = (transferred_qty[0].qty
|
|
||||||
if transferred_qty and transferred_qty[0] else 0.0) or 0.0
|
|
||||||
|
|
||||||
if not stock_entries: return None
|
|
||||||
|
|
||||||
cond = ''
|
|
||||||
for data, transferred_qty in stock_entries.items():
|
|
||||||
cond += """ WHEN (parent = %s and name = %s) THEN %s
|
|
||||||
""" %(frappe.db.escape(data[0]), frappe.db.escape(data[1]), transferred_qty)
|
|
||||||
|
|
||||||
if cond and stock_entries_child_list:
|
|
||||||
frappe.db.sql(""" UPDATE `tabStock Entry Detail`
|
|
||||||
SET
|
|
||||||
transferred_qty = CASE {cond} END
|
|
||||||
WHERE
|
|
||||||
name in ({ste_details}) """.format(cond=cond,
|
|
||||||
ste_details = ','.join(['%s'] * len(stock_entries_child_list))),
|
|
||||||
tuple(stock_entries_child_list))
|
|
||||||
|
|
||||||
args = {
|
|
||||||
'source_dt': 'Stock Entry Detail',
|
'source_dt': 'Stock Entry Detail',
|
||||||
'target_field': 'transferred_qty',
|
'target_field': 'transferred_qty',
|
||||||
'target_ref_field': 'qty',
|
'target_ref_field': 'qty',
|
||||||
@@ -1348,10 +1323,11 @@ class StockEntry(StockController):
|
|||||||
'target_parent_dt': 'Stock Entry',
|
'target_parent_dt': 'Stock Entry',
|
||||||
'target_parent_field': 'per_transferred',
|
'target_parent_field': 'per_transferred',
|
||||||
'source_field': 'qty',
|
'source_field': 'qty',
|
||||||
'percent_join_field': 'against_stock_entry'
|
'percent_join_field': 'against_stock_entry',
|
||||||
}
|
'no_allowance': 1
|
||||||
|
})
|
||||||
|
|
||||||
self._update_percent_field_in_targets(args, update_modified=True)
|
self.update_prevdoc_status()
|
||||||
|
|
||||||
def update_quality_inspection(self):
|
def update_quality_inspection(self):
|
||||||
if self.inspection_required:
|
if self.inspection_required:
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
|||||||
from erpnext.stock.doctype.item.test_item import set_item_variant_settings, make_item_variant, create_item
|
from erpnext.stock.doctype.item.test_item import set_item_variant_settings, make_item_variant, create_item
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry import (move_sample_to_retention_warehouse,
|
||||||
|
make_stock_in_entry, ExtraMaterialReceived)
|
||||||
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
|
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
from six import iteritems
|
from six import iteritems
|
||||||
@@ -871,6 +872,30 @@ class TestStockEntry(unittest.TestCase):
|
|||||||
doc = frappe.get_doc('Stock Entry', outward_entry.name)
|
doc = frappe.get_doc('Stock Entry', outward_entry.name)
|
||||||
self.assertEqual(doc.per_transferred, 100)
|
self.assertEqual(doc.per_transferred, 100)
|
||||||
|
|
||||||
|
def test_raise_extra_transfer_materials(self):
|
||||||
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
warehouse = "_Test Warehouse FG 1 - _TC"
|
||||||
|
|
||||||
|
if not frappe.db.exists('Warehouse', warehouse):
|
||||||
|
create_warehouse("_Test Warehouse FG 1")
|
||||||
|
|
||||||
|
outward_entry = make_stock_entry(item_code="_Test Item",
|
||||||
|
purpose="Send to Warehouse",
|
||||||
|
source="_Test Warehouse - _TC",
|
||||||
|
target="_Test Warehouse 1 - _TC", qty=50, basic_rate=100)
|
||||||
|
|
||||||
|
inward_entry1 = make_stock_in_entry(outward_entry.name)
|
||||||
|
inward_entry1.items[0].t_warehouse = warehouse
|
||||||
|
inward_entry1.items[0].qty = 25
|
||||||
|
inward_entry1.submit()
|
||||||
|
|
||||||
|
inward_entry2 = make_stock_in_entry(outward_entry.name)
|
||||||
|
inward_entry2.items[0].t_warehouse = warehouse
|
||||||
|
inward_entry2.items[0].qty = 35
|
||||||
|
|
||||||
|
self.assertRaises(ExtraMaterialReceived, inward_entry2.submit)
|
||||||
|
print(inward_entry2.name)
|
||||||
|
|
||||||
def test_gle_for_opening_stock_entry(self):
|
def test_gle_for_opening_stock_entry(self):
|
||||||
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
|
mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
|
||||||
|
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
|
|
||||||
serialized_items = False
|
serialized_items = []
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
item = frappe.get_cached_doc("Item", row.item_code)
|
item = frappe.get_cached_doc("Item", row.item_code)
|
||||||
if not (item.has_serial_no):
|
if not (item.has_serial_no):
|
||||||
@@ -229,27 +229,29 @@ class StockReconciliation(StockController):
|
|||||||
sl_entries.append(sle_data)
|
sl_entries.append(sle_data)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
serialized_items = True
|
serialized_items.append(row.item_code)
|
||||||
|
|
||||||
if serialized_items:
|
if serialized_items:
|
||||||
self.get_sle_for_serialized_items(sl_entries)
|
self.get_sle_for_serialized_items(sl_entries, serialized_items)
|
||||||
|
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
allow_negative_stock = frappe.get_cached_value("Stock Settings", None, "allow_negative_stock")
|
allow_negative_stock = frappe.get_cached_value("Stock Settings", None, "allow_negative_stock")
|
||||||
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock)
|
||||||
|
|
||||||
def get_sle_for_serialized_items(self, sl_entries):
|
def get_sle_for_serialized_items(self, sl_entries, serialized_items=[]):
|
||||||
self.issue_existing_serial_and_batch(sl_entries)
|
self.issue_existing_serial_and_batch(sl_entries, serialized_items)
|
||||||
self.add_new_serial_and_batch(sl_entries)
|
self.add_new_serial_and_batch(sl_entries, serialized_items)
|
||||||
self.update_valuation_rate_for_serial_no()
|
self.update_valuation_rate_for_serial_no(serialized_items)
|
||||||
|
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
|
||||||
|
|
||||||
def issue_existing_serial_and_batch(self, sl_entries):
|
def issue_existing_serial_and_batch(self, sl_entries, serialized_items=[]):
|
||||||
from erpnext.stock.stock_ledger import get_stock_ledger_entries
|
from erpnext.stock.stock_ledger import get_stock_ledger_entries
|
||||||
|
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
|
if row.item_code not in serialized_items: continue
|
||||||
|
|
||||||
serial_nos = get_serial_nos(row.serial_no) or []
|
serial_nos = get_serial_nos(row.serial_no) or []
|
||||||
|
|
||||||
# To issue existing serial nos
|
# To issue existing serial nos
|
||||||
@@ -303,8 +305,10 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
sl_entries.append(new_args)
|
sl_entries.append(new_args)
|
||||||
|
|
||||||
def add_new_serial_and_batch(self, sl_entries):
|
def add_new_serial_and_batch(self, sl_entries, serialized_items=[]):
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
|
if row.item_code not in serialized_items: continue
|
||||||
|
|
||||||
if row.qty:
|
if row.qty:
|
||||||
args = self.get_sle_for_items(row)
|
args = self.get_sle_for_items(row)
|
||||||
|
|
||||||
@@ -316,9 +320,9 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
sl_entries.append(args)
|
sl_entries.append(args)
|
||||||
|
|
||||||
def update_valuation_rate_for_serial_no(self):
|
def update_valuation_rate_for_serial_no(self, serialized_items=[]):
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if not d.serial_no: continue
|
if d.item_code not in serialized_items: continue
|
||||||
|
|
||||||
serial_nos = get_serial_nos(d.serial_no)
|
serial_nos = get_serial_nos(d.serial_no)
|
||||||
self.update_valuation_rate_for_serial_nos(d, serial_nos)
|
self.update_valuation_rate_for_serial_nos(d, serial_nos)
|
||||||
@@ -372,7 +376,16 @@ class StockReconciliation(StockController):
|
|||||||
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
|
where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name))
|
||||||
|
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
self.get_sle_for_serialized_items(sl_entries)
|
|
||||||
|
serialized_items = []
|
||||||
|
|
||||||
|
for row in self.items:
|
||||||
|
has_serial_no = frappe.get_cached_value("Item", row.item_code, "has_serial_no")
|
||||||
|
if has_serial_no:
|
||||||
|
serialized_items.append(row.item_code)
|
||||||
|
|
||||||
|
if serialized_items:
|
||||||
|
self.get_sle_for_serialized_items(sl_entries, serialized_items)
|
||||||
|
|
||||||
if sl_entries:
|
if sl_entries:
|
||||||
sl_entries.reverse()
|
sl_entries.reverse()
|
||||||
|
|||||||
@@ -207,9 +207,9 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
def test_stock_reco_for_serial_and_batch_item(self):
|
def test_stock_reco_for_serial_and_batch_item(self):
|
||||||
set_perpetual_inventory()
|
set_perpetual_inventory()
|
||||||
|
|
||||||
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
|
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item 1'})
|
||||||
if not item:
|
if not item:
|
||||||
item = create_item("Batched and Serialised Item")
|
item = create_item("Batched and Serialised Item 1")
|
||||||
item.has_batch_no = 1
|
item.has_batch_no = 1
|
||||||
item.create_new_batch = 1
|
item.create_new_batch = 1
|
||||||
item.has_serial_no = 1
|
item.has_serial_no = 1
|
||||||
@@ -217,7 +217,7 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
item.serial_no_series = "S-.####"
|
item.serial_no_series = "S-.####"
|
||||||
item.save()
|
item.save()
|
||||||
else:
|
else:
|
||||||
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
|
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item 1'})
|
||||||
|
|
||||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||||
|
|
||||||
@@ -236,7 +236,7 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
self.assertEqual(frappe.db.exists("Batch", batch_no), None)
|
||||||
|
|
||||||
if frappe.db.exists("Serial No", serial_nos[0]):
|
if frappe.db.exists("Serial No", serial_nos[0]):
|
||||||
frappe.delete_doc("Serial No", serial_nos[0])
|
frappe.delete_doc("Serial No", serial_nos[0])
|
||||||
|
|
||||||
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
|
def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
|
||||||
"""
|
"""
|
||||||
@@ -255,9 +255,9 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
|
|
||||||
set_perpetual_inventory()
|
set_perpetual_inventory()
|
||||||
|
|
||||||
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
|
item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item 1'})
|
||||||
if not item:
|
if not item:
|
||||||
item = create_item("Batched and Serialised Item")
|
item = create_item("Batched and Serialised Item 1")
|
||||||
item.has_batch_no = 1
|
item.has_batch_no = 1
|
||||||
item.create_new_batch = 1
|
item.create_new_batch = 1
|
||||||
item.has_serial_no = 1
|
item.has_serial_no = 1
|
||||||
@@ -265,7 +265,7 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
item.serial_no_series = "S-.####"
|
item.serial_no_series = "S-.####"
|
||||||
item.save()
|
item.save()
|
||||||
else:
|
else:
|
||||||
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
|
item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item 1'})
|
||||||
|
|
||||||
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
|
||||||
|
|
||||||
@@ -392,6 +392,35 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
doc.cancel()
|
doc.cancel()
|
||||||
frappe.delete_doc(doc.doctype, doc.name)
|
frappe.delete_doc(doc.doctype, doc.name)
|
||||||
|
|
||||||
|
def test_stock_reco_with_serial_and_batch(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
warehouse = "_Test Warehouse for Stock Reco1 - _TC"
|
||||||
|
ste1=make_stock_entry(item_code="Stock-Reco-Serial-Item-1",
|
||||||
|
target=warehouse, qty=2, basic_rate=100)
|
||||||
|
|
||||||
|
ste2=make_stock_entry(item_code="Stock-Reco-batch-Item-1",
|
||||||
|
target=warehouse, qty=2, basic_rate=100)
|
||||||
|
|
||||||
|
sr = create_stock_reconciliation(item_code="Stock-Reco-Serial-Item-1",
|
||||||
|
warehouse = warehouse, rate=200, do_not_submit=True)
|
||||||
|
|
||||||
|
sr.append("items", {
|
||||||
|
"item_code": "Stock-Reco-batch-Item-1",
|
||||||
|
"warehouse": warehouse,
|
||||||
|
"batch_no": ste2.items[0].batch_no,
|
||||||
|
"valuation_rate": 200
|
||||||
|
})
|
||||||
|
|
||||||
|
sr.submit()
|
||||||
|
sle = frappe.get_all("Stock Ledger Entry", filters={"item_code": "Stock-Reco-batch-Item-1",
|
||||||
|
"warehouse": warehouse, "voucher_no": sr.name, "voucher_type": sr.doctype})
|
||||||
|
|
||||||
|
self.assertEquals(len(sle), 1)
|
||||||
|
|
||||||
|
for doc in [sr, ste2, ste1]:
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
def insert_existing_sle(warehouse):
|
def insert_existing_sle(warehouse):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
|||||||
@@ -527,23 +527,40 @@ def get_default_deferred_account(args, item, fieldname=None):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_default_cost_center(args, item, item_group, brand, company=None):
|
def get_default_cost_center(args, item=None, item_group=None, brand=None, company=None):
|
||||||
cost_center = None
|
cost_center = None
|
||||||
|
|
||||||
|
if not company and args.get("company"):
|
||||||
|
company = args.get("company")
|
||||||
|
|
||||||
if args.get('project'):
|
if args.get('project'):
|
||||||
cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True)
|
cost_center = frappe.db.get_value("Project", args.get("project"), "cost_center", cache=True)
|
||||||
|
|
||||||
if not cost_center:
|
if not cost_center and (item and item_group and brand):
|
||||||
if args.get('customer'):
|
if args.get('customer'):
|
||||||
cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center')
|
cost_center = item.get('selling_cost_center') or item_group.get('selling_cost_center') or brand.get('selling_cost_center')
|
||||||
else:
|
else:
|
||||||
cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center')
|
cost_center = item.get('buying_cost_center') or item_group.get('buying_cost_center') or brand.get('buying_cost_center')
|
||||||
|
|
||||||
cost_center = cost_center or args.get("cost_center")
|
elif not cost_center and args.get("item_code") and company:
|
||||||
|
for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]:
|
||||||
|
path = "erpnext.stock.get_item_details.{0}".format(method)
|
||||||
|
data = frappe.get_attr(path)(args.get("item_code"), company)
|
||||||
|
|
||||||
|
if data and (data.selling_cost_center or data.buying_cost_center):
|
||||||
|
return data.selling_cost_center or data.buying_cost_center
|
||||||
|
|
||||||
|
if not cost_center and args.get("cost_center"):
|
||||||
|
cost_center = args.get("cost_center")
|
||||||
|
|
||||||
if (company and cost_center
|
if (company and cost_center
|
||||||
and frappe.get_cached_value("Cost Center", cost_center, "company") != company):
|
and frappe.get_cached_value("Cost Center", cost_center, "company") != company):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
if not cost_center and company:
|
||||||
|
cost_center = frappe.get_cached_value("Company",
|
||||||
|
company, "cost_center")
|
||||||
|
|
||||||
return cost_center
|
return cost_center
|
||||||
|
|
||||||
def get_default_supplier(args, item, item_group, brand):
|
def get_default_supplier(args, item, item_group, brand):
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def execute(filters=None):
|
|||||||
|
|
||||||
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
|
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
|
||||||
details = item_dict["details"]
|
details = item_dict["details"]
|
||||||
if not fifo_queue and (not item_dict.get("total_qty")): continue
|
if not fifo_queue: continue
|
||||||
|
|
||||||
average_age = get_average_age(fifo_queue, to_date)
|
average_age = get_average_age(fifo_queue, to_date)
|
||||||
|
|
||||||
|
|||||||
@@ -284,7 +284,6 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
|
|||||||
return
|
return
|
||||||
|
|
||||||
convertible_cols = {}
|
convertible_cols = {}
|
||||||
|
|
||||||
is_dict_obj = False
|
is_dict_obj = False
|
||||||
if isinstance(result[0], dict):
|
if isinstance(result[0], dict):
|
||||||
is_dict_obj = True
|
is_dict_obj = True
|
||||||
@@ -306,13 +305,13 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto
|
|||||||
for row_idx, row in enumerate(result):
|
for row_idx, row in enumerate(result):
|
||||||
data = row.items() if is_dict_obj else enumerate(row)
|
data = row.items() if is_dict_obj else enumerate(row)
|
||||||
for key, value in data:
|
for key, value in data:
|
||||||
if not key in convertible_columns or not conversion_factors[row_idx]:
|
if key not in convertible_columns or not conversion_factors[row_idx-1]:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if convertible_columns.get(key) == 'rate':
|
if convertible_columns.get(key) == 'rate':
|
||||||
new_value = flt(value) * conversion_factors[row_idx]
|
new_value = flt(value) * conversion_factors[row_idx-1]
|
||||||
else:
|
else:
|
||||||
new_value = flt(value) / conversion_factors[row_idx]
|
new_value = flt(value) / conversion_factors[row_idx-1]
|
||||||
|
|
||||||
if not is_dict_obj:
|
if not is_dict_obj:
|
||||||
row.insert(key+1, new_value)
|
row.insert(key+1, new_value)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<p class='lead'>{{ education_settings.description }}</p>
|
<p class='lead'>{{ education_settings.description }}</p>
|
||||||
<p class="mt-4">
|
<p class="mt-4">
|
||||||
{% if frappe.session.user == 'Guest' %}
|
{% if frappe.session.user == 'Guest' %}
|
||||||
<a class="btn btn-primary btn-lg" href="'/login#signup'">{{_('Sign Up')}}</a>
|
<a class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user