Merge branch 'version-13-beta' of https://github.com/frappe/erpnext into enconnex_erpnext

This commit is contained in:
Deepesh Garg
2021-04-20 13:02:49 +05:30
93 changed files with 2764 additions and 1616 deletions

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.0.0-beta.13' __version__ = '13.0.0-beta.14'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@@ -92,14 +92,16 @@ frappe.ui.form.on('Payment Entry', {
}); });
frm.set_query("reference_doctype", "references", function() { frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") { if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
} else if (frm.doc.party_type=="Supplier") { } else if (frm.doc.party_type == "Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else if (frm.doc.party_type=="Employee") { } else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"]; var doctypes = ["Expense Claim", "Journal Entry"];
} else if (frm.doc.party_type=="Student") { } else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"]; var doctypes = ["Fees"];
} else if (frm.doc.party_type == "Donor") {
var doctypes = ["Donation"];
} else { } else {
var doctypes = ["Journal Entry"]; var doctypes = ["Journal Entry"];
} }
@@ -128,7 +130,7 @@ frappe.ui.form.on('Payment Entry', {
const child = locals[cdt][cdn]; const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company}; const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation'];
if (in_list(party_type_doctypes, child.reference_doctype)) { if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party; filters[doc.party_type.toLowerCase()] = doc.party;
@@ -281,7 +283,7 @@ frappe.ui.form.on('Payment Entry', {
let party_types = Object.keys(frappe.boot.party_account_types); let party_types = Object.keys(frappe.boot.party_account_types);
if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){ if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){
frm.set_value("party_type", ""); frm.set_value("party_type", "");
frappe.throw(__("Party can only be one of "+ party_types.join(", "))); frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")]));
} }
frm.set_query("party", function() { frm.set_query("party", function() {
@@ -603,12 +605,22 @@ frappe.ui.form.on('Payment Entry', {
{fieldtype:"Column Break"}, {fieldtype:"Column Break"},
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"}, {fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"}, {fieldtype:"Section Break"},
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
"get_query": function() {
return {
"filters": {"company": frm.doc.company}
}
}
},
{fieldtype:"Column Break"},
{fieldtype:"Section Break"},
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1}, {fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
]; ];
frappe.prompt(fields, function(filters){ frappe.prompt(fields, function(filters){
frappe.flags.allocate_payment_amount = true; frappe.flags.allocate_payment_amount = true;
frm.events.validate_filters_data(frm, filters); frm.events.validate_filters_data(frm, filters);
frm.doc.cost_center = filters.cost_center;
frm.events.get_outstanding_documents(frm, filters); frm.events.get_outstanding_documents(frm, filters);
}, __("Filters"), __("Get Outstanding Documents")); }, __("Filters"), __("Get Outstanding Documents"));
}, },
@@ -705,7 +717,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) { ) {
if(total_positive_outstanding > total_negative_outstanding) if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount) if (!frm.doc.paid_amount)
@@ -748,7 +761,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) { ) {
if(total_positive_outstanding_including_order > paid_amount) { if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
@@ -905,6 +919,12 @@ frappe.ui.form.on('Payment Entry', {
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx])); frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false; return false;
} }
if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") {
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx]));
return false;
}
} }
if (row) { if (row) {
@@ -1056,11 +1076,6 @@ frappe.ui.form.on('Payment Entry', {
frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance); frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance); frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance); frm.set_value("party_balance", r.message.party_balance);
},
() => {
if(frm.doc.payment_type != "Internal") {
frm.clear_table("references");
}
} }
]); ]);

View File

@@ -72,6 +72,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation()
self.update_payment_schedule() self.update_payment_schedule()
self.set_status() self.set_status()
@@ -82,6 +83,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation(cancel=1)
self.delink_advance_entry_references() self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1) self.update_payment_schedule(cancel=1)
self.set_payment_req_status() self.set_payment_req_status()
@@ -245,6 +247,8 @@ class PaymentEntry(AccountsController):
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance")
elif self.party_type == "Shareholder": elif self.party_type == "Shareholder":
valid_reference_doctypes = ("Journal Entry") valid_reference_doctypes = ("Journal Entry")
elif self.party_type == "Donor":
valid_reference_doctypes = ("Donation")
for d in self.get("references"): for d in self.get("references"):
if not d.allocated_amount: if not d.allocated_amount:
@@ -614,6 +618,13 @@ class PaymentEntry(AccountsController):
doc = frappe.get_doc("Expense Claim", d.reference_name) doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, self.name) update_reimbursed_amount(doc, self.name)
def update_donation(self, cancel=0):
if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:
for d in self.get("references"):
if d.reference_doctype=="Donation" and d.reference_name:
is_paid = 0 if cancel else 1
frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
def on_recurring(self, reference_doc, auto_repeat_doc): def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name self.reference_no = reference_doc.name
self.reference_date = nowdate() self.reference_date = nowdate()
@@ -913,6 +924,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
total_amount = ref_doc.get("grand_total") total_amount = ref_doc.get("grand_total")
exchange_rate = 1 exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount") outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Donation":
total_amount = ref_doc.get("amount")
exchange_rate = 1
elif reference_doctype == "Dunning": elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount") total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1 exchange_rate = 1
@@ -1162,8 +1176,10 @@ def set_party_type(dt):
party_type = "Supplier" party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"): elif dt in ("Expense Claim", "Employee Advance"):
party_type = "Employee" party_type = "Employee"
elif dt in ("Fees"): elif dt == "Fees":
party_type = "Student" party_type = "Student"
elif dt == "Donation":
party_type = "Donor"
return party_type return party_type
def set_party_account(dt, dn, doc, party_type): def set_party_account(dt, dn, doc, party_type):
@@ -1189,7 +1205,7 @@ def set_party_account_currency(dt, party_account, doc):
return party_account_currency return party_account_currency
def set_payment_type(dt, doc): def set_payment_type(dt, doc):
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive" payment_type = "Receive"
else: else:
@@ -1222,6 +1238,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
elif dt == "Dunning": elif dt == "Dunning":
grand_total = doc.grand_total grand_total = doc.grand_total
outstanding_amount = doc.grand_total outstanding_amount = doc.grand_total
elif dt == "Donation":
grand_total = doc.amount
outstanding_amount = doc.amount
else: else:
if party_account_currency == doc.company_currency: if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)

View File

@@ -20,15 +20,16 @@ class POSOpeningEntry(StatusUpdater):
if not cint(frappe.db.get_value("User", self.user, "enabled")): if not cint(frappe.db.get_value("User", self.user, "enabled")):
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user)) frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
def validate_payment_method_account(self): def validate_payment_method_account(self):
invalid_modes = [] invalid_modes = []
for d in self.balance_details: for d in self.balance_details:
account = frappe.db.get_value("Mode of Payment Account", if d.mode_of_payment:
{"parent": d.mode_of_payment, "company": self.company}, "default_account") account = frappe.db.get_value("Mode of Payment Account",
if not account: {"parent": d.mode_of_payment, "company": self.company}, "default_account")
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) if not account:
invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
if invalid_modes: if invalid_modes:
if invalid_modes == 1: if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}") msg = _("Please set default Cash or Bank account in Mode of Payment {}")

View File

@@ -45,6 +45,14 @@
"column_break_21", "column_break_21",
"min_amt", "min_amt",
"max_amt", "max_amt",
"product_discount_scheme_section",
"same_item",
"free_item",
"free_qty",
"free_item_rate",
"column_break_42",
"free_item_uom",
"is_recursive",
"section_break_23", "section_break_23",
"valid_from", "valid_from",
"valid_upto", "valid_upto",
@@ -63,13 +71,6 @@
"discount_amount", "discount_amount",
"discount_percentage", "discount_percentage",
"for_price_list", "for_price_list",
"product_discount_scheme_section",
"same_item",
"free_item",
"free_qty",
"column_break_51",
"free_item_uom",
"free_item_rate",
"section_break_13", "section_break_13",
"threshold_percentage", "threshold_percentage",
"priority", "priority",
@@ -460,10 +461,6 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Qty" "label": "Qty"
}, },
{
"fieldname": "column_break_51",
"fieldtype": "Column Break"
},
{ {
"fieldname": "free_item_uom", "fieldname": "free_item_uom",
"fieldtype": "Link", "fieldtype": "Link",
@@ -554,19 +551,33 @@
"fieldname": "promotional_scheme", "fieldname": "promotional_scheme",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Promotional Scheme", "label": "Promotional Scheme",
"options": "Promotional Scheme" "no_copy": 1,
"options": "Promotional Scheme",
"print_hide": 1,
"read_only": 1
}, },
{ {
"description": "Simple Python Expression, Example: territory != 'All Territories'", "description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition", "fieldname": "condition",
"fieldtype": "Code", "fieldtype": "Code",
"label": "Condition" "label": "Condition"
},
{
"fieldname": "column_break_42",
"fieldtype": "Column Break"
},
{
"default": "0",
"description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on",
"fieldname": "is_recursive",
"fieldtype": "Check",
"label": "Is Recursive"
} }
], ],
"icon": "fa fa-gift", "icon": "fa fa-gift",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2020-12-04 00:36:24.698219", "modified": "2021-03-06 22:01:24.840422",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Pricing Rule", "name": "Pricing Rule",

View File

@@ -237,6 +237,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
"doctype": args.doctype, "doctype": args.doctype,
"has_margin": False, "has_margin": False,
"name": args.name, "name": args.name,
"free_item_data": [],
"parent": args.parent, "parent": args.parent,
"parenttype": args.parenttype, "parenttype": args.parenttype,
"child_docname": args.get('child_docname') "child_docname": args.get('child_docname')

View File

@@ -328,6 +328,21 @@ class TestPricingRule(unittest.TestCase):
self.assertEquals(item.discount_amount, 110) self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990) self.assertEquals(item.rate, 990)
def test_pricing_rule_with_margin_and_discount_amount(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10,
rate_or_discount="Discount Amount", discount_amount=110)
si = create_sales_invoice(do_not_save=True)
si.items[0].price_list_rate = 1000
si.payment_schedule = []
si.insert(ignore_permissions=True)
item = si.items[0]
self.assertEquals(item.margin_rate_or_amount, 10)
self.assertEquals(item.rate_with_margin, 1100)
self.assertEquals(item.discount_amount, 110)
self.assertEquals(item.rate, 990)
def test_pricing_rule_for_product_discount_on_same_item(self): def test_pricing_rule_for_product_discount_on_same_item(self):
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
test_record = { test_record = {
@@ -560,6 +575,7 @@ def make_pricing_rule(**args):
"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 '', "condition": args.condition or '',
"priority": 1, "priority": 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
}) })

View File

@@ -367,7 +367,7 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args):
if items and doc.get("items"): if items and doc.get("items"):
for row in doc.get('items'): for row in doc.get('items'):
if row.get(apply_on) not in items: continue if (row.get(apply_on) or args.get(apply_on)) not in items: continue
if pr_doc.mixed_conditions: if pr_doc.mixed_conditions:
amt = args.get('qty') * args.get("price_list_rate") amt = args.get('qty') * args.get("price_list_rate")
@@ -479,7 +479,7 @@ def apply_pricing_rule_on_transaction(doc):
doc.calculate_taxes_and_totals() doc.calculate_taxes_and_totals()
elif d.price_or_product_discount == 'Product': elif d.price_or_product_discount == 'Product':
item_details = frappe._dict({'parenttype': doc.doctype}) item_details = frappe._dict({'parenttype': doc.doctype, 'free_item_data': []})
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()
@@ -508,9 +508,16 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
frappe.throw(_("Free item not set in the pricing rule {0}") frappe.throw(_("Free item not set in the pricing rule {0}")
.format(get_link_to_form("Pricing Rule", pricing_rule.name))) .format(get_link_to_form("Pricing Rule", pricing_rule.name)))
item_details.free_item_data = { qty = pricing_rule.free_qty or 1
if pricing_rule.is_recursive:
transaction_qty = args.get('qty') if args else doc.total_qty
if transaction_qty:
qty = flt(transaction_qty) * qty
free_item_data_args = {
'item_code': free_item, 'item_code': free_item,
'qty': pricing_rule.free_qty or 1, 'qty': qty,
'pricing_rules': pricing_rule.name,
'rate': pricing_rule.free_item_rate or 0, 'rate': pricing_rule.free_item_rate or 0,
'price_list_rate': pricing_rule.free_item_rate or 0, 'price_list_rate': pricing_rule.free_item_rate or 0,
'is_free_item': 1 'is_free_item': 1
@@ -519,24 +526,26 @@ def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
item_data = frappe.get_cached_value('Item', free_item, ['item_name', item_data = frappe.get_cached_value('Item', free_item, ['item_name',
'description', 'stock_uom'], as_dict=1) 'description', 'stock_uom'], as_dict=1)
item_details.free_item_data.update(item_data) free_item_data_args.update(item_data)
item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom free_item_data_args['uom'] = pricing_rule.free_item_uom or item_data.stock_uom
item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item, free_item_data_args['conversion_factor'] = get_conversion_factor(free_item,
item_details.free_item_data['uom']).get("conversion_factor", 1) free_item_data_args['uom']).get("conversion_factor", 1)
if item_details.get("parenttype") == 'Purchase Order': if item_details.get("parenttype") == 'Purchase Order':
item_details.free_item_data['schedule_date'] = doc.schedule_date if doc else today() free_item_data_args['schedule_date'] = doc.schedule_date if doc else today()
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() free_item_data_args['delivery_date'] = doc.delivery_date if doc else today()
item_details.free_item_data.append(free_item_data_args)
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:
items = [d.item_code for d in doc.items items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item])
if d.item_code == (pricing_rule_args.get("item_code")) and d.is_free_item]
if not items: for args in pricing_rule_args:
doc.append('items', pricing_rule_args) if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
doc.append('items', args)
def get_pricing_rule_items(pr_doc): def get_pricing_rule_items(pr_doc):
apply_on_data = [] apply_on_data = []

View File

@@ -12,16 +12,16 @@ from frappe.model.document import Document
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group' pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group'
'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from', 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from',
'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier', 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier',
'supplier_group', 'company', 'currency'] 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']
other_fields = ['min_qty', 'max_qty', 'min_amt', other_fields = ['min_qty', 'max_qty', 'min_amt',
'max_amt', 'priority','warehouse', 'threshold_percentage', 'rule_description'] 'max_amt', 'priority','warehouse', 'threshold_percentage', 'rule_description']
price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discount_on_rate', price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discount_on_rate',
'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule'] 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule', 'apply_multiple_pricing_rules']
product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
'free_item_rate', 'same_item'] 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules']
class PromotionalScheme(Document): class PromotionalScheme(Document):
def validate(self): def validate(self):

View File

