Merge branch 'track-changes-for-stock-settings-v12' of https://github.com/pateljannat/erpnext into track-changes-for-stock-settings-v12

This commit is contained in:
pateljannat
2020-12-17 13:26:21 +05:30
70 changed files with 1307 additions and 503 deletions

View File

@@ -6,8 +6,8 @@ import frappe, json
from frappe import _ from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute from erpnext.accounts.report.general_ledger.general_ledger import execute
from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan from frappe.core.page.dashboard.dashboard import cache_source
from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of from frappe.utils.nestedset import get_descendants_of

View File

@@ -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);
});
}
} }
}); });
@@ -27,4 +34,80 @@ let add_fields_to_mapping_table = function (frm) {
frm.doc.name).options = options; frm.doc.name).options = options;
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' });
}
};

View File

@@ -9,11 +9,13 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Item", "Cost Center"] test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase): class TestBankTransaction(unittest.TestCase):
def setUp(self): def setUp(self):
make_pos_profile()
add_transactions() add_transactions()
add_payments() add_payments()
@@ -27,6 +29,9 @@ class TestBankTransaction(unittest.TestCase):
frappe.db.sql("""delete from `tabPayment Entry Reference`""") frappe.db.sql("""delete from `tabPayment Entry Reference`""")
frappe.db.sql("""delete from `tabPayment Entry`""") frappe.db.sql("""delete from `tabPayment Entry`""")
# Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`")
frappe.flags.test_bank_transactions_created = False frappe.flags.test_bank_transactions_created = False
frappe.flags.test_payments_created = False frappe.flags.test_payments_created = False

View File

@@ -181,7 +181,8 @@ class OpeningInvoiceCreationTool(Document):
"due_date": row.due_date, "due_date": row.due_date,
"posting_date": row.posting_date, "posting_date": row.posting_date,
frappe.scrub(party_type): row.party, frappe.scrub(party_type): row.party,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice" "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0
}) })
accounting_dimension = get_accounting_dimensions() accounting_dimension = get_accounting_dimensions()

View File

