Merge: upstream/develop into pps190/fix-reserve-qty

This commit is contained in:
Devin Slauenwhite 2023-02-14 10:22:54 -05:00
commit 4c5147f270
47 changed files with 917 additions and 395 deletions

View File

@ -4,7 +4,7 @@
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar

View File

@ -40,7 +40,7 @@ class Dunning(AccountsController):
def on_cancel(self): def on_cancel(self):
if self.dunning_amount: if self.dunning_amount:
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def make_gl_entries(self): def make_gl_entries(self):

View File

@ -234,7 +234,7 @@ class PaymentReconciliation(Document):
def allocate_entries(self, args): def allocate_entries(self, args):
self.validate_entries() self.validate_entries()
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices")) invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
default_exchange_gain_loss_account = frappe.get_cached_value( default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account" "Company", self.company, "exchange_gain_loss_account"
) )
@ -253,6 +253,9 @@ class PaymentReconciliation(Document):
pay["amount"] = 0 pay["amount"] = 0
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number")) inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name"))
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate") res.exchange_rate = inv.get("exchange_rate")
@ -407,13 +410,21 @@ class PaymentReconciliation(Document):
if not self.get("payments"): if not self.get("payments"):
frappe.throw(_("No records found in the Payments table")) frappe.throw(_("No records found in the Payments table"))
def get_invoice_exchange_map(self, invoices): def get_invoice_exchange_map(self, invoices, payments):
sales_invoices = [ sales_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice" d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
] ]
sales_invoices.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"]
)
purchase_invoices = [ purchase_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice" d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
] ]
purchase_invoices.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"]
)
invoice_exchange_map = frappe._dict() invoice_exchange_map = frappe._dict()
if sales_invoices: if sales_invoices:

View File

@ -473,6 +473,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
pr.get_unreconciled_entries() pr.get_unreconciled_entries()
@ -506,6 +511,11 @@ class TestPaymentReconciliation(FrappeTestCase):
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = allocated_amount pr.allocation[0].allocated_amount = allocated_amount
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
# assert outstanding # assert outstanding

View File

@ -45,21 +45,20 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required")) frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self): def validate_payment_request_amount(self):
existing_payment_request_amount = get_existing_payment_request_amount( existing_payment_request_amount = flt(
self.reference_doctype, self.reference_name get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
) )
if existing_payment_request_amount: ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": ref_amount = get_amount(ref_doc, self.payment_account)
ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total) > ref_amount: if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw( frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format( _("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype self.reference_doctype
)
) )
)
def validate_currency(self): def validate_currency(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)

View File

@ -1512,9 +1512,12 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
ref_doc = frappe.get_doc(voucher_type, voucher_no) ref_doc = frappe.get_doc(voucher_type, voucher_no)
# Didn't use db_set for optimisation purpose # Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
frappe.db.set_value( frappe.db.set_value(
voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"] voucher_type,
voucher_no,
"outstanding_amount",
outstanding["outstanding_in_account_currency"] or 0.0,
) )
ref_doc.set_status(update=True) ref_doc.set_status(update=True)

View File

@ -252,6 +252,7 @@ def get_already_returned_items(doc):
child.parent = par.name and par.docstatus = 1 child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s and par.is_return = 1 and par.return_against = %s
group by item_code group by item_code
for update
""".format( """.format(
column, doc.doctype, doc.doctype column, doc.doctype, doc.doctype
), ),

View File

@ -26,10 +26,11 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-02-08 12:51:48.971517", "modified": "2023-02-10 00:51:44.973957",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead Source", "name": "Lead Source",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -58,5 +59,7 @@
], ],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": [],
"translated_doctype": 1
} }

View File

@ -18,10 +18,11 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-05-20 12:22:01.866472", "modified": "2023-02-10 01:40:23.713390",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Sales Stage", "name": "Sales Stage",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -40,5 +41,7 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "states": [],
"track_changes": 1,
"translated_doctype": 1
} }

View File

@ -11,6 +11,40 @@ frappe.query_reports["Loan Interest Report"] = {
"options": "Company", "options": "Company",
"default": frappe.defaults.get_user_default("Company"), "default": frappe.defaults.get_user_default("Company"),
"reqd": 1 "reqd": 1
} },
{
"fieldname":"applicant_type",
"label": __("Applicant Type"),
"fieldtype": "Select",
"options": ["Customer", "Employee"],
"reqd": 1,
"default": "Customer",
on_change: function() {
frappe.query_report.set_filter_value('applicant', "");
}
},
{
"fieldname": "applicant",
"label": __("Applicant"),
"fieldtype": "Dynamic Link",
"get_options": function() {
var applicant_type = frappe.query_report.get_filter_value('applicant_type');
var applicant = frappe.query_report.get_filter_value('applicant');
if(applicant && !applicant_type) {
frappe.throw(__("Please select Applicant Type first"));
}
return applicant_type;
}
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
},
{
"fieldname":"to_date",
"label": __("From Date"),
"fieldtype": "Date",
},
] ]
}; };

View File

