mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-25 22:21:31 +01:00
Merge branch 'version-12-hotfix' into v12-pre-release
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
frappe.provide('erpnext.integrations');
|
||||
|
||||
frappe.ui.form.on('Bank', {
|
||||
onload: function(frm) {
|
||||
@@ -7,6 +8,12 @@ frappe.ui.form.on('Bank', {
|
||||
},
|
||||
refresh: function(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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,4 +34,80 @@ let add_fields_to_mapping_table = function (frm) {
|
||||
frm.doc.name).options = options;
|
||||
|
||||
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' });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -342,8 +342,14 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
||||
pricing_rule_rate = 0.0
|
||||
if pricing_rule.currency == args.currency:
|
||||
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({
|
||||
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
|
||||
"discount_percentage": 0.0
|
||||
})
|
||||
|
||||
|
||||
@@ -385,7 +385,7 @@ class TestPricingRule(unittest.TestCase):
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.items[1].is_free_item, 1)
|
||||
self.assertEqual(so.items[1].item_code, "_Test Item 2")
|
||||
|
||||
|
||||
def test_cumulative_pricing_rule(self):
|
||||
frappe.delete_doc_if_exists('Pricing Rule', '_Test Cumulative Pricing Rule')
|
||||
test_record = {
|
||||
@@ -430,6 +430,43 @@ class TestPricingRule(unittest.TestCase):
|
||||
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
|
||||
item_code: function(frm) {
|
||||
if(frm.doc.item_code) {
|
||||
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
|
||||
frm.trigger('set_finance_book');
|
||||
}
|
||||
},
|
||||
@@ -323,6 +323,7 @@ frappe.ui.form.on('Asset', {
|
||||
|
||||
calculate_depreciation: function(frm) {
|
||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||
frm.trigger('set_finance_book');
|
||||
},
|
||||
|
||||
gross_purchase_amount: function(frm) {
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Depreciation Posting Date",
|
||||
"mandatory_depends_on": "eval:parent.doctype == 'Asset'",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -86,7 +87,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-09-16 12:11:30.631788",
|
||||
"modified": "2020-10-30 15:22:29.119868",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
||||
@@ -29,21 +29,32 @@ class PlaidConnector():
|
||||
response = self.client.Item.public_token.exchange(public_token)
|
||||
access_token = response["access_token"]
|
||||
return access_token
|
||||
|
||||
def get_link_token(self):
|
||||
token_request = {
|
||||
|
||||
def get_token_request(self, update_mode=False):
|
||||
args = {
|
||||
"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)
|
||||
"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": {
|
||||
"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:
|
||||
response = self.client.LinkToken.create(token_request)
|
||||
except InvalidRequestError:
|
||||
|
||||
@@ -12,7 +12,7 @@ frappe.ui.form.on('Plaid Settings', {
|
||||
|
||||
refresh: function (frm) {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -30,10 +30,18 @@ erpnext.integrations.plaidLink = class plaidLink {
|
||||
this.product = ["auth", "transactions"];
|
||||
this.plaid_env = this.frm.doc.plaid_env;
|
||||
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();
|
||||
}
|
||||
|
||||
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() {
|
||||
const me = this;
|
||||
me.loadScript(me.plaidUrl)
|
||||
@@ -78,8 +86,8 @@ erpnext.integrations.plaidLink = class plaidLink {
|
||||
}
|
||||
|
||||
onScriptError(error) {
|
||||
frappe.msgprint("There was an issue connecting to Plaid's authentication server");
|
||||
frappe.msgprint(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) {
|
||||
@@ -107,4 +115,4 @@ erpnext.integrations.plaidLink = class plaidLink {
|
||||
});
|
||||
}, __("Select a company"), __("Continue"));
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -239,3 +239,8 @@ def automatic_synchronization():
|
||||
bank=plaid_account.bank,
|
||||
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)
|
||||
@@ -15,6 +15,7 @@ class ProductBundle(Document):
|
||||
def validate(self):
|
||||
self.validate_main_item()
|
||||
self.validate_child_items()
|
||||
self.validate_duplicate_packing_item()
|
||||
from erpnext.utilities.transaction_base import validate_uom_is_integer
|
||||
validate_uom_is_integer(self, "uom", "qty")
|
||||
|
||||
@@ -28,6 +29,14 @@ class ProductBundle(Document):
|
||||
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)))
|
||||
|
||||
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.validate_and_sanitize_search_inputs
|
||||
|
||||
@@ -844,7 +844,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address"
|
||||
"shipping_address",
|
||||
"terms"
|
||||
],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1]
|
||||
@@ -863,7 +864,10 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
"field_no_map": [
|
||||
"rate",
|
||||
"price_list_rate",
|
||||
"item_tax_template"
|
||||
"item_tax_template",
|
||||
"discount_percentage",
|
||||
"discount_amount",
|
||||
"pricing_rules"
|
||||
],
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
|
||||
@@ -917,7 +921,8 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address"
|
||||
"shipping_address",
|
||||
"terms"
|
||||
],
|
||||
"validation": {
|
||||
"docstatus": ["=", 1]
|
||||
@@ -937,7 +942,10 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
"rate",
|
||||
"price_list_rate",
|
||||
"item_tax_template",
|
||||
"supplier"
|
||||
"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
|
||||
|
||||
Reference in New Issue
Block a user