@@ -7,17 +7,25 @@ import frappe
import unittest import unittest
test_dependencies = ["Customer", "Supplier"] test_dependencies = ["Customer", "Supplier"]
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
from erpnext.controllers.accounts_controller import AccountMissingError
class TestOpeningInvoiceCreationTool(unittest.TestCase): class TestOpeningInvoiceCreationTool(unittest.TestCase):
def make_invoices(self, invoice_type="Sales"): def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
doc = frappe.get_single("Opening Invoice Creation Tool") doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type) args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2)
doc.update(args) doc.update(args)
return doc.make_invoices() return doc.make_invoices()
def test_opening_sales_invoice_creation(self): def test_opening_sales_invoice_creation(self):
invoices = self.make_invoices() property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
@@ -27,6 +35,13 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
} }
self.check_expected_values(invoices, expected_value) self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled
self.assertEqual(si.update_stock, 0)
property_setter.delete()
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@@ -36,7 +51,7 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx]) self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_purchase_invoice_creation(self): def test_opening_purchase_invoice_creation(self):
invoices = self.make_invoices(invoice_type="Purchase") invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
@@ -46,6 +61,28 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
} }
self.check_expected_values(invoices, expected_value, invoice_type="Purchase", ) self.check_expected_values(invoices, expected_value, invoice_type="Purchase", )
def test_opening_sales_invoice_creation_with_missing_debit_account(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
"is_group": 1, "company": "_Test Opening Invoice Company"})
cc.insert(ignore_mandatory=True)
cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
"company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
self.assertRaises(AccountMissingError, self.make_invoices, company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def get_opening_invoice_creation_dict(**args): def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company") company = args.get("company", "_Test Company")
@@ -57,7 +94,7 @@ def get_opening_invoice_creation_dict(**args):
{ {
"qty": 1.0, "qty": 1.0,
"outstanding_amount": 300, "outstanding_amount": 300,
"party": "_Test {0}".format(party), "party": args.get("party_1") or "_Test {0}".format(party),
"item_name": "Opening Item", "item_name": "Opening Item",
"due_date": "2016-09-10", "due_date": "2016-09-10",
"posting_date": "2016-09-05", "posting_date": "2016-09-05",
@@ -66,7 +103,7 @@ def get_opening_invoice_creation_dict(**args):
{ {
"qty": 2.0, "qty": 2.0,
"outstanding_amount": 250, "outstanding_amount": 250,
"party": "_Test {0} 1".format(party), "party": args.get("party_2") or "_Test {0} 1".format(party),
"item_name": "Opening Item", "item_name": "Opening Item",
"due_date": "2016-09-10", "due_date": "2016-09-10",
"posting_date": "2016-09-05", "posting_date": "2016-09-05",
@@ -76,4 +113,31 @@ def get_opening_invoice_creation_dict(**args):
}) })
invoice_dict.update(args) invoice_dict.update(args)
return invoice_dict return invoice_dict
def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company")
company = frappe.new_doc("Company")
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
company.country = "India"
company.insert()
return company
def make_customer(customer=None):
customer_name = customer or "Opening Customer"
customer = frappe.get_doc({
"doctype": "Customer",
"customer_name": customer_name,
"customer_group": "All Customer Groups",
"customer_type": "Company",
"territory": "All Territories"
})
if not frappe.db.exists("Customer", customer_name):
customer.insert(ignore_permissions=True)
return customer.name
else:
return frappe.db.exists("Customer", customer_name)

View File

@@ -1,12 +1,14 @@
frappe.listview_settings['Payment Entry'] = { frappe.listview_settings['Payment Entry'] = {
onload: function(listview) { onload: function(listview) {
listview.page.fields_dict.party_type.get_query = function() { if (listview.page.fields_dict.party_type) {
return { listview.page.fields_dict.party_type.get_query = function() {
"filters": { return {
"name": ["in", Object.keys(frappe.boot.party_account_types)], "filters": {
} "name": ["in", Object.keys(frappe.boot.party_account_types)],
}
};
}; };
}; }
} }
}; };

View File

@@ -37,6 +37,11 @@ frappe.ui.form.on("Payment Reconciliation Payment", {
erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({
onload: function() { onload: function() {
var me = this; var me = this;
this.frm.set_query("party", function() {
check_mandatory(me.frm);
});
this.frm.set_query("party_type", function() { this.frm.set_query("party_type", function() {
return { return {
"filters": { "filters": {
@@ -46,37 +51,39 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
}); });
this.frm.set_query('receivable_payable_account', function() { this.frm.set_query('receivable_payable_account', function() {
if(!me.frm.doc.company || !me.frm.doc.party_type) { check_mandatory(me.frm);
frappe.msgprint(__("Please select Company and Party Type first")); return {
} else { filters: {
return{ "company": me.frm.doc.company,
filters: { "is_group": 0,
"company": me.frm.doc.company, "account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
"is_group": 0, }
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type] };
}
};
}
}); });
this.frm.set_query('bank_cash_account', function() { this.frm.set_query('bank_cash_account', function() {
if(!me.frm.doc.company) { check_mandatory(me.frm, true);
frappe.msgprint(__("Please select Company first")); return {
} else { filters:[
return{ ['Account', 'company', '=', me.frm.doc.company],
filters:[ ['Account', 'is_group', '=', 0],
['Account', 'company', '=', me.frm.doc.company], ['Account', 'account_type', 'in', ['Bank', 'Cash']]
['Account', 'is_group', '=', 0], ]
['Account', 'account_type', 'in', ['Bank', 'Cash']] };
]
};
}
}); });
this.frm.set_value('party_type', ''); this.frm.set_value('party_type', '');
this.frm.set_value('party', ''); this.frm.set_value('party', '');
this.frm.set_value('receivable_payable_account', ''); this.frm.set_value('receivable_payable_account', '');
var check_mandatory = (frm, only_company=false) => {
var title = __("Mandatory");
if (only_company && !frm.doc.company) {
frappe.throw({message: __("Please Select a Company First"), title: title});
} else if (!frm.doc.company || !frm.doc.party_type) {
frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title});
}
};
}, },
refresh: function() { refresh: function() {
@@ -90,7 +97,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
party: function() { party: function() {
var me = this var me = this
if(!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
return frappe.call({ return frappe.call({
method: "erpnext.accounts.party.get_party_account", method: "erpnext.accounts.party.get_party_account",
args: { args: {
@@ -99,7 +106,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
party: me.frm.doc.party party: me.frm.doc.party
}, },
callback: function(r) { callback: function(r) {
if(!r.exc && r.message) { if (!r.exc && r.message) {
me.frm.set_value("receivable_payable_account", r.message); me.frm.set_value("receivable_payable_account", r.message);
} }
} }

View File

@@ -404,6 +404,7 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"default": "0",
"depends_on": "eval:doc.rate_or_discount==\"Rate\"", "depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
@@ -467,6 +468,7 @@
"options": "UOM" "options": "UOM"
}, },
{ {
"description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate", "fieldname": "free_item_rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rate" "label": "Rate"
@@ -554,7 +556,8 @@
], ],
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"modified": "2019-12-18 17:29:22.957077", "links": [],
"modified": "2020-12-04 00:36:24.698219",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@@ -342,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
}) })

View File

@@ -385,7 +385,7 @@ class TestPricingRule(unittest.TestCase):
so.load_from_db() so.load_from_db()
self.assertEqual(so.items[1].is_free_item, 1) self.assertEqual(so.items[1].is_free_item, 1)
self.assertEqual(so.items[1].item_code, "_Test Item 2") self.assertEqual(so.items[1].item_code, "_Test Item 2")
def test_cumulative_pricing_rule(self): def test_cumulative_pricing_rule(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Cumulative Pricing Rule') frappe.delete_doc_if_exists('Pricing Rule', '_Test Cumulative Pricing Rule')
test_record = { test_record = {
@@ -430,6 +430,59 @@ 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 test_pricing_rule_for_transaction(self):
make_item("Water Flask 1")
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product",
apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
si = create_sales_invoice(qty=5, do_not_submit=True)
self.assertEquals(len(si.items), 2)
self.assertEquals(si.items[1].rate, 10)
si1 = create_sales_invoice(qty=2, do_not_submit=True)
self.assertEquals(len(si1.items), 1)
for doc in [si, si1]:
doc.delete()
def make_pricing_rule(**args): def make_pricing_rule(**args):
args = frappe._dict(args) args = frappe._dict(args)
@@ -447,15 +500,23 @@ def make_pricing_rule(**args):
"rate_or_discount": args.rate_or_discount or "Discount Percentage", "rate_or_discount": args.rate_or_discount or "Discount Percentage",
"discount_percentage": args.discount_percentage or 0.0, "discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0, "rate": args.rate or 0.0,
"margin_type": args.margin_type, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0 "condition": args.condition or '',
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
}) })
for field in ["free_item", "free_qty", "free_item_rate", "priority",
"margin_type", "price_or_product_discount"]:
if args.get(field):
doc.set(field, args.get(field))
apply_on = doc.apply_on.replace(' ', '_').lower() apply_on = doc.apply_on.replace(' ', '_').lower()
child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'} child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
doc.append(child_table.get(doc.apply_on), {
apply_on: args.get(apply_on) or "_Test Item" if doc.apply_on != "Transaction":
}) doc.append(child_table.get(doc.apply_on), {
apply_on: args.get(apply_on) or "_Test Item"
})
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)
if args.get(apply_on) and apply_on != "item_code": if args.get(apply_on) and apply_on != "item_code":

View File

@@ -453,6 +453,9 @@ def apply_pricing_rule_on_transaction(doc):
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty,
doc.total, pricing_rules) doc.total, pricing_rules)
if not pricing_rules:
remove_free_item(doc)
for d in pricing_rules: for d in pricing_rules:
if d.price_or_product_discount == 'Price': if d.price_or_product_discount == 'Price':
if d.apply_discount_on: if d.apply_discount_on:
@@ -476,6 +479,12 @@ def apply_pricing_rule_on_transaction(doc):
get_product_discount_rule(d, item_details, doc=doc) get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data) apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values() doc.set_missing_values()
doc.calculate_taxes_and_totals()
def remove_free_item(doc):
for d in doc.items:
if d.is_free_item:
doc.remove(d)
def get_applied_pricing_rules(pricing_rules): def get_applied_pricing_rules(pricing_rules):
if pricing_rules: if pricing_rules:
@@ -488,7 +497,7 @@ def get_applied_pricing_rules(pricing_rules):
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item free_item = pricing_rule.free_item
if pricing_rule.same_item: if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction':
free_item = item_details.item_code or args.item_code free_item = item_details.item_code or args.item_code
if not free_item: if not free_item:
@@ -517,13 +526,17 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
if item_details.get("parenttype") == 'Sales Order': if item_details.get("parenttype") == 'Sales Order':
item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today() item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today()
company = args.get('company') or doc.company company = doc.company
item_details.free_item_data['income_account'] = get_default_income_account( if args and args.get("company"):
args=args, company = args.get("company")
item=get_item_defaults(free_item, company),
item_group=get_item_group_defaults(free_item, company), if args:
brand=get_brand_defaults(free_item, company), item_details.free_item_data['income_account'] = get_default_income_account(
) args=args,
item=get_item_defaults(free_item, company),
item_group=get_item_group_defaults(free_item, company),
brand=get_brand_defaults(free_item, company),
)
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
if pricing_rule_args.get('item_code'): if pricing_rule_args.get('item_code'):

View File

@@ -142,6 +142,11 @@ class PurchaseInvoice(BuyingController):
throw(_("Conversion rate cannot be 0 or 1")) throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self): def validate_credit_to_acc(self):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
if not self.credit_to:
self.raise_missing_debit_credit_account_error("Supplier", self.supplier)
account = frappe.db.get_value("Account", self.credit_to, account = frappe.db.get_value("Account", self.credit_to,
["account_type", "report_type", "account_currency"], as_dict=True) ["account_type", "report_type", "account_currency"], as_dict=True)

View File

@@ -398,6 +398,8 @@ class SalesInvoice(SellingController):
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile: if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {} pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name') self.pos_profile = pos_profile.get('name')
pos = {} pos = {}
@@ -467,6 +469,11 @@ class SalesInvoice(SellingController):
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0] return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
def validate_debit_to_acc(self): def validate_debit_to_acc(self):
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
if not self.debit_to:
self.raise_missing_debit_credit_account_error("Customer", self.customer)
account = frappe.get_cached_value("Account", self.debit_to, account = frappe.get_cached_value("Account", self.debit_to,
["account_type", "report_type", "account_currency"], as_dict=True) ["account_type", "report_type", "account_currency"], as_dict=True)
@@ -1395,6 +1402,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):

View File

@@ -690,7 +690,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(gle) self.assertFalse(gle)
def test_pos_gl_entry_with_perpetual_inventory(self): def test_pos_gl_entry_with_perpetual_inventory(self):
make_pos_profile() make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
@@ -773,7 +774,8 @@ class TestSalesInvoice(unittest.TestCase):
def test_pos_change_amount(self): def test_pos_change_amount(self):
make_pos_profile() make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
@@ -795,7 +797,8 @@ class TestSalesInvoice(unittest.TestCase):
def test_make_pos_invoice(self): def test_make_pos_invoice(self):
from erpnext.accounts.doctype.sales_invoice.pos import make_invoice from erpnext.accounts.doctype.sales_invoice.pos import make_invoice
pos_profile = make_pos_profile() pos_profile = make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",supplier_warehouse= "Work In Progress - TCP1", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True) pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True)

View File

@@ -13,6 +13,7 @@
"cancelation_date", "cancelation_date",
"trial_period_start", "trial_period_start",
"trial_period_end", "trial_period_end",
"generate_new_invoices_past_due_date",
"column_break_11", "column_break_11",
"current_invoice_start", "current_invoice_start",
"current_invoice_end", "current_invoice_end",
@@ -183,8 +184,7 @@
"fieldname": "invoices", "fieldname": "invoices",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Invoices", "label": "Invoices",
"options": "Subscription Invoice", "options": "Subscription Invoice"
"read_only": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
@@ -195,9 +195,16 @@
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"default": "0",
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
"fieldname": "generate_new_invoices_past_due_date",
"fieldtype": "Check",
"label": "Generate New Invoices Past Due Date"
} }
], ],
"modified": "2020-08-27 23:30:02.504042", "modified": "2020-11-29 22:46:14.879289",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription", "name": "Subscription",