@@ -1,792 +1,181 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2019-03-24 14:48:59.649168", "creation": "2019-03-24 14:48:59.649168",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"disable",
"apply_multiple_pricing_rules",
"column_break_2",
"rule_description",
"section_break_2",
"min_qty",
"max_qty",
"column_break_3",
"min_amount",
"max_amount",
"section_break_6",
"rate_or_discount",
"column_break_10",
"rate",
"discount_amount",
"discount_percentage",
"section_break_11",
"warehouse",
"threshold_percentage",
"validate_applied_rule",
"column_break_14",
"priority",
"apply_discount_on_rate"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "disable", "fieldname": "disable",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Disable"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Disable",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rule_description", "fieldname": "rule_description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rule Description", "label": "Rule Description",
"length": 0,
"no_copy": 1, "no_copy": 1,
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_2", "fieldname": "section_break_2",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 1, "columns": 1,
"default": "0", "default": "0",
"fieldname": "min_qty", "fieldname": "min_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Min Qty"
"label": "Min Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 1, "columns": 1,
"default": "0", "default": "0",
"fieldname": "max_qty", "fieldname": "max_qty",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Max Qty"
"label": "Max Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"fieldname": "min_amount", "fieldname": "min_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Min Amount"
"label": "Min Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"depends_on": "",
"fieldname": "max_amount", "fieldname": "max_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Max Amount"
"label": "Max Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Discount Percentage", "default": "Discount Percentage",
"depends_on": "",
"fieldname": "rate_or_discount", "fieldname": "rate_or_discount",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Discount Type", "label": "Discount Type",
"length": 0, "options": "\nRate\nDiscount Percentage\nDiscount Amount"
"no_copy": 0,
"options": "\nRate\nDiscount Percentage\nDiscount Amount",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "column_break_10", "fieldname": "column_break_10",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2, "columns": 2,
"depends_on": "eval:doc.rate_or_discount==\"Rate\"", "depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate", "fieldname": "rate",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Rate"
"label": "Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.rate_or_discount==\"Discount Amount\"", "depends_on": "eval:doc.rate_or_discount==\"Discount Amount\"",
"fieldname": "discount_amount", "fieldname": "discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"hidden": 0, "label": "Discount Amount"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Discount Amount",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "eval:doc.rate_or_discount==\"Discount Percentage\"", "depends_on": "eval:doc.rate_or_discount==\"Discount Percentage\"",
"fieldname": "discount_percentage", "fieldname": "discount_percentage",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "Discount Percentage"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Discount Percentage",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_11", "fieldname": "section_break_11",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "warehouse", "fieldname": "warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Warehouse", "label": "Warehouse",
"length": 0, "options": "Warehouse"
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "threshold_percentage", "fieldname": "threshold_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"hidden": 0, "label": "Threshold for Suggestion"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Threshold for Suggestion",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "1", "default": "1",
"fieldname": "validate_applied_rule", "fieldname": "validate_applied_rule",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Validate Applied Rule"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Validate Applied Rule",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_14", "fieldname": "column_break_14",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "priority", "fieldname": "priority",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Priority", "label": "Priority",
"length": 0, "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20"
"no_copy": 0,
"options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "priority", "depends_on": "priority",
"fieldname": "apply_multiple_pricing_rules", "fieldname": "apply_multiple_pricing_rules",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Apply Multiple Pricing Rules"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Apply Multiple Pricing Rules",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0", "default": "0",
"depends_on": "eval:in_list(['Discount Percentage', 'Discount Amount'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules", "depends_on": "eval:in_list(['Discount Percentage', 'Discount Amount'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules",
"fieldname": "apply_discount_on_rate", "fieldname": "apply_discount_on_rate",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Apply Discount on Rate"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Apply Discount on Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2019-03-24 14:48:59.649168", "modified": "2021-03-07 11:56:23.424137",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Promotional Scheme Price Discount", "name": "Promotional Scheme Price Discount",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"track_changes": 0,
"track_seen": 0,
"track_views": 0
} }

View File

@@ -1,10 +1,12 @@
{ {
"actions": [],
"creation": "2019-03-24 14:48:59.649168", "creation": "2019-03-24 14:48:59.649168",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"disable", "disable",
"apply_multiple_pricing_rules",
"column_break_2", "column_break_2",
"rule_description", "rule_description",
"section_break_1", "section_break_1",
@@ -25,7 +27,7 @@
"threshold_percentage", "threshold_percentage",
"column_break_15", "column_break_15",
"priority", "priority",
"apply_multiple_pricing_rules" "is_recursive"
], ],
"fields": [ "fields": [
{ {
@@ -152,10 +154,19 @@
"fieldname": "apply_multiple_pricing_rules", "fieldname": "apply_multiple_pricing_rules",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Apply Multiple Pricing Rules" "label": "Apply Multiple Pricing Rules"
},
{
"default": "0",
"description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on",
"fieldname": "is_recursive",
"fieldtype": "Check",
"label": "Is Recursive"
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"modified": "2019-07-21 00:00:56.674284", "links": [],
"modified": "2021-03-06 21:58:18.162346",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Promotional Scheme Product Discount", "name": "Promotional Scheme Product Discount",

View File

@@ -546,7 +546,7 @@ frappe.ui.form.on("Purchase Invoice", {
}, },
onload: function(frm) { onload: function(frm) {
if(frm.doc.__onload) { if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) { if(frm.doc.supplier) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0; frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
} }

View File

@@ -58,6 +58,7 @@
"rejected_warehouse", "rejected_warehouse",
"col_break_warehouse", "col_break_warehouse",
"set_from_warehouse", "set_from_warehouse",
"supplier_warehouse",
"is_subcontracted", "is_subcontracted",
"items_section", "items_section",
"update_stock", "update_stock",
@@ -1351,7 +1352,7 @@
"options": "Company" "options": "Company"
}, },
{ {
"depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)", "depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
"description": "Sets 'From Warehouse' in each row of the items table.", "description": "Sets 'From Warehouse' in each row of the items table.",
"fieldname": "set_from_warehouse", "fieldname": "set_from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",

View File

@@ -1166,10 +1166,12 @@ class TestSalesInvoice(unittest.TestCase):
def test_create_so_with_margin(self): def test_create_so_with_margin(self):
si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True) si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True)
price_list_rate = 100 price_list_rate = flt(100) * flt(si.plc_conversion_rate)
si.items[0].price_list_rate = price_list_rate si.items[0].price_list_rate = price_list_rate
si.items[0].margin_type = 'Percentage' si.items[0].margin_type = 'Percentage'
si.items[0].margin_rate_or_amount = 25 si.items[0].margin_rate_or_amount = 25
si.items[0].discount_amount = 0.0
si.items[0].discount_percentage = 0.0
si.save() si.save()
self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate))

View File

@@ -240,8 +240,7 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters): def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values(): for entries in gl_entries_by_account.values():
for entry in entries: for entry in entries:
key = entry.account_number or entry.account_name d = accounts_by_name.get(entry.account_name)
d = accounts_by_name.get(key)
if d: if d:
for company in companies: for company in companies:
# check if posting date is within the period # check if posting date is within the period
@@ -256,7 +255,8 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts""" """accumulate children's values in parent accounts"""
for d in reversed(accounts): for d in reversed(accounts):
if d.parent_account: if d.parent_account:
account = d.parent_account.split(' - ')[0].strip() account = d.parent_account_name
if not accounts_by_name.get(account): if not accounts_by_name.get(account):
continue continue
@@ -267,16 +267,34 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account]["opening_balance"] = \ accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters): def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, filters)
if not accounts: if not accounts:
return None, None return None, None
accounts = update_parent_account_names(accounts)
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name return accounts, accounts_by_name
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.
parent_name is `name` of parent account which could have other prefix
of account_number and suffix of company abbr. This function adds key called
`parent_account_name` which does not have such prefix/suffix.
"""
name_to_account_map = { d.name : d.account_name for d in accounts }
for account in accounts:
if account.parent_account:
account["parent_account_name"] = name_to_account_map[account.parent_account]
return accounts
def get_companies(filters): def get_companies(filters):
companies = {} companies = {}
all_companies = get_subsidiary_companies(filters.get('company')) all_companies = get_subsidiary_companies(filters.get('company'))
@@ -381,9 +399,9 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries: for entry in gl_entries:
key = entry.account_number or entry.account_name account_name = entry.account_name
validate_entries(key, entry, accounts_by_name, accounts) validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry) gl_entries_by_account.setdefault(account_name, []).append(entry)
return gl_entries_by_account return gl_entries_by_account
@@ -452,8 +470,7 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {} parent_children_map = {}
accounts_by_name = {} accounts_by_name = {}
for d in accounts: for d in accounts:
key = d.account_number or d.account_name accounts_by_name[d.account_name] = d
accounts_by_name[key] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d) parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = [] filtered_accounts = []

View File

@@ -51,7 +51,11 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_
"from_date": start_date "from_date": start_date
}) })
to_date = add_months(start_date, months_to_add) if i==0 and filter_based_on == 'Date Range':
to_date = add_months(get_first_day(start_date), months_to_add)
else:
to_date = add_months(start_date, months_to_add)
start_date = to_date start_date = to_date
# Subtract one day from to_date, as it may be first day in next fiscal year or month # Subtract one day from to_date, as it may be first day in next fiscal year or month

View File

@@ -0,0 +1,25 @@
## Version 13.0.0 Beta 14 Release Notes
### Fixes and Enhancements
- Repost incompleted backdated transactions ([#24991](https://github.com/frappe/erpnext/pull/24991))
- Revert stock balance value calculation ([#24957](https://github.com/frappe/erpnext/pull/24957))
- Allow user to update exchange rate in Multi-currency LCV ([#24947](https://github.com/frappe/erpnext/pull/24947))
- Added correct path in hooks ([#24865](https://github.com/frappe/erpnext/pull/24865))
- Unequal debit and credit issue on RCM Invoice ([#24838](https://github.com/frappe/erpnext/pull/24838))
- Period list for exponential smoothing forecasting report ([#24983](https://github.com/frappe/erpnext/pull/24983))
- POS Opening Entry with empty balance detail rows ([#24891](https://github.com/frappe/erpnext/pull/24891))
- Use account_name only in consolidated report ([#24840](https://github.com/frappe/erpnext/pull/24840))
- Validation of job card in stock entry ([#24882](https://github.com/frappe/erpnext/pull/24882))
- Added supplier warehouse field back again ([#24827](https://github.com/frappe/erpnext/pull/24827))
- Don't throw exception on invoice lines when there is no item_cod… ([#24864](https://github.com/frappe/erpnext/pull/24864))
- Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24918](https://github.com/frappe/erpnext/pull/24918))
- Payment References on adding Cost Center in PE and Report Issue Summary fix for V13 beta pre-release ([#24951](https://github.com/frappe/erpnext/pull/24951))
- TDS check getting checked after reload ([#24973](https://github.com/frappe/erpnext/pull/24973))
- Membership and Donation API fixes ([#24900](https://github.com/frappe/erpnext/pull/24900))
- Serial no trim issue ([#24981](https://github.com/frappe/erpnext/pull/24981))
- Add method for regional round off account back ([#24894](https://github.com/frappe/erpnext/pull/24894))
- Allow zero valuation in stock reconciliation ([#24985](https://github.com/frappe/erpnext/pull/24985))
- Simplified logic for additional salary ([#24907](https://github.com/frappe/erpnext/pull/24907))
- Allow to select item code in batch naming ([#24825](https://github.com/frappe/erpnext/pull/24825))
- 80G Certificates and Donations ([#24848](https://github.com/frappe/erpnext/pull/24848))
- Membership renewal validation (#24963) ([#24964](https://github.com/frappe/erpnext/pull/24964))

View File

@@ -25,7 +25,8 @@ from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
class AccountMissingError(frappe.ValidationError): pass 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", "weight_per_unit", "weight_uom", "total_weight")
class AccountsController(TransactionBase): class AccountsController(TransactionBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@@ -288,7 +288,7 @@ class BuyingController(StockController):
if self.is_subcontracted == "Yes": if self.is_subcontracted == "Yes":
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
frappe.throw(_("Supplier Warehouse mandatory for sub-contracted Purchase Receipt")) frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
for item in self.get("items"): for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom: if item in self.sub_contracted_items and not item.bom:

View File

@@ -405,7 +405,7 @@ class StockController(AccountsController):
def set_rate_of_stock_uom(self): def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"): for d in self.get("items"):
d.stock_uom_rate = d.rate / d.conversion_factor d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
def validate_internal_transfer(self): def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \

View File

@@ -113,7 +113,12 @@ class calculate_taxes_and_totals(object):
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0: if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
item.discount_amount = item.rate_with_margin - item.rate
if item.discount_amount and not item.discount_percentage:
item.rate = item.rate_with_margin - item.discount_amount
else:
item.discount_amount = item.rate_with_margin - item.rate
elif flt(item.price_list_rate) > 0: elif flt(item.price_list_rate) > 0:
item.discount_amount = item.price_list_rate - item.rate item.discount_amount = item.price_list_rate - item.rate
elif flt(item.price_list_rate) > 0 and not item.discount_amount: elif flt(item.price_list_rate) > 0 and not item.discount_amount:
@@ -798,7 +803,7 @@ class init_landed_taxes_and_totals(object):
for d in self.doc.get(self.tax_field): for d in self.doc.get(self.tax_field):
if d.account_currency == company_currency: if d.account_currency == company_currency:
d.exchange_rate = 1 d.exchange_rate = 1
elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date: elif not d.exchange_rate:
d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account, d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account,
account_currency=d.account_currency, company=self.doc.company) account_currency=d.account_currency, company=self.doc.company)

View File

@@ -117,7 +117,7 @@ def call_mws_method(mws_method, *args, **kwargs):
return response return response
except Exception as e: except Exception as e:
delay = math.pow(4, x) * 125 delay = math.pow(4, x) * 125
frappe.log_error(message=e, title=str(mws_method)) frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
time.sleep(delay) time.sleep(delay)
continue continue

View File

@@ -320,6 +320,7 @@ scheduler_events = {
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts", "erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance", "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders", "erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
], ],
"daily": [ "daily": [
"erpnext.stock.reorder_item.reorder_item", "erpnext.stock.reorder_item.reorder_item",
@@ -357,13 +358,13 @@ scheduler_events = {
"erpnext.hr.utils.generate_leave_encashment", "erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves", "erpnext.hr.utils.allocate_earned_leaves",
"erpnext.hr.utils.grant_leaves_automatically", "erpnext.hr.utils.grant_leaves_automatically",
"erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead" "erpnext.crm.doctype.lead.lead.daily_open_lead"
], ],
"monthly_long": [ "monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting", "erpnext.accounts.deferred_revenue.process_deferred_accounting",
"erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans" "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
] ]
} }

View File

@@ -255,6 +255,9 @@ class JobCard(Document):
data.actual_operation_time = time_in_mins data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None 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 data.actual_end_time = time_data[0].end_time if time_data else None
if data.get("workstation") != self.workstation:
# workstations can change in a job card
data.workstation = self.workstation
wo.flags.ignore_validate_update_after_submit = True wo.flags.ignore_validate_update_after_submit = True
wo.update_operation_status() wo.update_operation_status()
@@ -267,6 +270,17 @@ class JobCard(Document):
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, "operation_id": self.operation_id}) filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
def set_transferred_qty_in_job_card(self, ste_doc):
for row in ste_doc.items:
if not row.job_card_item: continue
qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
se.purpose = 'Material Transfer for Manufacture'
""", (row.job_card_item))[0][0]
frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
def set_transferred_qty(self, update_status=False): def set_transferred_qty(self, update_status=False):
if not self.items: if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0 self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@@ -279,7 +293,8 @@ class JobCard(Document):
self.transferred_qty = frappe.db.get_value('Stock Entry', { self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name, 'job_card': self.name,
'work_order': self.work_order, 'work_order': self.work_order,
'docstatus': 1 'docstatus': 1,
'purpose': 'Material Transfer for Manufacture'
}, 'sum(fg_completed_qty)') or 0 }, 'sum(fg_completed_qty)') or 0
self.db_set("transferred_qty", self.transferred_qty) self.db_set("transferred_qty", self.transferred_qty)
@@ -415,11 +430,13 @@ def make_material_request(source_name, target_doc=None):
def make_stock_entry(source_name, target_doc=None): def make_stock_entry(source_name, target_doc=None):
def update_item(obj, target, source_parent): def update_item(obj, target, source_parent):
target.t_warehouse = source_parent.wip_warehouse target.t_warehouse = source_parent.wip_warehouse
target.conversion_factor = 1
def set_missing_values(source, target): def set_missing_values(source, target):
target.purpose = "Material Transfer for Manufacture" target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1 target.from_bom = 1
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0) target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
target.set_transfer_qty()
target.calculate_rate_and_amount() target.calculate_rate_and_amount()
target.set_missing_values() target.set_missing_values()
target.set_stock_entry_type() target.set_stock_entry_type()
@@ -437,9 +454,10 @@ def make_stock_entry(source_name, target_doc=None):
"field_map": { "field_map": {
"source_warehouse": "s_warehouse", "source_warehouse": "s_warehouse",
"required_qty": "qty", "required_qty": "qty",
"uom": "stock_uom" "name": "job_card_item"
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: doc.required_qty > 0
} }
}, target_doc, set_missing_values) }, target_doc, set_missing_values)

View File