@ -13,12 +13,12 @@ from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applic
def execute(filters=None): def execute(filters=None):
columns = get_columns(filters) columns = get_columns()
data = get_active_loan_details(filters) data = get_active_loan_details(filters)
return columns, data return columns, data
def get_columns(filters): def get_columns():
columns = [ columns = [
{"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160},
@ -70,6 +70,13 @@ def get_columns(filters):
"options": "currency", "options": "currency",
"width": 120, "width": 120,
}, },
{
"label": _("Accrued Principal"),
"fieldname": "accrued_principal",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{ {
"label": _("Total Repayment"), "label": _("Total Repayment"),
"fieldname": "total_repayment", "fieldname": "total_repayment",
@ -137,11 +144,16 @@ def get_columns(filters):
def get_active_loan_details(filters): def get_active_loan_details(filters):
filter_obj = {
filter_obj = {"status": ("!=", "Closed")} "status": ("!=", "Closed"),
"docstatus": 1,
}
if filters.get("company"): if filters.get("company"):
filter_obj.update({"company": filters.get("company")}) filter_obj.update({"company": filters.get("company")})
if filters.get("applicant"):
filter_obj.update({"applicant": filters.get("applicant")})
loan_details = frappe.get_all( loan_details = frappe.get_all(
"Loan", "Loan",
fields=[ fields=[
@ -167,8 +179,8 @@ def get_active_loan_details(filters):
sanctioned_amount_map = get_sanctioned_amount_map() sanctioned_amount_map = get_sanctioned_amount_map()
penal_interest_rate_map = get_penal_interest_rate_map() penal_interest_rate_map = get_penal_interest_rate_map()
payments = get_payments(loan_list) payments = get_payments(loan_list, filters)
accrual_map = get_interest_accruals(loan_list) accrual_map = get_interest_accruals(loan_list, filters)
currency = erpnext.get_company_currency(filters.get("company")) currency = erpnext.get_company_currency(filters.get("company"))
for loan in loan_details: for loan in loan_details:
@ -183,6 +195,7 @@ def get_active_loan_details(filters):
- flt(loan.written_off_amount), - flt(loan.written_off_amount),
"total_repayment": flt(payments.get(loan.loan)), "total_repayment": flt(payments.get(loan.loan)),
"accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
"accrued_principal": flt(accrual_map.get(loan.loan, {}).get("accrued_principal")),
"interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")),
"penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
"penalty_interest": penal_interest_rate_map.get(loan.loan_type), "penalty_interest": penal_interest_rate_map.get(loan.loan_type),
@ -212,20 +225,35 @@ def get_sanctioned_amount_map():
) )
def get_payments(loans): def get_payments(loans, filters):
query_filters = {"against_loan": ("in", loans)}
if filters.get("from_date"):
query_filters.update({"posting_date": (">=", filters.get("from_date"))})
if filters.get("to_date"):
query_filters.update({"posting_date": ("<=", filters.get("to_date"))})
return frappe._dict( return frappe._dict(
frappe.get_all( frappe.get_all(
"Loan Repayment", "Loan Repayment",
fields=["against_loan", "sum(amount_paid)"], fields=["against_loan", "sum(amount_paid)"],
filters={"against_loan": ("in", loans)}, filters=query_filters,
group_by="against_loan", group_by="against_loan",
as_list=1, as_list=1,
) )
) )
def get_interest_accruals(loans): def get_interest_accruals(loans, filters):
accrual_map = {} accrual_map = {}
query_filters = {"loan": ("in", loans)}
if filters.get("from_date"):
query_filters.update({"posting_date": (">=", filters.get("from_date"))})
if filters.get("to_date"):
query_filters.update({"posting_date": ("<=", filters.get("to_date"))})
interest_accruals = frappe.get_all( interest_accruals = frappe.get_all(
"Loan Interest Accrual", "Loan Interest Accrual",
@ -236,8 +264,9 @@ def get_interest_accruals(loans):
"penalty_amount", "penalty_amount",
"paid_interest_amount", "paid_interest_amount",
"accrual_type", "accrual_type",
"payable_principal_amount",
], ],
filters={"loan": ("in", loans)}, filters=query_filters,
order_by="posting_date desc", order_by="posting_date desc",
) )
@ -246,6 +275,7 @@ def get_interest_accruals(loans):
entry.loan, entry.loan,
{ {
"accrued_interest": 0.0, "accrued_interest": 0.0,
"accrued_principal": 0.0,
"undue_interest": 0.0, "undue_interest": 0.0,
"interest_outstanding": 0.0, "interest_outstanding": 0.0,
"last_accrual_date": "", "last_accrual_date": "",
@ -270,6 +300,7 @@ def get_interest_accruals(loans):
accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount
accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount
accrual_map[entry.loan]["accrued_principal"] += entry.payable_principal_amount
if last_accrual_date and getdate(entry.posting_date) == last_accrual_date: if last_accrual_date and getdate(entry.posting_date) == last_accrual_date:
accrual_map[entry.loan]["penalty"] = entry.penalty_amount accrual_map[entry.loan]["penalty"] = entry.penalty_amount

View File

@ -0,0 +1,315 @@
{
"charts": [],
"content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"t7o_K__1jB\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"id\":\"IRiNDC6w1p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"id\":\"xbbo0FYbq0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"7ZL4Bro-Vi\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yhyioTViZ3\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"oYFn4b1kSw\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"id\":\"vZepJF5tl9\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"id\":\"k-393Mjhqe\",\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"id\":\"6crJ0DBiBJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"id\":\"Um5YwxVLRJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-12 16:35:55.299820",
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "loan",
"idx": 0,
"is_hidden": 0,
"label": "Loans",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Loan",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Type",
"link_count": 0,
"link_to": "Loan Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Application",
"link_count": 0,
"link_to": "Loan Application",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan",
"link_count": 0,
"link_to": "Loan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Loan Processes",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Process Loan Security Shortfall",
"link_count": 0,
"link_to": "Process Loan Security Shortfall",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Process Loan Interest Accrual",
"link_count": 0,
"link_to": "Process Loan Interest Accrual",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Disbursement and Repayment",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Disbursement",
"link_count": 0,
"link_to": "Loan Disbursement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Repayment",
"link_count": 0,
"link_to": "Loan Repayment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Write Off",
"link_count": 0,
"link_to": "Loan Write Off",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Interest Accrual",
"link_count": 0,
"link_to": "Loan Interest Accrual",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Type",
"link_count": 0,
"link_to": "Loan Security Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Price",
"link_count": 0,
"link_to": "Loan Security Price",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security",
"link_count": 0,
"link_to": "Loan Security",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Pledge",
"link_count": 0,
"link_to": "Loan Security Pledge",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Unpledge",
"link_count": 0,
"link_to": "Loan Security Unpledge",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Shortfall",
"link_count": 0,
"link_to": "Loan Security Shortfall",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Reports",
"link_count": 6,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Loan Repayment and Closure",
"link_count": 0,
"link_to": "Loan Repayment and Closure",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Loan Security Status",
"link_count": 0,
"link_to": "Loan Security Status",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Loan Interest Report",
"link_count": 0,
"link_to": "Loan Interest Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Loan Security Exposure",
"link_count": 0,
"link_to": "Loan Security Exposure",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Applicant-Wise Loan Security Exposure",
"link_count": 0,
"link_to": "Applicant-Wise Loan Security Exposure",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Loan Security Status",
"link_count": 0,
"link_to": "Loan Security Status",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
"modified": "2023-01-31 19:47:13.114415",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loans",
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 16.0,
"shortcuts": [
{
"color": "Green",
"format": "{} Open",
"label": "Loan Application",
"link_to": "Loan Application",
"stats_filter": "{ \"status\": \"Open\" }",
"type": "DocType"
},
{
"label": "Loan",
"link_to": "Loan",
"type": "DocType"
},
{
"doc_view": "",
"label": "Dashboard",
"link_to": "Loan Dashboard",
"type": "Dashboard"
}
],
"title": "Loans"
}

View File

@ -289,7 +289,7 @@
{ {
"fieldname": "scrap_items", "fieldname": "scrap_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items", "label": "Scrap Items",
"options": "BOM Scrap Item" "options": "BOM Scrap Item"
}, },
{ {
@ -605,7 +605,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-10 07:47:08.652616", "modified": "2023-02-13 17:31:37.504565",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@ -1,16 +1,17 @@
import frappe import frappe
from frappe import _
def execute(): def execute():
from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type from erpnext.setup.setup_wizard.operations.install_fixtures import read_lines
frappe.reload_doc("selling", "doctype", "sales_partner_type") frappe.reload_doc("selling", "doctype", "sales_partner_type")
frappe.local.lang = frappe.db.get_default("lang") or "en" frappe.local.lang = frappe.db.get_default("lang") or "en"
default_sales_partner_type = read_lines("sales_partner_type.txt")
for s in default_sales_partner_type: for s in default_sales_partner_type:
insert_sales_partner_type(_(s)) insert_sales_partner_type(s)
# get partner type in existing forms (customized) # get partner type in existing forms (customized)
# and create a document if not created # and create a document if not created

View File

@ -161,6 +161,37 @@ class TestTimesheet(unittest.TestCase):
to_time = timesheet.time_logs[0].to_time to_time = timesheet.time_logs[0].to_time
self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True)) self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True))
def test_per_billed_hours(self):
"""If amounts are 0, per_billed should be calculated based on hours."""
ts = frappe.new_doc("Timesheet")
ts.total_billable_amount = 0
ts.total_billed_amount = 0
ts.total_billable_hours = 2
ts.total_billed_hours = 0.5
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 25)
ts.total_billed_hours = 2
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 100)
def test_per_billed_amount(self):
"""If amounts are > 0, per_billed should be calculated based on amounts, regardless of hours."""
ts = frappe.new_doc("Timesheet")
ts.total_billable_hours = 2
ts.total_billed_hours = 1
ts.total_billable_amount = 200
ts.total_billed_amount = 50
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 25)
ts.total_billed_hours = 3
ts.total_billable_amount = 200
ts.total_billed_amount = 200
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 100)
def make_timesheet( def make_timesheet(
employee, employee,

View File

@ -64,6 +64,8 @@ class Timesheet(Document):
self.per_billed = 0 self.per_billed = 0
if self.total_billed_amount > 0 and self.total_billable_amount > 0: if self.total_billed_amount > 0 and self.total_billable_amount > 0:
self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount
elif self.total_billed_hours > 0 and self.total_billable_hours > 0:
self.per_billed = (self.total_billed_hours * 100) / self.total_billable_hours
def update_billing_hours(self, args): def update_billing_hours(self, args):
if args.is_billable: if args.is_billable:

View File

@ -126,7 +126,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
frappe.model.round_floats_in(item); frappe.model.round_floats_in(item);
item.net_rate = item.rate; item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
if (!(me.frm.doc.is_return || me.frm.doc.is_debit_note)) {
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
}
else {
let qty = item.qty || 1;
qty = me.frm.doc.is_return ? -1 * qty : qty;
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
item.item_tax_amount = 0.0; item.item_tax_amount = 0.0;
item.total_weight = flt(item.weight_per_unit * item.stock_qty); item.total_weight = flt(item.weight_per_unit * item.stock_qty);

View File

@ -1,123 +1,68 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:industry", "autoname": "field:industry",
"beta": 0,
"creation": "2012-03-27 14:36:09", "creation": "2012-03-27 14:36:09",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0, "engine": "InnoDB",
"field_order": [
"industry"
],
"fields": [ "fields": [
{ {
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "industry", "fieldname": "industry",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0, "in_list_view": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Industry", "label": "Industry",
"length": 0,
"no_copy": 0,
"oldfieldname": "industry", "oldfieldname": "industry",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "unique": 1
"set_only_once": 0,
"unique": 0
} }
], ],
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-flag", "icon": "fa fa-flag",
"idx": 1, "idx": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2023-02-10 03:14:40.735763",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Industry Type", "name": "Industry Type",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 0,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Sales Manager", "role": "Sales Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Sales User", "role": "Sales User"
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 0,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Sales Master Manager", "role": "Sales Master Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "sort_field": "modified",
"read_only_onload": 0, "sort_order": "DESC",
"track_seen": 0 "states": [],
"translated_doctype": 1
} }

View File

@ -85,11 +85,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
} }
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
this.frm.add_custom_button( if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation
__("Sales Order"), || (!doc.valid_till)
this.frm.cscript["Make Sales Order"], || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
__("Create") this.frm.add_custom_button(
); __("Sales Order"),
this.frm.cscript["Make Sales Order"],
__("Create")
);
}
if(doc.status!=="Ordered") { if(doc.status!=="Ordered") {
this.frm.add_custom_button(__('Set as Lost'), () => { this.frm.add_custom_button(__('Set as Lost'), () => {

View File

@ -195,6 +195,17 @@ def get_list_context(context=None):
@frappe.whitelist() @frappe.whitelist()
def make_sales_order(source_name: str, target_doc=None): def make_sales_order(source_name: str, target_doc=None):
if not frappe.db.get_singles_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
):
quotation = frappe.db.get_value(
"Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
)
if quotation.valid_till and (
quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
):
frappe.throw(_("Validity period of this quotation has ended."))
return _make_sales_order(source_name, target_doc) return _make_sales_order(source_name, target_doc)

View File

@ -144,11 +144,21 @@ class TestQuotation(FrappeTestCase):
def test_so_from_expired_quotation(self): def test_so_from_expired_quotation(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.selling.doctype.quotation.quotation import make_sales_order
frappe.db.set_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0
)
quotation = frappe.copy_doc(test_records[0]) quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(nowdate(), -1) quotation.valid_till = add_days(nowdate(), -1)
quotation.insert() quotation.insert()
quotation.submit() quotation.submit()
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
frappe.db.set_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1
)
make_sales_order(quotation.name) make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self): def test_shopping_cart_without_website_item(self):

View File

@ -17,6 +17,8 @@
"customer_name", "customer_name",
"tax_id", "tax_id",
"order_type", "order_type",
"col_breaktest123",
"test_my_field",
"column_break_7", "column_break_7",
"transaction_date", "transaction_date",
"delivery_date", "delivery_date",
@ -248,6 +250,15 @@
"print_hide": 1, "print_hide": 1,
"reqd": 1 "reqd": 1
}, },
{
"fieldname": "col_breaktest123",
"fieldtype": "Column Break"
},
{
"fieldname": "test_my_field",
"fieldtype": "Data",
"label": "Test My Field"
},
{ {
"fieldname": "column_break1", "fieldname": "column_break1",
"fieldtype": "Column Break", "fieldtype": "Column Break",
@ -1643,7 +1654,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-12-12 18:34:00.681780", "modified": "2023-02-13 11:59:00.681780",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@ -1,94 +1,47 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:sales_partner_type", "autoname": "field:sales_partner_type",
"beta": 0,
"creation": "2018-06-11 13:15:57.404716", "creation": "2018-06-11 13:15:57.404716",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"sales_partner_type"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sales_partner_type", "fieldname": "sales_partner_type",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Sales Partner Type", "label": "Sales Partner Type",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "unique": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "links": [],
"hide_heading": 0, "modified": "2023-02-10 01:00:20.110800",
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-06-11 13:45:13.554307",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Partner Type", "name": "Sales Partner Type",
"name_case": "", "naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "states": [],
"track_seen": 0 "translated_doctype": 1
} }

View File

@ -27,6 +27,7 @@
"column_break_5", "column_break_5",
"allow_multiple_items", "allow_multiple_items",
"allow_against_multiple_purchase_orders", "allow_against_multiple_purchase_orders",
"allow_sales_order_creation_for_expired_quotation",
"dont_reserve_sales_order_qty_on_sales_return", "dont_reserve_sales_order_qty_on_sales_return",
"hide_tax_id", "hide_tax_id",
"enable_discount_accounting" "enable_discount_accounting"
@ -174,6 +175,12 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Discount Accounting for Selling" "label": "Enable Discount Accounting for Selling"
}, },
{
"default": "0",
"fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation"
},
{ {
"default": "0", "default": "0",
"fieldname": "dont_reserve_sales_order_qty_on_sales_return", "fieldname": "dont_reserve_sales_order_qty_on_sales_return",
@ -186,7 +193,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-06-17 15:50:43.968334", "modified": "2023-02-04 12:37:53.380857",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

View File

@ -322,6 +322,11 @@ erpnext.PointOfSale.Payment = class {
this.focus_on_default_mop(); this.focus_on_default_mop();
} }
after_render() {
const frm = this.events.get_frm();
frm.script_manager.trigger("after_payment_render", frm.doc.doctype, frm.doc.docname);
}
edit_cart() { edit_cart() {
this.events.toggle_other_sections(false); this.events.toggle_other_sections(false);
this.toggle_component(false); this.toggle_component(false);
@ -332,6 +337,7 @@ erpnext.PointOfSale.Payment = class {
this.toggle_component(true); this.toggle_component(true);
this.render_payment_section(); this.render_payment_section();
this.after_render();
} }
toggle_remarks_control() { toggle_remarks_control() {

View File

@ -103,6 +103,11 @@ function get_filters() {
return options return options
} }
}, },
{
"fieldname":"only_immediate_upcoming_term",
"label": __("Show only the Immediate Upcoming Term"),
"fieldtype": "Check",
},
] ]
return filters; return filters;
} }