View File

@@ -408,6 +408,15 @@ class Subscription(Document):
else: else:
self.set_status_grace_period() self.set_status_grace_period()
if getdate() > getdate(self.current_invoice_end):
self.update_subscription_period(add_days(self.current_invoice_end, 1))
# Generate invoices periodically even if current invoice are unpaid
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
or self.is_prepaid_to_invoice()):
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate)
@staticmethod @staticmethod
def is_not_outstanding(invoice): def is_not_outstanding(invoice):
""" """

View File

@@ -260,7 +260,11 @@ def check_amount_vs_description(amount_matching, description_matching):
continue continue
if "reference_no" in am_match and "reference_no" in des_match: if "reference_no" in am_match and "reference_no" in des_match:
if difflib.SequenceMatcher(lambda x: x == " ", am_match["reference_no"], des_match["reference_no"]).ratio() > 70: # Sequence Matcher does not handle None as input
am_reference = am_match["reference_no"] or ""
des_reference = des_match["reference_no"] or ""
if difflib.SequenceMatcher(lambda x: x == " ", am_reference, des_reference).ratio() > 70:
if am_match not in result: if am_match not in result:
result.append(am_match) result.append(am_match)
if result: if result:

View File

@@ -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
@@ -204,7 +204,7 @@ def set_account_and_due_date(party, account, party_type, company, posting_date,
return out return out
@frappe.whitelist() @frappe.whitelist()
def get_party_account(party_type, party, company): def get_party_account(party_type, party, company=None):
"""Returns the account for the given `party`. """Returns the account for the given `party`.
Will first search in party (Customer / Supplier) record, if not found, Will first search in party (Customer / Supplier) record, if not found,
will search in group (Customer Group / Supplier Group), will search in group (Customer Group / Supplier Group),
@@ -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

View File

@@ -42,11 +42,13 @@
{% if(filters.show_future_payments) { %} {% if(filters.show_future_payments) { %}
{% var balance_row = data.slice(-1).pop(); {% var balance_row = data.slice(-1).pop();
var range1 = report.columns[11].label; var start = filters.based_on_payment_terms ? 13 : 11;
var range2 = report.columns[12].label; var range1 = report.columns[start].label;
var range3 = report.columns[13].label; var range2 = report.columns[start+1].label;
var range4 = report.columns[14].label; var range3 = report.columns[start+2].label;
var range5 = report.columns[15].label; var range4 = report.columns[start+3].label;
var range5 = report.columns[start+4].label;
var range6 = report.columns[start+5].label;
%} %}
{% if(balance_row) { %} {% if(balance_row) { %}
<table class="table table-bordered table-condensed"> <table class="table table-bordered table-condensed">
@@ -70,20 +72,34 @@
<th>{%= __(range3) %}</th> <th>{%= __(range3) %}</th>
<th>{%= __(range4) %}</th> <th>{%= __(range4) %}</th>
<th>{%= __(range5) %}</th> <th>{%= __(range5) %}</th>
<th>{%= __(range6) %}</th>
<th>{%= __("Total") %}</th> <th>{%= __("Total") %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{%= __("Total Outstanding") %}</td> <td>{%= __("Total Outstanding") %}</td>
<td class="text-right">{%= format_number(balance_row["range1"], null, 2) %}</td> <td class="text-right">
<td class="text-right">{%= format_currency(balance_row["range2"]) %}</td> {%= format_number(balance_row["age"], null, 2) %}
<td class="text-right">{%= format_currency(balance_row["range3"]) %}</td> </td>
<td class="text-right">{%= format_currency(balance_row["range4"]) %}</td> <td class="text-right">
<td class="text-right">{%= format_currency(balance_row["range5"]) %}</td> {%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right">
{%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %}
</td>
<td class="text-right"> <td class="text-right">
{%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %} {%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %}
</td> </td>
</tr> </tr>
<td>{%= __("Future Payments") %}</td> <td>{%= __("Future Payments") %}</td>
<td></td> <td></td>
@@ -91,6 +107,7 @@
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td></td>
<td></td>
<td class="text-right"> <td class="text-right">
{%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %} {%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
</td> </td>
@@ -101,6 +118,7 @@
<th></th> <th></th>
<th></th> <th></th>
<th></th> <th></th>
<th></th>
<th class="text-right"> <th class="text-right">
{%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th> {%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th>
</tr> </tr>
@@ -218,15 +236,15 @@
<td></td> <td></td>
<td style="text-align: right"><b>{%= __("Total") %}</b></td> <td style="text-align: right"><b>{%= __("Total") %}</b></td>
<td style="text-align: right"> <td style="text-align: right">
{%= format_currency(data[i]["invoiced"], data[0]["currency"] ) %}</td> {%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %}</td>
{% if(!filters.show_future_payments) { %} {% if(!filters.show_future_payments) { %}
<td style="text-align: right"> <td style="text-align: right">
{%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td> {%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %} </td> <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} </td>
{% } %} {% } %}
<td style="text-align: right"> <td style="text-align: right">
{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td> {%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %} {% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %} {% if(report.report_name === "Accounts Receivable") { %}
@@ -234,8 +252,8 @@
{%= data[i]["po_no"] %}</td> {%= data[i]["po_no"] %}</td>
{% } %} {% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td> <td style="text-align: right">{%= data[i]["future_ref"] %}</td>
<td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %} {% } %}
{% } %} {% } %}
{% } else { %} {% } else { %}
@@ -256,10 +274,10 @@
{% } else { %} {% } else { %}
<td><b>{%= __("Total") %}</b></td> <td><b>{%= __("Total") %}</b></td>
{% } %} {% } %}
<td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
<td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td> <td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% } %} {% } %}
{% } %} {% } %}
</tr> </tr>

View File

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

View File

@@ -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,10 @@ 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);
if (frm.doc.calculate_depreciation) {
frm.trigger('set_finance_book');
}
}, },
gross_purchase_amount: function(frm) { gross_purchase_amount: function(frm) {

View File

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

View File

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

View File

@@ -148,24 +148,23 @@ def get_data(filters):
for asset in assets_record: for asset in assets_record:
asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \
- flt(depreciation_amount_map.get(asset.name)) - flt(depreciation_amount_map.get(asset.name))
if asset_value: row = {
row = { "asset_id": asset.asset_id,
"asset_id": asset.name, "asset_name": asset.asset_name,
"asset_name": asset.asset_name, "status": asset.status,
"status": asset.status, "department": asset.department,
"department": asset.department, "cost_center": asset.cost_center,
"cost_center": asset.cost_center, "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice),
"vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice), "gross_purchase_amount": asset.gross_purchase_amount,
"gross_purchase_amount": asset.gross_purchase_amount, "opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
"opening_accumulated_depreciation": asset.opening_accumulated_depreciation, "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
"depreciated_amount": depreciation_amount_map.get(asset.name) or 0.0, "available_for_use_date": asset.available_for_use_date,
"available_for_use_date": asset.available_for_use_date, "location": asset.location,
"location": asset.location, "asset_category": asset.asset_category,
"asset_category": asset.asset_category, "purchase_date": asset.purchase_date,
"purchase_date": asset.purchase_date, "asset_value": asset_value
"asset_value": asset_value }
} data.append(row)
data.append(row)
return data return data

View File

@@ -23,6 +23,8 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
class AccountMissingError(frappe.ValidationError): pass
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
class AccountsController(TransactionBase): class AccountsController(TransactionBase):
@@ -711,6 +713,21 @@ class AccountsController(TransactionBase):
return self._abbr return self._abbr
def raise_missing_debit_credit_account_error(self, party_type, party):
"""Raise an error if debit to/credit to account does not exist."""
db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To")
rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"
link_to_party = frappe.utils.get_link_to_form(party_type, party)
link_to_company = frappe.utils.get_link_to_form("Company", self.company)
message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or '')
message += "<br>" + _("Please set one of the following:") + "<br>"
message += "<br><ul><li>" + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "</li>"
message += "<li>" + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "</li></ul>"
frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError)
def validate_party(self): def validate_party(self):
party_type, party = self.get_party() party_type, party = self.get_party()
validate_party_frozen_disabled(party_type, party) validate_party_frozen_disabled(party_type, party)