@@ -1,363 +1,120 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2018-07-09 17:20:44.737289",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2018-07-09 17:20:44.737289", "field_order": [
"custom": 0, "item_code",
"docstatus": 0, "source_warehouse",
"doctype": "DocType", "uom",
"document_type": "", "item_group",
"editable_grid": 1, "column_break_3",
"engine": "InnoDB", "stock_uom",
"item_name",
"description",
"qty_section",
"required_qty",
"column_break_9",
"transferred_qty",
"allow_alternative_item"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "item_code",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Item Code",
"collapsible": 0, "options": "Item",
"columns": 0, "read_only": 1
"fieldname": "item_code", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Code",
"length": 0,
"no_copy": 0,
"options": "Item",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "source_warehouse",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "ignore_user_permissions": 1,
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Source Warehouse",
"columns": 0, "options": "Warehouse"
"fieldname": "source_warehouse", },
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Source Warehouse",
"length": 0,
"no_copy": 0,
"options": "Warehouse",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "uom",
"allow_in_quick_entry": 0, "fieldtype": "Link",
"allow_on_submit": 0, "label": "UOM",
"bold": 0, "options": "UOM"
"collapsible": 0, },
"columns": 0,
"fieldname": "uom",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "UOM",
"length": 0,
"no_copy": 0,
"options": "UOM",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "item_name",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "label": "Item Name",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "item_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Item Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "description",
"allow_in_quick_entry": 0, "fieldtype": "Text",
"allow_on_submit": 0, "label": "Description",
"bold": 0, "read_only": 1
"collapsible": 0, },
"columns": 0,
"fieldname": "description",
"fieldtype": "Text",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "qty_section",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0, "label": "Qty"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "qty_section",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "required_qty",
"allow_in_quick_entry": 0, "fieldtype": "Float",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Required Qty",
"collapsible": 0, "read_only": 1
"columns": 0, },
"fieldname": "required_qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Required Qty",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_9",
"allow_in_quick_entry": 0, "fieldtype": "Column Break"
"allow_on_submit": 0, },
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_9",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0, "fieldname": "allow_alternative_item",
"allow_on_submit": 0, "fieldtype": "Check",
"bold": 0, "label": "Allow Alternative Item"
"collapsible": 0, },
"columns": 0, {
"fieldname": "allow_alternative_item", "fetch_from": "item_code.item_group",
"fieldtype": "Check", "fieldname": "item_group",
"hidden": 0, "fieldtype": "Link",
"ignore_user_permissions": 0, "label": "Item Group",
"ignore_xss_filter": 0, "options": "Item Group",
"in_filter": 0, "read_only": 1
"in_global_search": 0, },
"in_list_view": 0, {
"in_standard_filter": 0, "fetch_from": "item_code.stock_uom",
"label": "Allow Alternative Item", "fieldname": "stock_uom",
"length": 0, "fieldtype": "Link",
"no_copy": 0, "label": "Stock UOM",
"permlevel": 0, "options": "UOM"
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "fieldname": "transferred_qty",
"read_only": 0, "fieldtype": "Float",
"remember_last_selected_value": 0, "label": "Transferred Qty",
"report_hide": 0, "no_copy": 1,
"reqd": 0, "print_hide": 1,
"search_index": 0, "read_only": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "istable": 1,
"hide_toolbar": 0, "links": [],
"idx": 0, "modified": "2021-02-11 13:50:13.804108",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Manufacturing",
"is_submittable": 0, "name": "Job Card Item",
"issingle": 0, "owner": "Administrator",
"istable": 1, "permissions": [],
"max_attachments": 0, "quick_entry": 1,
"modified": "2018-08-28 15:23:48.099459", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Manufacturing", "track_changes": 1
"name": "Job Card Item",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@@ -11,10 +11,14 @@
"from_warehouse", "from_warehouse",
"warehouse", "warehouse",
"column_break_4", "column_break_4",
"required_bom_qty",
"quantity", "quantity",
"uom", "uom",
"projected_qty", "projected_qty",
"actual_qty", "actual_qty",
"ordered_qty",
"reserved_qty_for_production",
"safety_stock",
"item_details", "item_details",
"description", "description",
"min_order_qty", "min_order_qty",
@@ -129,11 +133,40 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "From Warehouse", "label": "From Warehouse",
"options": "Warehouse" "options": "Warehouse"
},
{
"fetch_from": "item_code.safety_stock",
"fieldname": "safety_stock",
"fieldtype": "Float",
"label": "Safety Stock",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "ordered_qty",
"fieldtype": "Float",
"label": "Ordered Qty",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "reserved_qty_for_production",
"fieldtype": "Float",
"label": "Reserved Qty for Production",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "required_bom_qty",
"fieldtype": "Float",
"label": "Required Qty as per BOM",
"no_copy": 1,
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-02-03 12:22:29.913302", "modified": "2021-03-26 12:41:13.013149",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Material Request Plan Item", "name": "Material Request Plan Item",

View File

@@ -251,7 +251,8 @@ frappe.ui.form.on('Production Plan', {
get_items_for_material_requests: function(frm, warehouses) { get_items_for_material_requests: function(frm, warehouses) {
const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse', const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse',
'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type']; 'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty',
'reserved_qty_for_production', 'material_request_type'];
frappe.call({ frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",

View File

@@ -32,6 +32,7 @@
"material_request_planning", "material_request_planning",
"include_non_stock_items", "include_non_stock_items",
"include_subcontracted_items", "include_subcontracted_items",
"include_safety_stock",
"ignore_existing_ordered_qty", "ignore_existing_ordered_qty",
"column_break_25", "column_break_25",
"for_warehouse", "for_warehouse",
@@ -309,13 +310,19 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Sales Order Status", "label": "Sales Order Status",
"options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver" "options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver"
},
{
"default": "0",
"fieldname": "include_safety_stock",
"fieldtype": "Check",
"label": "Include Safety Stock in Required Qty Calculation"
} }
], ],
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-11-10 18:01:54.991970", "modified": "2021-03-08 11:17:25.470147",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Plan", "name": "Production Plan",

View File

@@ -434,12 +434,14 @@ def download_raw_materials(doc):
if isinstance(doc, string_types): if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse', item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
'projected Qty', 'Actual Qty']] 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production',
'Safety Stock', 'Required Qty']]
for d in get_items_for_material_requests(doc): for d in get_items_for_material_requests(doc):
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('quantity'), item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
d.get('warehouse'), d.get('projected_qty'), d.get('actual_qty')]) d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
if not doc.get('for_warehouse'): if not doc.get('for_warehouse'):
row = {'item_code': d.get('item_code')} row = {'item_code': d.get('item_code')}
@@ -447,8 +449,9 @@ def download_raw_materials(doc):
if d.get("warehouse") == bin_dict.get('warehouse'): if d.get("warehouse") == bin_dict.get('warehouse'):
continue continue
item_list.append(['', '', '', '', bin_dict.get('warehouse'), item_list.append(['', '', '', bin_dict.get('warehouse'), '',
bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0)]) bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0),
bin_dict.get('ordered_qty', 0), bin_dict.get('reserved_qty_for_production', 0)])
build_csv_response(item_list, doc.name) build_csv_response(item_list, doc.name)
@@ -482,7 +485,7 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite
ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty,
item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
item.default_bom as default_bom, bom_item.description as description, item.default_bom as default_bom, bom_item.description as description,
bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock,
item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor
FROM FROM
`tabBOM Item` bom_item `tabBOM Item` bom_item
@@ -518,8 +521,8 @@ def get_subitems(doc, data, item_details, bom_no, company, include_non_stock_ite
include_non_stock_items, include_subcontracted_items, d.qty) include_non_stock_items, include_subcontracted_items, d.qty)
return item_details return item_details
def get_material_request_items(row, sales_order, def get_material_request_items(row, sales_order, company,
company, ignore_existing_ordered_qty, warehouse, bin_dict): ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict):
total_qty = row['qty'] total_qty = row['qty']
required_qty = 0 required_qty = 0
@@ -543,17 +546,24 @@ def get_material_request_items(row, sales_order,
if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"): if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"):
required_qty = ceil(required_qty) required_qty = ceil(required_qty)
if include_safety_stock:
required_qty += flt(row['safety_stock'])
if required_qty > 0: if required_qty > 0:
return { return {
'item_code': row.item_code, 'item_code': row.item_code,
'item_name': row.item_name, 'item_name': row.item_name,
'quantity': required_qty, 'quantity': required_qty,
'required_bom_qty': total_qty,
'description': row.description, 'description': row.description,
'stock_uom': row.get("stock_uom"), 'stock_uom': row.get("stock_uom"),
'warehouse': warehouse or row.get('source_warehouse') \ 'warehouse': warehouse or row.get('source_warehouse') \
or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"), or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"),
'safety_stock': row.safety_stock,
'actual_qty': bin_dict.get("actual_qty", 0), 'actual_qty': bin_dict.get("actual_qty", 0),
'projected_qty': bin_dict.get("projected_qty", 0), 'projected_qty': bin_dict.get("projected_qty", 0),
'ordered_qty': bin_dict.get("ordered_qty", 0),
'reserved_qty_for_production': bin_dict.get("reserved_qty_for_production", 0),
'min_order_qty': row['min_order_qty'], 'min_order_qty': row['min_order_qty'],
'material_request_type': row.get("default_material_request_type"), 'material_request_type': row.get("default_material_request_type"),
'sales_order': sales_order, 'sales_order': sales_order,
@@ -620,7 +630,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
""".format(lft, rgt, company) """.format(lft, rgt, company)
return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty, return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
ifnull(sum(actual_qty),0) as actual_qty, warehouse from `tabBin` ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin`
where item_code = %(item_code)s {conditions} where item_code = %(item_code)s {conditions}
group by item_code, warehouse group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
@@ -660,6 +671,7 @@ def get_items_for_material_requests(doc, warehouses=None):
company = doc.get('company') company = doc.get('company')
ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty') ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty')
include_safety_stock = doc.get('include_safety_stock')
so_item_details = frappe._dict() so_item_details = frappe._dict()
for data in po_items: for data in po_items:
@@ -711,6 +723,7 @@ def get_items_for_material_requests(doc, warehouses=None):
'description' : item_master.description, 'description' : item_master.description,
'stock_uom' : item_master.stock_uom, 'stock_uom' : item_master.stock_uom,
'conversion_factor' : conversion_factor, 'conversion_factor' : conversion_factor,
'safety_stock': item_master.safety_stock
} }
) )
@@ -732,7 +745,7 @@ def get_items_for_material_requests(doc, warehouses=None):
if details.qty > 0: if details.qty > 0:
items = get_material_request_items(details, sales_order, company, items = get_material_request_items(details, sales_order, company,
ignore_existing_ordered_qty, warehouse, bin_dict) ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict)
if items: if items:
mr_items.append(items) mr_items.append(items)

View File

@@ -333,8 +333,7 @@
"fieldname": "operations", "fieldname": "operations",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Operations", "label": "Operations",
"options": "Work Order Operation", "options": "Work Order Operation"
"read_only": 1
}, },
{ {
"depends_on": "operations", "depends_on": "operations",
@@ -496,7 +495,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-05-05 19:32:43.323054", "modified": "2021-03-16 13:27:51.116484",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Work Order", "name": "Work Order",

View File

@@ -61,7 +61,7 @@ class ForecastingReport(ExponentialSmoothingForecast):
from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1) from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1)
self.period_list = get_period_list(from_date, self.filters.to_date, self.period_list = get_period_list(from_date, self.filters.to_date,
from_date, self.filters.to_date, None, self.filters.periodicity, ignore_fiscal_year=True) from_date, self.filters.to_date, "Date Range", self.filters.periodicity, ignore_fiscal_year=True)
order_data = self.get_data_for_forecast() or [] order_data = self.get_data_for_forecast() or []

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Donation', {
refresh: function(frm) {
if (frm.doc.docstatus === 1 && !frm.doc.paid) {
frm.add_custom_button(__('Create Payment Entry'), function() {
frm.events.make_payment_entry(frm);
});
}
},
make_payment_entry: function(frm) {
return frappe.call({
method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
args: {
'dt': frm.doc.doctype,
'dn': frm.doc.name
},
callback: function(r) {
var doc = frappe.model.sync(r.message);
frappe.set_route('Form', doc[0].doctype, doc[0].name);
}
});
},
});

View File

@@ -0,0 +1,156 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2021-02-17 10:28:52.645731",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"donor",
"donor_name",
"email",
"column_break_4",
"company",
"date",
"payment_details_section",
"paid",
"amount",
"mode_of_payment",
"razorpay_payment_id",
"amended_from"
],
"fields": [
{
"fieldname": "donor",
"fieldtype": "Link",
"label": "Donor",
"options": "Donor",
"reqd": 1
},
{
"fetch_from": "donor.donor_name",
"fieldname": "donor_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Donor Name",
"read_only": 1
},
{
"fetch_from": "donor.email",
"fieldname": "email",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Email",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"reqd": 1
},
{
"fieldname": "payment_details_section",
"fieldtype": "Section Break",
"label": "Payment Details"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"reqd": 1
},
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment"
},
{
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "Razorpay Payment ID",
"read_only": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "NPO-DTN-.YYYY.-"
},
{
"default": "0",
"fieldname": "paid",
"fieldtype": "Check",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Paid"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Donation",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-03-11 10:53:11.269005",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Donation",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Non Profit Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
}
],
"search_fields": "donor_name, email",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "donor_name",
"track_changes": 1
}

View File

@@ -0,0 +1,219 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import six
import json
from frappe.model.document import Document
from frappe import _
from frappe.utils import getdate, flt, get_link_to_form
from frappe.email import sendmail_to_system_managers
from erpnext.non_profit.doctype.membership.membership import verify_signature
class Donation(Document):
def validate(self):
if not self.donor or not frappe.db.exists('Donor', self.donor):
# for web forms
user_type = frappe.db.get_value('User', frappe.session.user, 'user_type')
if user_type == 'Website User':
self.create_donor_for_website_user()
else:
frappe.throw(_('Please select a Member'))
def create_donor_for_website_user(self):
donor_name = frappe.get_value('Donor', dict(email=frappe.session.user))
if not donor_name:
user = frappe.get_doc('User', frappe.session.user)
donor = frappe.get_doc(dict(
doctype='Donor',
donor_type=self.get('donor_type'),
email=frappe.session.user,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
donor_name = donor.name
if self.get('__islocal'):
self.donor = donor_name
def on_payment_authorized(self, *args, **kwargs):
self.load_from_db()
self.create_payment_entry()
def create_payment_entry(self):
settings = frappe.get_doc('Non Profit Settings')
if not settings.automate_donation_payment_entries:
return
if not settings.donation_payment_account:
frappe.throw(_('You need to set <b>Payment Account</b> for Donation in {0}').format(
get_link_to_form('Non Profit Settings', 'Non Profit Settings')))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt=self.doctype, dn=self.name)
frappe.flags.ignore_account_permission = False
pe.paid_from = settings.donation_debit_account
pe.paid_to = settings.donation_payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
pe.flags.ignore_mandatory = True
pe.insert()
pe.submit()
@frappe.whitelist(allow_guest=True)
def capture_razorpay_donations(*args, **kwargs):
"""
Creates Donation from Razorpay Webhook Request Data on payment.captured event
Creates Donor from email if not found
"""
data = frappe.request.get_data(as_text=True)
try:
verify_signature(data, endpoint='Donation')
except Exception as e:
log = frappe.log_error(e, 'Donation Webhook Verification Error')
notify_failure(log)
return { 'status': 'Failed', 'reason': e }
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
payment = data.payload.get('payment', {}).get('entity', {})
payment = frappe._dict(payment)
try:
if not data.event == 'payment.captured':
return
# to avoid capturing subscription payments as donations
if payment.description and 'subscription' in str(payment.description).lower():
return
donor = get_donor(payment.email)
if not donor:
donor = create_donor(payment)
donation = create_donation(donor, payment)
donation.run_method('create_payment_entry')
except Exception as e:
message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id)
log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name))
notify_failure(log)
return { 'status': 'Failed', 'reason': e }
return { 'status': 'Success' }
def create_donation(donor, payment):
if not frappe.db.exists('Mode of Payment', payment.method):
create_mode_of_payment(payment.method)
company = get_company_for_donations()
donation = frappe.get_doc({
'doctype': 'Donation',
'company': company,
'donor': donor.name,
'donor_name': donor.donor_name,
'email': donor.email,
'date': getdate(),
'amount': flt(payment.amount) / 100, # Convert to rupees from paise
'mode_of_payment': payment.method,
'razorpay_payment_id': payment.id
}).insert(ignore_mandatory=True)
donation.submit()
return donation
def get_donor(email):
donors = frappe.get_all('Donor',
filters={'email': email},
order_by='creation desc')
try:
return frappe.get_doc('Donor', donors[0]['name'])
except Exception:
return None
@frappe.whitelist()
def create_donor(payment):
donor_details = frappe._dict(payment)
donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type')
donor = frappe.new_doc('Donor')
donor.update({
'donor_name': donor_details.email,
'donor_type': donor_type,
'email': donor_details.email,
'contact': donor_details.contact
})
if donor_details.get('notes'):
donor = get_additional_notes(donor, donor_details)
donor.insert(ignore_mandatory=True)
return donor
def get_company_for_donations():
company = frappe.db.get_single_value('Non Profit Settings', 'donation_company')
if not company:
from erpnext.healthcare.setup import get_company
company = get_company()
return company
def get_additional_notes(donor, donor_details):
if type(donor_details.notes) == dict:
for k, v in donor_details.notes.items():
notes = '\n'.join('{}: {}'.format(k, v))
# extract donor name from notes
if 'name' in k.lower():
donor.update({
'donor_name': donor_details.notes.get(k)
})
# extract pan from notes
if 'pan' in k.lower():
donor.update({
'pan_number': donor_details.notes.get(k)
})
donor.add_comment('Comment', notes)
elif type(donor_details.notes) == str:
donor.add_comment('Comment', donor_details.notes)
return donor
def create_mode_of_payment(method):
frappe.get_doc({
'doctype': 'Mode of Payment',
'mode_of_payment': method
}).insert(ignore_mandatory=True)
def notify_failure(log):
try:
content = '''
Dear System Manager,
Razorpay webhook for creating donation failed due to some reason.
Please check the error log linked below
Error Log: {0}
Regards, Administrator
'''.format(get_link_to_form('Error Log', log.name))
sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content)
except Exception:
pass

View File

@@ -0,0 +1,16 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'donation',
'non_standard_fieldnames': {
'Payment Entry': 'reference_name'
},
'transactions': [
{
'label': _('Payment'),
'items': ['Payment Entry']
}
]
}

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.non_profit.doctype.donation.donation import create_donation
class TestDonation(unittest.TestCase):
def setUp(self):
create_donor_type()
settings = frappe.get_doc('Non Profit Settings')
settings.company = '_Test Company'
settings.donation_company = '_Test Company'
settings.default_donor_type = '_Test Donor'
settings.automate_donation_payment_entries = 1
settings.donation_debit_account = 'Debtors - _TC'
settings.donation_payment_account = 'Cash - _TC'
settings.creation_user = 'Administrator'
settings.flags.ignore_permissions = True
settings.save()
def test_payment_entry_for_donations(self):
donor = create_donor()
create_mode_of_payment()
payment = frappe._dict({
'amount': 100,
'method': 'Debit Card',
'id': 'pay_MeXAmsgeKOhq7O'
})
donation = create_donation(donor, payment)
self.assertTrue(donation.name)
# Naive test to check if at all payment entry is generated
# This method is actually triggered from Payment Gateway
# In any case if details were missing, this would throw an error
donation.on_payment_authorized()
donation.reload()
self.assertEquals(donation.paid, 1)
self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name}))
def create_donor_type():
if not frappe.db.exists('Donor Type', '_Test Donor'):
frappe.get_doc({
'doctype': 'Donor Type',
'donor_type': '_Test Donor'
}).insert()
def create_donor():
donor = frappe.db.exists('Donor', 'donor@test.com')
if donor:
return frappe.get_doc('Donor', 'donor@test.com')
else:
return frappe.get_doc({
'doctype': 'Donor',
'donor_name': '_Test Donor',
'donor_type': '_Test Donor',
'email': 'donor@test.com'
}).insert()
def create_mode_of_payment():
if not frappe.db.exists('Mode of Payment', 'Debit Card'):
frappe.get_doc({
'doctype': 'Mode of Payment',
'mode_of_payment': 'Debit Card',
'accounts': [{
'company': '_Test Company',
'default_account': 'Cash - _TC'
}]
}).insert()

View File

@@ -76,8 +76,13 @@
} }
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [
"modified": "2020-09-16 23:46:04.083274", {
"link_doctype": "Donation",
"link_fieldname": "donor"
}
],
"modified": "2021-02-17 16:36:33.470731",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Donor", "name": "Donor",

View File

@@ -11,3 +11,8 @@ class Donor(Document):
"""Load address and contacts in `__onload`""" """Load address and contacts in `__onload`"""
load_address_and_contact(self) load_address_and_contact(self)
def validate(self):
from frappe.utils import validate_email_address
if self.email:
validate_email_address(self.email.strip(), True)

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on('Member', { frappe.ui.form.on('Member', {
setup: function(frm) { setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val && (frm.doc.subscription_id || frm.doc.customer_id)) { if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
frm.set_df_property('razorpay_details_section', 'hidden', false); frm.set_df_property('razorpay_details_section', 'hidden', false);
} }

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.utils import cint from frappe.utils import cint, get_link_to_form
from frappe.integrations.utils import get_payment_gateway_controller from frappe.integrations.utils import get_payment_gateway_controller
from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type
@@ -26,9 +26,10 @@ class Member(Document):
validate_email_address(email.strip(), True) validate_email_address(email.strip(), True)
def setup_subscription(self): def setup_subscription(self):
membership_settings = frappe.get_doc("Membership Settings") non_profit_settings = frappe.get_doc('Non Profit Settings')
if not membership_settings.enable_razorpay: if not non_profit_settings.enable_razorpay_for_memberships:
frappe.throw("Please enable Razorpay to setup subscription") frappe.throw('Please check Enable Razorpay for Memberships in {0} to setup subscription').format(
get_link_to_form('Non Profit Settings', 'Non Profit Settings'))
controller = get_payment_gateway_controller("Razorpay") controller = get_payment_gateway_controller("Razorpay")
settings = controller.get_settings({}) settings = controller.get_settings({})
@@ -40,7 +41,7 @@ class Member(Document):
subscription_details = { subscription_details = {
"plan_id": plan_id, "plan_id": plan_id,
"billing_frequency": cint(membership_settings.billing_frequency), "billing_frequency": cint(non_profit_settings.billing_frequency),
"customer_notify": 1 "customer_notify": 1
} }

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on('Membership', { frappe.ui.form.on('Membership', {
setup: function(frm) { setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => {
if (val) frm.set_df_property("razorpay_details_section", "hidden", false); if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
}) })
}, },
@@ -26,7 +26,7 @@ frappe.ui.form.on('Membership', {
}); });
}); });
frappe.db.get_single_value("Membership Settings", "send_email").then(val => { frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => {
if (val) frm.add_custom_button("Send Acknowledgement", () => { if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => { frm.call("send_acknowlement").then(() => {
frm.reload_doc(); frm.reload_doc();

View File

@@ -10,6 +10,7 @@
"member_name", "member_name",
"membership_type", "membership_type",
"column_break_3", "column_break_3",
"company",
"membership_status", "membership_status",
"membership_validity_section", "membership_validity_section",
"from_date", "from_date",
@@ -132,11 +133,18 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Member Name", "label": "Member Name",
"read_only": 1 "read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-01-21 16:31:20.032656", "modified": "2021-02-19 14:33:44.925122",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership", "name": "Membership",

View File

@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import json import json
import frappe import frappe
import six import six
import os
from datetime import datetime from datetime import datetime
from frappe.model.document import Document from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers from frappe.email import sendmail_to_system_managers
@@ -47,7 +48,7 @@ class Membership(Document):
last_membership = erpnext.get_last_membership(self.member) last_membership = erpnext.get_last_membership(self.member)
# if person applied for offline membership # if person applied for offline membership
if last_membership and not frappe.session.user == "Administrator": if last_membership and last_membership.name != self.name and not frappe.session.user == "Administrator":
# if last membership does not expire in 30 days, then do not allow to renew # if last membership does not expire in 30 days, then do not allow to renew
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) : if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
frappe.throw(_("You can only renew if your membership expires within 30 days")) frappe.throw(_("You can only renew if your membership expires within 30 days"))
@@ -58,7 +59,7 @@ class Membership(Document):
else: else:
self.from_date = nowdate() self.from_date = nowdate()
if frappe.db.get_single_value("Membership Settings", "billing_cycle") == "Yearly": if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly":
self.to_date = add_years(self.from_date, 1) self.to_date = add_years(self.from_date, 1)
else: else:
self.to_date = add_months(self.from_date, 1) self.to_date = add_months(self.from_date, 1)
@@ -68,9 +69,9 @@ class Membership(Document):
return return
self.load_from_db() self.load_from_db()
self.db_set("paid", 1) self.db_set("paid", 1)
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Non Profit Settings")
if settings.enable_invoicing and settings.create_for_web_forms: if settings.allow_invoicing and settings.automate_membership_invoicing:
self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True) self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
def generate_invoice(self, save=True, with_payment_entry=False): def generate_invoice(self, save=True, with_payment_entry=False):
@@ -85,10 +86,11 @@ class Membership(Document):
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type) plan = frappe.get_doc("Membership Type", self.membership_type)
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Non Profit Settings")
self.validate_membership_type_and_settings(plan, settings) self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings) invoice = make_invoice(self, member, plan, settings)
self.reload()
self.invoice = invoice.name self.invoice = invoice.name
if with_payment_entry: if with_payment_entry:
@@ -102,7 +104,7 @@ class Membership(Document):
def validate_membership_type_and_settings(self, plan, settings): def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type) settings_link = get_link_to_form("Membership Type", self.membership_type)
if not settings.debit_account: if not settings.membership_debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link)) frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
if not settings.company: if not settings.company:
@@ -113,25 +115,26 @@ class Membership(Document):
get_link_to_form("Membership Type", self.membership_type))) get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice): def make_payment_entry(self, settings, invoice):
if not settings.payment_account: if not settings.membership_payment_account:
frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format( frappe.throw(_("You need to set <b>Payment Account</b> for Membership in {0}").format(
get_link_to_form("Membership Type", self.membership_type))) get_link_to_form("Non Profit Settings", "Non Profit Settings")))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False frappe.flags.ignore_account_permission=False
pe.paid_to = settings.payment_account pe.paid_to = settings.membership_payment_account
pe.reference_no = self.name pe.reference_no = self.name
pe.reference_date = getdate() pe.reference_date = getdate()
pe.save(ignore_permissions=True) pe.flags.ignore_mandatory = True
pe.save()
pe.submit() pe.submit()
def send_acknowlement(self): def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Non Profit Settings")
if not settings.send_email: if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format( frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format(
get_link_to_form("Membership Settings", "Membership Settings"))) get_link_to_form("Non Profit Settings", "Non Profit Settings")))
member = frappe.get_doc("Member", self.member) member = frappe.get_doc("Member", self.member)
if not member.email_id: if not member.email_id:
@@ -170,7 +173,7 @@ def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({ invoice = frappe.get_doc({
"doctype": "Sales Invoice", "doctype": "Sales Invoice",
"customer": member.customer, "customer": member.customer,
"debit_to": settings.debit_account, "debit_to": settings.membership_debit_account,
"currency": membership.currency, "currency": membership.currency,
"company": settings.company, "company": settings.company,
"is_pos": 0, "is_pos": 0,
@@ -183,7 +186,7 @@ def make_invoice(membership, member, plan, settings):
] ]
}) })
invoice.set_missing_values() invoice.set_missing_values()
invoice.insert(ignore_permissions=True) invoice.insert()
invoice.submit() invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully")) frappe.msgprint(_("Sales Invoice created successfully"))
@@ -203,17 +206,18 @@ def get_member_based_on_subscription(subscription_id, email):
return None return None
def verify_signature(data): def verify_signature(data, endpoint="Membership"):
if frappe.flags.in_test: if frappe.flags.in_test or os.environ.get("CI"):
return True return True
signature = frappe.request.headers.get("X-Razorpay-Signature") signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Non Profit Settings")
key = settings.get_webhook_secret() key = settings.get_webhook_secret(endpoint)
controller = frappe.get_doc("Razorpay Settings") controller = frappe.get_doc("Razorpay Settings")
controller.verify_signature(data, signature, key) controller.verify_signature(data, signature, key)
frappe.set_user(settings.creation_user)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@@ -222,7 +226,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
try: try:
verify_signature(data) verify_signature(data)
except Exception as e: except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error") log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log) notify_failure(log)
return { "status": "Failed", "reason": e} return { "status": "Failed", "reason": e}
@@ -250,16 +254,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member.subscription_id = subscription.id member.subscription_id = subscription.id
member.customer_id = payment.customer_id member.customer_id = payment.customer_id
if subscription.notes and type(subscription.notes) == dict:
notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
member.add_comment("Comment", notes)
elif subscription.notes and type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes)
if subscription.get("notes"):
member = get_additional_notes(member, subscription)
company = get_company_for_memberships()
# Update Membership # Update Membership
membership = frappe.new_doc("Membership") membership = frappe.new_doc("Membership")
membership.update({ membership.update({
"company": company,
"member": member.name, "member": member.name,
"membership_status": "Current", "membership_status": "Current",
"membership_type": member.membership_type, "membership_type": member.membership_type,
@@ -270,15 +273,23 @@ def trigger_razorpay_subscription(*args, **kwargs):
"to_date": datetime.fromtimestamp(subscription.current_end), "to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise "amount": payment.amount / 100 # Convert to rupees from paise
}) })
membership.insert(ignore_permissions=True) membership.flags.ignore_mandatory = True
membership.insert()
# Update membership values # Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1 member.subscription_activated = 1
member.save(ignore_permissions=True) member.flags.ignore_mandatory = True
member.save()
settings = frappe.get_doc("Non Profit Settings")
if settings.allow_invoicing and settings.automate_membership_invoicing:
membership.reload()
membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
except Exception as e: except Exception as e:
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log) notify_failure(log)
return { "status": "Failed", "reason": e} return { "status": "Failed", "reason": e}
@@ -286,6 +297,39 @@ def trigger_razorpay_subscription(*args, **kwargs):
return { "status": "Success" } return { "status": "Success" }
def get_company_for_memberships():
company = frappe.db.get_single_value("Non Profit Settings", "company")
if not company:
from erpnext.healthcare.setup import get_company
company = get_company()
return company
def get_additional_notes(member, subscription):
if type(subscription.notes) == dict:
for k, v in subscription.notes.items():
notes = "\n".join("{}: {}".format(k, v))
# extract member name from notes
if "name" in k.lower():
member.update({
"member_name": subscription.notes.get(k)
})
# extract pan number from notes
if "pan" in k.lower():
member.update({
"pan_number": subscription.notes.get(k)
})
member.add_comment("Comment", notes)
elif type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes)
return member
def notify_failure(log): def notify_failure(log):
try: try:
content = """ content = """

View File

@@ -10,33 +10,7 @@ from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase): class TestMembership(unittest.TestCase):
def setUp(self): def setUp(self):
# Get default company plan = setup_membership()
company = frappe.get_doc("Company", erpnext.get_default_company())
# update membership settings
settings = frappe.get_doc("Membership Settings")
# Enable razorpay
settings.enable_razorpay = 1
settings.billing_cycle = "Monthly"
settings.billing_frequency = 24
# Enable invoicing
settings.enable_invoicing = 1
settings.make_payment_entry = 1
settings.company = company.name
settings.payment_account = company.default_cash_account
settings.debit_account = company.default_receivable_account
settings.save()
# make test plan
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
plan = frappe.new_doc("Membership Type")
plan.membership_type = "_rzpy_test_milythm"
plan.amount = 100
plan.razorpay_plan_id = "_rzpy_test_milythm"
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
plan.insert()
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
# make test member # make test member
self.member_doc = create_member(frappe._dict({ self.member_doc = create_member(frappe._dict({
@@ -78,7 +52,7 @@ class TestMembership(unittest.TestCase):
}) })
def set_config(key, value): def set_config(key, value):
frappe.db.set_value("Membership Settings", None, key, value) frappe.db.set_value("Non Profit Settings", None, key, value)
def make_membership(member, payload={}): def make_membership(member, payload={}):
data = { data = {
@@ -109,3 +83,36 @@ def create_item(item_code):
else: else:
item = frappe.get_doc("Item", item_code) item = frappe.get_doc("Item", item_code)
return item return item
def setup_membership():
# Get default company
company = frappe.get_doc("Company", erpnext.get_default_company())
# update non profit settings
settings = frappe.get_doc("Non Profit Settings")
# Enable razorpay
settings.enable_razorpay_for_memberships = 1
settings.billing_cycle = "Monthly"
settings.billing_frequency = 24
# Enable invoicing
settings.allow_invoicing = 1
settings.automate_membership_payment_entries = 1
settings.company = company.name
settings.donation_company = company.name
settings.membership_payment_account = company.default_cash_account
settings.membership_debit_account = company.default_receivable_account
settings.flags.ignore_mandatory = True
settings.save()
# make test plan
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
plan = frappe.new_doc("Membership Type")
plan.membership_type = "_rzpy_test_milythm"
plan.amount = 100
plan.razorpay_plan_id = "_rzpy_test_milythm"
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
plan.insert()
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
return plan

View File

@@ -1,192 +0,0 @@
{
"actions": [],
"creation": "2020-03-29 12:57:03.005120",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable_razorpay",
"razorpay_settings_section",
"billing_cycle",
"billing_frequency",
"webhook_secret",
"column_break_6",
"enable_invoicing",
"create_for_web_forms",
"make_payment_entry",
"company",
"debit_account",
"payment_account",
"column_break_9",
"send_email",
"send_invoice",
"membership_print_format",
"inv_print_format",
"email_template"
],
"fields": [
{
"fieldname": "billing_cycle",
"fieldtype": "Select",
"label": "Billing Cycle",
"options": "Monthly\nYearly"
},
{
"default": "0",
"fieldname": "enable_razorpay",
"fieldtype": "Check",
"label": "Enable RazorPay For Memberships"
},
{
"depends_on": "eval:doc.enable_razorpay",
"fieldname": "razorpay_settings_section",
"fieldtype": "Section Break",
"label": "RazorPay Settings"
},
{
"description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
"fieldname": "billing_frequency",
"fieldtype": "Int",
"label": "Billing Frequency"
},
{
"fieldname": "webhook_secret",
"fieldtype": "Password",
"label": "Webhook Secret",
"read_only": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Section Break",
"label": "Invoicing"
},
{
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Account"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Company"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
},
{
"default": "0",
"fieldname": "send_email",
"fieldtype": "Check",
"label": "Send Membership Acknowledgement"
},
{
"depends_on": "eval: doc.send_invoice",
"fieldname": "inv_print_format",
"fieldtype": "Link",
"label": "Invoice Print Format",
"mandatory_depends_on": "eval: doc.send_invoice",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "membership_print_format",
"fieldtype": "Link",
"label": "Membership Print Format",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "email_template",
"fieldtype": "Link",
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
},
{
"default": "0",
"fieldname": "enable_invoicing",
"fieldtype": "Check",
"label": "Enable Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
"fieldname": "make_payment_entry",
"fieldtype": "Check",
"label": "Make Payment Entry"
},
{
"depends_on": "eval:doc.make_payment_entry",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment To",
"mandatory_depends_on": "eval:doc.make_payment_entry",
"options": "Account"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Automatically create an invoice when payment is authorized from a web form entry",
"fieldname": "create_for_web_forms",
"fieldtype": "Check",
"label": "Auto Create Invoice for Web Forms"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Member",
"share": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.integrations.utils import get_payment_gateway_controller
from frappe.model.document import Document
class MembershipSettings(Document):
def generate_webhook_key(self):
key = frappe.generate_hash(length=20)
self.webhook_secret = key
self.save()
frappe.msgprint(
_("Here is your webhook secret, this will be shown to you only once.") + "<br><br>" + key,
_("Webhook Secret")
);
def revoke_key(self):
self.webhook_secret = None;
self.save()
def get_webhook_secret(self):
return self.get_password(fieldname="webhook_secret", raise_exception=False)
@frappe.whitelist()
def get_plans_for_membership(*args, **kwargs):
controller = get_payment_gateway_controller("Razorpay")
plans = controller.get_plans()
return [plan.get("item") for plan in plans.get("items")]

View File

@@ -3,11 +3,11 @@
frappe.ui.form.on('Membership Type', { frappe.ui.form.on('Membership Type', {
refresh: function (frm) { refresh: function (frm) {
frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => { frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
}); });
frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => { frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false); if (val) frm.set_df_property('linked_item', 'hidden', false);
}); });

View File

@@ -1,16 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Membership Settings", { frappe.ui.form.on("Non Profit Settings", {
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.webhook_secret) {
frm.add_custom_button(__("Revoke <Key></Key>"), () => {
frm.call("revoke_key").then(() => {
frm.refresh();
})
});
}
frm.set_query("inv_print_format", function() { frm.set_query("inv_print_format", function() {
return { return {
filters: { filters: {
@@ -37,7 +29,7 @@ frappe.ui.form.on("Membership Settings", {
}; };
}); });
frm.set_query("payment_account", function () { frm.set_query("membership_payment_account", function () {
var account_types = ["Bank", "Cash"]; var account_types = ["Bank", "Cash"];
return { return {
filters: { filters: {
@@ -51,31 +43,70 @@ frappe.ui.form.on("Membership Settings", {
let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true); frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
frm.trigger("setup_buttons_for_membership");
frm.trigger("add_generate_button"); frm.trigger("setup_buttons_for_donation");
frm.trigger("add_copy_buttonn");
}, },
add_generate_button: function(frm) { setup_buttons_for_membership: function(frm) {
let label; let label;
if (frm.doc.webhook_secret) { if (frm.doc.membership_webhook_secret) {
frm.add_custom_button(__("Copy Webhook URL"), () => {
frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
}, __("Memberships"));
frm.add_custom_button(__("Revoke Key"), () => {
frm.call("revoke_key", {
key: "membership_webhook_secret"
}).then(() => {
frm.refresh();
});
}, __("Memberships"));
label = __("Regenerate Webhook Secret"); label = __("Regenerate Webhook Secret");
} else { } else {
label = __("Generate Webhook Secret"); label = __("Generate Webhook Secret");
} }
frm.add_custom_button(label, () => { frm.add_custom_button(label, () => {
frm.call("generate_webhook_key").then(() => { frm.call("generate_webhook_secret", {
field: "membership_webhook_secret"
}).then(() => {
frm.refresh(); frm.refresh();
}); });
}); }, __("Memberships"));
}, },
add_copy_buttonn: function(frm) { setup_buttons_for_donation: function(frm) {
if (frm.doc.webhook_secret) { let label;
if (frm.doc.donation_webhook_secret) {
label = __("Regenerate Webhook Secret");
frm.add_custom_button(__("Copy Webhook URL"), () => { frm.add_custom_button(__("Copy Webhook URL"), () => {
frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.donation.donation.capture_razorpay_donations`);
}); }, __("Donations"));
frm.add_custom_button(__("Revoke Key"), () => {
frm.call("revoke_key", {
key: "donation_webhook_secret"
}).then(() => {
frm.refresh();
});
}, __("Donations"));
} else {
label = __("Generate Webhook Secret");
} }
frm.add_custom_button(label, () => {
frm.call("generate_webhook_secret", {
field: "donation_webhook_secret"
}).then(() => {
frm.refresh();
});
}, __("Donations"));
} }
}); });