View File

@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _, qb, query_builder from frappe import _, qb, query_builder
from frappe.query_builder import Criterion, functions from frappe.query_builder import Criterion, functions
from frappe.utils.dateutils import getdate
def get_columns(): def get_columns():
@ -208,6 +209,7 @@ def get_so_with_invoices(filters):
) )
.where( .where(
(so.docstatus == 1) (so.docstatus == 1)
& (so.status.isin(["To Deliver and Bill", "To Bill"]))
& (so.payment_terms_template != "NULL") & (so.payment_terms_template != "NULL")
& (so.company == conditions.company) & (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date]) & (so.transaction_date[conditions.start_date : conditions.end_date])
@ -291,6 +293,18 @@ def filter_on_calculated_status(filters, sales_orders):
return sales_orders return sales_orders
def filter_for_immediate_upcoming_term(filters, sales_orders):
if filters.only_immediate_upcoming_term and sales_orders:
immediate_term_found = set()
filtered_data = []
for order in sales_orders:
if order.name not in immediate_term_found and order.due_date > getdate():
filtered_data.append(order)
immediate_term_found.add(order.name)
return filtered_data
return sales_orders
def execute(filters=None): def execute(filters=None):
columns = get_columns() columns = get_columns()
sales_orders, so_invoices = get_so_with_invoices(filters) sales_orders, so_invoices = get_so_with_invoices(filters)
@ -298,6 +312,8 @@ def execute(filters=None):
sales_orders = filter_on_calculated_status(filters, sales_orders) sales_orders = filter_on_calculated_status(filters, sales_orders)
sales_orders = filter_for_immediate_upcoming_term(filters, sales_orders)
prepare_chart(sales_orders) prepare_chart(sales_orders)
data = sales_orders data = sales_orders

View File

@ -175,7 +175,9 @@ def prepare_data(data, so_elapsed_time, filters):
# update existing entry # update existing entry
so_row = sales_order_map[so_name] so_row = sales_order_map[so_name]
so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"])) so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"]))
so_row["delay"] = min(so_row["delay"], row["delay"]) so_row["delay"] = (
min(so_row["delay"], row["delay"]) if row["delay"] and so_row["delay"] else so_row["delay"]
)
# sum numeric columns # sum numeric columns
fields = [ fields = [

View File

@ -31,7 +31,7 @@
"icon": "fa fa-bookmark", "icon": "fa fa-bookmark",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2022-06-28 17:10:26.853753", "modified": "2023-02-10 01:53:41.319386",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Designation", "name": "Designation",
@ -58,5 +58,6 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [] "states": [],
"translated_doctype": 1
} }