View File

@@ -42,7 +42,7 @@ class SellingController(StockController):
self.validate_max_discount() self.validate_max_discount()
self.validate_selling_price() self.validate_selling_price()
self.set_qty_as_per_stock_uom() self.set_qty_as_per_stock_uom()
self.set_po_nos() self.set_po_nos(for_validate=True)
self.set_gross_profit() self.set_gross_profit()
set_default_income_account_for_item(self) set_default_income_account_for_item(self)
self.set_customer_address() self.set_customer_address()
@@ -364,14 +364,36 @@ 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, for_validate=False):
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" if for_validate and self.po_no:
sales_orders = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)])) return
if sales_orders: self.set_pos_for_sales_invoice()
po_nos = frappe.get_all('Sales Order', 'po_no', filters = {'name': ('in', sales_orders)}) if self.doctype == 'Delivery Note' and hasattr(self, "items"):
if po_nos and po_nos[0].get('po_no'): if for_validate and self.po_no:
self.po_no = ', '.join(list(set([d.po_no for d in po_nos if d.po_no]))) return
self.set_pos_for_delivery_note()
def set_pos_for_sales_invoice(self):
po_nos = []
if self.po_no:
po_nos.append(self.po_no)
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 = []
if self.po_no:
po_nos.append(self.po_no)
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":

View File

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

View File

@@ -227,9 +227,9 @@ class StockController(AccountsController):
def check_expense_account(self, item): def check_expense_account(self, item):
if not item.get("expense_account"): if not item.get("expense_account"):
frappe.throw(_("Row #{0}: Expense Account not set for Item {1}. Please set an Expense \ msg = _("Please set an Expense Account in the Items table")
Account in the Items table").format(item.idx, frappe.bold(item.item_code)), frappe.throw(_("Row #{0}: Expense Account not set for the Item {1}. {2}")
title=_("Expense Account Missing")) .format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else: else:
is_expense_account = frappe.db.get_value("Account", is_expense_account = frappe.db.get_value("Account",
@@ -242,11 +242,12 @@ class StockController(AccountsController):
_(self.doctype), self.name, item.get("item_code"))) _(self.doctype), self.name, item.get("item_code")))
def delete_auto_created_batches(self): def delete_auto_created_batches(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.items: for d in self.items:
if not d.batch_no: continue if not d.batch_no: continue
serial_nos = get_serial_nos(d.serial_no) serial_nos = [sr.name for sr in frappe.get_all("Serial No",
{'batch_no': d.batch_no, 'status': 'Inactive'})]
if serial_nos: if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None) frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)

View File

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

View File

@@ -244,6 +244,15 @@ def update_taxes_with_shipping_lines(taxes, shipping_lines, shopify_settings):
"""Shipping lines represents the shipping details, """Shipping lines represents the shipping details,
each such shipping detail consists of a list of tax_lines""" each such shipping detail consists of a list of tax_lines"""
for shipping_charge in shipping_lines: for shipping_charge in shipping_lines:
if shipping_charge.get("price"):
taxes.append({
"charge_type": _("Actual"),
"account_head": get_tax_account_head(shipping_charge),
"description": shipping_charge["title"],
"tax_amount": shipping_charge["price"],
"cost_center": shopify_settings.cost_center
})
for tax in shipping_charge.get("tax_lines"): for tax in shipping_charge.get("tax_lines"):
taxes.append({ taxes.append({
"charge_type": _("Actual"), "charge_type": _("Actual"),

View File

@@ -29,21 +29,32 @@ class PlaidConnector():
response = self.client.Item.public_token.exchange(public_token) response = self.client.Item.public_token.exchange(public_token)
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:

View File

@@ -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) {
@@ -107,4 +115,4 @@ erpnext.integrations.plaidLink = class plaidLink {
}); });
}, __("Select a company"), __("Continue")); }, __("Select a company"), __("Continue"));
} }
}; };

View File

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

View File

@@ -7,6 +7,7 @@ import frappe
import unittest import unittest
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from frappe.utils import nowdate, add_days, getdate from frappe.utils import nowdate, add_days, getdate
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Company"] test_dependencies = ["Company"]
@@ -14,6 +15,7 @@ class TestFeeValidity(unittest.TestCase):
def test_fee_validity(self): def test_fee_validity(self):
frappe.db.sql("""delete from `tabPatient Appointment`""") frappe.db.sql("""delete from `tabPatient Appointment`""")
frappe.db.sql("""delete from `tabFee Validity`""") frappe.db.sql("""delete from `tabFee Validity`""")
make_pos_profile()
patient = get_random("Patient") patient = get_random("Patient")
practitioner = get_random("Healthcare Practitioner") practitioner = get_random("Healthcare Practitioner")
department = get_random("Medical Department") department = get_random("Medical Department")

View File

@@ -239,6 +239,9 @@ doc_events = {
"Website Settings": { "Website Settings": {
"validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products" "validate": "erpnext.portal.doctype.products_settings.products_settings.home_page_is_products"
}, },
"Tax Category": {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": { "Sales Invoice": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"], "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.regional.italy.utils.sales_invoice_on_submit"],
"on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel", "on_cancel": "erpnext.regional.italy.utils.sales_invoice_on_cancel",

View File