View File

@@ -0,0 +1,273 @@
{
"actions": [],
"creation": "2020-03-29 12:57:03.005120",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable_razorpay_for_memberships",
"razorpay_settings_section",
"billing_cycle",
"billing_frequency",
"membership_webhook_secret",
"column_break_6",
"allow_invoicing",
"automate_membership_invoicing",
"automate_membership_payment_entries",
"company",
"membership_debit_account",
"membership_payment_account",
"column_break_9",
"send_email",
"send_invoice",
"membership_print_format",
"inv_print_format",
"email_template",
"donation_settings_section",
"donation_company",
"default_donor_type",
"donation_webhook_secret",
"column_break_22",
"automate_donation_payment_entries",
"donation_debit_account",
"donation_payment_account",
"section_break_27",
"creation_user"
],
"fields": [
{
"fieldname": "billing_cycle",
"fieldtype": "Select",
"label": "Billing Cycle",
"options": "Monthly\nYearly"
},
{
"depends_on": "eval:doc.enable_razorpay_for_memberships",
"fieldname": "razorpay_settings_section",
"fieldtype": "Section Break",
"label": "RazorPay Settings for Memberships"
},
{
"description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
"fieldname": "billing_frequency",
"fieldtype": "Int",
"label": "Billing Frequency"
},
{
"fieldname": "column_break_6",
"fieldtype": "Section Break",
"label": "Membership Invoicing"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"description": "This company will be set for the Memberships created via webhook.",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"default": "0",
"depends_on": "eval:doc.allow_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
},
{
"default": "0",
"fieldname": "send_email",
"fieldtype": "Check",
"label": "Send Membership Acknowledgement"
},
{
"depends_on": "eval: doc.send_invoice",
"fieldname": "inv_print_format",
"fieldtype": "Link",
"label": "Invoice Print Format",
"mandatory_depends_on": "eval: doc.send_invoice",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "membership_print_format",
"fieldtype": "Link",
"label": "Membership Print Format",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "email_template",
"fieldtype": "Link",
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
},
{
"default": "0",
"fieldname": "allow_invoicing",
"fieldtype": "Check",
"label": "Allow Invoicing for Memberships",
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
},
{
"default": "0",
"depends_on": "eval:doc.allow_invoicing",
"description": "Automatically create an invoice when payment is authorized from a web form entry",
"fieldname": "automate_membership_invoicing",
"fieldtype": "Check",
"label": "Automate Invoicing for Web Forms"
},
{
"default": "0",
"depends_on": "eval:doc.allow_invoicing",
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
"fieldname": "automate_membership_payment_entries",
"fieldtype": "Check",
"label": "Automate Payment Entry Creation"
},
{
"default": "0",
"fieldname": "enable_razorpay_for_memberships",
"fieldtype": "Check",
"label": "Enable RazorPay For Memberships"
},
{
"depends_on": "eval:doc.automate_membership_payment_entries",
"description": "Account for accepting membership payments",
"fieldname": "membership_payment_account",
"fieldtype": "Link",
"label": "Membership Payment To",
"mandatory_depends_on": "eval:doc.automate_membership_payment_entries",
"options": "Account"
},
{
"fieldname": "membership_webhook_secret",
"fieldtype": "Password",
"label": "Membership Webhook Secret",
"read_only": 1
},
{
"fieldname": "donation_webhook_secret",
"fieldtype": "Password",
"label": "Donation Webhook Secret",
"read_only": 1
},
{
"depends_on": "automate_donation_payment_entries",
"description": "Account for accepting donation payments",
"fieldname": "donation_payment_account",
"fieldtype": "Link",
"label": "Donation Payment To",
"mandatory_depends_on": "automate_donation_payment_entries",
"options": "Account"
},
{
"default": "0",
"description": "Auto creates Payment Entry for Donations created from web forms.",
"fieldname": "automate_donation_payment_entries",
"fieldtype": "Check",
"label": "Automate Donation Payment Entries"
},
{
"depends_on": "eval:doc.allow_invoicing",
"fieldname": "membership_debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "eval:doc.allow_invoicing",
"options": "Account"
},
{
"depends_on": "automate_donation_payment_entries",
"fieldname": "donation_debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "automate_donation_payment_entries",
"options": "Account"
},
{
"description": "This company will be set for the Donations created via webhook.",
"fieldname": "donation_company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "donation_settings_section",
"fieldtype": "Section Break",
"label": "Donation Settings"
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"description": "This Donor Type will be set for the Donor created via Donation web form entry.",
"fieldname": "default_donor_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Donor Type",
"options": "Donor Type",
"reqd": 1
},
{
"fieldname": "section_break_27",
"fieldtype": "Section Break"
},
{
"description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.",
"fieldname": "creation_user",
"fieldtype": "Link",
"label": "Creation User",
"options": "User",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-11 10:43:38.124240",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Member",
"share": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.integrations.utils import get_payment_gateway_controller
from frappe.model.document import Document
class NonProfitSettings(Document):
def generate_webhook_secret(self, field="membership_webhook_secret"):
key = frappe.generate_hash(length=20)
self.set(field, key)
self.save()
secret_for = "Membership" if field == "membership_webhook_secret" else "Donation"
frappe.msgprint(
_("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "<br><br>" + key,
_("Webhook Secret")
)
def revoke_key(self, key):
self.set(key, None)
self.save()
def get_webhook_secret(self, endpoint="Membership"):
fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret"
return self.get_password(fieldname=fieldname, raise_exception=False)
@frappe.whitelist()
def get_plans_for_membership(*args, **kwargs):
controller = get_payment_gateway_controller("Razorpay")
plans = controller.get_plans()
return [plan.get("item") for plan in plans.get("items")]

View File

@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe # import frappe
import unittest import unittest
class TestMembershipSettings(unittest.TestCase): class TestNonProfitSettings(unittest.TestCase):
pass pass

View File

@@ -0,0 +1,251 @@
{
"category": "Domains",
"charts": [],
"creation": "2020-03-02 17:23:47.811421",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends_another_page": 0,
"hide_custom": 0,
"icon": "non-profit",
"idx": 0,
"is_default": 0,
"is_standard": 1,
"label": "Non Profit",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Loan Management",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Type",
"link_to": "Loan Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Application",
"link_to": "Loan Application",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan",
"link_to": "Loan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Grant Application",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Grant Application",
"link_to": "Grant Application",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Membership",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Member",
"link_to": "Member",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Membership",
"link_to": "Membership",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Membership Type",
"link_to": "Membership Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Membership Settings",
"link_to": "Non Profit Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Volunteer",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Volunteer",
"link_to": "Volunteer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Volunteer Type",
"link_to": "Volunteer Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Chapter",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Chapter",
"link_to": "Chapter",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Donation",
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Donor",
"link_to": "Donor",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Donor Type",
"link_to": "Donor Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Donation",
"link_to": "Donation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Tax Exemption Certification (India)",
"link_type": "DocType",
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Tax Exemption 80G Certificate",
"link_to": "Tax Exemption 80G Certificate",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2021-03-11 11:38:09.140655",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
"restrict_to_domain": "Non Profit",
"shortcuts": [
{
"label": "Member",
"link_to": "Member",
"type": "DocType"
},
{
"label": "Non Profit Settings",
"link_to": "Non Profit Settings",
"type": "DocType"
},
{
"label": "Membership",
"link_to": "Membership",
"type": "DocType"
},
{
"label": "Chapter",
"link_to": "Chapter",
"type": "DocType"
},
{
"label": "Chapter Member",
"link_to": "Chapter Member",
"type": "DocType"
}
]
}

View File

@@ -753,3 +753,5 @@ erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v12_0.add_state_code_for_ladakh erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings

View File

@@ -20,9 +20,11 @@ def execute():
frappe.clear_cache() frappe.clear_cache()
frappe.flags.warehouse_account_map = {} frappe.flags.warehouse_account_map = {}
company_list = []
data = frappe.db.sql(''' data = frappe.db.sql('''
SELECT SELECT
name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company
FROM FROM
`tabStock Ledger Entry` `tabStock Ledger Entry`
WHERE WHERE
@@ -36,6 +38,9 @@ def execute():
total_sle = len(data) total_sle = len(data)
i = 0 i = 0
for d in data: for d in data:
if d.company not in company_list:
company_list.append(d.company)
update_entries_after({ update_entries_after({
"item_code": d.item_code, "item_code": d.item_code,
"warehouse": d.warehouse, "warehouse": d.warehouse,
@@ -53,8 +58,10 @@ def execute():
print("Reposting General Ledger Entries...") print("Reposting General Ledger Entries...")
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): if data:
update_gl_entries_after(posting_date, posting_time, company=row.name) for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
if row.name in company_list:
update_gl_entries_after(posting_date, posting_time, company=row.name)
frappe.db.auto_commit_on_many_writes = 0 frappe.db.auto_commit_on_many_writes = 0

View File

@@ -0,0 +1,22 @@
from __future__ import unicode_literals
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.table_exists("Membership Settings"):
frappe.rename_doc("DocType", "Membership Settings", "Non Profit Settings")
frappe.reload_doctype("Non Profit Settings", force=True)
if frappe.db.table_exists("Non Profit Settings"):
rename_fields_map = {
"enable_invoicing": "allow_invoicing",
"create_for_web_forms": "automate_membership_invoicing",
"make_payment_entry": "automate_membership_payment_entries",
"enable_razorpay": "enable_razorpay_for_memberships",
"debit_account": "membership_debit_account",
"payment_account": "membership_payment_account",
"webhook_secret": "membership_webhook_secret"
}
for old_name, new_name in rename_fields_map.items():
rename_field("Non Profit Settings", old_name, new_name)

View File

@@ -0,0 +1,16 @@
import frappe
from erpnext.regional.india.setup import make_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
make_custom_fields()
if not frappe.db.exists('Party Type', 'Donor'):
frappe.get_doc({
'doctype': 'Party Type',
'party_type': 'Donor',
'account_type': 'Receivable'
}).insert(ignore_permissions=True)

View File

@@ -89,10 +89,11 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days return amount_per_day * no_of_days
@frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type):
def get_additional_salary_component(employee, start_date, end_date, component_type): additional_salary_list = frappe.db.sql("""
additional_salaries = frappe.db.sql(""" select name, salary_component as component, type, amount,
select name, salary_component, type, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date overwrite_salary_structure_amount as overwrite,
deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary` from `tabAdditional Salary`
where employee=%(employee)s where employee=%(employee)s
and docstatus = 1 and docstatus = 1
@@ -102,7 +103,7 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty
from_date <= %(to_date)s and to_date >= %(to_date)s from_date <= %(to_date)s and to_date >= %(to_date)s
) )
and type = %(component_type)s and type = %(component_type)s
order by salary_component, overwrite_salary_structure_amount DESC order by salary_component, overwrite ASC
""", { """, {
'employee': employee, 'employee': employee,
'from_date': start_date, 'from_date': start_date,
@@ -110,38 +111,18 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty
'component_type': "Earning" if component_type == "earnings" else "Deduction" 'component_type': "Earning" if component_type == "earnings" else "Deduction"
}, as_dict=1) }, as_dict=1)
existing_salary_components= [] additional_salaries = []
salary_components_details = {} components_to_overwrite = []
additional_salary_details = []
overwrites_components = [ele.salary_component for ele in additional_salaries if ele.overwrite_salary_structure_amount == 1] for d in additional_salary_list:
if d.overwrite:
if d.component in components_to_overwrite:
frappe.throw(_("Multiple Additional Salaries with overwrite "
"property exist for Salary Component {0} between {1} and {2}.").format(
frappe.bold(d.component), start_date, end_date), title=_("Error"))
component_fields = ["depends_on_payment_days", "salary_component_abbr", "is_tax_applicable", "variable_based_on_taxable_salary", 'type'] components_to_overwrite.append(d.component)
for d in additional_salaries:
if d.salary_component not in existing_salary_components: additional_salaries.append(d)
component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields)
struct_row = frappe._dict({'salary_component': d.salary_component})
if component:
struct_row.update(component[0])
struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date return additional_salaries
struct_row['is_additional_component'] = 1
salary_components_details[d.salary_component] = struct_row
if overwrites_components.count(d.salary_component) > 1:
frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component: {0} between {1} and {2}.".format(d.salary_component, start_date, end_date)), title=_("Error"))
else:
additional_salary_details.append({
'name': d.name,
'component': d.salary_component,
'amount': d.amount,
'type': d.type,
'overwrite': d.overwrite_salary_structure_amount,
})
existing_salary_components.append(d.salary_component)
return salary_components_details, additional_salary_details

View File

@@ -13,7 +13,7 @@ from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_da
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase from erpnext.utilities.transaction_base import TransactionBase
from frappe.utils.background_jobs import enqueue from frappe.utils.background_jobs import enqueue
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salary_component from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
@@ -540,15 +540,16 @@ class SalarySlip(TransactionBase):
self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings") self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings")
def add_additional_salary_components(self, component_type): def add_additional_salary_components(self, component_type):
salary_components_details, additional_salary_details = get_additional_salary_component(self.employee, additional_salaries = get_additional_salaries(self.employee,
self.start_date, self.end_date, component_type) self.start_date, self.end_date, component_type)
if salary_components_details and additional_salary_details:
for additional_salary in additional_salary_details: for additional_salary in additional_salaries:
additional_salary =frappe._dict(additional_salary) self.update_component_row(
amount = additional_salary.amount get_salary_component_data(additional_salary.component),
overwrite = additional_salary.overwrite additional_salary.amount,
self.update_component_row(frappe._dict(salary_components_details[additional_salary.component]), amount, component_type,
component_type, overwrite=overwrite, additional_salary=additional_salary.name) additional_salary
)
def add_tax_components(self, payroll_period): def add_tax_components(self, payroll_period):
# Calculate variable_based_on_taxable_salary after all components updated in salary slip # Calculate variable_based_on_taxable_salary after all components updated in salary slip
@@ -565,46 +566,59 @@ class SalarySlip(TransactionBase):
for d in tax_components: for d in tax_components:
tax_amount = self.calculate_variable_based_on_taxable_salary(d, payroll_period) tax_amount = self.calculate_variable_based_on_taxable_salary(d, payroll_period)
tax_row = self.get_salary_slip_row(d) tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions") self.update_component_row(tax_row, tax_amount, "deductions")
def update_component_row(self, struct_row, amount, key, overwrite=1, additional_salary = ''): def update_component_row(self, component_data, amount, component_type, additional_salary=None):
component_row = None component_row = None
for d in self.get(key): for d in self.get(component_type):
if d.salary_component == struct_row.salary_component: if d.salary_component != component_data.salary_component:
continue
if (
(not d.additional_salary
and (not additional_salary or additional_salary.overwrite))
or (additional_salary
and additional_salary.name == d.additional_salary)
):
component_row = d component_row = d
if not component_row or (struct_row.get("is_additional_component") and not overwrite): break
if amount:
self.append(key, { if additional_salary and additional_salary.overwrite:
'amount': amount, # Additional Salary with overwrite checked, remove default rows of same component
'default_amount': amount if not struct_row.get("is_additional_component") else 0, self.set(component_type, [
'depends_on_payment_days' : struct_row.depends_on_payment_days, d for d in self.get(component_type)
'salary_component' : struct_row.salary_component, if d.salary_component != component_data.salary_component
'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"), or (d.additional_salary and additional_salary.name != d.additional_salary)
'additional_salary': additional_salary, or d == component_row
'do_not_include_in_total' : struct_row.do_not_include_in_total, ])
'is_tax_applicable': struct_row.is_tax_applicable,
'is_flexible_benefit': struct_row.is_flexible_benefit, if not component_row:
'variable_based_on_taxable_salary': struct_row.variable_based_on_taxable_salary, if not amount:
'deduct_full_tax_on_selected_payroll_date': struct_row.deduct_full_tax_on_selected_payroll_date, return
'additional_amount': amount if struct_row.get("is_additional_component") else 0,
'exempted_from_income_tax': struct_row.exempted_from_income_tax component_row = self.append(component_type)
}) for attr in (
'depends_on_payment_days', 'salary_component', 'abbr'
'do_not_include_in_total', 'is_tax_applicable',
'is_flexible_benefit', 'variable_based_on_taxable_salary',
'exempted_from_income_tax'
):
component_row.set(attr, component_data.get(attr))
if additional_salary:
component_row.default_amount = 0
component_row.additional_amount = amount
component_row.additional_salary = additional_salary.name
component_row.deduct_full_tax_on_selected_payroll_date = \
additional_salary.deduct_full_tax_on_selected_payroll_date
else: else:
if struct_row.get("is_additional_component"): component_row.default_amount = amount
if overwrite: component_row.additional_amount = 0
component_row.additional_amount = amount - component_row.get("default_amount", 0) component_row.deduct_full_tax_on_selected_payroll_date = \
component_row.additional_salary = additional_salary component_data.deduct_full_tax_on_selected_payroll_date
else:
component_row.additional_amount = amount
if not overwrite and component_row.default_amount: component_row.amount = amount
amount += component_row.default_amount
else:
component_row.default_amount = amount
component_row.amount = amount
component_row.deduct_full_tax_on_selected_payroll_date = struct_row.deduct_full_tax_on_selected_payroll_date
def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period): def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period):
if not payroll_period: if not payroll_period:
@@ -937,19 +951,6 @@ class SalarySlip(TransactionBase):
frappe.throw(_("Error in formula or condition: {0}").format(e)) frappe.throw(_("Error in formula or condition: {0}").format(e))
raise raise
def get_salary_slip_row(self, salary_component):
component = frappe.get_doc("Salary Component", salary_component)
# Data for update_component_row
struct_row = frappe._dict()
struct_row['depends_on_payment_days'] = component.depends_on_payment_days
struct_row['salary_component'] = component.name
struct_row['abbr'] = component.salary_component_abbr
struct_row['do_not_include_in_total'] = component.do_not_include_in_total
struct_row['is_tax_applicable'] = component.is_tax_applicable
struct_row['is_flexible_benefit'] = component.is_flexible_benefit
struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
return struct_row
def get_component_totals(self, component_type, depends_on_payment_days=0): def get_component_totals(self, component_type, depends_on_payment_days=0):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"]) ["date_of_joining", "relieving_date"])
@@ -1012,7 +1013,6 @@ class SalarySlip(TransactionBase):
self.total_loan_repayment += payment.total_payment self.total_loan_repayment += payment.total_payment
def get_loan_details(self): def get_loan_details(self):
return frappe.get_all("Loan", return frappe.get_all("Loan",
fields=["name", "interest_income_account", "loan_account", "loan_type"], fields=["name", "interest_income_account", "loan_account", "loan_type"],
filters = { filters = {
@@ -1241,4 +1241,20 @@ def unlink_ref_doc_from_salary_slip(ref_no):
def generate_password_for_pdf(policy_template, employee): def generate_password_for_pdf(policy_template, employee):
employee = frappe.get_doc("Employee", employee) employee = frappe.get_doc("Employee", employee)
return policy_template.format(**employee.as_dict()) return policy_template.format(**employee.as_dict())
def get_salary_component_data(component):
return frappe.get_value(
"Salary Component",
component,
[
"name as salary_component",
"depends_on_payment_days",
"salary_component_abbr as abbr",
"do_not_include_in_total",
"is_tax_applicable",
"is_flexible_benefit",
"variable_based_on_taxable_salary",
],
as_dict=1,
)

View File

@@ -245,7 +245,7 @@ class TestSalarySlip(unittest.TestCase):
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period) payroll_period=payroll_period)
frappe.db.sql("""delete from `tabLoan""") frappe.db.sql("delete from tabLoan")
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1 loan.repay_from_salary = 1
loan.submit() loan.submit()

View File

@@ -576,7 +576,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate); me.add_taxes_from_item_tax_template(d.item_tax_rate);
if (d.free_item_data) { if (d.free_item_data) {
me.apply_product_discount(d.free_item_data); me.apply_product_discount(d);
} }
}, },
() => { () => {
@@ -713,21 +713,15 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
else { else {
var valid_serial_nos = []; var valid_serial_nos = [];
var serialnos = [];
// Replacing all occurences of comma with carriage return // Replacing all occurences of comma with carriage return
var serial_nos = item.serial_no.trim().replace(/,/g, '\n'); item.serial_no = item.serial_no.replace(/,/g, '\n');
serialnos = item.serial_no.split("\n");
serial_nos = serial_nos.trim().split('\n'); for (var i = 0; i < serialnos.length; i++) {
if (serialnos[i] != "") {
// Trim each string and push unique string to new list valid_serial_nos.push(serialnos[i]);
for (var x=0; x<=serial_nos.length - 1; x++) {
if (serial_nos[x].trim() != "" && valid_serial_nos.indexOf(serial_nos[x].trim()) == -1) {
valid_serial_nos.push(serial_nos[x].trim());
} }
} }
// Add the new list to the serial no. field in grid with each in new line
item.serial_no = valid_serial_nos.join('\n');
item.conversion_factor = item.conversion_factor || 1; item.conversion_factor = item.conversion_factor || 1;
refresh_field("serial_no", item.name, item.parentfield); refresh_field("serial_no", item.name, item.parentfield);
@@ -1148,6 +1142,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.calculate_net_weight(); this.calculate_net_weight();
} }
// for handling customization not to fetch price list rate
if(frappe.flags.dont_fetch_price_list_rate) {
return
}
if (!dont_fetch_price_list_rate && if (!dont_fetch_price_list_rate &&
frappe.meta.has_field(doc.doctype, "price_list_currency")) { frappe.meta.has_field(doc.doctype, "price_list_currency")) {
this.apply_price_list(item, true); this.apply_price_list(item, true);
@@ -1508,7 +1507,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if(k=="price_list_rate") { if(k=="price_list_rate") {
if(flt(v) != flt(d.price_list_rate)) price_list_rate_changed = true; if(flt(v) != flt(d.price_list_rate)) price_list_rate_changed = true;
} }
frappe.model.set_value(d.doctype, d.name, k, v);
if (k !== 'free_item_data') {
frappe.model.set_value(d.doctype, d.name, k, v);
}
} }
} }
@@ -1520,7 +1522,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
if (d.free_item_data) { if (d.free_item_data) {
me.apply_product_discount(d.free_item_data); me.apply_product_discount(d);
} }
if (d.apply_rule_on_other_items) { if (d.apply_rule_on_other_items) {
@@ -1554,20 +1556,31 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
} }
}, },
apply_product_discount: function(free_item_data) { apply_product_discount: function(args) {
const items = this.frm.doc.items.filter(d => (d.item_code == free_item_data.item_code const items = this.frm.doc.items.filter(d => (d.is_free_item)) || [];
&& d.is_free_item)) || [];
if (!items.length) { const exist_items = items.map(row => (row.item_code, row.pricing_rules));
let row_to_modify = frappe.model.add_child(this.frm.doc,
this.frm.doc.doctype + ' Item', 'items');
for (let key in free_item_data) { args.free_item_data.forEach(pr_row => {
row_to_modify[key] = free_item_data[key]; let row_to_modify = {};
if (!items || !in_list(exist_items, (pr_row.item_code, pr_row.pricing_rules))) {
row_to_modify = frappe.model.add_child(this.frm.doc,
this.frm.doc.doctype + ' Item', 'items');
} else if(items) {
row_to_modify = items.filter(d => (d.item_code === pr_row.item_code
&& d.pricing_rules === pr_row.pricing_rules))[0];
} }
} if (items && items.length && free_item_data) {
items[0].qty = free_item_data.qty for (let key in pr_row) {
} row_to_modify[key] = pr_row[key];
}
});
// free_item_data is a temporary variable
args.free_item_data = '';
refresh_field('items');
}, },
apply_price_list: function(item, reset_plc_conversion) { apply_price_list: function(item, reset_plc_conversion) {
@@ -1894,7 +1907,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.throw(__("Please enter Item Code to get batch no")); frappe.throw(__("Please enter Item Code to get batch no"));
} else if (doc.doctype == "Purchase Receipt" || } else if (doc.doctype == "Purchase Receipt" ||
(doc.doctype == "Purchase Invoice" && doc.update_stock)) { (doc.doctype == "Purchase Invoice" && doc.update_stock)) {
return { return {
filters: {'item': item.item_code} filters: {'item': item.item_code}
} }
@@ -1920,9 +1932,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
set_query_for_item_tax_template: function(doc, cdt, cdn) { set_query_for_item_tax_template: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if(!item.item_code) { if(!item.item_code) {
frappe.throw(__("Please enter Item Code to get item taxes")); return doc.company ? {filters: {company: doc.company}} : {};
} else { } else {
let filters = { let filters = {
'item_code': item.item_code, 'item_code': item.item_code,
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
@@ -2133,4 +2144,4 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => {
} }
} }
}); });
}; };

View File

@@ -349,13 +349,12 @@ class GSTR3BReport(Document):
return inter_state_supply_details return inter_state_supply_details
def get_inward_nil_exempt(self, state): def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount, inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
where p.docstatus = 1 and p.name = i.parent where p.docstatus = 1 and p.name = i.parent
and i.is_nil_exempt = 1 or i.is_non_gst = 1 and and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s
group by p.place_of_supply """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = { inward_nil_exempt_details = {
"gst": { "gst": {

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Tax Exemption 80G Certificate', {
refresh: function(frm) {
if (frm.doc.donor) {
frm.set_query('donation', function() {
return {
filters: {
docstatus: 1,
donor: frm.doc.donor
}
};
});
}
},
recipient: function(frm) {
if (frm.doc.recipient === 'Donor') {
frm.set_value({
'member': '',
'member_name': '',
'member_email': '',
'member_pan_number': '',
'fiscal_year': '',
'total': 0,
'payments': []
});
} else {
frm.set_value({
'donor': '',
'donor_name': '',
'donor_email': '',
'donor_pan_number': '',
'donation': '',
'date_of_donation': '',
'amount': 0,
'mode_of_payment': '',
'razorpay_payment_id': ''
});
}
},
get_payments: function(frm) {
frm.call({
doc: frm.doc,
method: 'get_payments',
freeze: true
});
},
company: function(frm) {
if ((frm.doc.member || frm.doc.donor) && frm.doc.company) {
frm.call({
doc: frm.doc,
method: 'set_company_address',
freeze: true
});
}
},
donation: function(frm) {
if (frm.doc.recipient === 'Donor' && !frm.doc.donor) {
frappe.msgprint(__('Please select donor first'));
}
}
});

View File

@@ -0,0 +1,297 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2021-02-15 12:37:21.577042",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"recipient",
"member",
"member_name",
"member_email",
"member_pan_number",
"donor",
"donor_name",
"donor_email",
"donor_pan_number",
"column_break_4",
"date",
"fiscal_year",
"section_break_11",
"company",
"company_address",
"company_address_display",
"column_break_14",
"company_pan_number",
"company_80g_number",
"company_80g_wef",
"title",
"section_break_6",
"get_payments",
"payments",
"total",
"donation_details_section",
"donation",
"date_of_donation",
"amount",
"column_break_27",
"mode_of_payment",
"razorpay_payment_id"
],
"fields": [
{
"fieldname": "recipient",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Certificate Recipient",
"options": "Member\nDonor",
"reqd": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Member",
"mandatory_depends_on": "eval:doc.recipient === \"Member\";",
"options": "Member"
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fetch_from": "member.member_name",
"fieldname": "member_name",
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fieldname": "donor",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Donor",
"mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
"options": "Donor"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"reqd": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "payments",
"fieldtype": "Table",
"label": "Payments",
"options": "Tax Exemption 80G Certificate Detail"
},
{
"fieldname": "total",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Fiscal Year",
"options": "Fiscal Year"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "get_payments",
"fieldtype": "Button",
"label": "Get Memberships"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "NPO-80G-.YYYY.-"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"label": "Company Details"
},
{
"fieldname": "company_address",
"fieldtype": "Link",
"label": "Company Address",
"options": "Address"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"fetch_from": "company.pan_details",
"fieldname": "company_pan_number",
"fieldtype": "Data",
"label": "PAN Number",
"read_only": 1
},
{
"fieldname": "company_address_display",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Company Address Display",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "company.company_80g_number",
"fieldname": "company_80g_number",
"fieldtype": "Data",
"label": "80G Number",
"read_only": 1
},
{
"fetch_from": "company.with_effect_from",
"fieldname": "company_80g_wef",
"fieldtype": "Date",
"label": "80G With Effect From",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fieldname": "donation_details_section",
"fieldtype": "Section Break",
"label": "Donation Details"
},
{
"fieldname": "donation",
"fieldtype": "Link",
"label": "Donation",
"mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
"options": "Donation"
},
{
"fetch_from": "donation.amount",
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1
},
{
"fetch_from": "donation.mode_of_payment",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1
},
{
"fetch_from": "donation.razorpay_payment_id",
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "RazorPay Payment ID",
"read_only": 1
},
{
"fetch_from": "donation.date",
"fieldname": "date_of_donation",
"fieldtype": "Date",
"label": "Date of Donation",
"read_only": 1
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fetch_from": "donor.donor_name",
"fieldname": "donor_name",
"fieldtype": "Data",
"label": "Donor Name",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fetch_from": "donor.email",
"fieldname": "donor_email",
"fieldtype": "Data",
"label": "Email",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fetch_from": "member.email_id",
"fieldname": "member_email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Email",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fetch_from": "member.pan_number",
"fieldname": "member_pan_number",
"fieldtype": "Data",
"label": "PAN Details",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fetch_from": "donor.pan_number",
"fieldname": "donor_pan_number",
"fieldtype": "Data",
"label": "PAN Details",
"read_only": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"print_hide": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-22 00:03:34.215633",
"modified_by": "Administrator",
"module": "Regional",
"name": "Tax Exemption 80G Certificate",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "member, member_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1
}

View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, flt, get_link_to_form
from erpnext.accounts.utils import get_fiscal_year
from frappe.contacts.doctype.address.address import get_company_address
class TaxExemption80GCertificate(Document):
def validate(self):
self.validate_date()
self.validate_duplicates()
self.validate_company_details()
self.set_company_address()
self.calculate_total()
self.set_title()
def validate_date(self):
if self.recipient == 'Member':
if getdate(self.date):
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
if not (fiscal_year.year_start_date <= getdate(self.date) \
<= fiscal_year.year_end_date):
frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
def validate_duplicates(self):
if self.recipient == 'Donor':
certificate = frappe.db.exists(self.doctype, {
'donation': self.donation,
'name': ('!=', self.name)
})
if certificate:
frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format(
get_link_to_form(self.doctype, certificate), frappe.bold(self.donation)
), title=_('Duplicate Certificate'))
def validate_company_details(self):
fields = ['company_80g_number', 'with_effect_from', 'pan_details']
company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True)
if not company_details.company_80g_number:
frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'),
get_link_to_form('Company', self.company)))
if not company_details.pan_details:
frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
get_link_to_form('Company', self.company)))
def set_company_address(self):
address = get_company_address(self.company)
self.company_address = address.company_address
self.company_address_display = address.company_address_display
def calculate_total(self):
if self.recipient == 'Donor':
return
total = 0
for entry in self.payments:
total += flt(entry.amount)
self.total = total
def set_title(self):
if self.recipient == 'Member':
self.title = self.member_name
else:
self.title = self.donor_name
def get_payments(self):
if not self.member:
frappe.throw(_('Please select a Member first.'))
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
memberships = frappe.db.get_all('Membership', {
'member': self.member,
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled')
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'])
if not memberships:
frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
total = 0
self.payments = []
for doc in memberships:
self.append('payments', {
'date': doc.from_date,
'amount': doc.amount,
'invoice_id': doc.invoice,
'razorpay_payment_id': doc.payment_id,
'membership': doc.name
})
total += flt(doc.amount)
self.total = total

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.non_profit.doctype.donation.test_donation import create_donor, create_mode_of_payment, create_donor_type
from erpnext.non_profit.doctype.donation.donation import create_donation
from erpnext.non_profit.doctype.membership.test_membership import setup_membership, make_membership
from erpnext.non_profit.doctype.member.member import create_member
class TestTaxExemption80GCertificate(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabTax Exemption 80G Certificate`')
frappe.db.sql('delete from `tabMembership`')
create_donor_type()
settings = frappe.get_doc('Non Profit Settings')
settings.company = '_Test Company'
settings.donation_company = '_Test Company'
settings.default_donor_type = '_Test Donor'
settings.creation_user = 'Administrator'
settings.save()
company = frappe.get_doc('Company', '_Test Company')
company.pan_details = 'BBBTI3374C'
company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087'
company.with_effect_from = getdate()
company.save()
def test_duplicate_donation_certificate(self):
donor = create_donor()
create_mode_of_payment()
payment = frappe._dict({
'amount': 100,
'method': 'Debit Card',
'id': 'pay_MeXAmsgeKOhq7O'
})
donation = create_donation(donor, payment)
args = frappe._dict({
'recipient': 'Donor',
'donor': donor.name,
'donation': donation.name
})
certificate = create_80g_certificate(args)
certificate.insert()
# check company details
self.assertEquals(certificate.company_pan_number, 'BBBTI3374C')
self.assertEquals(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087')
# check donation details
self.assertEquals(certificate.amount, donation.amount)
duplicate_certificate = create_80g_certificate(args)
# duplicate validation
self.assertRaises(frappe.ValidationError, duplicate_certificate.insert)
def test_membership_80g_certificate(self):
plan = setup_membership()
# make test member
member_doc = create_member(frappe._dict({
'fullname': "_Test_Member",
'email': "_test_member_erpnext@example.com",
'plan_id': plan.name
}))
member_doc.make_customer_and_link()
member = member_doc.name
membership = make_membership(member, { "from_date": getdate() })
invoice = membership.generate_invoice(save=True)
args = frappe._dict({
'recipient': 'Member',
'member': member,
'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name')
})
certificate = create_80g_certificate(args)
certificate.get_payments()
certificate.insert()
self.assertEquals(len(certificate.payments), 1)
self.assertEquals(certificate.payments[0].amount, membership.amount)
self.assertEquals(certificate.payments[0].invoice_id, invoice.name)
def create_80g_certificate(args):
certificate = frappe.get_doc({
'doctype': 'Tax Exemption 80G Certificate',
'recipient': args.recipient,
'date': getdate(),
'company': '_Test Company'
})
certificate.update(args)
return certificate

View File

@@ -0,0 +1,66 @@
{
"actions": [],
"creation": "2021-02-15 12:43:52.754124",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"date",
"amount",
"invoice_id",
"column_break_4",
"razorpay_payment_id",
"membership"
],
"fields": [
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"reqd": 1
},
{
"fieldname": "invoice_id",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Invoice ID",
"options": "Sales Invoice",
"reqd": 1
},
{
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "Razorpay Payment ID"
},
{
"fieldname": "membership",
"fieldtype": "Link",
"label": "Membership",
"options": "Membership"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-15 16:35:10.777587",
"modified_by": "Administrator",
"module": "Regional",
"name": "Tax Exemption 80G Certificate Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class TaxExemption80GCertificateDetail(Document):
pass

View File

@@ -1,7 +1,8 @@
erpnext.setup_einvoice_actions = (doctype) => { erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, { frappe.ui.form.on(doctype, {
refresh(frm) { async refresh(frm) {
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable"); const { message } = await frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
const einvoicing_enabled = cint(message.enable);
const supply_type = frm.doc.gst_category; const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;

View File

@@ -86,10 +86,10 @@ def get_doc_details(invoice):
invoice_date=invoice_date invoice_date=invoice_date
)) ))
def get_party_details(address_name): def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None):
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
if (not d.gstin if ((not d.gstin and not shipping_address)
or not d.city or not d.city
or not d.pincode or not d.pincode
or not d.address_title or not d.address_title
@@ -107,13 +107,17 @@ def get_party_details(address_name):
# according to einvoice standard # according to einvoice standard
pincode = 999999 pincode = 999999
return frappe._dict(dict( party_address_details = frappe._dict(dict(
gstin=d.gstin, legal_name=d.address_title, legal_name=d.address_title,
location=d.city, pincode=d.pincode, location=d.city, pincode=d.pincode,
state_code=d.gst_state_number, state_code=d.gst_state_number,
address_line1=d.address_line1, address_line1=d.address_line1,
address_line2=d.address_line2 address_line2=d.address_line2
)) ))
if d.gstin:
party_address_details.gstin = d.gstin
return party_address_details
def get_gstin_details(gstin): def get_gstin_details(gstin):
if not hasattr(frappe.local, 'gstin_cache'): if not hasattr(frappe.local, 'gstin_cache'):
@@ -320,14 +324,18 @@ def make_einvoice(invoice):
item_list = get_item_list(invoice) item_list = get_item_list(invoice)
doc_details = get_doc_details(invoice) doc_details = get_doc_details(invoice)
invoice_value_details = get_invoice_value_details(invoice) invoice_value_details = get_invoice_value_details(invoice)
seller_details = get_party_details(invoice.company_address) seller_details = get_party_details(invoice.company_address, company_address=1)
if invoice.gst_category == 'Overseas': if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address) buyer_details = get_overseas_address_details(invoice.customer_address)
else: else:
buyer_details = get_party_details(invoice.customer_address) buyer_details = get_party_details(invoice.customer_address, billing_address=1)
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin place_of_supply = get_place_of_supply(invoice, invoice.doctype)
place_of_supply = place_of_supply[:2] if place_of_supply:
place_of_supply = place_of_supply.split('-')[0]
else:
place_of_supply = invoice.billing_address_gstin[:2]
buyer_details.update(dict(place_of_supply=place_of_supply)) buyer_details.update(dict(place_of_supply=place_of_supply))
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
@@ -335,7 +343,7 @@ def make_einvoice(invoice):
if invoice.gst_category == 'Overseas': if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name) shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else: else:
shipping_details = get_party_details(invoice.shipping_address_name) shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1)
if invoice.is_pos and invoice.base_paid_amount: if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice) payment_details = get_payment_details(invoice)
@@ -343,7 +351,7 @@ def make_einvoice(invoice):
if invoice.is_return and invoice.return_against: if invoice.is_return and invoice.return_against:
prev_doc_details = get_return_doc_reference(invoice) prev_doc_details = get_return_doc_reference(invoice)
if invoice.transporter: if invoice.transporter and cint(invoice.distance):
eway_bill_details = get_eway_bill_details(invoice) eway_bill_details = get_eway_bill_details(invoice)
# not yet implemented # not yet implemented
@@ -371,7 +379,10 @@ def make_einvoice(invoice):
return einvoice return einvoice
def validate_einvoice(validations, einvoice, errors=[]): def validate_einvoice(validations, einvoice, errors=None):
if errors is None:
errors = []
for fieldname, field_validation in validations.items(): for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None) value = einvoice.get(fieldname, None)
if not value or value == "None": if not value or value == "None":
@@ -417,7 +428,7 @@ def validate_einvoice(validations, einvoice, errors=[]):
errors.append(_('{} should not exceed {} characters').format(label, max_length)) errors.append(_('{} should not exceed {} characters').format(label, max_length))
if value_type == 'number' and (value > maximum or value < minimum): if value_type == 'number' and (value > maximum or value < minimum):
errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
if pattern_str and not pattern.match(value): if pattern_str and not pattern.match(value) and field_validation.get('validationMsg'):
errors.append(field_validation.get('validationMsg')) errors.append(field_validation.get('validationMsg'))
return errors return errors
@@ -448,13 +459,18 @@ class GSPConnector():
gstin = self.get_seller_gstin() gstin = self.get_seller_gstin()
if not self.e_invoice_settings.enable: if not self.e_invoice_settings.enable:
frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin]
if credentials_for_gstin:
credentials = credentials_for_gstin[0]
else:
frappe.throw(_('Cannot find e-invoicing credentials for GSTIN {}. Please check E-Invoice Settings').format(gstin))
else: else:
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
return credentials return credentials
def get_seller_gstin(self): def get_seller_gstin(self):
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
if not gstin: if not gstin:
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
return gstin return gstin

View File

@@ -498,6 +498,14 @@ def make_custom_fields(update=True):
fieldtype='Link', options='Salary Component', insert_after='basic_component'), fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='arrear_component', label='Arrear Component', dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_component'), fieldtype='Link', options='Salary Component', insert_after='hra_component'),
dict(fieldname='non_profit_section', label='Non Profit Settings',
fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
dict(fieldname='company_80g_number', label='80G Number',
fieldtype='Data', insert_after='non_profit_section'),
dict(fieldname='with_effect_from', label='80G With Effect From',
fieldtype='Date', insert_after='company_80g_number'),
dict(fieldname='pan_details', label='PAN Number',
fieldtype='Data', insert_after='with_effect_from')
], ],
'Employee Tax Exemption Declaration':[ 'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption', dict(fieldname='hra_section', label='HRA Exemption',
@@ -580,7 +588,15 @@ def make_custom_fields(update=True):
'options': '\nWith Payment of Tax\nWithout Payment of Tax' 'options': '\nWith Payment of Tax\nWithout Payment of Tax'
} }
], ],
"Member": [ 'Member': [
{
'fieldname': 'pan_number',
'label': 'PAN Details',
'fieldtype': 'Data',
'insert_after': 'email_id'
}
],
'Donor': [
{ {
'fieldname': 'pan_number', 'fieldname': 'pan_number',
'label': 'PAN Details', 'label': 'PAN Details',
@@ -642,7 +658,7 @@ def set_tax_withholding_category(company):
pass pass
docs = get_tds_details(accounts, fiscal_year) docs = get_tds_details(accounts, fiscal_year)
for d in docs: for d in docs:
try: try:
doc = frappe.get_doc(d) doc = frappe.get_doc(d)
@@ -660,7 +676,7 @@ def set_tax_withholding_category(company):
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
if not fy_exist: if not fy_exist:
doc.append("rates", d.get('rates')[0]) doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.save() doc.save()

View File

@@ -698,25 +698,12 @@ def update_grand_total_for_rcm(doc, method):
if country != 'India': if country != 'India':
return return
if not doc.total_taxes_and_charges: gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return return
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
base_gst_tax = 0
gst_tax = 0
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
doc.taxes_and_charges_added -= gst_tax doc.taxes_and_charges_added -= gst_tax
doc.total_taxes_and_charges -= gst_tax doc.total_taxes_and_charges -= gst_tax
doc.base_taxes_and_charges_added -= base_gst_tax doc.base_taxes_and_charges_added -= base_gst_tax
@@ -750,7 +737,9 @@ def make_regional_gl_entries(gl_entries, doc):
if country != 'India': if country != 'India':
return gl_entries return gl_entries
if not doc.total_taxes_and_charges: gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return gl_entries return gl_entries
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
@@ -781,6 +770,24 @@ def make_regional_gl_entries(gl_entries, doc):
return gl_entries return gl_entries
def get_gst_tax_amount(doc):
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account', []) + gst_accounts.get('sgst_account', []) \
+ gst_accounts.get('igst_account', [])
base_gst_tax = 0
gst_tax = 0
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
return gst_tax, base_gst_tax
@frappe.whitelist() @frappe.whitelist()
def get_regional_round_off_accounts(company, account_list): def get_regional_round_off_accounts(company, account_list):
country = frappe.get_cached_value('Company', company, 'country') country = frappe.get_cached_value('Company', company, 'country')
@@ -800,4 +807,4 @@ def get_regional_round_off_accounts(company, account_list):
account_list.extend(gst_account_list) account_list.extend(gst_account_list)
return account_list return account_list

View File

@@ -0,0 +1,26 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-02-22 00:17:33.878581",
"css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
"custom_format": 1,
"default_print_language": "en",
"disabled": 0,
"doc_type": "Tax Exemption 80G Certificate",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} 80G Donor Certificate</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n\n This is to confirm that the {{ doc.company }} received an amount of <b>{{doc.get_formatted(\"amount\")}}</b>\n from <b>{{ doc.donor_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n <br><br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
"idx": 0,
"line_breaks": 0,
"modified": "2021-02-22 00:20:08.516600",
"modified_by": "Administrator",
"module": "Regional",
"name": "80G Certificate for Donation",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@@ -0,0 +1,26 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-02-15 16:53:55.026611",
"css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
"custom_format": 1,
"default_print_language": "en",
"disabled": 0,
"doc_type": "Tax Exemption 80G Certificate",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} Members 80G Donor Certificate</h3>\n <h3 class=\"text-center\">Financial Cycle {{ doc.fiscal_year }}</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n This is to confirm that the {{ doc.company }} received a total amount of <b>{{doc.get_formatted(\"total\")}}</b>\n from <b>{{ doc.member_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n as per the payment details given below:\n \n <br><br>\n <table class=\"table table-bordered table-condensed\">\n \t<thead>\n \t\t<tr>\n \t\t\t<th >{{ _(\"Date\") }}</th>\n \t\t\t<th class=\"text-right\">{{ _(\"Amount\") }}</th>\n \t\t\t<th class=\"text-right\">{{ _(\"Invoice ID\") }}</th>\n \t\t</tr>\n \t</thead>\n \t<tbody>\n \t\t{%- for payment in doc.payments -%}\n \t\t<tr>\n \t\t\t<td> {{ payment.date }} </td>\n \t\t\t<td class=\"text-right\">{{ payment.get_formatted(\"amount\") }}</td>\n \t\t\t<td class=\"text-right\">{{ payment.invoice_id }}</td>\n \t\t</tr>\n \t\t{%- endfor -%}\n \t</tbody>\n </table>\n \n <br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
"idx": 0,
"line_breaks": 0,
"modified": "2021-02-21 23:29:00.778973",
"modified_by": "Administrator",
"module": "Regional",
"name": "80G Certificate for Membership",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@@ -106,6 +106,10 @@ erpnext.PointOfSale.Controller = class {
}) })
return frappe.utils.play_sound("error"); return frappe.utils.play_sound("error");
} }
// filter balance details for empty rows
balance_details = balance_details.filter(d => d.mode_of_payment);
const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher"; const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher";
const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true }); const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true });
!res.exc && this.prepare_app_defaults(res.message); !res.exc && this.prepare_app_defaults(res.message);

View File

@@ -10,6 +10,7 @@ from frappe import msgprint, throw, _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.naming import parse_naming_series from frappe.model.naming import parse_naming_series
from frappe.permissions import get_doctypes_with_read from frappe.permissions import get_doctypes_with_read
from frappe.core.doctype.doctype.doctype import validate_series
class NamingSeriesNotSetError(frappe.ValidationError): pass class NamingSeriesNotSetError(frappe.ValidationError): pass
@@ -126,7 +127,7 @@ class NamingSeries(Document):
dt = frappe.get_doc("DocType", self.select_doc_for_series) dt = frappe.get_doc("DocType", self.select_doc_for_series)
options = self.scrub_options_list(self.set_options.split("\n")) options = self.scrub_options_list(self.set_options.split("\n"))
for series in options: for series in options:
dt.validate_series(series) validate_series(dt, series)
for i in sr: for i in sr:
if i[0]: if i[0]:
existing_series = [d.split('.')[0] for d in i[0].split("\n")] existing_series = [d.split('.')[0] for d in i[0].split("\n")]

View File

@@ -195,6 +195,7 @@ def install(country=None):
{'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"},
{'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"},
{'doctype': "Opportunity Type", "name": "Hub"}, {'doctype': "Opportunity Type", "name": "Hub"},
{'doctype': "Opportunity Type", "name": _("Sales")}, {'doctype': "Opportunity Type", "name": _("Sales")},

View File

@@ -93,7 +93,7 @@ class Batch(Document):
if create_new_batch: if create_new_batch:
if batch_number_series: if batch_number_series:
self.batch_id = make_autoname(batch_number_series) self.batch_id = make_autoname(batch_number_series, doc=self)
elif batch_uses_naming_series(): elif batch_uses_naming_series():
self.batch_id = self.get_name_from_naming_series() self.batch_id = self.get_name_from_naming_series()
else: else:

View File

@@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form from frappe.utils import cint, get_link_to_form, add_to_date, today
from erpnext.stock.stock_ledger import repost_future_sle from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
@@ -29,7 +29,7 @@ class RepostItemValuation(Document):
self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company") self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
elif self.warehouse: elif self.warehouse:
self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company") self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
def set_status(self, status=None): def set_status(self, status=None):
if not status: if not status:
status = 'Queued' status = 'Queued'
@@ -54,7 +54,6 @@ def repost(doc):
repost_sl_entries(doc) repost_sl_entries(doc)
repost_gl_entries(doc) repost_gl_entries(doc)
check_if_stock_and_account_balance_synced(doc.posting_date, doc.company)
doc.set_status('Completed') doc.set_status('Completed')
except Exception: except Exception:
@@ -103,7 +102,7 @@ def notify_error_to_stock_managers(doc, traceback):
recipients = get_users_with_role("Stock Manager") recipients = get_users_with_role("Stock Manager")
if not recipients: if not recipients:
get_users_with_role("System Manager") get_users_with_role("System Manager")
subject = _("Error while reposting item valuation") subject = _("Error while reposting item valuation")
message = (_("Hi,") + "<br>" message = (_("Hi,") + "<br>"
+ _("An error has been appeared while reposting item valuation via {0}") + _("An error has been appeared while reposting item valuation via {0}")
@@ -112,4 +111,24 @@ def notify_error_to_stock_managers(doc, traceback):
) )
frappe.sendmail(recipients=recipients, subject=subject, message=message) frappe.sendmail(recipients=recipients, subject=subject, message=message)
def repost_entries():
riv_entries = get_repost_item_valuation_entries()
for row in riv_entries:
doc = frappe.get_cached_doc('Repost Item Valuation', row.name)
repost(doc)
riv_entries = get_repost_item_valuation_entries()
if riv_entries:
return
for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
check_if_stock_and_account_balance_synced(today(), d.name)
def get_repost_item_valuation_entries():
date = add_to_date(today(), hours=-12)
return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
WHERE status != 'Completed' and creation <= %s and docstatus = 1
ORDER BY timestamp(posting_date, posting_time) asc, creation asc
""", date, as_dict=1)

View File

@@ -817,7 +817,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
} }
erpnext.hide_company(); erpnext.hide_company();
erpnext.utils.add_item(this.frm); erpnext.utils.add_item(this.frm);
this.frm.trigger('add_to_transit');
}, },
scan_barcode: function() { scan_barcode: function() {

View File

@@ -163,7 +163,7 @@ class StockEntry(StockController):
if self.purpose not in valid_purposes: if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
if self.job_card and self.purpose != 'Material Transfer for Manufacture': if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']:
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))
@@ -823,6 +823,7 @@ class StockEntry(StockController):
if self.job_card: if self.job_card:
job_doc = frappe.get_doc('Job Card', self.job_card) job_doc = frappe.get_doc('Job Card', self.job_card)
job_doc.set_transferred_qty(update_status=True) job_doc.set_transferred_qty(update_status=True)
job_doc.set_transferred_qty_in_job_card(self)
if self.work_order: if self.work_order:
pro_doc = frappe.get_doc("Work Order", self.work_order) pro_doc = frappe.get_doc("Work Order", self.work_order)

View File

@@ -69,7 +69,8 @@
"putaway_rule", "putaway_rule",
"column_break_51", "column_break_51",
"reference_purchase_receipt", "reference_purchase_receipt",
"quality_inspection" "quality_inspection",
"job_card_item"
], ],
"fields": [ "fields": [
{ {
@@ -532,13 +533,22 @@
"fieldname": "is_finished_item", "fieldname": "is_finished_item",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Finished Item" "label": "Is Finished Item"
},
{
"fieldname": "job_card_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Job Card Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-12-30 15:00:44.489442", "modified": "2021-02-11 13:47:50.158754",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Entry Detail", "name": "Stock Entry Detail",

View File

@@ -29,6 +29,8 @@ class StockReconciliation(StockController):
self.remove_items_with_no_change() self.remove_items_with_no_change()
self.validate_data() self.validate_data()
self.validate_expense_account() self.validate_expense_account()
self.validate_customer_provided_item()
self.set_zero_value_for_customer_provided_items()
self.set_total_qty_and_amount() self.set_total_qty_and_amount()
self.validate_putaway_capacity() self.validate_putaway_capacity()
@@ -217,7 +219,7 @@ class StockReconciliation(StockController):
if row.valuation_rate in ("", None): if row.valuation_rate in ("", None):
row.valuation_rate = previous_sle.get("valuation_rate", 0) row.valuation_rate = previous_sle.get("valuation_rate", 0)
if row.qty and not row.valuation_rate: if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate:
frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx)) frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction") if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction")
@@ -436,6 +438,20 @@ class StockReconciliation(StockController):
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss": if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError) frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError)
def set_zero_value_for_customer_provided_items(self):
changed_any_values = False
for d in self.get('items'):
is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item')
if is_customer_item and d.valuation_rate:
d.valuation_rate = 0.0
changed_any_values = True
if changed_any_values:
msgprint(_("Valuation rate for customer provided items has been set to zero."),
title=_("Note"), indicator="blue")
def set_total_qty_and_amount(self): def set_total_qty_and_amount(self):
for d in self.get("items"): for d in self.get("items"):
d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate")) d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
@@ -531,4 +547,4 @@ def get_difference_account(purpose, company):
account = frappe.db.get_value('Account', {'is_group': 0, account = frappe.db.get_value('Account', {'is_group': 0,
'company': company, 'account_type': 'Temporary'}, 'name') 'company': company, 'account_type': 'Temporary'}, 'name')
return account return account

View File

@@ -193,6 +193,16 @@ class TestStockReconciliation(unittest.TestCase):
stock_doc = frappe.get_doc("Stock Reconciliation", d) stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel() stock_doc.cancel()
def test_customer_provided_items(self):
item_code = 'Stock-Reco-customer-Item-100'
create_item(item_code, is_customer_provided_item = 1,
customer = '_Test Customer', is_purchase_item = 0)
sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420)
self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(sr.get("items")[0].valuation_rate, 0)
self.assertEqual(sr.get("items")[0].amount, 0)
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

@@ -13,6 +13,7 @@
"qty", "qty",
"valuation_rate", "valuation_rate",
"amount", "amount",
"allow_zero_valuation_rate",
"serial_no_and_batch_section", "serial_no_and_batch_section",
"serial_no", "serial_no",
"column_break_11", "column_break_11",
@@ -166,10 +167,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
"options": "Batch" "options": "Batch"
},
{
"default": "0",
"fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check",
"label": "Allow Zero Valuation Rate",
"print_hide": 1,
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"modified": "2019-06-14 17:10:53.188305", "links": [],
"modified": "2021-03-23 11:09:44.407157",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Reconciliation Item", "name": "Stock Reconciliation Item",
@@ -179,4 +189,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -314,7 +314,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
"last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0, "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0,
"transaction_date": args.get("transaction_date"), "transaction_date": args.get("transaction_date"),
"against_blanket_order": args.get("against_blanket_order"), "against_blanket_order": args.get("against_blanket_order"),
"bom_no": item.get("default_bom") "bom_no": item.get("default_bom"),
"weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"),
"weight_uom": args.get("weight_uom") or item.get("weight_uom")
}) })
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"): if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
@@ -369,6 +371,9 @@ def get_basic_details(args, item, overwrite_warehouse=True):
if meta.get_field("barcode"): if meta.get_field("barcode"):
update_barcode_value(out) update_barcode_value(out)
if out.get("weight_per_unit"):
out['total_weight'] = out.weight_per_unit * out.stock_qty
return out return out
def get_item_warehouse(item, args, overwrite_warehouse, defaults={}): def get_item_warehouse(item, args, overwrite_warehouse, defaults={}):

View File

@@ -198,7 +198,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) - flt(qty_dict.bal_val) value_diff = flt(d.stock_value_difference)
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

@@ -260,8 +260,7 @@ class IssueSummary(object):
self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0 self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
def get_chart_data(self): def get_chart_data(self):
if not self.data: self.chart = []
return None
labels = [] labels = []
open_issues = [] open_issues = []
@@ -310,8 +309,7 @@ class IssueSummary(object):
} }
def get_report_summary(self): def get_report_summary(self):
if not self.data: self.report_summary = []
return None
open_issues = 0 open_issues = 0
replied = 0 replied = 0