View File

@ -1,13 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
// frappe.ui.form.on("Terms and Conditions", {
// refresh(frm) {}
//--------- ONLOAD ------------- // });
cur_frm.cscript.onload = function(doc, cdt, cdn) {
}
cur_frm.cscript.refresh = function(doc, cdt, cdn) {
}

View File

@ -33,7 +33,6 @@
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Disabled" "label": "Disabled"
}, },
{ {
@ -60,12 +59,14 @@
"default": "1", "default": "1",
"fieldname": "selling", "fieldname": "selling",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Selling" "label": "Selling"
}, },
{ {
"default": "1", "default": "1",
"fieldname": "buying", "fieldname": "buying",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Buying" "label": "Buying"
}, },
{ {
@ -76,10 +77,11 @@
"icon": "icon-legal", "icon": "icon-legal",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2022-06-16 15:07:38.094844", "modified": "2023-02-01 14:33:39.246532",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Terms and Conditions", "name": "Terms and Conditions",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -133,5 +135,6 @@
"quick_entry": 1, "quick_entry": 1,
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@ -0,0 +1,31 @@
Accountant
Administrative Assistant
Administrative Officer
Analyst
Associate
Business Analyst
Business Development Manager
Consultant
Chief Executive Officer
Chief Financial Officer
Chief Operating Officer
Chief Technology Officer
Customer Service Representative
Designer
Engineer
Executive Assistant
Finance Manager
HR Manager
Head of Marketing and Sales
Manager
Managing Director
Marketing Manager
Marketing Specialist
President
Product Manager
Project Manager
Researcher
Sales Representative
Secretary
Software Developer
Vice President

View File

@ -1,57 +0,0 @@
from frappe import _
def get_industry_types():
return [
_("Accounting"),
_("Advertising"),
_("Aerospace"),
_("Agriculture"),
_("Airline"),
_("Apparel & Accessories"),
_("Automotive"),
_("Banking"),
_("Biotechnology"),
_("Broadcasting"),
_("Brokerage"),
_("Chemical"),
_("Computer"),
_("Consulting"),
_("Consumer Products"),
_("Cosmetics"),
_("Defense"),
_("Department Stores"),
_("Education"),
_("Electronics"),
_("Energy"),
_("Entertainment & Leisure"),
_("Executive Search"),
_("Financial Services"),
_("Food, Beverage & Tobacco"),
_("Grocery"),
_("Health Care"),
_("Internet Publishing"),
_("Investment Banking"),
_("Legal"),
_("Manufacturing"),
_("Motion Picture & Video"),
_("Music"),
_("Newspaper Publishers"),
_("Online Auctions"),
_("Pension Funds"),
_("Pharmaceuticals"),
_("Private Equity"),
_("Publishing"),
_("Real Estate"),
_("Retail & Wholesale"),
_("Securities & Commodity Exchanges"),
_("Service"),
_("Soap & Detergent"),
_("Software"),
_("Sports"),
_("Technology"),
_("Telecommunications"),
_("Television"),
_("Transportation"),
_("Venture Capital"),
]

View File

@ -0,0 +1,51 @@
Accounting
Advertising
Aerospace
Agriculture
Airline
Apparel & Accessories
Automotive
Banking
Biotechnology
Broadcasting
Brokerage
Chemical
Computer
Consulting
Consumer Products
Cosmetics
Defense
Department Stores
Education
Electronics
Energy
Entertainment & Leisure
Executive Search
Financial Services
Food, Beverage & Tobacco
Grocery
Health Care
Internet Publishing
Investment Banking
Legal
Manufacturing
Motion Picture & Video
Music
Newspaper Publishers
Online Auctions
Pension Funds
Pharmaceuticals
Private Equity
Publishing
Real Estate
Retail & Wholesale
Securities & Commodity Exchanges
Service
Soap & Detergent
Software
Sports
Technology
Telecommunications
Television
Transportation
Venture Capital

View File

@ -0,0 +1,10 @@
Existing Customer
Reference
Advertisement
Cold Calling
Exhibition
Supplier Reference
Mass Mailing
Customer's Vendor
Campaign
Walk In

View File

@ -0,0 +1,7 @@
Channel Partner
Distributor
Dealer
Agent
Retailer
Implementation Partner
Reseller

View File

@ -0,0 +1,8 @@
Prospecting
Qualification
Needs Analysis
Value Proposition
Identifying Decision Makers
Perception Analysis
Proposal/Price Quote
Negotiation/Review

View File

@ -4,6 +4,7 @@
import json import json
import os import os
from pathlib import Path
import frappe import frappe
from frappe import _ from frappe import _
@ -16,28 +17,10 @@ from frappe.utils import cstr, getdate
from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.accounts.doctype.account.account import RootNotEditable
from erpnext.regional.address_template.setup import set_up_address_templates from erpnext.regional.address_template.setup import set_up_address_templates
default_lead_sources = [
"Existing Customer",
"Reference",
"Advertisement",
"Cold Calling",
"Exhibition",
"Supplier Reference",
"Mass Mailing",
"Customer's Vendor",
"Campaign",
"Walk In",
]
default_sales_partner_type = [ def read_lines(filename: str) -> list[str]:
"Channel Partner", """Return a list of lines from a file in the data directory."""
"Distributor", return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines()
"Dealer",
"Agent",
"Retailer",
"Implementation Partner",
"Reseller",
]
def install(country=None): def install(country=None):
@ -85,7 +68,11 @@ def install(country=None):
# Stock Entry Type # Stock Entry Type
{"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"},
{"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"},
{"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, {
"doctype": "Stock Entry Type",
"name": "Material Transfer",
"purpose": "Material Transfer",
},
{"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"},
{"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"},
{ {
@ -103,22 +90,6 @@ def install(country=None):
"name": "Material Consumption for Manufacture", "name": "Material Consumption for Manufacture",
"purpose": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture",
}, },
# Designation
{"doctype": "Designation", "designation_name": _("CEO")},
{"doctype": "Designation", "designation_name": _("Manager")},
{"doctype": "Designation", "designation_name": _("Analyst")},
{"doctype": "Designation", "designation_name": _("Engineer")},
{"doctype": "Designation", "designation_name": _("Accountant")},
{"doctype": "Designation", "designation_name": _("Secretary")},
{"doctype": "Designation", "designation_name": _("Associate")},
{"doctype": "Designation", "designation_name": _("Administrative Officer")},
{"doctype": "Designation", "designation_name": _("Business Development Manager")},
{"doctype": "Designation", "designation_name": _("HR Manager")},
{"doctype": "Designation", "designation_name": _("Project Manager")},
{"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")},
{"doctype": "Designation", "designation_name": _("Software Developer")},
{"doctype": "Designation", "designation_name": _("Designer")},
{"doctype": "Designation", "designation_name": _("Researcher")},
# territory: with two default territories, one for home country and one named Rest of the World # territory: with two default territories, one for home country and one named Rest of the World
{ {
"doctype": "Territory", "doctype": "Territory",
@ -291,28 +262,18 @@ def install(country=None):
{"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Lower Income")},
{"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")},
{"doctype": "Market Segment", "market_segment": _("Upper Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")},
# Sales Stages
{"doctype": "Sales Stage", "stage_name": _("Prospecting")},
{"doctype": "Sales Stage", "stage_name": _("Qualification")},
{"doctype": "Sales Stage", "stage_name": _("Needs Analysis")},
{"doctype": "Sales Stage", "stage_name": _("Value Proposition")},
{"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")},
{"doctype": "Sales Stage", "stage_name": _("Perception Analysis")},
{"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")},
{"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")},
# Warehouse Type # Warehouse Type
{"doctype": "Warehouse Type", "name": "Transit"}, {"doctype": "Warehouse Type", "name": "Transit"},
] ]
from erpnext.setup.setup_wizard.data.industry_type import get_industry_types for doctype, title_field, filename in (
("Designation", "designation_name", "designation.txt"),
records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] ("Sales Stage", "stage_name", "sales_stage.txt"),
# records += [{"doctype":"Operation", "operation": d} for d in get_operations()] ("Industry Type", "industry", "industry_type.txt"),
records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] ("Lead Source", "source_name", "lead_source.txt"),
("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"),
records += [ ):
{"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)]
]
base_path = frappe.get_app_path("erpnext", "stock", "doctype") base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file( response = frappe.read_file(
@ -335,16 +296,11 @@ def install(country=None):
make_default_records() make_default_records()
make_records(records) make_records(records)
set_up_address_templates(default_country=country) set_up_address_templates(default_country=country)
set_more_defaults()
update_global_search_doctypes()
def set_more_defaults():
# Do more setup stuff that can be done here with no dependencies
update_selling_defaults() update_selling_defaults()
update_buying_defaults() update_buying_defaults()
add_uom_data() add_uom_data()
update_item_variant_settings() update_item_variant_settings()
update_global_search_doctypes()
def update_selling_defaults(): def update_selling_defaults():
@ -381,7 +337,7 @@ def add_uom_data():
) )
for d in uoms: for d in uoms:
if not frappe.db.exists("UOM", _(d.get("uom_name"))): if not frappe.db.exists("UOM", _(d.get("uom_name"))):
uom_doc = frappe.get_doc( frappe.get_doc(
{ {
"doctype": "UOM", "doctype": "UOM",
"uom_name": _(d.get("uom_name")), "uom_name": _(d.get("uom_name")),
@ -402,9 +358,10 @@ def add_uom_data():
frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert()
if not frappe.db.exists( if not frappe.db.exists(
"UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} "UOM Conversion Factor",
{"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))},
): ):
uom_conversion = frappe.get_doc( frappe.get_doc(
{ {
"doctype": "UOM Conversion Factor", "doctype": "UOM Conversion Factor",
"category": _(d.get("category")), "category": _(d.get("category")),
@ -412,7 +369,7 @@ def add_uom_data():
"to_uom": _(d.get("to_uom")), "to_uom": _(d.get("to_uom")),
"value": d.get("value"), "value": d.get("value"),
} }
).insert(ignore_permissions=True) ).db_insert()
def add_market_segments(): def add_market_segments():
@ -468,7 +425,7 @@ def install_company(args):
make_records(records) make_records(records)
def install_defaults(args=None): def install_defaults(args=None): # nosemgrep
records = [ records = [
# Price Lists # Price Lists
{ {
@ -493,7 +450,7 @@ def install_defaults(args=None):
# enable default currency # enable default currency
frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) frappe.db.set_value("Currency", args.get("currency"), "enabled", 1)
frappe.db.set_value("Stock Settings", None, "email_footer_address", args.get("company_name")) frappe.db.set_single_value("Stock Settings", "email_footer_address", args.get("company_name"))
set_global_defaults(args) set_global_defaults(args)
update_stock_settings() update_stock_settings()
@ -540,7 +497,8 @@ def create_bank_account(args):
company_name = args.get("company_name") company_name = args.get("company_name")
bank_account_group = frappe.db.get_value( bank_account_group = frappe.db.get_value(
"Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} "Account",
{"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name},
) )
if bank_account_group: if bank_account_group:
bank_account = frappe.get_doc( bank_account = frappe.get_doc(

View File

@ -158,6 +158,7 @@ def make_taxes_and_charges_template(company_name, doctype, template):
# Ingone validations to make doctypes faster # Ingone validations to make doctypes faster
doc.flags.ignore_links = True doc.flags.ignore_links = True
doc.flags.ignore_validate = True doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)
return doc return doc

View File

@ -25,6 +25,12 @@ def boot_session(bootinfo):
frappe.db.get_single_value("CRM Settings", "default_valid_till") frappe.db.get_single_value("CRM Settings", "default_valid_till")
) )
bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint(
frappe.db.get_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
)
)
# if no company, show a dialog box to create a new company # if no company, show a dialog box to create a new company
bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0]

View File

@ -38,5 +38,19 @@
"price_list_rate": 1000, "price_list_rate": 1000,
"valid_from": "2017-04-10", "valid_from": "2017-04-10",
"valid_upto": "2017-04-17" "valid_upto": "2017-04-17"
},
{
"doctype": "Item Price",
"item_code": "_Test Item",
"price_list": "_Test Buying Price List",
"price_list_rate": 100,
"supplier": "_Test Supplier"
},
{
"doctype": "Item Price",
"item_code": "_Test Item",
"price_list": "_Test Selling Price List",
"price_list_rate": 200,
"customer": "_Test Customer"
} }
] ]

View File

@ -31,5 +31,21 @@
"enabled": 1, "enabled": 1,
"price_list_name": "_Test Price List Rest of the World", "price_list_name": "_Test Price List Rest of the World",
"selling": 1 "selling": 1
},
{
"buying": 0,
"currency": "USD",
"doctype": "Price List",
"enabled": 1,
"price_list_name": "_Test Selling Price List",
"selling": 1
},
{
"buying": 1,
"currency": "USD",
"doctype": "Price List",
"enabled": 1,
"price_list_name": "_Test Buying Price List",
"selling": 0
} }
] ]