@@ -20,7 +20,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
approvers = [] approvers = []
department_details = {} department_details = {}
department_list = [] department_list = []
employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True) employee = frappe.get_value("Employee", filters.get("employee"), ["employee_name","department", "leave_approver"], as_dict=True)
employee_department = filters.get("department") or employee.department employee_department = filters.get("department") or employee.department
if employee_department: if employee_department:
@@ -36,8 +36,10 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
if filters.get("doctype") == "Leave Application": if filters.get("doctype") == "Leave Application":
parentfield = "leave_approvers" parentfield = "leave_approvers"
else: field_name = "Leave Approver"
elif filters.get("doctype") == "Expense Claim":
parentfield = "expense_approvers" parentfield = "expense_approvers"
field_name = "Expense Approver"
if department_list: if department_list:
for d in department_list: for d in department_list:
approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from approvers += frappe.db.sql("""select user.name, user.first_name, user.last_name from
@@ -47,4 +49,10 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters):
and approver.parentfield = %s and approver.parentfield = %s
and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True)
if len(approvers) == 0:
error_msg = _("Please set {0} for the Employee: {1}").format(field_name, frappe.bold(employee.employee_name))
if department_list:
error_msg += _(" or for Department: {0}").format(frappe.bold(employee_department))
frappe.throw(error_msg, title=_(field_name + " Missing"))
return set(tuple(approver) for approver in approvers) return set(tuple(approver) for approver in approvers)

View File

@@ -172,8 +172,11 @@ class Employee(NestedSet):
) )
if reports_to: if reports_to:
link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to] link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to]
throw(_("Employee status cannot be set to 'Left' as following employees are currently reporting to this employee:&nbsp;") message = _("The following employees are currently still reporting to {0}:").format(frappe.bold(self.employee_name))
+ ', '.join(link_to_employees), EmployeeLeftValidationError) message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees)
message += "</li></ul><br>"
message += _("Please make sure the employees above report to another Active employee.")
throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee"))
if not self.relieving_date: if not self.relieving_date:
throw(_("Please enter relieving date.")) throw(_("Please enter relieving date."))
@@ -206,7 +209,7 @@ class Employee(NestedSet):
def validate_preferred_email(self): def validate_preferred_email(self):
if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)): if self.prefered_contact_email and not self.get(scrub(self.prefered_contact_email)):
frappe.msgprint(_("Please enter " + self.prefered_contact_email)) frappe.msgprint(_("Please enter {0}").format(self.prefered_contact_email))
def validate_onboarding_process(self): def validate_onboarding_process(self):
employee_onboarding = frappe.get_all("Employee Onboarding", employee_onboarding = frappe.get_all("Employee Onboarding",
@@ -407,7 +410,11 @@ def get_employee_emails(employee_list):
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False): def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False):
filters = [['company', '=', company]]
filters = [['status', '!=', 'Left']]
if company and company != 'All Companies':
filters.append(['company', '=', company])
fields = ['name as value', 'employee_name as title'] fields = ['name as value', 'employee_name as title']
if is_root: if is_root:

View File

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

View File

@@ -302,7 +302,9 @@ class PayrollEntry(Document):
jv_name = journal_entry.name jv_name = journal_entry.name
self.update_salary_slip_status(jv_name = jv_name) self.update_salary_slip_status(jv_name = jv_name)
except Exception as e: except Exception as e:
frappe.msgprint(e) if type(e) in (str, list, tuple):
frappe.msgprint(e)
raise
return jv_name return jv_name
@@ -379,9 +381,13 @@ class PayrollEntry(Document):
employees_to_mark_attendance = [] employees_to_mark_attendance = []
days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0 days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0
for employee_detail in self.employees: for employee_detail in self.employees:
days_holiday = self.get_count_holidays_of_employee(employee_detail.employee) employee_joining_date = frappe.db.get_value("Employee", employee_detail.employee, 'date_of_joining')
days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee) start_date = self.start_date
days_in_payroll = date_diff(self.end_date, self.start_date) + 1 if employee_joining_date > getdate(self.start_date):
start_date = employee_joining_date
days_holiday = self.get_count_holidays_of_employee(employee_detail.employee, start_date)
days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee, start_date)
days_in_payroll = date_diff(self.end_date, start_date) + 1
if days_in_payroll > days_holiday + days_attendance_marked: if days_in_payroll > days_holiday + days_attendance_marked:
employees_to_mark_attendance.append({ employees_to_mark_attendance.append({
"employee": employee_detail.employee, "employee": employee_detail.employee,
@@ -389,22 +395,25 @@ class PayrollEntry(Document):
}) })
return employees_to_mark_attendance return employees_to_mark_attendance
def get_count_holidays_of_employee(self, employee): def get_count_holidays_of_employee(self, employee, start_date):
holiday_list = get_holiday_list_for_employee(employee) holiday_list = get_holiday_list_for_employee(employee)
holidays = 0 holidays = 0
if holiday_list: if holiday_list:
days = frappe.db.sql("""select count(*) from tabHoliday where days = frappe.db.sql("""select count(*) from tabHoliday where
parent=%s and holiday_date between %s and %s""", (holiday_list, parent=%s and holiday_date between %s and %s""", (holiday_list,
self.start_date, self.end_date)) start_date, self.end_date))
if days and days[0][0]: if days and days[0][0]:
holidays = days[0][0] holidays = days[0][0]
return holidays return holidays
def get_count_employee_attendance(self, employee): def get_count_employee_attendance(self, employee, start_date):
marked_days = 0 marked_days = 0
attendances = frappe.db.sql("""select count(*) from tabAttendance where attendances = frappe.get_all("Attendance",
employee=%s and docstatus=1 and attendance_date between %s and %s""", fields = ["count(*)"],
(employee, self.start_date, self.end_date)) filters = {
"employee": employee,
"attendance_date": ('between', [start_date, self.end_date])
}, as_list=1)
if attendances and attendances[0][0]: if attendances and attendances[0][0]:
marked_days = attendances[0][0] marked_days = attendances[0][0]
return marked_days return marked_days

View File

@@ -10,6 +10,7 @@ from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
class OperationMismatchError(frappe.ValidationError): pass class OperationMismatchError(frappe.ValidationError): pass
class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document): class JobCard(Document):
def validate(self): def validate(self):
@@ -110,39 +111,54 @@ class JobCard(Document):
for_quantity, time_in_mins = 0, 0 for_quantity, time_in_mins = 0, 0
from_time_list, to_time_list = [], [] from_time_list, to_time_list = [], []
field = "operation_id"
data = frappe.get_all('Job Card', data = frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"], fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, field: self.get(field)}) filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
if data and len(data) > 0: if data and len(data) > 0:
for_quantity = data[0].completed_qty for_quantity = flt(data[0].completed_qty)
time_in_mins = data[0].time_in_mins time_in_mins = flt(data[0].time_in_mins)
if self.get(field): wo = frappe.get_doc('Work Order', self.work_order)
time_data = frappe.db.sql(""" if self.operation_id:
self.validate_produced_quantity(for_quantity, wo)
self.update_work_order_data(for_quantity, time_in_mins, wo)
def validate_produced_quantity(self, for_quantity, wo):
if self.docstatus < 2: return
if wo.produced_qty > for_quantity:
first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.")
.format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)))
second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.")
.format(frappe.bold(get_link_to_form("Work Order", self.work_order))))
frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg),
JobCardCancelError, title = _("Error"))
def update_work_order_data(self, for_quantity, time_in_mins, wo):
time_data = frappe.db.sql("""
SELECT SELECT
min(from_time) as start_time, max(to_time) as end_time min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE WHERE
jctl.parent = jc.name and jc.work_order = %s jctl.parent = jc.name and jc.work_order = %s
and jc.{0} = %s and jc.docstatus = 1 and jc.operation_id = %s and jc.docstatus = 1
""".format(field), (self.work_order, self.get(field)), as_dict=1) """, (self.work_order, self.operation_id), as_dict=1)
wo = frappe.get_doc('Work Order', self.work_order) for data in wo.operations:
if data.get("name") == self.operation_id:
data.completed_qty = for_quantity
data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None
data.actual_end_time = time_data[0].end_time if time_data else None
for data in wo.operations: wo.flags.ignore_validate_update_after_submit = True
if data.get("name") == self.get(field): wo.update_operation_status()
data.completed_qty = for_quantity wo.calculate_operating_cost()
data.actual_operation_time = time_in_mins wo.set_actual_dates()
data.actual_start_time = time_data[0].start_time if time_data else None wo.save()
data.actual_end_time = time_data[0].end_time if time_data else None
wo.flags.ignore_validate_update_after_submit = True
wo.update_operation_status()
wo.calculate_operating_cost()
wo.set_actual_dates()
wo.save()
def set_transferred_qty(self, update_status=False): def set_transferred_qty(self, update_status=False):
if not self.items: if not self.items:
@@ -224,17 +240,19 @@ def get_operation_details(work_order, operation):
@frappe.whitelist() @frappe.whitelist()
def get_operations(doctype, txt, searchfield, start, page_len, filters): def get_operations(doctype, txt, searchfield, start, page_len, filters):
if filters.get("work_order"): if not filters.get("work_order"):
args = {"parent": filters.get("work_order")} frappe.msgprint(_("Please select a Work Order first."))
if txt: return []
args["operation"] = ("like", "%{0}%".format(txt)) args = {"parent": filters.get("work_order")}
if txt:
args["operation"] = ("like", "%{0}%".format(txt))
return frappe.get_all("Work Order Operation", return frappe.get_all("Work Order Operation",
filters = args, filters = args,
fields = ["distinct operation as operation"], fields = ["distinct operation as operation"],
limit_start = start, limit_start = start,
limit_page_length = page_len, limit_page_length = page_len,
order_by="idx asc", as_list=1) order_by="idx asc", as_list=1)
@frappe.whitelist() @frappe.whitelist()
def make_material_request(source_name, target_doc=None): def make_material_request(source_name, target_doc=None):

View File

@@ -5,16 +5,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import flt, time_diff_in_hours, now, add_days, cint from frappe.utils import flt, now, cint, add_to_date
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.manufacturing.doctype.work_order.work_order \
import make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError)
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
class TestWorkOrder(unittest.TestCase): class TestWorkOrder(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -193,6 +194,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)
@@ -283,6 +320,29 @@ class TestWorkOrder(unittest.TestCase):
allow_overproduction("overproduction_percentage_for_work_order", 0) allow_overproduction("overproduction_percentage_for_work_order", 0)
def test_finished_good_valuation_rate(self):
allow_overproduction("overproduction_percentage_for_work_order", 0)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=2)
test_stock_entry.make_stock_entry(item_code="_Test Item",
target="_Test Warehouse - _TC", qty=10, basic_rate=5000.0)
test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target="_Test Warehouse - _TC", qty=10, basic_rate=1000.0)
ste_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 2))
ste_doc.submit()
ste_doc = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
ste_doc.save()
self.assertEquals(ste_doc.total_incoming_value, ste_doc.total_outgoing_value)
for row in ste_doc.items:
if row.t_warehouse and not row.s_warehouse:
row.valuation_rate = 120
ste_doc.save()
self.assertEquals(ste_doc.total_incoming_value, ste_doc.total_outgoing_value)
def test_over_production_for_sales_order(self): def test_over_production_for_sales_order(self):
so = make_sales_order(item_code="_Test FG Item", qty=2) so = make_sales_order(item_code="_Test FG Item", qty=2)
@@ -338,14 +398,41 @@ class TestWorkOrder(unittest.TestCase):
data = frappe.get_cached_value('BOM', data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
if data: bom, bom_item = data
bom, bom_item = data
bom_doc = frappe.get_doc('BOM', bom) bom_doc = frappe.get_doc('BOM', bom)
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) work_order = make_wo_order_test_record(item=bom_item, qty=1,
bom_no=bom, source_warehouse="_Test Warehouse - _TC")
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) for row in work_order.required_items:
self.assertEqual(len(job_cards), len(bom_doc.operations)) test_stock_entry.make_stock_entry(item_code=row.item_code,
target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100)
ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
ste.submit()
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
self.assertEqual(len(job_cards), len(bom_doc.operations))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", {
"from_time": now(),
"hours": i,
"to_time": add_to_date(now(), i),
"completed_qty": doc.for_quantity
})
doc.submit()
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
ste1.submit()
for job_card in job_cards:
doc = frappe.get_doc("Job Card", job_card)
self.assertRaises(JobCardCancelError, doc.cancel)
ste1.cancel()
ste.cancel()
def test_work_order_with_non_transfer_item(self): def test_work_order_with_non_transfer_item(self):
items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0}
@@ -371,6 +458,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"
@@ -414,6 +506,39 @@ class TestWorkOrder(unittest.TestCase):
work_order1.save() work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0) self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
def test_partial_material_consumption(self):
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
ste_cancel_list = []
ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0)
ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0)
ste_cancel_list.extend([ste1, ste2])
s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4))
s.submit()
ste_cancel_list.append(s)
ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
ste1.submit()
ste_cancel_list.append(ste1)
print(wo_order.name)
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
self.assertEquals(ste3.fg_completed_qty, 2)
expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4}
for row in ste3.items:
self.assertEquals(row.qty, expected_qty.get(row.item_code))
for ste_doc in ste_cancel_list:
ste_doc.cancel()
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0)
def get_scrap_item_details(bom_no): def get_scrap_item_details(bom_no):
scrap_items = {} scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`

View File

@@ -521,7 +521,8 @@ erpnext.work_order = {
var tbl = frm.doc.required_items || []; var tbl = frm.doc.required_items || [];
var tbl_lenght = tbl.length; var tbl_lenght = tbl.length;
for (var i = 0, len = tbl_lenght; i < len; i++) { for (var i = 0, len = tbl_lenght; i < len; i++) {
if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) { let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1; counter += 1;
} }
} }

View File

@@ -529,7 +529,7 @@ class WorkOrder(Document):
and (entry.purpose = "Material Consumption for Manufacture" and (entry.purpose = "Material Consumption for Manufacture"
or entry.purpose = "Manufacture") or entry.purpose = "Manufacture")
and entry.docstatus = 1 and entry.docstatus = 1
and detail.parent = entry.name and detail.parent = entry.name and IFNULL(t_warehouse, "") = ""
and (detail.item_code = %(item)s or detail.original_item = %(item)s)''', { and (detail.item_code = %(item)s or detail.original_item = %(item)s)''', {
'name': self.name, 'name': self.name,
'item': d.item_code 'item': d.item_code

View File

@@ -25,11 +25,11 @@ frappe.query_reports["BOM Stock Report"] = {
], ],
"formatter": function(value, row, column, data, default_formatter) { "formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);
if (column.id == "Item"){ if (column.id == "item") {
if (data["Enough Parts to Build"] > 0){ if (data["enough_parts_to_build"] > 0) {
value = `<a style='color:green' href="#Form/Item/${data['Item']}" data-doctype="Item">${data['Item']}</a>` value = `<a style='color:green' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else { } else {
value = `<a style='color:red' href="#Form/Item/${data['Item']}" data-doctype="Item">${data['Item']}</a>` value = `<a style='color:red' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} }
} }
return value return value

View File

@@ -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 #27-08-2020 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

View File

@@ -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")
@@ -53,7 +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}):
frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry() frappe.get_doc("Leave Encashment", 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 '''

View File

@@ -2,6 +2,7 @@ import frappe
import numpy as np import numpy as np
from frappe.utils import cint from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import get_shopping_cart_settings
def get_field_filter_data(): def get_field_filter_data():
product_settings = get_product_settings() product_settings = get_product_settings()
@@ -249,6 +250,8 @@ def get_next_attribute_and_values(item_code, selected_attributes):
optional_attributes = item_cache.get_optional_attributes() optional_attributes = item_cache.get_optional_attributes()
exact_match = [] exact_match = []
shopping_cart_settings = get_shopping_cart_settings()
allow_items_not_in_stock = cint(shopping_cart_settings.allow_items_not_in_stock)
# search for exact match if all selected attributes are required attributes # search for exact match if all selected attributes are required attributes
if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)): if len(selected_attributes.keys()) >= (len(attribute_list) - len(optional_attributes)):
item_attribute_value_map = item_cache.get_item_attribute_value_map() item_attribute_value_map = item_cache.get_item_attribute_value_map()
@@ -263,7 +266,6 @@ def get_next_attribute_and_values(item_code, selected_attributes):
if exact_match: if exact_match:
data = get_product_info_for_website(exact_match[0]) data = get_product_info_for_website(exact_match[0])
product_info = data.product_info product_info = data.product_info
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
if not data.cart_settings.show_price: if not data.cart_settings.show_price:
product_info = None product_info = None
else: else:
@@ -275,6 +277,7 @@ def get_next_attribute_and_values(item_code, selected_attributes):
'filtered_items_count': filtered_items_count, 'filtered_items_count': filtered_items_count,
'filtered_items': filtered_items if filtered_items_count < 10 else [], 'filtered_items': filtered_items if filtered_items_count < 10 else [],
'exact_match': exact_match, 'exact_match': exact_match,
'allow_items_not_in_stock': allow_items_not_in_stock,
'product_info': product_info 'product_info': product_info
} }

View File

@@ -665,9 +665,13 @@ erpnext.utils.map_current_doc = function(opts) {
} }
frappe.form.link_formatters['Item'] = function(value, doc) { frappe.form.link_formatters['Item'] = function(value, doc) {
if(doc && doc.item_name && doc.item_name !== value) { if (doc && value && doc.item_name && doc.item_name !== value) {
return value? value + ': ' + doc.item_name: doc.item_name; return value + ': ' + doc.item_name;
} else if (!value && doc.doctype && doc.item_name) {
// format blank value in child table
return doc.item_name;
} else { } else {
// if value is blank in report view or item code and name are the same, return as is
return value; return value;
} }
} }

View File

@@ -192,19 +192,20 @@ class GSTR3BReport(Document):
for d in self.report_dict["itc_elg"]["itc_avl"]: for d in self.report_dict["itc_elg"]["itc_avl"]:
itc_type = itc_type_map.get(d["ty"]) itc_type = itc_type_map.get(d["ty"])
gst_category = ["Registered Regular"]
if d["ty"] == 'ISRC': if d["ty"] == 'ISRC':
reverse_charge = "Y" reverse_charge = ["Y"]
itc_type = 'All Other ITC' itc_type = 'All Other ITC'
gst_category = ['Unregistered', 'Overseas', 'Registered Regular'] gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
else: else:
reverse_charge = "N" gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
reverse_charge = ["N", "Y"]
for account_head in self.account_heads: for account_head in self.account_heads:
for category in gst_category: for category in gst_category:
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: for charge_type in reverse_charge:
d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2) for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2)
for key in ['iamt', 'camt', 'samt', 'csamt']: for key in ['iamt', 'camt', 'samt', 'csamt']:
net_itc[key] += flt(d[key], 2) net_itc[key] += flt(d[key], 2)
@@ -264,7 +265,8 @@ class GSTR3BReport(Document):
def get_itc_details(self): def get_itc_details(self):
itc_amount = frappe.db.sql(""" itc_amount = frappe.db.sql("""
select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount,
t.account_head, s.eligibility_for_itc, s.reverse_charge
from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t
where s.docstatus = 1 and t.parent = s.name where s.docstatus = 1 and t.parent = s.name
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
@@ -388,7 +390,7 @@ class GSTR3BReport(Document):
tax_template = 'Purchase Taxes and Charges' tax_template = 'Purchase Taxes and Charges'
tax_amounts = frappe.db.sql(""" tax_amounts = frappe.db.sql("""
select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head
from `tab{doctype}` s , `tab{template}` t from `tab{doctype}` s , `tab{template}` t
where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s

View File

@@ -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
@@ -51,6 +51,13 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number)) .format(doc.gst_state_number))
def validate_tax_category(doc, method):
if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
if doc.is_inter_state:
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
else:
frappe.throw(_("Intra State tax category for GST State {0} already exists").format(doc.gst_state))
def update_gst_category(doc, method): def update_gst_category(doc, method):
for link in doc.links: for link in doc.links:
if link.link_doctype in ['Customer', 'Supplier']: if link.link_doctype in ['Customer', 'Supplier']:
@@ -85,8 +92,7 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
total += digit total += digit
factor = 2 if factor == 1 else 1 factor = 2 if factor == 1 else 1
if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]: if gstin[-1] != code_point_chars[((mod - (total % mod)) % mod)]:
frappe.throw(_("""Invalid {0}! The check digit validation has failed. frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
Please ensure you've typed the {0} correctly.""".format(label)))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts): def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'): if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
@@ -218,10 +224,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):
@@ -502,6 +507,9 @@ def get_address_details(data, doc, company_address, billing_address):
data.actualToStateCode = data.toStateCode data.actualToStateCode = data.toStateCode
shipping_address = billing_address shipping_address = billing_address
if doc.gst_category == 'SEZ':
data.toStateCode = 99
return data return data
def get_item_list(data, doc): def get_item_list(data, doc):
@@ -690,16 +698,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))
@@ -737,4 +743,4 @@ def make_regional_gl_entries(gl_entries, doc):
}, account_currency, item=tax) }, account_currency, item=tax)
) )
return gl_entries return gl_entries

