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

This commit is contained in:
Deepesh Garg
2021-09-29 16:13:28 +05:30
93 changed files with 1760 additions and 1043 deletions

View File

@@ -34,9 +34,9 @@ if __name__ == "__main__":
if response.ok: if response.ok:
payload = response.json() payload = response.json()
title = payload.get("title", "").lower().strip() title = (payload.get("title") or "").lower().strip()
head_sha = payload.get("head", {}).get("sha") head_sha = (payload.get("head") or {}).get("sha")
body = payload.get("body", "").lower() body = (payload.get("body") or "").lower()
if (title.startswith("feat") if (title.startswith("feat")
and head_sha and head_sha

View File

@@ -79,7 +79,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file $(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
} else { } else {
generate_tree_preview(frm); generate_tree_preview(frm);
validate_csv_data(frm);
} }
}, },
@@ -104,23 +103,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
} }
}); });
var validate_csv_data = function(frm) {
frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_accounts",
args: {file_name: frm.doc.import_file},
callback: function(r) {
if(r.message && r.message[0]===true) {
frm.page["show_import_button"] = true;
frm.page["total_accounts"] = r.message[1];
frm.trigger("refresh");
} else {
frm.page.set_indicator(__('Resolve error and upload again.'), 'orange');
frappe.throw(__(r.message));
}
}
});
};
var create_import_button = function(frm) { var create_import_button = function(frm) {
frm.page.set_primary_action(__("Import"), function () { frm.page.set_primary_action(__("Import"), function () {
frappe.call({ frappe.call({
@@ -151,6 +133,7 @@ var create_reset_button = function(frm) {
}; };
var generate_tree_preview = function(frm) { var generate_tree_preview = function(frm) {
if (frm.doc.import_file) {
let parent = __('All Accounts'); let parent = __('All Accounts');
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data $(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
@@ -170,4 +153,5 @@ var generate_tree_preview = function(frm) {
parent = node.value; parent = node.value;
} }
}); });
}
}; };

View File

@@ -25,8 +25,16 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
class ChartofAccountsImporter(Document): class ChartofAccountsImporter(Document):
def validate(self): pass
validate_accounts(self.import_file)
def validate_columns(data):
if not data:
frappe.throw(_('No data found. Seems like you uploaded a blank file'))
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'))
@frappe.whitelist() @frappe.whitelist()
def validate_company(company): def validate_company(company):
@@ -131,6 +139,8 @@ def get_coa(doctype, parent, is_root=False, file_name=None):
else: else:
data = generate_data_from_excel(file_doc, extension) data = generate_data_from_excel(file_doc, extension)
validate_columns(data)
validate_accounts(data)
forest = build_forest(data) forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
@@ -322,9 +332,6 @@ def validate_accounts(file_name):
def validate_root(accounts): def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')] roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
if len(roots) < 4:
frappe.throw(_("Number of root accounts cannot be less than 4"))
error_messages = [] error_messages = []
for account in roots: for account in roots:
@@ -364,20 +371,12 @@ def get_mandatory_account_types():
def validate_account_types(accounts): def validate_account_types(accounts):
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"] account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1] account_types = [accounts[d]["account_type"] for d in accounts if not cint(accounts[d]['is_group']) == 1]
missing = list(set(account_types_for_ledger) - set(account_types)) missing = list(set(account_types_for_ledger) - set(account_types))
if missing: if missing:
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing))) frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
account_types_for_group = ["Bank", "Cash", "Stock"]
# fix logic bug
account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1]
missing = list(set(account_types_for_group) - set(account_groups))
if missing:
frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company): def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField linked = frappe.db.sql('''select fieldname from tabDocField
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True) where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)

View File

@@ -93,6 +93,7 @@
"options": "Payment Term" "options": "Payment Term"
}, },
{ {
"depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss", "fieldname": "exchange_gain_loss",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Exchange Gain/Loss", "label": "Exchange Gain/Loss",
@@ -103,7 +104,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-04-21 13:30:11.605388", "modified": "2021-09-26 17:06:55.597389",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry Reference", "name": "Payment Entry Reference",

View File

@@ -7,6 +7,7 @@
"field_order": [ "field_order": [
"reference_type", "reference_type",
"reference_name", "reference_name",
"reference_row",
"column_break_3", "column_break_3",
"invoice_type", "invoice_type",
"invoice_number", "invoice_number",
@@ -121,11 +122,17 @@
"label": "Amount", "label": "Amount",
"options": "Currency", "options": "Currency",
"read_only": 1 "read_only": 1
},
{
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-30 10:58:42.665107", "modified": "2021-09-20 17:23:09.455803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Allocation", "name": "Payment Reconciliation Allocation",

View File

@@ -12,5 +12,10 @@ frappe.ui.form.on('POS Invoice Merge Log', {
} }
} }
}); });
},
merge_invoices_based_on: function(frm) {
frm.set_value('customer', '');
frm.set_value('customer_group', '');
} }
}); });

View File

@@ -6,9 +6,11 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"posting_date", "posting_date",
"customer", "merge_invoices_based_on",
"column_break_3", "column_break_3",
"pos_closing_entry", "pos_closing_entry",
"customer",
"customer_group",
"section_break_3", "section_break_3",
"pos_invoices", "pos_invoices",
"references_section", "references_section",
@@ -88,12 +90,27 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "POS Closing Entry", "label": "POS Closing Entry",
"options": "POS Closing Entry" "options": "POS Closing Entry"
},
{
"fieldname": "merge_invoices_based_on",
"fieldtype": "Select",
"label": "Merge Invoices Based On",
"options": "Customer\nCustomer Group",
"reqd": 1
},
{
"depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
"options": "Customer Group"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-01 11:53:57.267579", "modified": "2021-09-14 11:17:19.001142",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice Merge Log", "name": "POS Invoice Merge Log",

View File

@@ -23,6 +23,9 @@ class POSInvoiceMergeLog(Document):
self.validate_pos_invoice_status() self.validate_pos_invoice_status()
def validate_customer(self): def validate_customer(self):
if self.merge_invoices_based_on == 'Customer Group':
return
for d in self.pos_invoices: for d in self.pos_invoices:
if d.customer != self.customer: if d.customer != self.customer:
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer)) frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
@@ -124,7 +127,7 @@ class POSInvoiceMergeLog(Document):
found = False found = False
for i in items: for i in items:
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
i.uom == item.uom and i.net_rate == item.net_rate): i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
found = True found = True
i.qty = i.qty + item.qty i.qty = i.qty + item.qty
@@ -172,6 +175,11 @@ class POSInvoiceMergeLog(Document):
invoice.discount_amount = 0.0 invoice.discount_amount = 0.0
invoice.taxes_and_charges = None invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1 invoice.ignore_pricing_rule = 1
invoice.customer = self.customer
if self.merge_invoices_based_on == 'Customer Group':
invoice.flags.ignore_pos_profile = True
invoice.pos_profile = ''
return invoice return invoice
@@ -228,7 +236,7 @@ def get_all_unconsolidated_invoices():
return pos_invoices return pos_invoices
def get_invoice_customer_map(pos_invoices): def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] } # pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
pos_invoice_customer_map = {} pos_invoice_customer_map = {}
for invoice in pos_invoices: for invoice in pos_invoices:
customer = invoice.get('customer') customer = invoice.get('customer')

View File

@@ -15,6 +15,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry, check_if_return_invoice_linked_with_payment_entry,
is_overdue,
unlink_inter_company_doc, unlink_inter_company_doc,
update_linked_doc, update_linked_doc,
validate_inter_company_party, validate_inter_company_party,
@@ -1139,10 +1140,7 @@ class PurchaseInvoice(BuyingController):
self.status = 'Draft' self.status = 'Draft'
return return
precision = self.precision("outstanding_amount") outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
outstanding_amount = flt(self.outstanding_amount, precision)
due_date = getdate(self.due_date)
nowdate = getdate()
if not status: if not status:
if self.docstatus == 2: if self.docstatus == 2:
@@ -1150,9 +1148,11 @@ class PurchaseInvoice(BuyingController):
elif self.docstatus == 1: elif self.docstatus == 1:
if self.is_internal_transfer(): if self.is_internal_transfer():
self.status = 'Internal Transfer' self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate: elif is_overdue(self):
self.status = "Overdue" self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate: elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid" self.status = "Unpaid"
#Check if outstanding amount is 0 due to debit note issued against invoice #Check if outstanding amount is 0 due to debit note issued against invoice
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):

View File

@@ -2,28 +2,58 @@
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
// render // render
frappe.listview_settings['Purchase Invoice'] = { frappe.listview_settings["Purchase Invoice"] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company", add_fields: [
"currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"], "supplier",
get_indicator: function(doc) { "supplier_name",
if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') { "base_grand_total",
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"]; "outstanding_amount",
} else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) { "due_date",
if(cint(doc.on_hold) && !doc.release_date) { "company",
"currency",
"is_return",
"release_date",
"on_hold",
"represents_company",
"is_internal_supplier",
],
get_indicator(doc) {
if (doc.status == "Debit Note Issued") {
return [__(doc.status), "darkgrey", "status,=," + doc.status];
}
if (
flt(doc.outstanding_amount) > 0 &&
doc.docstatus == 1 &&
cint(doc.on_hold)
) {
if (!doc.release_date) {
return [__("On Hold"), "darkgrey"]; return [__("On Hold"), "darkgrey"];
} else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) { } else if (
frappe.datetime.get_diff(
doc.release_date,
frappe.datetime.nowdate()
) > 0
) {
return [__("Temporarily on Hold"), "darkgrey"]; return [__("Temporarily on Hold"), "darkgrey"];
} else if (frappe.datetime.get_diff(doc.due_date) < 0) {
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
} else {
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
}
} else if (cint(doc.is_return)) {
return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
} else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
return [__("Paid"), "green", "outstanding_amount,=,0"];
} }
} }
const status_colors = {
"Unpaid": "orange",
"Paid": "green",
"Return": "gray",
"Overdue": "red",
"Partly Paid": "yellow",
"Internal Transfer": "darkgrey",
};
if (status_colors[doc.status]) {
return [
__(doc.status),
status_colors[doc.status],
"status,=," + doc.status,
];
}
},
}; };

View File

@@ -97,6 +97,7 @@
"width": "100px" "width": "100px"
}, },
{ {
"depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss", "fieldname": "exchange_gain_loss",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Exchange Gain/Loss", "label": "Exchange Gain/Loss",
@@ -104,6 +105,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "exchange_gain_loss",
"fieldname": "ref_exchange_rate", "fieldname": "ref_exchange_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Reference Exchange Rate", "label": "Reference Exchange Rate",
@@ -115,7 +117,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-04-20 16:26:53.820530", "modified": "2021-09-26 15:47:28.167371",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice Advance", "name": "Purchase Invoice Advance",

View File

@@ -1652,7 +1652,7 @@
"label": "Status", "label": "Status",
"length": 30, "length": 30,
"no_copy": 1, "no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer", "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nUnpaid and Discounted\nPartly Paid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@@ -2032,11 +2032,12 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-09-08 15:24:25.486499", "modified": "2021-09-21 09:27:50.191854",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",
"name_case": "Title Case", "name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -501,7 +501,7 @@ class SalesInvoice(SellingController):
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
if not self.pos_profile: if not self.pos_profile and not self.flags.ignore_pos_profile:
pos_profile = get_pos_profile(self.company) or {} pos_profile = get_pos_profile(self.company) or {}
if not pos_profile: if not pos_profile:
return return
@@ -1472,14 +1472,7 @@ class SalesInvoice(SellingController):
self.status = 'Draft' self.status = 'Draft'
return return
precision = self.precision("outstanding_amount") outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
outstanding_amount = flt(self.outstanding_amount, precision)
due_date = getdate(self.due_date)
nowdate = getdate()
discounting_status = None
if self.is_discounted:
discounting_status = get_discounting_status(self.name)
if not status: if not status:
if self.docstatus == 2: if self.docstatus == 2:
@@ -1487,13 +1480,11 @@ class SalesInvoice(SellingController):
elif self.docstatus == 1: elif self.docstatus == 1:
if self.is_internal_transfer(): if self.is_internal_transfer():
self.status = 'Internal Transfer' self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discounting_status=='Disbursed': elif is_overdue(self):
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue" self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discounting_status=='Disbursed': elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
self.status = "Unpaid and Discounted" self.status = "Partly Paid"
elif outstanding_amount > 0 and due_date >= nowdate: elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid" self.status = "Unpaid"
# Check if outstanding amount is 0 due to credit note issued against invoice # Check if outstanding amount is 0 due to credit note issued against invoice
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
@@ -1504,12 +1495,42 @@ class SalesInvoice(SellingController):
self.status = "Paid" self.status = "Paid"
else: else:
self.status = "Submitted" self.status = "Submitted"
if (
self.status in ("Unpaid", "Partly Paid", "Overdue")
and self.is_discounted
and get_discounting_status(self.name) == "Disbursed"
):
self.status += " and Discounted"
else: else:
self.status = "Draft" self.status = "Draft"
if update: if update:
self.db_set('status', self.status, update_modified = update_modified) self.db_set('status', self.status, update_modified = update_modified)
def is_overdue(doc):
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
if outstanding_amount <= 0:
return
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
nowdate = getdate()
if doc.payment_schedule:
# calculate payable amount till date
payable_amount = sum(
payment.payment_amount
for payment in doc.payment_schedule
if getdate(payment.due_date) < nowdate
)
if (grand_total - outstanding_amount) < payable_amount:
return True
elif getdate(doc.due_date) < nowdate:
return True
def get_discounting_status(sales_invoice): def get_discounting_status(sales_invoice):
status = None status = None

View File

@@ -6,18 +6,20 @@ frappe.listview_settings['Sales Invoice'] = {
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company", add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
"currency", "is_return"], "currency", "is_return"],
get_indicator: function(doc) { get_indicator: function(doc) {
var status_color = { const status_colors = {
"Draft": "grey", "Draft": "grey",
"Unpaid": "orange", "Unpaid": "orange",
"Paid": "green", "Paid": "green",
"Return": "gray", "Return": "gray",
"Credit Note Issued": "gray", "Credit Note Issued": "gray",
"Unpaid and Discounted": "orange", "Unpaid and Discounted": "orange",
"Partly Paid and Discounted": "yellow",
"Overdue and Discounted": "red", "Overdue and Discounted": "red",
"Overdue": "red", "Overdue": "red",
"Partly Paid": "yellow",
"Internal Transfer": "darkgrey" "Internal Transfer": "darkgrey"
}; };
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
}, },
right_column: "grand_total" right_column: "grand_total"
}; };

View File

@@ -131,6 +131,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_payment_entry_unlink_against_invoice(self): def test_payment_entry_unlink_against_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si = frappe.copy_doc(test_records[0]) si = frappe.copy_doc(test_records[0])
si.is_pos = 0 si.is_pos = 0
si.insert() si.insert()
@@ -154,6 +155,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_payment_entry_unlink_against_standalone_credit_note(self): def test_payment_entry_unlink_against_standalone_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si1 = create_sales_invoice(rate=1000) si1 = create_sales_invoice(rate=1000)
si2 = create_sales_invoice(rate=300) si2 = create_sales_invoice(rate=300)
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1) si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
@@ -1646,6 +1648,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_credit_note(self): def test_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1) si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
outstanding_amount = get_outstanding_amount(si.doctype, outstanding_amount = get_outstanding_amount(si.doctype,
@@ -2275,6 +2278,54 @@ class TestSalesInvoice(unittest.TestCase):
party_link.delete() party_link.delete()
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0) frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
today = nowdate()
# Test Overdue
si = create_sales_invoice(do_not_submit=True)
si.payment_schedule = []
si.append("payment_schedule", {
"due_date": add_days(today, -5),
"invoice_portion": 50,
"payment_amount": si.grand_total / 2
})
si.append("payment_schedule", {
"due_date": add_days(today, 5),
"invoice_portion": 50,
"payment_amount": si.grand_total / 2
})
si.submit()
self.assertEqual(si.status, "Overdue")
# Test payment less than due amount
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_amount = 1
pe.references[0].allocated_amount = pe.paid_amount
pe.submit()
si.reload()
self.assertEqual(si.status, "Overdue")
# Test Partly Paid
pe = frappe.copy_doc(pe)
pe.paid_amount = si.grand_total / 2
pe.references[0].allocated_amount = pe.paid_amount
pe.submit()
si.reload()
self.assertEqual(si.status, "Partly Paid")
# Test Paid
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_amount = si.outstanding_amount
pe.submit()
si.reload()
self.assertEqual(si.status, "Paid")
def get_sales_invoice_for_e_invoice(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'

View File

@@ -98,6 +98,7 @@
"width": "120px" "width": "120px"
}, },
{ {
"depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss", "fieldname": "exchange_gain_loss",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Exchange Gain/Loss", "label": "Exchange Gain/Loss",
@@ -105,6 +106,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "exchange_gain_loss",
"fieldname": "ref_exchange_rate", "fieldname": "ref_exchange_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Reference Exchange Rate", "label": "Reference Exchange Rate",
@@ -116,7 +118,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-04 20:25:49.832052", "modified": "2021-09-26 15:47:46.911595",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice Advance", "name": "Sales Invoice Advance",

View File

@@ -6,7 +6,8 @@ frappe.query_reports["Unpaid Expense Claim"] = {
{ {
"fieldname": "employee", "fieldname": "employee",
"label": __("Employee"), "label": __("Employee"),
"fieldtype": "Link" "fieldtype": "Link",
"options": "Employee"
} }
] ]
} }

View File

@@ -394,10 +394,6 @@ class Asset(AccountsController):
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations): if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations")) frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(nowdate()):
frappe.msgprint(_("Depreciation Row {0}: Depreciation Start Date is entered as past date")
.format(row.idx), title=_('Warning'), indicator='red')
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date") frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
.format(row.idx)) .format(row.idx))

View File

@@ -645,12 +645,18 @@ class TestAsset(unittest.TestCase):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location") qty=1, rate=8000.0, location="Test Location")
finance_book = frappe.new_doc('Finance Book')
finance_book.finance_book_name = 'Income Tax'
finance_book.for_income_tax = 1
finance_book.insert(ignore_if_duplicate=1)
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name) asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1 asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-07-12' asset.available_for_use_date = '2030-07-12'
asset.purchase_date = '2030-01-01' asset.purchase_date = '2030-01-01'
asset.append("finance_books", { asset.append("finance_books", {
"finance_book": finance_book.name,
"expected_value_after_useful_life": 1000, "expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value", "depreciation_method": "Written Down Value",
"total_number_of_depreciations": 3, "total_number_of_depreciations": 3,

View File

@@ -433,12 +433,12 @@
"image_field": "image", "image_field": "image",
"links": [ "links": [
{ {
"group": "Item Group", "group": "Allowed Items",
"link_doctype": "Supplier Item Group", "link_doctype": "Party Specific Item",
"link_fieldname": "supplier" "link_fieldname": "party"
} }
], ],
"modified": "2021-08-27 18:02:44.314077", "modified": "2021-09-06 17:37:56.522233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier", "name": "Supplier",

View File

@@ -1,77 +0,0 @@
{
"actions": [],
"creation": "2021-05-07 18:16:40.621421",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"supplier",
"item_group"
],
"fields": [
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"options": "Supplier",
"reqd": 1
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Group",
"options": "Item Group",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-19 13:48:16.742303",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Item Group",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,20 +0,0 @@
# -*- 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
class SupplierItemGroup(Document):
def validate(self):
exists = frappe.db.exists({
'doctype': 'Supplier Item Group',
'supplier': self.supplier,
'item_group': self.item_group
})
if exists:
frappe.throw(_("Item Group has already been linked to this supplier."))

View File

@@ -1,11 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestSupplierItemGroup(unittest.TestCase):
pass

View File

@@ -690,13 +690,17 @@ class AccountsController(TransactionBase):
.format(d.reference_name, d.against_order)) .format(d.reference_name, d.against_order))
def set_advance_gain_or_loss(self): def set_advance_gain_or_loss(self):
if not self.get("advances"): if self.get('conversion_rate') == 1 or not self.get("advances"):
return
is_purchase_invoice = self.doctype == 'Purchase Invoice'
party_account = self.credit_to if is_purchase_invoice else self.debit_to
if get_account_currency(party_account) != self.currency:
return return
for d in self.get("advances"): for d in self.get("advances"):
advance_exchange_rate = d.ref_exchange_rate advance_exchange_rate = d.ref_exchange_rate
if (d.allocated_amount and self.conversion_rate != 1 if (d.allocated_amount and self.conversion_rate != advance_exchange_rate):
and self.conversion_rate != advance_exchange_rate):
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
@@ -715,7 +719,7 @@ class AccountsController(TransactionBase):
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
if not gain_loss_account: if not gain_loss_account:
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}") frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}")
.format(self.get('company'))) .format(self.get('company')))
account_currency = get_account_currency(gain_loss_account) account_currency = get_account_currency(gain_loss_account)
if account_currency != self.company_currency: if account_currency != self.company_currency:
@@ -734,7 +738,7 @@ class AccountsController(TransactionBase):
"against": party, "against": party,
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss), dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
dr_or_cr: abs(d.exchange_gain_loss), dr_or_cr: abs(d.exchange_gain_loss),
"cost_center": self.cost_center, "cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
"project": self.project "project": self.project
}, item=d) }, item=d)
) )
@@ -985,15 +989,23 @@ class AccountsController(TransactionBase):
item_allowance = {} item_allowance = {}
global_qty_allowance, global_amount_allowance = None, None global_qty_allowance, global_amount_allowance = None, None
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
user_roles = frappe.get_roles()
total_overbilled_amt = 0.0
for item in self.get("items"): for item in self.get("items"):
if item.get(item_ref_dn): if not item.get(item_ref_dn):
continue
ref_amt = flt(frappe.db.get_value(ref_dt + " Item", ref_amt = flt(frappe.db.get_value(ref_dt + " Item",
item.get(item_ref_dn), based_on), self.precision(based_on, item)) item.get(item_ref_dn), based_on), self.precision(based_on, item))
if not ref_amt: if not ref_amt:
frappe.msgprint( frappe.msgprint(
_("Warning: System will not check overbilling since amount for Item {0} in {1} is zero") _("System will not check overbilling since amount for Item {0} in {1} is zero")
.format(item.item_code, ref_dt)) .format(item.item_code, ref_dt), title=_("Warning"), indicator="orange")
else: continue
already_billed = frappe.db.sql(""" already_billed = frappe.db.sql("""
select sum(%s) select sum(%s)
from `tab%s` from `tab%s`
@@ -1014,14 +1026,19 @@ class AccountsController(TransactionBase):
total_billed_amt = abs(total_billed_amt) total_billed_amt = abs(total_billed_amt)
max_allowed_amt = abs(max_allowed_amt) max_allowed_amt = abs(max_allowed_amt)
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill') overbill_amt = total_billed_amt - max_allowed_amt
total_overbilled_amt += overbill_amt
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles(): if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
if self.doctype != "Purchase Invoice": if self.doctype != "Purchase Invoice":
self.throw_overbill_exception(item, max_allowed_amt) self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")): elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
self.throw_overbill_exception(item, max_allowed_amt) self.throw_overbill_exception(item, max_allowed_amt)
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
.format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
def throw_overbill_exception(self, item, max_allowed_amt): def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings") frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
.format(item.item_code, item.idx, max_allowed_amt)) .format(item.item_code, item.idx, max_allowed_amt))
@@ -1673,14 +1690,18 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
return list(payment_entries_against_order) + list(unallocated_payment_entries) return list(payment_entries_against_order) + list(unallocated_payment_entries)
def update_invoice_status(): def update_invoice_status():
# Daily update the status of the invoices """Updates status as Overdue for applicable invoices. Runs daily."""
frappe.db.sql(""" update `tabSales Invoice` set status = 'Overdue'
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
frappe.db.sql(""" update `tabPurchase Invoice` set status = 'Overdue'
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
update `tab{}` as dt set dt.status = 'Overdue'
where dt.docstatus = 1
and dt.status != 'Overdue'
and dt.outstanding_amount > 0
and (dt.grand_total - dt.outstanding_amount) <
(select sum(payment_amount) from `tabPayment Schedule` as ps
where ps.parent = dt.name and ps.due_date < %s)
""".format(doctype), getdate())
@frappe.whitelist() @frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None): def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):

View File

@@ -7,6 +7,7 @@ import json
from collections import defaultdict from collections import defaultdict
import frappe import frappe
from frappe import scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.utils import nowdate, unique from frappe.utils import nowdate, unique
@@ -223,18 +224,29 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if not field in searchfields] if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields]) searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters and isinstance(filters, dict) and filters.get('supplier'): if filters and isinstance(filters, dict):
item_group_list = frappe.get_all('Supplier Item Group', if filters.get('customer') or filters.get('supplier'):
filters = {'supplier': filters.get('supplier')}, fields = ['item_group']) party = filters.get('customer') or filters.get('supplier')
item_rules_list = frappe.get_all('Party Specific Item',
filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value'])
item_groups = [] filters_dict = {}
for i in item_group_list: for rule in item_rules_list:
item_groups.append(i.item_group) if rule['restrict_based_on'] == 'Item':
rule['restrict_based_on'] = 'name'
filters_dict[rule.restrict_based_on] = []
for rule in item_rules_list:
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
for filter in filters_dict:
filters[scrub(filter)] = ['in', filters_dict[filter]]
if filters.get('customer'):
del filters['customer']
else:
del filters['supplier'] del filters['supplier']
if item_groups:
filters['item_group'] = ['in', item_groups]
description_cond = '' description_cond = ''
if frappe.db.count('Item', cache=True) < 50000: if frappe.db.count('Item', cache=True) < 50000:

View File

@@ -7,6 +7,7 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.modules.utils import get_module_app
from frappe.utils import flt, has_common from frappe.utils import flt, has_common
from frappe.utils.user import is_website_user from frappe.utils.user import is_website_user
@@ -21,8 +22,32 @@ def get_list_context(context=None):
"get_list": get_transaction_list "get_list": get_transaction_list
} }
def get_webform_list_context(module):
if get_module_app(module) != 'erpnext':
return
return {
"get_list": get_webform_transaction_list
}
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"): def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
""" Get List of transactions for custom doctypes """
from frappe.www.list import get_list
if not filters:
filters = []
meta = frappe.get_meta(doctype)
for d in meta.fields:
if d.fieldtype == 'Link' and d.fieldname != 'amended_from':
allowed_docs = [d.name for d in get_transaction_list(doctype=d.options, custom=True)]
allowed_docs.append('')
filters.append((d.fieldname, 'in', allowed_docs))
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=False,
fields=None, order_by="modified")
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified", custom=False):
user = frappe.session.user user = frappe.session.user
ignore_permissions = False ignore_permissions = False
@@ -46,7 +71,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
filters.append(('customer', 'in', customers)) filters.append(('customer', 'in', customers))
elif suppliers: elif suppliers:
filters.append(('supplier', 'in', suppliers)) filters.append(('supplier', 'in', suppliers))
else: elif not custom:
return [] return []
if doctype == 'Request for Quotation': if doctype == 'Request for Quotation':
@@ -56,9 +81,16 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
# Since customers and supplier do not have direct access to internal doctypes # Since customers and supplier do not have direct access to internal doctypes
ignore_permissions = True ignore_permissions = True
if not customers and not suppliers and custom:
ignore_permissions = False
filters = []
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length, transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc') fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
if custom:
return transactions
return post_process(doctype, transactions) return post_process(doctype, transactions)
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20, def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,

View File

@@ -12,8 +12,6 @@ from frappe.website.doctype.website_slideshow.website_slideshow import get_slide
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
# SEARCH
from erpnext.e_commerce.redisearch import ( from erpnext.e_commerce.redisearch import (
delete_item_from_index, delete_item_from_index,
insert_item_to_index, insert_item_to_index,
@@ -138,10 +136,10 @@ class WebsiteItem(WebsiteGenerator):
self.website_image = None self.website_image = None
def make_thumbnail(self): def make_thumbnail(self):
"""Make a thumbnail of `website_image`"""
if frappe.flags.in_import or frappe.flags.in_migrate: if frappe.flags.in_import or frappe.flags.in_migrate:
return return
"""Make a thumbnail of `website_image`"""
import requests.exceptions import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"): if not self.is_new() and self.website_image != frappe.db.get_value(self.doctype, self.name, "website_image"):

View File

@@ -20,14 +20,16 @@ def get_indexable_web_fields():
return [df.fieldname for df in valid_fields] return [df.fieldname for df in valid_fields]
def is_search_module_loaded(): def is_search_module_loaded():
try:
cache = frappe.cache() cache = frappe.cache()
out = cache.execute_command('MODULE LIST') out = cache.execute_command('MODULE LIST')
parsed_output = " ".join( parsed_output = " ".join(
(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out) (" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
) )
return "search" in parsed_output return "search" in parsed_output
except Exception:
return False
def if_redisearch_loaded(function): def if_redisearch_loaded(function):
"Decorator to check if Redisearch is loaded." "Decorator to check if Redisearch is loaded."

View File

@@ -105,7 +105,7 @@ def place_order():
if is_stock_item: if is_stock_item:
item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse") item_stock = get_web_item_qty_in_stock(item.item_code, "website_warehouse")
if not cint(item_stock.in_stock): if not cint(item_stock.in_stock):
throw(_("{1} Not in Stock").format(item.item_code)) throw(_("{0} Not in Stock").format(item.item_code))
if item.qty > item_stock.stock_qty[0][0]: if item.qty > item_stock.stock_qty[0][0]:
throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code)) throw(_("Only {0} in Stock for item {1}").format(item_stock.stock_qty[0][0], item.item_code))
@@ -168,8 +168,10 @@ def update_cart(item_code, qty, additional_notes=None, with_items=False):
return { return {
"items": frappe.render_template("templates/includes/cart/cart_items.html", "items": frappe.render_template("templates/includes/cart/cart_items.html",
context), context),
"taxes": frappe.render_template("templates/includes/order/order_taxes.html", "total": frappe.render_template("templates/includes/cart/cart_items_total.html",
context), context),
"taxes_and_totals": frappe.render_template("templates/includes/cart/cart_payment_summary.html",
context)
} }
else: else:
return { return {

View File

@@ -67,12 +67,16 @@ class ItemVariantsCacheManager:
as_list=1 as_list=1
) )
disabled_items = set([i.name for i in frappe.db.get_all('Item', {'disabled': 1})]) unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
attribute_value_item_map = frappe._dict({}) attribute_value_item_map = frappe._dict({})
item_attribute_value_map = frappe._dict({}) item_attribute_value_map = frappe._dict({})
item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] # dont consider variants that are unpublished
# (either have no Website Item or are unpublished in Website Item)
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
for row in item_variants_data: for row in item_variants_data:
item_code, attribute, attribute_value = row item_code, attribute, attribute_value = row
# (attr, value) => [item1, item2] # (attr, value) => [item1, item2]

View File

@@ -85,10 +85,8 @@ def add_bank_accounts(response, bank, company):
if not acc_subtype: if not acc_subtype:
add_account_subtype(account["subtype"]) add_account_subtype(account["subtype"])
existing_bank_account = frappe.db.exists("Bank Account", { bank_account_name = "{} - {}".format(account["name"], bank["bank_name"])
'account_name': account["name"], existing_bank_account = frappe.db.exists("Bank Account", bank_account_name)
'bank': bank["bank_name"]
})
if not existing_bank_account: if not existing_bank_account:
try: try:
@@ -197,6 +195,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
plaid = PlaidConnector(access_token) plaid = PlaidConnector(access_token)
transactions = []
try: try:
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id) transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
except ItemError as e: except ItemError as e:
@@ -205,7 +204,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " " msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
frappe.log_error(msg, title=_("Plaid Link Refresh Required")) frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
return transactions or [] return transactions
def new_bank_transaction(transaction): def new_bank_transaction(transaction):

View File

@@ -40,7 +40,7 @@ class Patient(Document):
frappe.db.set_value('Patient', self.name, 'status', 'Disabled') frappe.db.set_value('Patient', self.name, 'status', 'Disabled')
else: else:
send_registration_sms(self) send_registration_sms(self)
self.reload() # self.notify_update() self.reload()
def on_update(self): def on_update(self):
if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'): if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
@@ -93,7 +93,11 @@ class Patient(Document):
self.language = frappe.db.get_single_value('System Settings', 'language') self.language = frappe.db.get_single_value('System Settings', 'language')
def create_website_user(self): def create_website_user(self):
if self.email and not frappe.db.exists('User', self.email): users = frappe.db.get_all('User', fields=['email', 'mobile_no'], or_filters={'email': self.email, 'mobile_no': self.mobile})
if users and users[0]:
frappe.throw(_("User exists with Email {}, Mobile {}<br>Please check email / mobile or disable 'Invite as User' to skip creating User")
.format(frappe.bold(users[0].email), frappe.bold(users[0].mobile_no)), frappe.DuplicateEntryError)
user = frappe.get_doc({ user = frappe.get_doc({
'doctype': 'User', 'doctype': 'User',
'first_name': self.first_name, 'first_name': self.first_name,
@@ -109,7 +113,7 @@ class Patient(Document):
user.enabled = True user.enabled = True
user.send_welcome_email = True user.send_welcome_email = True
user.add_roles('Patient') user.add_roles('Patient')
frappe.db.set_value(self.doctype, self.name, 'user_id', user.name) self.db_set('user_id', user.name)
def autoname(self): def autoname(self):
patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by') patient_name_by = frappe.db.get_single_value('Healthcare Settings', 'patient_name_by')
@@ -159,10 +163,22 @@ class Patient(Document):
return {'invoice': sales_invoice.name} return {'invoice': sales_invoice.name}
def set_contact(self): def set_contact(self):
if frappe.db.exists('Dynamic Link', {'parenttype':'Contact', 'link_doctype':'Patient', 'link_name':self.name}): contact = get_default_contact(self.doctype, self.name)
if contact:
old_doc = self.get_doc_before_save() old_doc = self.get_doc_before_save()
if not old_doc:
return
if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone: if old_doc.email != self.email or old_doc.mobile != self.mobile or old_doc.phone != self.phone:
self.update_contact() self.update_contact(contact)
else:
if self.customer:
# customer contact exists, link patient
contact = get_default_contact('Customer', self.customer)
if contact:
self.update_contact(contact)
else: else:
self.reload() self.reload()
if self.email or self.mobile or self.phone: if self.email or self.mobile or self.phone:
@@ -179,15 +195,14 @@ class Patient(Document):
contact.append('links', dict(link_doctype='Customer', link_name=self.customer)) contact.append('links', dict(link_doctype='Customer', link_name=self.customer))
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)
self.update_contact(contact) # update email, mobile and phone self.update_contact(contact.name)
def update_contact(self, contact=None): def update_contact(self, contact):
if not contact: contact = frappe.get_doc('Contact', contact)
contact_name = get_default_contact(self.doctype, self.name)
if contact_name: if not contact.has_link(self.doctype, self.name):
contact = frappe.get_doc('Contact', contact_name) contact.append('links', dict(link_doctype=self.doctype, link_name=self.name))
if contact:
if self.email and self.email != contact.email_id: if self.email and self.email != contact.email_id:
for email in contact.email_ids: for email in contact.email_ids:
email.is_primary = True if email.email_id == self.email else False email.is_primary = True if email.email_id == self.email else False
@@ -206,7 +221,7 @@ class Patient(Document):
contact.add_phone(self.phone, is_primary_phone=True) contact.add_phone(self.phone, is_primary_phone=True)
contact.set_primary('phone') contact.set_primary('phone')
contact.flags.ignore_validate = True # disable hook TODO: safe? contact.flags.skip_patient_update = True
contact.save(ignore_permissions=True) contact.save(ignore_permissions=True)

View File

@@ -35,3 +35,40 @@ class TestPatient(unittest.TestCase):
settings.collect_registration_fee = 0 settings.collect_registration_fee = 0
settings.save() settings.save()
def test_patient_contact(self):
frappe.db.sql("""delete from `tabPatient` where name like '_Test Patient%'""")
frappe.db.sql("""delete from `tabCustomer` where name like '_Test Patient%'""")
frappe.db.sql("""delete from `tabContact` where name like'_Test Patient%'""")
frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""")
patient = create_patient(patient_name='_Test Patient Contact', email='test-patient@example.com', mobile='+91 0000000001')
customer = frappe.db.get_value('Patient', patient, 'customer')
self.assertTrue(customer)
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': patient}))
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer}))
# a second patient linking with same customer
new_patient = create_patient(email='test-patient@example.com', mobile='+91 0000000009', customer=customer)
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Patient', 'link_name': new_patient}))
self.assertTrue(frappe.db.exists('Dynamic Link', {'parenttype': 'Contact', 'link_doctype': 'Customer', 'link_name': customer}))
def test_patient_user(self):
frappe.db.sql("""delete from `tabUser` where email='test-patient-user@example.com'""")
frappe.db.sql("""delete from `tabDynamic Link` where parent like '_Test Patient%'""")
frappe.db.sql("""delete from `tabPatient` where name like '_Test Patient%'""")
patient = create_patient(patient_name='_Test Patient User', email='test-patient-user@example.com', mobile='+91 0000000009', create_user=True)
user = frappe.db.get_value('Patient', patient, 'user_id')
self.assertTrue(frappe.db.exists('User', user))
new_patient = frappe.get_doc({
'doctype': 'Patient',
'first_name': '_Test Patient Duplicate User',
'sex': 'Male',
'email': 'test-patient-user@example.com',
'mobile': '+91 0000000009',
'invite_user': 1
})
self.assertRaises(frappe.exceptions.DuplicateEntryError, new_patient.insert)

View File

@@ -307,14 +307,18 @@ def create_healthcare_docs(id=0):
return patient, practitioner return patient, practitioner
def create_patient(id=0): def create_patient(id=0, patient_name=None, email=None, mobile=None, customer=None, create_user=False):
if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}): if frappe.db.exists('Patient', {'firstname':f'_Test Patient {str(id)}'}):
patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name']) patient = frappe.db.get_value('Patient', {'first_name': f'_Test Patient {str(id)}'}, ['name'])
return patient return patient
patient = frappe.new_doc('Patient') patient = frappe.new_doc('Patient')
patient.first_name = f'_Test Patient {str(id)}' patient.first_name = patient_name if patient_name else f'_Test Patient {str(id)}'
patient.sex = 'Female' patient.sex = 'Female'
patient.mobile = mobile
patient.email = email
patient.customer = customer
patient.invite_user = create_user
patient.save(ignore_permissions=True) patient.save(ignore_permissions=True)
return patient.name return patient.name

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import unittest import unittest
import frappe import frappe
from frappe.utils import nowdate from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import ( from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
@@ -38,7 +38,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': vital_signs.name}) medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': vital_signs.name})
self.assertTrue(medical_rec) self.assertTrue(medical_rec)
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1, procedure_template=1) appointment = create_appointment(patient, practitioner, add_days(nowdate(), 1), invoice=1, procedure_template=1)
procedure = create_procedure(appointment) procedure = create_procedure(appointment)
procedure.start_procedure() procedure.start_procedure()
procedure.complete_procedure() procedure.complete_procedure()

View File

@@ -776,7 +776,7 @@ def update_patient_email_and_phone_numbers(contact, method):
Hook validate Contact Hook validate Contact
Update linked Patients' primary mobile and phone numbers Update linked Patients' primary mobile and phone numbers
''' '''
if 'Healthcare' not in frappe.get_active_domains(): if 'Healthcare' not in frappe.get_active_domains() or contact.flags.skip_patient_update:
return return
if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone): if contact.is_primary_contact and (contact.email_id or contact.mobile_no or contact.phone):
@@ -784,9 +784,15 @@ def update_patient_email_and_phone_numbers(contact, method):
for link in patient_links: for link in patient_links:
contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1) contact_details = frappe.db.get_value('Patient', link.get('link_name'), ['email', 'mobile', 'phone'], as_dict=1)
new_contact_details = {}
if contact.email_id and contact.email_id != contact_details.get('email'): if contact.email_id and contact.email_id != contact_details.get('email'):
frappe.db.set_value('Patient', link.get('link_name'), 'email', contact.email_id) new_contact_details.update({'email': contact.email_id})
if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'): if contact.mobile_no and contact.mobile_no != contact_details.get('mobile'):
frappe.db.set_value('Patient', link.get('link_name'), 'mobile', contact.mobile_no) new_contact_details.update({'mobile': contact.mobile_no})
if contact.phone and contact.phone != contact_details.get('phone'): if contact.phone and contact.phone != contact_details.get('phone'):
frappe.db.set_value('Patient', link.get('link_name'), 'phone', contact.phone) new_contact_details.update({'phone': contact.phone})
if new_contact_details:
frappe.db.set_value('Patient', link.get('link_name'), new_contact_details)

View File

@@ -61,6 +61,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
# website # website
update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"] update_website_context = ["erpnext.e_commerce.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context" my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"] calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
@@ -436,7 +437,7 @@ accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice"
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
"Subscription Plan" "Subscription Plan", "POS Invoice", "POS Invoice Item"
] ]
regional_overrides = { regional_overrides = {

View File

@@ -184,7 +184,7 @@ def get_employees_having_an_event_today(event_type):
# -------------------------- # --------------------------
def send_work_anniversary_reminders(): def send_work_anniversary_reminders():
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked""" """Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1) to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders"))
if not to_send: if not to_send:
return return

View File

@@ -21,7 +21,7 @@ frappe.ui.form.on('Training Result', {
frm.set_value("employees" ,""); frm.set_value("employees" ,"");
if (r.message) { if (r.message) {
$.each(r.message, function(i, d) { $.each(r.message, function(i, d) {
var row = frappe.model.add_child(cur_frm.doc, "Training Result Employee", "employees"); var row = frappe.model.add_child(frm.doc, "Training Result Employee", "employees");
row.employee = d.employee; row.employee = d.employee;
row.employee_name = d.employee_name; row.employee_name = d.employee_name;
}); });

View File

@@ -18,7 +18,7 @@ frappe.ui.form.on('Maintenance Schedule', {
}, },
refresh: function (frm) { refresh: function (frm) {
setTimeout(() => { setTimeout(() => {
frm.toggle_display('generate_schedule', !(frm.is_new())); frm.toggle_display('generate_schedule', !(frm.is_new() || frm.doc.docstatus));
frm.toggle_display('schedule', !(frm.is_new())); frm.toggle_display('schedule', !(frm.is_new()));
}, 10); }, 10);
}, },

View File

@@ -16,9 +16,9 @@ from erpnext.utilities.transaction_base import TransactionBase, delete_events
class MaintenanceSchedule(TransactionBase): class MaintenanceSchedule(TransactionBase):
@frappe.whitelist() @frappe.whitelist()
def generate_schedule(self): def generate_schedule(self):
if self.docstatus != 0:
return
self.set('schedules', []) self.set('schedules', [])
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
where parent=%s""", (self.name))
count = 1 count = 1
for d in self.get('items'): for d in self.get('items'):
self.validate_maintenance_detail() self.validate_maintenance_detail()

View File

@@ -1135,7 +1135,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
query_filters["has_variants"] = 0 query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"): if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1 or_cond_filters["is_stock_item"] = 1
or_cond_filters["has_variants"] = 1
return frappe.get_list("Item", return frappe.get_list("Item",
fields = fields, filters=query_filters, fields = fields, filters=query_filters,

View File

@@ -38,6 +38,8 @@
"total_time_in_mins", "total_time_in_mins",
"section_break_8", "section_break_8",
"items", "items",
"scrap_items_section",
"scrap_items",
"corrective_operation_section", "corrective_operation_section",
"for_job_card", "for_job_card",
"is_corrective_job_card", "is_corrective_job_card",
@@ -392,11 +394,24 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Batch No", "label": "Batch No",
"options": "Batch" "options": "Batch"
},
{
"fieldname": "scrap_items_section",
"fieldtype": "Section Break",
"label": "Scrap Items"
},
{
"fieldname": "scrap_items",
"fieldtype": "Table",
"label": "Scrap Items",
"no_copy": 1,
"options": "Job Card Scrap Item",
"print_hide": 1
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-09-13 21:34:15.177928", "modified": "2021-09-14 00:38:46.873105",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Job Card", "name": "Job Card",

View File

@@ -677,7 +677,7 @@ def get_job_details(start, end, filters=None):
conditions = get_filters_cond("Job Card", filters, []) conditions = get_filters_cond("Job Card", filters, [])
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order, job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
`tabJob Card`.employee_name, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''), `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
min(`tabJob Card Time Log`.from_time) as from_time, min(`tabJob Card Time Log`.from_time) as from_time,
max(`tabJob Card Time Log`.to_time) as to_time max(`tabJob Card Time Log`.to_time) as to_time
FROM `tabJob Card` , `tabJob Card Time Log` FROM `tabJob Card` , `tabJob Card Time Log`
@@ -687,7 +687,7 @@ def get_job_details(start, end, filters=None):
for d in job_cards: for d in job_cards:
subject_data = [] subject_data = []
for field in ["name", "work_order", "remarks", "employee_name"]: for field in ["name", "work_order", "remarks"]:
if not d.get(field): continue if not d.get(field): continue
subject_data.append(d.get(field)) subject_data.append(d.get(field))

View File

@@ -0,0 +1,82 @@
{
"actions": [],
"creation": "2021-09-14 00:30:28.533884",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"column_break_3",
"description",
"quantity_and_rate",
"stock_qty",
"column_break_6",
"stock_uom"
],
"fields": [
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Scrap Item Code",
"options": "Item",
"reqd": 1
},
{
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Scrap Item Name"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"read_only": 1
},
{
"fieldname": "quantity_and_rate",
"fieldtype": "Section Break",
"label": "Quantity and Rate"
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"reqd": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"fetch_from": "item_code.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-14 01:20:48.588052",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Job Card Scrap Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -242,6 +242,8 @@ frappe.ui.form.on('Production Plan', {
}, },
get_sub_assembly_items: function(frm) { get_sub_assembly_items: function(frm) {
frm.dirty();
frappe.call({ frappe.call({
method: "get_sub_assembly_items", method: "get_sub_assembly_items",
freeze: true, freeze: true,

View File

@@ -457,7 +457,8 @@ class ProductionPlan(Document):
def prepare_args_for_sub_assembly_items(self, row, args): def prepare_args_for_sub_assembly_items(self, row, args):
for field in ["production_item", "item_name", "qty", "fg_warehouse", for field in ["production_item", "item_name", "qty", "fg_warehouse",
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]: "description", "bom_no", "stock_uom", "bom_level",
"production_plan_item", "schedule_date"]:
args[field] = row.get(field) args[field] = row.get(field)
args.update({ args.update({
@@ -561,8 +562,6 @@ class ProductionPlan(Document):
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
self.save()
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
bom_data = sorted(bom_data, key = lambda i: i.bom_level) bom_data = sorted(bom_data, key = lambda i: i.bom_level)

View File

@@ -404,6 +404,7 @@ def make_bom(**args):
'uom': item_doc.stock_uom, 'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom, 'stock_uom': item_doc.stock_uom,
'rate': item_doc.valuation_rate or args.rate, 'rate': item_doc.valuation_rate or args.rate,
'source_warehouse': args.source_warehouse
}) })
if not args.do_not_save: if not args.do_not_save:

View File

@@ -16,7 +16,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
stop_unstop, stop_unstop,
) )
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin from erpnext.stock.utils import get_bin
@@ -768,6 +768,60 @@ class TestWorkOrder(unittest.TestCase):
total_pl_qty total_pl_qty
) )
def test_job_card_scrap_item(self):
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
'Test RM Item 2 for Scrap Item Test']
company = '_Test Company with perpetual inventory'
for item_code in items:
create_item(item_code = item_code, is_stock_item = 1,
is_purchase_item=1, opening_stock=100, valuation_rate=10, company=company, warehouse='Stores - TCP1')
item = 'Test FG Item for Scrap Item Test'
raw_materials = ['Test RM Item 1 for Scrap Item Test', 'Test RM Item 2 for Scrap Item Test']
if not frappe.db.get_value('BOM', {'item': item}):
bom = make_bom(item=item, source_warehouse='Stores - TCP1', raw_materials=raw_materials, do_not_save=True)
bom.with_operations = 1
bom.append('operations', {
'operation': '_Test Operation 1',
'workstation': '_Test Workstation 1',
'hour_rate': 20,
'time_in_mins': 60
})
bom.submit()
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=now(), qty=20, skip_transfer=1)
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
update_job_card(job_card)
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
for row in stock_entry.items:
if row.is_scrap_item:
self.assertEqual(row.qty, 1)
def update_job_card(job_card):
job_card_doc = frappe.get_doc('Job Card', job_card)
job_card_doc.set('scrap_items', [
{
'item_code': 'Test RM Item 1 for Scrap Item Test',
'stock_qty': 2
},
{
'item_code': 'Test RM Item 2 for Scrap Item Test',
'stock_qty': 2
},
])
job_card_doc.append('time_logs', {
'from_time': now(),
'time_in_mins': 60,
'completed_qty': job_card_doc.for_quantity
})
job_card_doc.submit()
def get_scrap_item_details(bom_no): def get_scrap_item_details(bom_no):
scrap_items = {} scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`

View File

@@ -313,3 +313,7 @@ erpnext.patches.v13_0.create_website_items
erpnext.patches.v13_0.populate_e_commerce_settings erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items erpnext.patches.v13_0.make_homepage_products_website_items
erpnext.patches.v13_0.update_dates_in_tax_withholding_category erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
erpnext.patches.v13_0.create_custom_field_for_finance_book
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries

View File

@@ -0,0 +1,42 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def execute():
frappe.reload_doc('accounts', 'doctype', 'accounting_dimension')
accounting_dimensions = frappe.db.sql("""select fieldname, label, document_type, disabled from
`tabAccounting Dimension`""", as_dict=1)
if not accounting_dimensions:
return
count = 1
for d in accounting_dimensions:
if count % 2 == 0:
insert_after_field = 'dimension_col_break'
else:
insert_after_field = 'accounting_dimensions_section'
for doctype in ["POS Invoice", "POS Invoice Item"]:
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
if field:
continue
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
df = {
"fieldname": d.fieldname,
"label": d.label,
"fieldtype": "Link",
"options": d.document_type,
"insert_after": insert_after_field
}
if df['fieldname'] not in fieldnames:
create_custom_field(doctype, df)
frappe.clear_cache(doctype=doctype)
count += 1

View File

@@ -0,0 +1,21 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
custom_field = {
'Finance Book': [
{
'fieldname': 'for_income_tax',
'label': 'For Income Tax',
'fieldtype': 'Check',
'insert_after': 'finance_book_name',
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
}
]
}
create_custom_fields(custom_field, update=1)

View File

@@ -0,0 +1,49 @@
from __future__ import unicode_literals
import json
import frappe
def execute():
frappe.reload_doc('accounts', 'doctype', 'purchase_invoice_advance')
frappe.reload_doc('accounts', 'doctype', 'sales_invoice_advance')
purchase_invoices = frappe.db.sql("""
select
parenttype as type, parent as name
from
`tabPurchase Invoice Advance`
where
ref_exchange_rate = 1
and docstatus = 1
and ifnull(exchange_gain_loss, '') != ''
group by
parent
""", as_dict=1)
sales_invoices = frappe.db.sql("""
select
parenttype as type, parent as name
from
`tabSales Invoice Advance`
where
ref_exchange_rate = 1
and docstatus = 1
and ifnull(exchange_gain_loss, '') != ''
group by
parent
""", as_dict=1)
if purchase_invoices + sales_invoices:
frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
for invoice in purchase_invoices + sales_invoices:
doc = frappe.get_doc(invoice.type, invoice.name)
doc.docstatus = 2
doc.make_gl_entries()
for advance in doc.advances:
if advance.ref_exchange_rate == 1:
advance.db_set('exchange_gain_loss', 0, False)
doc.docstatus = 1
doc.make_gl_entries()

View File

@@ -0,0 +1,17 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
if frappe.db.table_exists('Supplier Item Group'):
frappe.reload_doc("selling", "doctype", "party_specific_item")
sig = frappe.db.get_all("Supplier Item Group", fields=["name", "supplier", "item_group"])
for item in sig:
psi = frappe.new_doc("Party Specific Item")
psi.party_type = "Supplier"
psi.party = item.supplier
psi.restrict_based_on = "Item Group"
psi.based_on_value = item.item_group
psi.insert()

View File

@@ -141,6 +141,9 @@ class Project(Document):
if self.sales_order: if self.sales_order:
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name) frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
def on_trash(self):
frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
def update_percent_complete(self): def update_percent_complete(self):
if self.percent_complete_method == "Manual": if self.percent_complete_method == "Manual":
if self.status == "Completed": if self.status == "Completed":

View File

@@ -9,6 +9,8 @@ from frappe.utils import add_days, getdate, nowdate
from erpnext.projects.doctype.project_template.test_project_template import make_project_template from erpnext.projects.doctype.project_template.test_project_template import make_project_template
from erpnext.projects.doctype.task.test_task import create_task from erpnext.projects.doctype.task.test_task import create_task
from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
test_records = frappe.get_test_records('Project') test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"] test_ignore = ["Sales Order"]
@@ -96,6 +98,21 @@ class TestProject(unittest.TestCase):
self.assertEqual(len(tasks), 2) self.assertEqual(len(tasks), 2)
def test_project_linking_with_sales_order(self):
so = make_sales_order()
project = make_project_from_so(so.name)
project.save()
self.assertEqual(project.sales_order, so.name)
so.reload()
self.assertEqual(so.project, project.name)
project.delete()
so.reload()
self.assertFalse(so.project)
def get_project(name, template): def get_project(name, template):
project = frappe.get_doc(dict( project = frappe.get_doc(dict(

View File

@@ -617,6 +617,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
me.frm.script_manager.trigger('qty', item.doctype, item.name); me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse) if (!me.frm.doc.set_warehouse)
me.frm.script_manager.trigger('warehouse', item.doctype, item.name); me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
me.apply_price_list(item, true);
}, undefined, !frappe.flags.hide_serial_batch_dialog); }, undefined, !frappe.flags.hide_serial_batch_dialog);
} }
}, },
@@ -864,9 +865,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
if (r.message) { if (r.message) {
me.frm.set_value("billing_address", r.message); me.frm.set_value("billing_address", r.message);
} else { } else {
if (frappe.meta.get_docfield(me.frm.doctype, 'company_address')) {
me.frm.set_value("company_address", ""); me.frm.set_value("company_address", "");
} }
} }
}
}); });
} }

View File

@@ -105,6 +105,8 @@ $.extend(shopping_cart, {
}, },
set_cart_count: function(animate=false) { set_cart_count: function(animate=false) {
$(".intermediate-empty-cart").remove();
var cart_count = frappe.get_cookie("cart_count"); var cart_count = frappe.get_cookie("cart_count");
if(frappe.session.user==="Guest") { if(frappe.session.user==="Guest") {
cart_count = 0; cart_count = 0;
@@ -119,13 +121,20 @@ $.extend(shopping_cart, {
if(parseInt(cart_count) === 0 || cart_count === undefined) { if(parseInt(cart_count) === 0 || cart_count === undefined) {
$cart.css("display", "none"); $cart.css("display", "none");
$(".cart-items").html('Cart is Empty');
$(".cart-tax-items").hide(); $(".cart-tax-items").hide();
$(".btn-place-order").hide(); $(".btn-place-order").hide();
$(".cart-payment-addresses").hide(); $(".cart-payment-addresses").hide();
let intermediate_empty_cart_msg = `
<div class="text-center w-100 intermediate-empty-cart mt-4 mb-4 text-muted">
${ __("Cart is Empty") }
</div>
`;
$(".cart-table").after(intermediate_empty_cart_msg);
} }
else { else {
$cart.css("display", "inline"); $cart.css("display", "inline");
$("#cart-count").text(cart_count);
} }
if(cart_count) { if(cart_count) {
@@ -152,7 +161,10 @@ $.extend(shopping_cart, {
callback: function(r) { callback: function(r) {
if(!r.exc) { if(!r.exc) {
$(".cart-items").html(r.message.items); $(".cart-items").html(r.message.items);
$(".cart-tax-items").html(r.message.taxes); $(".cart-tax-items").html(r.message.total);
$(".payment-summary").html(r.message.taxes_and_totals);
shopping_cart.set_cart_count();
if (cart_dropdown != true) { if (cart_dropdown != true) {
$(".cart-icon").hide(); $(".cart-icon").hide();
} }

View File

@@ -719,7 +719,10 @@ erpnext.utils.map_current_doc = function(opts) {
return; return;
} }
opts.source_name = values; opts.source_name = values;
if (opts.allow_child_item_selection) {
// args contains filtered child docnames
opts.args = args; opts.args = args;
}
d.dialog.hide(); d.dialog.hide();
_map(); _map();
}, },

View File

@@ -706,6 +706,15 @@ def make_custom_fields(update=True):
'fieldtype': 'Data', 'fieldtype': 'Data',
'insert_after': 'email' 'insert_after': 'email'
} }
],
'Finance Book': [
{
'fieldname': 'for_income_tax',
'label': 'For Income Tax',
'fieldtype': 'Check',
'insert_after': 'finance_book_name',
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
}
] ]
} }
create_custom_fields(custom_fields, update=update) create_custom_fields(custom_fields, update=update)
@@ -795,7 +804,7 @@ def set_salary_components(docs):
def set_tax_withholding_category(company): def set_tax_withholding_category(company):
accounts = [] accounts = []
fiscal_year = None fiscal_year_details = None
abbr = frappe.get_value("Company", company, "abbr") abbr = frappe.get_value("Company", company, "abbr")
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name') tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')

View File

@@ -112,9 +112,6 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts): def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts
else:
return [_("Item"), _("Taxable Amount")] + tax_accounts return [_("Item"), _("Taxable Amount")] + tax_accounts
def get_itemised_tax_breakup_data(doc, account_wise=False): def get_itemised_tax_breakup_data(doc, account_wise=False):
@@ -859,6 +856,7 @@ def get_depreciation_amount(asset, depreciable_value, row):
rate_of_depreciation = row.rate_of_depreciation rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation # if its the first depreciation
if depreciable_value == asset.gross_purchase_amount: if depreciable_value == asset.gross_purchase_amount:
if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'):
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2 # as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
diff = date_diff(row.depreciation_start_date, asset.available_for_use_date) diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
if diff <= 180: if diff <= 180:

View File

@@ -214,7 +214,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2B": if self.filters.get("type_of_business") == "B2B":
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1" conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1 AND is_debit_note !=1"
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
@@ -223,7 +223,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2C Large": if self.filters.get("type_of_business") == "B2C Large":
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit)) AND grand_total > {0} AND is_return != 1 AND is_debit_note !=1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "B2C Small": elif self.filters.get("type_of_business") == "B2C Small":
conditions += """ AND ( conditions += """ AND (
@@ -236,8 +236,8 @@ class Gstr1Report(object):
elif self.filters.get("type_of_business") == "CDNR-UNREG": elif self.filters.get("type_of_business") == "CDNR-UNREG":
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
AND ABS(grand_total) > {0} AND (is_return = 1 OR is_debit_note = 1) AND (is_return = 1 OR is_debit_note = 1)
AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')""".format(flt(b2c_limit)) AND IFNULL(gst_category, '') in ('Unregistered', 'Overseas')"""
elif self.filters.get("type_of_business") == "EXPORT": elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """ conditions += """ AND is_return !=1 and gst_category = 'Overseas' """

View File

@@ -510,8 +510,14 @@
"idx": 363, "idx": 363,
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [
"modified": "2021-08-25 18:56:09.929905", {
"group": "Allowed Items",
"link_doctype": "Party Specific Item",
"link_fieldname": "party"
}
],
"modified": "2021-09-06 17:38:54.196663",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Supplier Item Group', { frappe.ui.form.on('Party Specific Item', {
// refresh: function(frm) { // refresh: function(frm) {
// } // }

View File

@@ -0,0 +1,77 @@
{
"actions": [],
"creation": "2021-08-27 19:28:07.559978",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"party_type",
"party",
"column_break_3",
"restrict_based_on",
"based_on_value"
],
"fields": [
{
"fieldname": "party_type",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Party Type",
"options": "Customer\nSupplier",
"reqd": 1
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Party Name",
"options": "party_type",
"reqd": 1
},
{
"fieldname": "restrict_based_on",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Restrict Items Based On",
"options": "Item\nItem Group\nBrand",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "based_on_value",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Based On Value",
"options": "restrict_based_on",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-14 13:27:58.612334",
"modified_by": "Administrator",
"module": "Selling",
"name": "Party Specific Item",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "party",
"track_changes": 1
}

View File

@@ -0,0 +1,19 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class PartySpecificItem(Document):
def validate(self):
exists = frappe.db.exists({
'doctype': 'Party Specific Item',
'party_type': self.party_type,
'party': self.party,
'restrict_based_on': self.restrict_based_on,
'based_on': self.based_on_value,
})
if exists:
frappe.throw(_("This item filter has already been applied for the {0}").format(self.party_type))

View File

@@ -0,0 +1,38 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from erpnext.controllers.queries import item_query
test_dependencies = ['Item', 'Customer', 'Supplier']
def create_party_specific_item(**args):
psi = frappe.new_doc("Party Specific Item")
psi.party_type = args.get('party_type')
psi.party = args.get('party')
psi.restrict_based_on = args.get('restrict_based_on')
psi.based_on_value = args.get('based_on_value')
psi.insert()
class TestPartySpecificItem(unittest.TestCase):
def setUp(self):
self.customer = frappe.get_last_doc("Customer")
self.supplier = frappe.get_last_doc("Supplier")
self.item = frappe.get_last_doc("Item")
def test_item_query_for_customer(self):
create_party_specific_item(party_type='Customer', party=self.customer.name, restrict_based_on='Item', based_on_value=self.item.name)
filters = {'is_sales_item': 1, 'customer': self.customer.name}
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
for item in items:
self.assertEqual(item[0], self.item.name)
def test_item_query_for_supplier(self):
create_party_specific_item(party_type='Supplier', party=self.supplier.name, restrict_based_on='Item Group', based_on_value=self.item.item_group)
filters = {'supplier': self.supplier.name, 'is_purchase_item': 1}
items = item_query(doctype= 'Item', txt= '', searchfield= 'name', start= 0, page_len= 20,filters=filters, as_dict= False)
for item in items:
self.assertEqual(item[2], self.item.item_group)

View File

@@ -25,7 +25,7 @@
"editable_price_list_rate", "editable_price_list_rate",
"validate_selling_price", "validate_selling_price",
"editable_bundle_item_rates", "editable_bundle_item_rates",
"transaction_settings_section", "sales_transactions_settings_section",
"so_required", "so_required",
"dn_required", "dn_required",
"sales_update_frequency", "sales_update_frequency",
@@ -143,15 +143,14 @@
{ {
"default": "Stop", "default": "Stop",
"depends_on": "maintain_same_sales_rate", "depends_on": "maintain_same_sales_rate",
"description": "Configure the action to stop the transaction or just warn if the same rate is not maintained.",
"fieldname": "maintain_same_rate_action", "fieldname": "maintain_same_rate_action",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Action If Same Rate is Not Maintained", "label": "Action if Same Rate is Not Maintained Throughout Sales Cycle",
"mandatory_depends_on": "maintain_same_sales_rate", "mandatory_depends_on": "maintain_same_sales_rate",
"options": "Stop\nWarn" "options": "Stop\nWarn"
}, },
{ {
"depends_on": "eval: doc.maintain_same_rate_action == 'Stop'", "depends_on": "eval: doc.maintain_same_sales_rate && doc.maintain_same_rate_action == 'Stop'",
"fieldname": "role_to_override_stop_action", "fieldname": "role_to_override_stop_action",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Role Allowed to Override Stop Action", "label": "Role Allowed to Override Stop Action",
@@ -191,13 +190,15 @@
"label": "Item Price Settings" "label": "Item Price Settings"
}, },
{ {
"fieldname": "transaction_settings_section", "fieldname": "sales_transactions_settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Transaction Settings" "label": "Transaction Settings"
}, },
{ {
"fieldname": "column_break_5", "default": "0",
"fieldtype": "Column Break" "fieldname": "editable_bundle_item_rates",
"fieldtype": "Check",
"label": "Calculate Product Bundle Price based on Child Items' Rates"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -205,7 +206,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-09-08 19:38:10.175989", "modified": "2021-09-14 22:05:06.139820",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

View File

@@ -63,7 +63,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({
this.frm.set_query("item_code", "items", function() { this.frm.set_query("item_code", "items", function() {
return { return {
query: "erpnext.controllers.queries.item_query", query: "erpnext.controllers.queries.item_query",
filters: {'is_sales_item': 1} filters: {'is_sales_item': 1, 'customer': cur_frm.doc.customer}
} }
}); });
} }

View File

@@ -36,6 +36,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
self.parent_item_group = _('All Item Groups') self.parent_item_group = _('All Item Groups')
self.make_route() self.make_route()
self.validate_item_group_defaults()
def on_update(self): def on_update(self):
NestedSet.on_update(self) NestedSet.on_update(self)
@@ -113,6 +114,10 @@ class ItemGroup(NestedSet, WebsiteGenerator):
def delete_child_item_groups_key(self): def delete_child_item_groups_key(self):
frappe.cache().hdel("child_item_groups", self.name) frappe.cache().hdel("child_item_groups", self.name)
def validate_item_group_defaults(self):
from erpnext.stock.doctype.item.item import validate_item_default_company_links
validate_item_default_company_links(self.item_group_defaults)
def get_child_groups_for_website(item_group_name, immediate=False): def get_child_groups_for_website(item_group_name, immediate=False):
"""Returns child item groups *excluding* passed group.""" """Returns child item groups *excluding* passed group."""
item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)

View File

@@ -7,7 +7,8 @@
"item_group_defaults": [{ "item_group_defaults": [{
"company": "_Test Company", "company": "_Test Company",
"buying_cost_center": "_Test Cost Center 2 - _TC", "buying_cost_center": "_Test Cost Center 2 - _TC",
"selling_cost_center": "_Test Cost Center 2 - _TC" "selling_cost_center": "_Test Cost Center 2 - _TC",
"default_warehouse": "_Test Warehouse - _TC"
}] }]
}, },
{ {

View File

@@ -2116,9 +2116,9 @@
}, },
"Saudi Arabia": { "Saudi Arabia": {
"KSA VAT 5%": { "KSA VAT 15%": {
"account_name": "VAT 5%", "account_name": "VAT 15%",
"tax_rate": 5.00 "tax_rate": 15.00
}, },
"KSA VAT Zero": { "KSA VAT Zero": {
"account_name": "VAT Zero", "account_name": "VAT Zero",

View File

@@ -3,6 +3,7 @@
import copy import copy
import json import json
from typing import List
import frappe import frappe
from frappe import _ from frappe import _
@@ -29,6 +30,7 @@ from erpnext.controllers.item_variant import (
validate_item_variant_attributes, validate_item_variant_attributes,
) )
from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for from erpnext.setup.doctype.item_group.item_group import invalidate_cache_for
from erpnext.stock.doctype.item_default.item_default import ItemDefault
class DuplicateReorderRows(frappe.ValidationError): class DuplicateReorderRows(frappe.ValidationError):
@@ -116,9 +118,9 @@ class Item(Document):
self.validate_fixed_asset() self.validate_fixed_asset()
self.validate_retain_sample() self.validate_retain_sample()
self.validate_uom_conversion_factor() self.validate_uom_conversion_factor()
self.validate_item_defaults()
self.validate_customer_provided_part() self.validate_customer_provided_part()
self.update_defaults_from_item_group() self.update_defaults_from_item_group()
self.validate_item_defaults()
self.validate_auto_reorder_enabled_in_stock_settings() self.validate_auto_reorder_enabled_in_stock_settings()
self.cant_change() self.cant_change()
self.validate_item_tax_net_rate_range() self.validate_item_tax_net_rate_range()
@@ -309,8 +311,12 @@ class Item(Document):
_("Default BOM ({0}) must be active for this item or its template").format(bom_item)) _("Default BOM ({0}) must be active for this item or its template").format(bom_item))
def fill_customer_code(self): def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """ """
self.customer_code = ','.join(d.ref_code for d in self.get("customer_items", [])) Append all the customer codes and insert into "customer_code" field of item table.
Used to search Item by customer code.
"""
customer_codes = set(d.ref_code for d in self.get("customer_items", []))
self.customer_code = ','.join(customer_codes)
def check_item_tax(self): def check_item_tax(self):
"""Check whether Tax Rate is not entered twice for same Tax Type""" """Check whether Tax Rate is not entered twice for same Tax Type"""
@@ -526,9 +532,14 @@ class Item(Document):
if len(companies) != len(self.item_defaults): if len(companies) != len(self.item_defaults):
frappe.throw(_("Cannot set multiple Item Defaults for a company.")) frappe.throw(_("Cannot set multiple Item Defaults for a company."))
validate_item_default_company_links(self.item_defaults)
def update_defaults_from_item_group(self): def update_defaults_from_item_group(self):
"""Get defaults from Item Group""" """Get defaults from Item Group"""
if self.item_group and not self.item_defaults: if self.item_defaults or not self.item_group:
return
item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group}, item_defaults = frappe.db.get_values("Item Default", {"parent": self.item_group},
['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier', ['company', 'default_warehouse','default_price_list','buying_cost_center','default_supplier',
'expense_account','selling_cost_center','income_account'], as_dict = 1) 'expense_account','selling_cost_center','income_account'], as_dict = 1)
@@ -545,7 +556,6 @@ class Item(Document):
'income_account': item.income_account 'income_account': item.income_account
}) })
else: else:
warehouse = ''
defaults = frappe.defaults.get_defaults() or {} defaults = frappe.defaults.get_defaults() or {}
# To check default warehouse is belong to the default company # To check default warehouse is belong to the default company
@@ -1024,3 +1034,25 @@ def update_variants(variants, template, publish_progress=True):
@erpnext.allow_regional @erpnext.allow_regional
def set_item_tax_from_hsn_code(item): def set_item_tax_from_hsn_code(item):
pass pass
def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> None:
for item_default in item_defaults:
for doctype, field in [
['Warehouse', 'default_warehouse'],
['Cost Center', 'buying_cost_center'],
['Cost Center', 'selling_cost_center'],
['Account', 'expense_account'],
['Account', 'income_account']
]:
if item_default.get(field):
company = frappe.db.get_value(doctype, item_default.get(field), 'company', cache=True)
if company and company != item_default.company:
frappe.throw(_("Row #{}: {} {} doesn't belong to Company {}. Please select valid {}.")
.format(
item_default.idx,
doctype,
frappe.bold(item_default.get(field)),
frappe.bold(item_default.company),
frappe.bold(frappe.unscrub(field))
), title=_("Invalid Item Defaults"))

View File

@@ -232,6 +232,23 @@ class TestItem(unittest.TestCase):
for key, value in purchase_item_check.items(): for key, value in purchase_item_check.items():
self.assertEqual(value, purchase_item_details.get(key)) self.assertEqual(value, purchase_item_details.get(key))
def test_item_default_validations(self):
with self.assertRaises(frappe.ValidationError) as ve:
make_item("Bad Item defaults", {
"item_group": "_Test Item Group",
"item_defaults": [{
"company": "_Test Company 1",
"default_warehouse": "_Test Warehouse - _TC",
"expense_account": "Stock In Hand - _TC",
"buying_cost_center": "_Test Cost Center - _TC",
"selling_cost_center": "_Test Cost Center - _TC",
}]
})
self.assertTrue("belong to company" in str(ve.exception).lower(),
msg="Mismatching company entities in item defaults should not be allowed.")
def test_item_attribute_change_after_variant(self): def test_item_attribute_change_after_variant(self):
frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1) frappe.delete_doc_if_exists("Item", "_Test Variant Item-L", force=1)

View File

@@ -272,8 +272,9 @@ def update_status(name, status):
material_request.update_status(status) material_request.update_status(status)
@frappe.whitelist() @frappe.whitelist()
def make_purchase_order(source_name, target_doc=None, args={}): def make_purchase_order(source_name, target_doc=None, args=None):
if args is None:
args = {}
if isinstance(args, string_types): if isinstance(args, string_types):
args = json.loads(args) args = json.loads(args)

View File

@@ -44,8 +44,10 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip
# check if exists # check if exists
exists = 0 exists = 0
for d in doc.get("packed_items"): for d in doc.get("packed_items"):
if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code and\ if d.parent_item == main_item_row.item_code and d.item_code == packing_item_code:
d.parent_detail_docname == main_item_row.name: if d.parent_detail_docname != main_item_row.name:
d.parent_detail_docname = main_item_row.name
pi, exists = d, 1 pi, exists = d, 1
break break

View File

@@ -36,7 +36,8 @@
"fieldname": "qty", "fieldname": "qty",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Qty" "label": "Qty",
"reqd": 1
}, },
{ {
"fieldname": "picked_qty", "fieldname": "picked_qty",
@@ -180,7 +181,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-06-24 17:18:57.357120", "modified": "2021-09-28 12:02:16.923056",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List Item", "name": "Pick List Item",

View File

@@ -4,6 +4,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
@@ -684,7 +685,7 @@ class StockEntry(StockController):
def validate_bom(self): def validate_bom(self):
for d in self.get('items'): for d in self.get('items'):
if d.bom_no and (d.t_warehouse != getattr(self, "pro_doc", frappe._dict()).scrap_warehouse): if d.bom_no and d.is_finished_item:
item_code = d.original_item or d.item_code item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no) validate_bom_no(item_code, d.bom_no)
@@ -1191,13 +1192,88 @@ class StockEntry(StockController):
# item dict = { item_code: {qty, description, stock_uom} } # item dict = { item_code: {qty, description, stock_uom} }
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty, item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=qty,
fetch_exploded = 0, fetch_scrap_items = 1) fetch_exploded = 0, fetch_scrap_items = 1) or {}
for item in itervalues(item_dict): for item in itervalues(item_dict):
item.from_warehouse = "" item.from_warehouse = ""
item.is_scrap_item = 1 item.is_scrap_item = 1
for row in self.get_scrap_items_from_job_card():
if row.stock_qty <= 0:
continue
item_row = item_dict.get(row.item_code)
if not item_row:
item_row = frappe._dict({})
item_row.update({
'uom': row.stock_uom,
'from_warehouse': '',
'qty': row.stock_qty + flt(item_row.stock_qty),
'converison_factor': 1,
'is_scrap_item': 1,
'item_name': row.item_name,
'description': row.description,
'allow_zero_valuation_rate': 1
})
item_dict[row.item_code] = item_row
return item_dict return item_dict
def get_scrap_items_from_job_card(self):
if not self.pro_doc:
self.set_work_order_details()
scrap_items = frappe.db.sql('''
SELECT
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
FROM
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
WHERE
JCSI.parent = JC.name AND JC.docstatus = 1
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
GROUP BY
JCSI.item_code
''', self.work_order, as_dict=1)
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
if pending_qty <=0:
return []
used_scrap_items = self.get_used_scrap_items()
for row in scrap_items:
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
row.stock_qty = (row.stock_qty) * flt(self.fg_completed_qty) / flt(pending_qty)
if used_scrap_items.get(row.item_code):
used_scrap_items[row.item_code] -= row.stock_qty
if cint(frappe.get_cached_value('UOM', row.stock_uom, 'must_be_whole_number')):
row.stock_qty = frappe.utils.ceil(row.stock_qty)
return scrap_items
def get_used_scrap_items(self):
used_scrap_items = defaultdict(float)
data = frappe.get_all(
'Stock Entry',
fields = [
'`tabStock Entry Detail`.`item_code`', '`tabStock Entry Detail`.`qty`'
],
filters = [
['Stock Entry', 'work_order', '=', self.work_order],
['Stock Entry Detail', 'is_scrap_item', '=', 1],
['Stock Entry', 'docstatus', '=', 1],
['Stock Entry', 'purpose', 'in', ['Repack', 'Manufacture']]
]
)
for row in data:
used_scrap_items[row.item_code] += row.qty
return used_scrap_items
def get_unconsumed_raw_materials(self): def get_unconsumed_raw_materials(self):
wo = frappe.get_doc("Work Order", self.work_order) wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item', wo_items = frappe.get_all('Work Order Item',
@@ -1417,8 +1493,8 @@ class StockEntry(StockController):
se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0) se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
se_child.is_process_loss = item_dict[d].get("is_process_loss", 0) se_child.is_process_loss = item_dict[d].get("is_process_loss", 0)
for field in ["idx", "po_detail", "original_item", for field in ["idx", "po_detail", "original_item", "expense_account",
"expense_account", "description", "item_name", "serial_no", "batch_no"]: "description", "item_name", "serial_no", "batch_no", "allow_zero_valuation_rate"]:
if item_dict[d].get(field): if item_dict[d].get(field):
se_child.set(field, item_dict[d].get(field)) se_child.set(field, item_dict[d].get(field))

View File

@@ -592,6 +592,11 @@ def get_stock_balance_for(item_code, warehouse,
item_dict = frappe.db.get_value("Item", item_code, item_dict = frappe.db.get_value("Item", item_code,
["has_serial_no", "has_batch_no"], as_dict=1) ["has_serial_no", "has_batch_no"], as_dict=1)
if not item_dict:
# In cases of data upload to Items table
msg = _("Item {} does not exist.").format(item_code)
frappe.throw(msg, title=_("Missing"))
serial_nos = "" serial_nos = ""
with_serial_no = True if item_dict.get("has_serial_no") else False with_serial_no = True if item_dict.get("has_serial_no") else False
data = get_stock_balance(item_code, warehouse, posting_date, posting_time, data = get_stock_balance(item_code, warehouse, posting_date, posting_time,

View File

@@ -0,0 +1,63 @@
import unittest
from typing import List, Tuple
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
}
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("Stock Ledger", {"_optional": True}),
("Stock Balance", {"_optional": True}),
("Stock Projected Qty", {"_optional": True}),
("Batch-Wise Balance History", {}),
("Itemwise Recommended Reorder Level", {"item_group": "All Item Groups"}),
("COGS By Item Group", {}),
("Stock Qty vs Serial No Count", {"warehouse": "_Test Warehouse - _TC"}),
(
"Stock and Account Value Comparison",
{
"company": "_Test Company with perpetual inventory",
"account": "Stock In Hand - TCP1",
"as_on_date": "2021-01-01",
},
),
("Product Bundle Balance", {"date": "2022-01-01", "_optional": True}),
(
"Stock Analytics",
{
"from_date": "2021-01-01",
"to_date": "2021-12-31",
"value_quantity": "Quantity",
"_optional": True,
},
),
("Warehouse wise Item Balance Age and Value", {"_optional": True}),
("Item Variant Details", {"item": "_Test Variant Item",}),
("Total Stock Summary", {"group_by": "warehouse",}),
("Batch Item Expiry Status", {}),
("Stock Ageing", {"range1": 30, "range2": 60, "range3": 90, "_optional": True}),
]
OPTIONAL_FILTERS = {
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item",
"item_group": "_Test Item Group",
}
class TestReports(unittest.TestCase):
def test_execute_all_stock_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report(
report_name=report,
module="Stock",
filters=filter,
default_filters=DEFAULT_FILTERS,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@@ -399,6 +399,7 @@ class update_entries_after(object):
return return
# Get dynamic incoming/outgoing rate # Get dynamic incoming/outgoing rate
if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle) self.get_dynamic_incoming_outgoing_rate(sle)
if sle.serial_no: if sle.serial_no:
@@ -439,6 +440,7 @@ class update_entries_after(object):
sle.doctype="Stock Ledger Entry" sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update() frappe.get_doc(sle).db_update()
if not self.args.get("sle_id"):
self.update_outgoing_rate_on_transaction(sle) self.update_outgoing_rate_on_transaction(sle)
def validate_negative_stock(self, sle): def validate_negative_stock(self, sle):

View File

@@ -11,6 +11,6 @@
{% endfor %} {% endfor %}
</ol> </ol>
{% else %} {% else %}
<p>You don't have no upcoming holidays this {{ frequency }}.</p> <p>You have no upcoming holidays this {{ frequency }}.</p>
{% endif %} {% endif %}
{% endif %} {% endif %}

View File

@@ -57,7 +57,7 @@ $.extend(shopping_cart, {
callback: function(r) { callback: function(r) {
d.hide(); d.hide();
if (!r.exc) { if (!r.exc) {
$(".cart-tax-items").html(r.message.taxes); $(".cart-tax-items").html(r.message.total);
shopping_cart.parent.find( shopping_cart.parent.find(
`.address-container[data-address-type="${address_type}"]` `.address-container[data-address-type="${address_type}"]`
).html(r.message.address); ).html(r.message.address);
@@ -214,12 +214,15 @@ $.extend(shopping_cart, {
}, },
place_order: function(btn) { place_order: function(btn) {
shopping_cart.freeze();
return frappe.call({ return frappe.call({
type: "POST", type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.place_order", method: "erpnext.e_commerce.shopping_cart.cart.place_order",
btn: btn, btn: btn,
callback: function(r) { callback: function(r) {
if(r.exc) { if(r.exc) {
shopping_cart.unfreeze();
var msg = ""; var msg = "";
if(r._server_messages) { if(r._server_messages) {
msg = JSON.parse(r._server_messages || []).join("<br>"); msg = JSON.parse(r._server_messages || []).join("<br>");
@@ -230,7 +233,6 @@ $.extend(shopping_cart, {
.html(msg || frappe._("Something went wrong!")) .html(msg || frappe._("Something went wrong!"))
.toggle(true); .toggle(true);
} else { } else {
$('.cart-container table').hide();
$(btn).hide(); $(btn).hide();
window.location.href = '/orders/' + encodeURIComponent(r.message); window.location.href = '/orders/' + encodeURIComponent(r.message);
} }
@@ -239,12 +241,15 @@ $.extend(shopping_cart, {
}, },
request_quotation: function(btn) { request_quotation: function(btn) {
shopping_cart.freeze();
return frappe.call({ return frappe.call({
type: "POST", type: "POST",
method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation", method: "erpnext.e_commerce.shopping_cart.cart.request_for_quotation",
btn: btn, btn: btn,
callback: function(r) { callback: function(r) {
if(r.exc) { if(r.exc) {
shopping_cart.unfreeze();
var msg = ""; var msg = "";
if(r._server_messages) { if(r._server_messages) {
msg = JSON.parse(r._server_messages || []).join("<br>"); msg = JSON.parse(r._server_messages || []).join("<br>");
@@ -255,7 +260,6 @@ $.extend(shopping_cart, {
.html(msg || frappe._("Something went wrong!")) .html(msg || frappe._("Something went wrong!"))
.toggle(true); .toggle(true);
} else { } else {
$('.cart-container table').hide();
$(btn).hide(); $(btn).hide();
window.location.href = '/quotations/' + encodeURIComponent(r.message); window.location.href = '/quotations/' + encodeURIComponent(r.message);
} }

View File

@@ -0,0 +1,10 @@
<!-- Total at the end of the cart items -->
<tr>
<th></th>
<th class="text-left item-grand-total" colspan="1">
{{ _("Total") }}
</th>
<th class="text-left item-grand-total totals" colspan="3">
{{ doc.get_formatted("total") }}
</th>
</tr>

View File

@@ -1,5 +1,4 @@
<!-- Payment --> <!-- Payment -->
<div class="mb-3 frappe-card p-5 payment-summary">
<h6> <h6>
{{ _("Payment Summary") }} {{ _("Payment Summary") }}
</h6> </h6>
@@ -7,7 +6,8 @@
<div class="card-body p-0"> <div class="card-body p-0">
<table class="table w-100"> <table class="table w-100">
<tr> <tr>
<td class="bill-label">{{ _("Net Total (") + frappe.utils.cstr(doc.items|len) + _(" Items)") }}</td> {% set total_items = frappe.utils.cstr(frappe.utils.flt(doc.total_qty, 0)) %}
<td class="bill-label">{{ _("Net Total (") + total_items + _(" Items)") }}</td>
<td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td> <td class="bill-content net-total text-right">{{ doc.get_formatted("net_total") }}</td>
</tr> </tr>
@@ -58,7 +58,6 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
<!-- TODO: Apply Coupon Dialog--> <!-- TODO: Apply Coupon Dialog-->
<!-- <script> <!-- <script>

View File

@@ -45,15 +45,7 @@
{% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %} {% if cart_settings.enable_checkout or cart_settings.show_price_in_quotation %}
<tfoot class="cart-tax-items"> <tfoot class="cart-tax-items">
<tr> {% include "templates/includes/cart/cart_items_total.html" %}
<th></th>
<th class="text-left item-grand-total" colspan="1">
{{ _("Total") }}
</th>
<th class="text-left item-grand-total totals" colspan="3">
{{ doc.get_formatted("total") }}
</th>
</tr>
</tfoot> </tfoot>
{% endif %} {% endif %}
</table> </table>
@@ -110,7 +102,9 @@
{% endif %} {% endif %}
{% if cart_settings.enable_checkout %} {% if cart_settings.enable_checkout %}
<div class="mb-3 frappe-card p-5 payment-summary">
{% include "templates/includes/cart/cart_payment_summary.html" %} {% include "templates/includes/cart/cart_payment_summary.html" %}
</div>
{% endif %} {% endif %}
{% include "templates/includes/cart/cart_address.html" %} {% include "templates/includes/cart/cart_address.html" %}
@@ -126,11 +120,11 @@
</div> </div>
<div class="cart-empty-message mt-4">{{ _('Your cart is Empty') }}</p> <div class="cart-empty-message mt-4">{{ _('Your cart is Empty') }}</p>
{% if cart_settings.enable_checkout %} {% if cart_settings.enable_checkout %}
<a class="btn btn-outline-primary" href="/orders"> <a class="btn btn-outline-primary" href="/orders" style="font-size: 16px;">
{{ _('See past orders') }} {{ _('See past orders') }}
</a> </a>
{% else %} {% else %}
<a class="btn btn-outline-primary" href="/quotations"> <a class="btn btn-outline-primary" href="/quotations" style="font-size: 16px;">
{{ _('See past quotations') }} {{ _('See past quotations') }}
</a> </a>
{% endif %} {% endif %}

View File

@@ -7,7 +7,7 @@
</div> </div>
{% else %} {% else %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}"> <div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(doc.meta.get_label('total')) }}</label></div> <label>{{ _(df.label) }}</label></div>
<div class="col-xs-7 text-right"> <div class="col-xs-7 text-right">
{{ doc.get_formatted("total", doc) }} {{ doc.get_formatted("total", doc) }}
</div> </div>

View File

@@ -0,0 +1,138 @@
import unittest
import frappe
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
class TestWebsite(unittest.TestCase):
def test_permission_for_custom_doctype(self):
create_user('Supplier 1', 'supplier1@gmail.com')
create_user('Supplier 2', 'supplier2@gmail.com')
create_supplier_with_contact('Supplier1', 'All Supplier Groups', 'Supplier 1', 'supplier1@gmail.com')
create_supplier_with_contact('Supplier2', 'All Supplier Groups', 'Supplier 2', 'supplier2@gmail.com')
po1 = create_purchase_order(supplier='Supplier1')
po2 = create_purchase_order(supplier='Supplier2')
create_custom_doctype()
create_webform()
create_order_assignment(supplier='Supplier1', po = po1.name)
create_order_assignment(supplier='Supplier2', po = po2.name)
frappe.set_user("Administrator")
# checking if data consist of all order assignment of Supplier1 and Supplier2
self.assertTrue('Supplier1' and 'Supplier2' in [data.supplier for data in get_data()])
frappe.set_user("supplier1@gmail.com")
# checking if data only consist of order assignment of Supplier1
self.assertTrue('Supplier1' in [data.supplier for data in get_data()])
self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier1'])
frappe.set_user("supplier2@gmail.com")
# checking if data only consist of order assignment of Supplier2
self.assertTrue('Supplier2' in [data.supplier for data in get_data()])
self.assertFalse([data.supplier for data in get_data() if data.supplier != 'Supplier2'])
frappe.set_user("Administrator")
def get_data():
webform_list_contexts = frappe.get_hooks('webform_list_context')
if webform_list_contexts:
context = frappe._dict(frappe.get_attr(webform_list_contexts[0])('Buying') or {})
kwargs = dict(doctype='Order Assignment', order_by = 'modified desc')
return context.get_list(**kwargs)
def create_user(name, email):
frappe.get_doc({
'doctype': 'User',
'send_welcome_email': 0,
'user_type': 'Website User',
'first_name': name,
'email': email,
'roles': [{"doctype": "Has Role", "role": "Supplier"}]
}).insert(ignore_if_duplicate = True)
def create_supplier_with_contact(name, group, contact_name, contact_email):
supplier = frappe.get_doc({
'doctype': 'Supplier',
'supplier_name': name,
'supplier_group': group
}).insert(ignore_if_duplicate = True)
if not frappe.db.exists('Contact', contact_name+'-1-'+name):
new_contact = frappe.new_doc("Contact")
new_contact.first_name = contact_name
new_contact.is_primary_contact = True,
new_contact.append('links', {
"link_doctype": "Supplier",
"link_name": supplier.name
})
new_contact.append('email_ids', {
"email_id": contact_email,
"is_primary": 1
})
new_contact.insert(ignore_mandatory=True)
def create_custom_doctype():
frappe.get_doc({
'doctype': 'DocType',
'name': 'Order Assignment',
'module': 'Buying',
'custom': 1,
'autoname': 'field:po',
'fields': [
{'label': 'PO', 'fieldname': 'po', 'fieldtype': 'Link', 'options': 'Purchase Order'},
{'label': 'Supplier', 'fieldname': 'supplier', 'fieldtype': 'Data', "fetch_from": "po.supplier"}
],
'permissions': [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"read": 1,
"role": "Supplier"
}
]
}).insert(ignore_if_duplicate = True)
def create_webform():
frappe.get_doc({
'doctype': 'Web Form',
'module': 'Buying',
'title': 'SO Schedule',
'route': 'so-schedule',
'doc_type': 'Order Assignment',
'web_form_fields': [
{
'doctype': 'Web Form Field',
'fieldname': 'po',
'fieldtype': 'Link',
'options': 'Purchase Order',
'label': 'PO'
},
{
'doctype': 'Web Form Field',
'fieldname': 'supplier',
'fieldtype': 'Data',
'label': 'Supplier'
}
]
}).insert(ignore_if_duplicate = True)
def create_order_assignment(supplier, po):
frappe.get_doc({
'doctype': 'Order Assignment',
'po': po,
'supplier': supplier,
}).insert(ignore_if_duplicate = True)

View File

@@ -3,8 +3,13 @@
import copy import copy
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Dict, NewType, Optional
import frappe import frappe
from frappe.core.doctype.report.report import get_report_module_dotted_path
ReportFilters = Dict[str, Any]
ReportName = NewType("ReportName", str)
def create_test_contact_and_address(): def create_test_contact_and_address():
@@ -78,3 +83,39 @@ def change_settings(doctype, settings_dict):
for key, value in previous_settings.items(): for key, value in previous_settings.items():
setattr(settings, key, value) setattr(settings, key, value)
settings.save() settings.save()
def execute_script_report(
report_name: ReportName,
module: str,
filters: ReportFilters,
default_filters: Optional[ReportFilters] = None,
optional_filters: Optional[ReportFilters] = None
):
"""Util for testing execution of a report with specified filters.
Tests the execution of report with default_filters + filters.
Tests the execution using optional_filters one at a time.
Args:
report_name: Human readable name of report (unscrubbed)
module: module to which report belongs to
filters: specific values for filters
default_filters: default values for filters such as company name.
optional_filters: filters which should be tested one at a time in addition to default filters.
"""
if default_filters is None:
default_filters = {}
report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
report_filters = frappe._dict(default_filters).copy().update(filters)
report_data = report_execute_fn(report_filters)
if optional_filters:
for key, value in optional_filters.items():
filter_with_optional_param = report_filters.copy().update({key: value})
report_execute_fn(filter_with_optional_param)
return report_data