View File

@ -2494,7 +2494,7 @@ def get_uom_details(item_code, uom, qty):
if not conversion_factor: if not conversion_factor:
frappe.msgprint( frappe.msgprint(
_("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) _("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)
) )
ret = {"uom": ""} ret = {"uom": ""}
else: else:

View File

@ -88,8 +88,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
update_party_blanket_order(args, out) update_party_blanket_order(args, out)
# Never try to find a customer price if customer is set in these Doctype
current_customer = args.customer
if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
args.customer = None
out.update(get_price_list_rate(args, item)) out.update(get_price_list_rate(args, item))
args.customer = current_customer
if args.customer and cint(args.is_pos): if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args, update_data=True)) out.update(get_pos_profile_item_details(args.company, args, update_data=True))

View File

@ -103,7 +103,7 @@ def get_reserved_qty(item_code, warehouse):
q.inner_join(SalesOrder) q.inner_join(SalesOrder)
.on(SalesOrder.name == child_table.parent) .on(SalesOrder.name == child_table.parent)
.where(SalesOrder.docstatus == 1) .where(SalesOrder.docstatus == 1)
.where(SalesOrder.status != "Closed") .where(SalesOrder.status.notin(["On Hold", "Closed"]))
) )
SalesOrder = DocType("Sales Order") SalesOrder = DocType("Sales Order")