View File

@@ -151,6 +151,7 @@ class Gstr1Report(object):
{select_columns} {select_columns}
from `tab{doctype}` from `tab{doctype}`
where docstatus = 1 {where_conditions} where docstatus = 1 {where_conditions}
and is_opening = 'No'
order by posting_date desc order by posting_date desc
""".format(select_columns=self.select_columns, doctype=self.doctype, """.format(select_columns=self.select_columns, doctype=self.doctype,
where_conditions=conditions), self.filters, as_dict=1) where_conditions=conditions), self.filters, as_dict=1)

View File

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

View File

@@ -8,7 +8,7 @@ frappe.ui.form.on("Sales Order", {
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Delivery Note': 'Delivery Note', 'Delivery Note': 'Delivery Note',
'Pick List': 'Pick List', 'Pick List': 'Pick List',
'Sales Invoice': 'Invoice', 'Sales Invoice': 'Sales Invoice',
'Material Request': 'Material Request', 'Material Request': 'Material Request',
'Purchase Order': 'Purchase Order', 'Purchase Order': 'Purchase Order',
'Project': 'Project', 'Project': 'Project',
@@ -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({

View File

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

View File

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

View File

@@ -1515,6 +1515,9 @@ class POSItems {
} }
get_items({start = 0, page_length = 40, search_value='', item_group=this.parent_item_group}={}) { get_items({start = 0, page_length = 40, search_value='', item_group=this.parent_item_group}={}) {
if (!this.frm.doc.pos_profile)
return;
const price_list = this.frm.doc.selling_price_list; const price_list = this.frm.doc.selling_price_list;
return new Promise(res => { return new Promise(res => {
frappe.call({ frappe.call({

View File

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

View File

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

View File

@@ -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")
@@ -536,7 +539,8 @@ def make_purchase_invoice(source_name, target_doc=None):
"doctype": "Purchase Invoice", "doctype": "Purchase Invoice",
"field_map": { "field_map": {
"supplier_warehouse":"supplier_warehouse", "supplier_warehouse":"supplier_warehouse",
"is_return": "is_return" "is_return": "is_return",
"bill_date": "bill_date"
}, },
"validation": { "validation": {
"docstatus": ["=", 1], "docstatus": ["=", 1],

View File

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

View File

@@ -736,6 +736,10 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
} }
}, },
fg_completed_qty: function() {
this.get_items();
},
get_items: function() { get_items: function() {
var me = this; var me = this;
if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no) if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no)
@@ -745,6 +749,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
// if work order / bom is mentioned, get items // if work order / bom is mentioned, get items
return this.frm.call({ return this.frm.call({
doc: me.frm.doc, doc: me.frm.doc,
freeze: true,
method: "get_items", method: "get_items",
callback: function(r) { callback: function(r) {
if(!r.exc) refresh_field("items"); if(!r.exc) refresh_field("items");

View File

@@ -83,7 +83,7 @@ class StockEntry(StockController):
self.set_incoming_rate() self.set_incoming_rate()
self.validate_serialized_batch() self.validate_serialized_batch()
self.set_actual_qty() self.set_actual_qty()
self.calculate_rate_and_amount(update_finished_item_rate=False) self.calculate_rate_and_amount()
def on_submit(self): def on_submit(self):
@@ -117,6 +117,7 @@ class StockEntry(StockController):
self.update_transferred_qty() self.update_transferred_qty()
self.update_quality_inspection() self.update_quality_inspection()
self.delete_auto_created_batches() self.delete_auto_created_batches()
self.delete_linked_stock_entry()
def set_job_card_data(self): def set_job_card_data(self):
if self.job_card and not self.work_order: if self.job_card and not self.work_order:
@@ -160,6 +161,12 @@ class StockEntry(StockController):
frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry") frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
.format(self.job_card)) .format(self.job_card))
def delete_linked_stock_entry(self):
if self.purpose == "Send to Warehouse":
for d in frappe.get_all("Stock Entry", filters={"docstatus": 0,
"outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}):
frappe.delete_doc("Stock Entry", d.name)
def set_transfer_qty(self): def set_transfer_qty(self):
for item in self.get("items"): for item in self.get("items"):
if not flt(item.qty): if not flt(item.qty):
@@ -1003,26 +1010,22 @@ class StockEntry(StockController):
wo = frappe.get_doc("Work Order", self.work_order) wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item', wo_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order}, filters={'parent': self.work_order},
fields=["item_code", "required_qty", "consumed_qty"] fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"]
) )
work_order_qty = wo.material_transferred_for_manufacturing or wo.qty
for item in wo_items: for item in wo_items:
qty = item.required_qty
item_account_details = get_item_defaults(item.item_code, self.company) item_account_details = get_item_defaults(item.item_code, self.company)
# Take into account consumption if there are any. # Take into account consumption if there are any.
if self.purpose == 'Manufacture':
req_qty_each = flt(item.required_qty / wo.qty) wo_item_qty = item.transferred_qty or item.required_qty
if (flt(item.consumed_qty) != 0):
remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each) req_qty_each = (
exhaust_qty = req_qty_each * wo.produced_qty (flt(wo_item_qty) - flt(item.consumed_qty)) /
if remaining_qty > exhaust_qty : (flt(work_order_qty) - flt(wo.produced_qty))
if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1: )
qty =0
else: qty = req_qty_each * flt(self.fg_completed_qty)
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else:
qty = req_qty_each * flt(self.fg_completed_qty)
if qty > 0: if qty > 0:
self.add_to_stock_entry_detail({ self.add_to_stock_entry_detail({
@@ -1108,12 +1111,17 @@ class StockEntry(StockController):
else: else:
qty = req_qty_each * flt(self.fg_completed_qty) qty = req_qty_each * flt(self.fg_completed_qty)
elif backflushed_materials.get(item.item_code): elif backflushed_materials.get(item.item_code):
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 consumed_qty:
qty -= consumed_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({
@@ -1194,8 +1202,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")
@@ -1206,9 +1212,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"]:

View File

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

View File

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

View File

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

View File

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

View File

@@ -164,7 +164,7 @@ def get_stock_ledger_entries(filters, items):
select select
sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.item_code, warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate,
sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference,
sle.item_code as name, sle.voucher_no sle.item_code as name, sle.voucher_no, sle.stock_value
from from
`tabStock Ledger Entry` sle force index (posting_sort_index) `tabStock Ledger Entry` sle force index (posting_sort_index)
where sle.docstatus < 2 %s %s where sle.docstatus < 2 %s %s
@@ -196,7 +196,7 @@ def get_item_warehouse_map(filters, sle):
else: else:
qty_diff = flt(d.actual_qty) qty_diff = flt(d.actual_qty)
value_diff = flt(d.stock_value_difference) value_diff = flt(d.stock_value) - flt(qty_dict.bal_val)
if d.posting_date < from_date: if d.posting_date < from_date:
qty_dict.opening_qty += qty_diff qty_dict.opening_qty += qty_diff

View File

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

View File

@@ -186,7 +186,7 @@ class ItemConfigure {
this.dialog.$status_area.empty(); this.dialog.$status_area.empty();
} }
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) { get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info, allow_items_not_in_stock }) {
const exact_match_message = __('1 exact match.'); const exact_match_message = __('1 exact match.');
const one_item = exact_match.length === 1 ? const one_item = exact_match.length === 1 ?
exact_match[0] : exact_match[0] :
@@ -194,7 +194,7 @@ class ItemConfigure {
filtered_items[0] : ''; filtered_items[0] : '';
// Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock. // Allow Add to Cart if adding out of stock items enabled in Shopping Cart else check stock.
const in_stock = product_info.allow_items_not_in_stock ? 1 : product_info.in_stock; const in_stock = allow_items_not_in_stock ? 1 : product_info && product_info.in_stock;
const add_to_cart = `<a href data-action="btn_add_to_cart" data-item-code="${one_item}">${__('Add to cart')}</a>`; const add_to_cart = `<a href data-action="btn_add_to_cart" data-item-code="${one_item}">${__('Add to cart')}</a>`;
const product_action = in_stock ? add_to_cart : `<a style="color:#74808b;">${__('Not in Stock')}</a>`; const product_action = in_stock ? add_to_cart : `<a style="color:#74808b;">${__('Not in Stock')}</a>`;

View File

@@ -20,10 +20,10 @@
{%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%} {%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
<div class="row"> <div class="row">
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}"> <div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ charge.get_formatted("description") }}</label></div> <label>{{ charge.get_formatted("description") }}</label>
</div>
<div class="col-xs-7 text-right"> <div class="col-xs-7 text-right">
{{ frappe.format_value(frappe.utils.flt(charge.tax_amount), {{ charge.get_formatted('tax_amount', doc) }}
table_meta.get_field("tax_amount"), doc, currency=doc.currency) }}
</div> </div>
</div> </div>
{%- endif -%} {%- endif -%}