View File

@ -0,0 +1,40 @@
import json
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from erpnext.stock.get_item_details import get_item_details
test_ignore = ["BOM"]
test_dependencies = ["Customer", "Supplier", "Item", "Price List", "Item Price"]
class TestGetItemDetail(FrappeTestCase):
def setUp(self):
make_test_records("Price List")
super().setUp()
def test_get_item_detail_purchase_order(self):
args = frappe._dict(
{
"item_code": "_Test Item",
"company": "_Test Company",
"customer": "_Test Customer",
"conversion_rate": 1.0,
"price_list_currency": "USD",
"plc_conversion_rate": 1.0,
"doctype": "Purchase Order",
"name": None,
"supplier": "_Test Supplier",
"transaction_date": None,
"conversion_rate": 1.0,
"price_list": "_Test Buying Price List",
"is_subcontracted": 0,
"ignore_pricing_rule": 1,
"qty": 1,
}
)
details = get_item_details(args)
self.assertEqual(details.get("price_list_rate"), 100)

View File

@ -51,21 +51,31 @@ def get_tabs(categories):
return tab_values return tab_values
def get_category_records(categories): def get_category_records(categories: list):
categorical_data = {} categorical_data = {}
for category in categories:
if category == "item_group": for c in categories:
if c == "item_group":
categorical_data["item_group"] = frappe.db.get_all( categorical_data["item_group"] = frappe.db.get_all(
"Item Group", "Item Group",
filters={"parent_item_group": "All Item Groups", "show_in_website": 1}, filters={"parent_item_group": "All Item Groups", "show_in_website": 1},
fields=["name", "parent_item_group", "is_group", "image", "route"], fields=["name", "parent_item_group", "is_group", "image", "route"],
) )
else:
doctype = frappe.unscrub(category) continue
fields = ["name"]
if frappe.get_meta(doctype, cached=True).get_field("image"): doctype = frappe.unscrub(c)
fields = ["name"]
try:
meta = frappe.get_meta(doctype, cached=True)
if meta.get_field("image"):
fields += ["image"] fields += ["image"]
categorical_data[category] = frappe.db.get_all(doctype, fields=fields) data = frappe.db.get_all(doctype, fields=fields)
categorical_data[c] = data
except BaseException:
frappe.throw(_("DocType {} not found").format(doctype))
continue
return categorical_data return categorical_data