Merge branch 'develop' into fixed-process-loss-in-job-card
This commit is contained in:
commit
93fe923e2a
@ -50,13 +50,15 @@ class AccountingDimension(Document):
|
||||
if frappe.flags.in_test:
|
||||
make_dimension_in_accounting_doctypes(doc=self)
|
||||
else:
|
||||
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
|
||||
frappe.enqueue(
|
||||
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
if frappe.flags.in_test:
|
||||
delete_accounting_dimension(doc=self)
|
||||
else:
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
|
||||
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
|
||||
|
||||
def set_fieldname_and_label(self):
|
||||
if not self.label:
|
||||
|
@ -41,7 +41,7 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.trigger("get_payment_entries")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
|
||||
frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary');
|
||||
},
|
||||
|
||||
update_clearance_date: function(frm) {
|
||||
@ -53,8 +53,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.refresh_fields();
|
||||
|
||||
if (!frm.doc.payment_entries.length) {
|
||||
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
|
||||
frm.change_custom_button_type('Update Clearance Date', null, 'default');
|
||||
frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary');
|
||||
frm.change_custom_button_type(__('Update Clearance Date'), null, 'default');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -72,8 +72,8 @@ frappe.ui.form.on("Bank Clearance", {
|
||||
frm.trigger("update_clearance_date")
|
||||
);
|
||||
|
||||
frm.change_custom_button_type('Get Payment Entries', null, 'default');
|
||||
frm.change_custom_button_type('Update Clearance Date', null, 'primary');
|
||||
frm.change_custom_button_type(__('Get Payment Entries'), null, 'default');
|
||||
frm.change_custom_button_type(__('Update Clearance Date'), null, 'primary');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -81,7 +81,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
|
||||
frm.add_custom_button(__('Get Unreconciled Entries'), function() {
|
||||
frm.trigger("make_reconciliation_tool");
|
||||
});
|
||||
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
|
||||
frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'primary');
|
||||
|
||||
},
|
||||
|
||||
|
@ -245,6 +245,7 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -315,10 +316,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-08-03 18:55:43.683053",
|
||||
"modified": "2023-06-03 16:24:01.677026",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -365,6 +367,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
@ -952,6 +952,7 @@ class JournalEntry(AccountsController):
|
||||
blank_row.debit_in_account_currency = abs(diff)
|
||||
blank_row.debit = abs(diff)
|
||||
|
||||
self.set_total_debit_credit()
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -65,22 +65,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
|
||||
this.frm.trigger("get_unreconciled_entries")
|
||||
);
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
|
||||
this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'primary');
|
||||
}
|
||||
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
|
||||
this.frm.add_custom_button(__('Allocate'), () =>
|
||||
this.frm.trigger("allocate")
|
||||
);
|
||||
this.frm.change_custom_button_type('Allocate', null, 'primary');
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
this.frm.change_custom_button_type(__('Allocate'), null, 'primary');
|
||||
this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'default');
|
||||
}
|
||||
if (this.frm.doc.allocation.length) {
|
||||
this.frm.add_custom_button(__('Reconcile'), () =>
|
||||
this.frm.trigger("reconcile")
|
||||
);
|
||||
this.frm.change_custom_button_type('Reconcile', null, 'primary');
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
this.frm.change_custom_button_type('Allocate', null, 'default');
|
||||
this.frm.change_custom_button_type(__('Reconcile'), null, 'primary');
|
||||
this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'default');
|
||||
this.frm.change_custom_button_type(__('Allocate'), null, 'default');
|
||||
}
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
|
@ -6,7 +6,6 @@ import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
@ -127,12 +126,29 @@ class PaymentReconciliation(Document):
|
||||
|
||||
return list(journal_entries)
|
||||
|
||||
def get_return_invoices(self):
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
self.return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
ConstantColumn(voucher_type).as_("voucher_type"),
|
||||
doc.name.as_("voucher_no"),
|
||||
doc.return_against,
|
||||
)
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
|
||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable":
|
||||
self.common_filter_conditions.append(ple.account_type == "Receivable")
|
||||
@ -140,19 +156,10 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions.append(ple.account_type == "Payable")
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
# get return invoices
|
||||
doc = qb.DocType(voucher_type)
|
||||
return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
& (IfNull(doc.return_against, "") == "")
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
self.get_return_invoices()
|
||||
return_invoices = [
|
||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||
]
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
@ -204,6 +211,9 @@ class PaymentReconciliation(Document):
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
)
|
||||
|
||||
cr_dr_notes = [x.voucher_no for x in self.return_invoices]
|
||||
non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes]
|
||||
|
||||
if self.invoice_limit:
|
||||
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
|
||||
|
||||
|
@ -44,6 +44,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
voucher_type="Period Closing Voucher",
|
||||
voucher_no=self.name,
|
||||
queue="long",
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
|
||||
|
@ -442,6 +442,7 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -1554,11 +1555,10 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-30 03:49:50.455199",
|
||||
"modified": "2023-06-03 16:23:41.083409",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
|
@ -158,7 +158,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
|
||||
return frappe.get_list(
|
||||
"Customer",
|
||||
fields=["name", "customer_name", "email_id"],
|
||||
filters=[[fields_dict[customer_collection], "IN", selected]],
|
||||
filters=[["disabled", "=", 0], [fields_dict[customer_collection], "IN", selected]],
|
||||
)
|
||||
|
||||
|
||||
|
@ -443,12 +443,14 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -1364,12 +1366,12 @@
|
||||
"depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
|
||||
"fieldname": "set_from_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Set From Warehouse",
|
||||
"no_copy": 1,
|
||||
"options": "Warehouse",
|
||||
"print_hide": 1,
|
||||
"print_width": "50px",
|
||||
"ignore_user_permissions": 1,
|
||||
"width": "50px"
|
||||
},
|
||||
{
|
||||
@ -1573,7 +1575,7 @@
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-29 12:57:50.832598",
|
||||
"modified": "2023-06-03 16:21:54.637245",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
@ -520,6 +520,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -2154,7 +2155,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-04-28 14:15:59.901154",
|
||||
"modified": "2023-06-03 16:22:16.219333",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -3,8 +3,10 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import cint, getdate
|
||||
|
||||
|
||||
@ -346,26 +348,33 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
|
||||
def get_advance_vouchers(
|
||||
parties, company=None, from_date=None, to_date=None, party_type="Supplier"
|
||||
):
|
||||
# for advance vouchers, debit and credit is reversed
|
||||
dr_or_cr = "debit" if party_type == "Supplier" else "credit"
|
||||
"""
|
||||
Use Payment Ledger to fetch unallocated Advance Payments
|
||||
"""
|
||||
|
||||
filters = {
|
||||
dr_or_cr: [">", 0],
|
||||
"is_opening": "No",
|
||||
"is_cancelled": 0,
|
||||
"party_type": party_type,
|
||||
"party": ["in", parties],
|
||||
}
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
if party_type == "Customer":
|
||||
filters.update({"against_voucher": ["is", "not set"]})
|
||||
conditions = []
|
||||
|
||||
conditions.append(ple.amount.lt(0))
|
||||
conditions.append(ple.delinked == 0)
|
||||
conditions.append(ple.party_type == party_type)
|
||||
conditions.append(ple.party.isin(parties))
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
|
||||
if company:
|
||||
filters["company"] = company
|
||||
if from_date and to_date:
|
||||
filters["posting_date"] = ["between", (from_date, to_date)]
|
||||
conditions.append(ple.company == company)
|
||||
|
||||
return frappe.get_all("GL Entry", filters=filters, distinct=1, pluck="voucher_no") or [""]
|
||||
if from_date and to_date:
|
||||
conditions.append(ple.posting_date[from_date:to_date])
|
||||
|
||||
advances = (
|
||||
qb.from_(ple).select(ple.voucher_no).distinct().where(Criterion.all(conditions)).run(as_list=1)
|
||||
)
|
||||
if advances:
|
||||
advances = [x[0] for x in advances]
|
||||
|
||||
return advances
|
||||
|
||||
|
||||
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
|
||||
@ -499,6 +508,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
|
||||
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
tcs_amount = 0
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
# sum of debit entries made from sales invoices
|
||||
invoiced_amt = (
|
||||
@ -516,18 +526,20 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
)
|
||||
|
||||
# sum of credit entries made from PE / JV with unset 'against voucher'
|
||||
|
||||
conditions = []
|
||||
conditions.append(ple.amount.lt(0))
|
||||
conditions.append(ple.delinked == 0)
|
||||
conditions.append(ple.party.isin(parties))
|
||||
conditions.append(ple.voucher_no == ple.against_voucher_no)
|
||||
conditions.append(ple.company == inv.company)
|
||||
|
||||
advances = (
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1)
|
||||
)
|
||||
|
||||
advance_amt = (
|
||||
frappe.db.get_value(
|
||||
"GL Entry",
|
||||
{
|
||||
"is_cancelled": 0,
|
||||
"party": ["in", parties],
|
||||
"company": inv.company,
|
||||
"voucher_no": ["in", adv_vouchers],
|
||||
},
|
||||
"sum(credit)",
|
||||
)
|
||||
or 0.0
|
||||
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
|
||||
)
|
||||
|
||||
# sum of credit entries made from sales invoice
|
||||
|
@ -152,6 +152,60 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in reversed(invoices):
|
||||
d.cancel()
|
||||
|
||||
def test_tcs_on_unallocated_advance_payments(self):
|
||||
frappe.db.set_value(
|
||||
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
|
||||
)
|
||||
|
||||
vouchers = []
|
||||
|
||||
# create advance payment
|
||||
pe = create_payment_entry(
|
||||
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
|
||||
)
|
||||
pe.paid_from = "Debtors - _TC"
|
||||
pe.paid_to = "Cash - _TC"
|
||||
pe.submit()
|
||||
vouchers.append(pe)
|
||||
|
||||
# create invoice
|
||||
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
|
||||
si1.submit()
|
||||
vouchers.append(si1)
|
||||
|
||||
# reconcile
|
||||
pr = frappe.get_doc("Payment Reconciliation")
|
||||
pr.company = "_Test Company"
|
||||
pr.party_type = "Customer"
|
||||
pr.party = "Test TCS Customer"
|
||||
pr.receivable_payable_account = "Debtors - _TC"
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# make another invoice
|
||||
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
|
||||
# TDS should be calculated
|
||||
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
|
||||
si2.submit()
|
||||
vouchers.append(si2)
|
||||
|
||||
si3 = create_sales_invoice(customer="Test TCS Customer", rate=10000)
|
||||
si3.submit()
|
||||
vouchers.append(si3)
|
||||
|
||||
# assert tax collection on total invoice amount created until now
|
||||
tcs_charged = sum([d.base_tax_amount for d in si2.taxes if d.account_head == "TCS - _TC"])
|
||||
tcs_charged += sum([d.base_tax_amount for d in si3.taxes if d.account_head == "TCS - _TC"])
|
||||
self.assertEqual(tcs_charged, 1500)
|
||||
|
||||
# cancel invoice and payments to avoid clashing
|
||||
for d in reversed(vouchers):
|
||||
d.reload()
|
||||
d.cancel()
|
||||
|
||||
def test_tds_calculation_on_net_total(self):
|
||||
frappe.db.set_value(
|
||||
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"
|
||||
|
@ -2,6 +2,8 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from typing import Optional
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.contacts.doctype.address.address import (
|
||||
@ -647,12 +649,12 @@ def set_taxes(
|
||||
else:
|
||||
args.update(get_party_details(party, party_type))
|
||||
|
||||
if party_type in ("Customer", "Lead"):
|
||||
if party_type in ("Customer", "Lead", "Prospect"):
|
||||
args.update({"tax_type": "Sales"})
|
||||
|
||||
if party_type == "Lead":
|
||||
if party_type in ["Lead", "Prospect"]:
|
||||
args["customer"] = None
|
||||
del args["lead"]
|
||||
del args[frappe.scrub(party_type)]
|
||||
else:
|
||||
args.update({"tax_type": "Purchase"})
|
||||
|
||||
@ -850,7 +852,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
|
||||
return company_wise_info
|
||||
|
||||
|
||||
def get_party_shipping_address(doctype, name):
|
||||
def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true.
|
||||
and/or `is_shipping_address = 1`.
|
||||
@ -861,22 +863,23 @@ def get_party_shipping_address(doctype, name):
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"SELECT dl.parent "
|
||||
"from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name "
|
||||
"where "
|
||||
"dl.link_doctype=%s "
|
||||
"and dl.link_name=%s "
|
||||
"and dl.parenttype='Address' "
|
||||
"and ifnull(ta.disabled, 0) = 0 and"
|
||||
"(ta.address_type='Shipping' or ta.is_shipping_address=1) "
|
||||
"order by ta.is_shipping_address desc, ta.address_type desc limit 1",
|
||||
(doctype, name),
|
||||
shipping_addresses = frappe.get_all(
|
||||
"Address",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
["disabled", "=", 0],
|
||||
],
|
||||
or_filters=[
|
||||
["is_shipping_address", "=", 1],
|
||||
["address_type", "=", "Shipping"],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_shipping_address DESC",
|
||||
)
|
||||
if out:
|
||||
return out[0][0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
return shipping_addresses[0] if shipping_addresses else None
|
||||
|
||||
|
||||
def get_partywise_advanced_payment_amount(
|
||||
@ -910,31 +913,32 @@ def get_partywise_advanced_payment_amount(
|
||||
return frappe._dict(data)
|
||||
|
||||
|
||||
def get_default_contact(doctype, name):
|
||||
def get_default_contact(doctype: str, name: str) -> Optional[str]:
|
||||
"""
|
||||
Returns default contact for the given doctype and name.
|
||||
Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
|
||||
Returns contact name only if there is a primary contact for given doctype and name.
|
||||
|
||||
Else returns None
|
||||
|
||||
:param doctype: Party Doctype
|
||||
:param name: Party name
|
||||
:return: String
|
||||
"""
|
||||
out = frappe.db.sql(
|
||||
"""
|
||||
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact
|
||||
FROM `tabDynamic Link` dl
|
||||
INNER JOIN `tabContact` c ON c.name = dl.parent
|
||||
WHERE
|
||||
dl.link_doctype=%s AND
|
||||
dl.link_name=%s AND
|
||||
dl.parenttype = 'Contact'
|
||||
ORDER BY is_primary_contact DESC, is_billing_contact DESC
|
||||
""",
|
||||
(doctype, name),
|
||||
contacts = frappe.get_all(
|
||||
"Contact",
|
||||
filters=[
|
||||
["Dynamic Link", "link_doctype", "=", doctype],
|
||||
["Dynamic Link", "link_name", "=", name],
|
||||
],
|
||||
or_filters=[
|
||||
["is_primary_contact", "=", 1],
|
||||
["is_billing_contact", "=", 1],
|
||||
],
|
||||
pluck="name",
|
||||
limit=1,
|
||||
order_by="is_primary_contact DESC, is_billing_contact DESC",
|
||||
)
|
||||
if out:
|
||||
try:
|
||||
return out[0][0]
|
||||
except Exception:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
|
||||
return contacts[0] if contacts else None
|
||||
|
||||
|
||||
def add_party_account(party_type, party, company, account):
|
||||
|
@ -181,6 +181,16 @@ class ReceivablePayableReport(object):
|
||||
return
|
||||
|
||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||
|
||||
# If payment is made against credit note
|
||||
# and credit note is made against a Sales Invoice
|
||||
# then consider the payment against original sales invoice.
|
||||
if ple.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||
if ple.against_voucher_no in self.return_entries:
|
||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||
if return_against:
|
||||
key = (ple.against_voucher_type, return_against, ple.party)
|
||||
|
||||
row = self.voucher_balance.get(key)
|
||||
|
||||
if not row:
|
||||
@ -610,7 +620,7 @@ class ReceivablePayableReport(object):
|
||||
|
||||
def get_return_entries(self):
|
||||
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
filters = {"is_return": 1, "docstatus": 1}
|
||||
filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
|
||||
party_field = scrub(self.filters.party_type)
|
||||
if self.filters.get(party_field):
|
||||
filters.update({party_field: self.filters.get(party_field)})
|
||||
|
@ -210,6 +210,67 @@ class TestAccountsReceivable(FrappeTestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_payment_against_credit_note(self):
|
||||
"""
|
||||
Payment against credit/debit note should be considered against the parent invoice
|
||||
"""
|
||||
company = "_Test Company 2"
|
||||
customer = "_Test Customer 2"
|
||||
|
||||
si1 = make_sales_invoice()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
|
||||
pe.paid_from = "Debtors - _TC2"
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
cr_note = make_credit_note(si1.name)
|
||||
|
||||
si2 = make_sales_invoice()
|
||||
|
||||
# manually link cr_note with si2 using journal entry
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.company = company
|
||||
je.voucher_type = "Credit Note"
|
||||
je.posting_date = today()
|
||||
|
||||
debit_account = "Debtors - _TC2"
|
||||
debit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"debit": 100,
|
||||
"debit_in_account_currency": 100,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
credit_entry = {
|
||||
"account": debit_account,
|
||||
"party_type": "Customer",
|
||||
"party": customer,
|
||||
"credit": 100,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si2.doctype,
|
||||
"reference_name": si2.name,
|
||||
"cost_center": "Main - _TC2",
|
||||
}
|
||||
|
||||
je.append("accounts", debit_entry)
|
||||
je.append("accounts", credit_entry)
|
||||
je = je.save().submit()
|
||||
|
||||
filters = {
|
||||
"company": company,
|
||||
"report_date": today(),
|
||||
"range1": 30,
|
||||
"range2": 60,
|
||||
"range3": 90,
|
||||
"range4": 120,
|
||||
}
|
||||
report = execute(filters)
|
||||
self.assertEqual(report[1], [])
|
||||
|
||||
|
||||
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
|
||||
frappe.set_user("Administrator")
|
||||
@ -256,7 +317,7 @@ def make_payment(docname):
|
||||
|
||||
|
||||
def make_credit_note(docname):
|
||||
create_sales_invoice(
|
||||
credit_note = create_sales_invoice(
|
||||
company="_Test Company 2",
|
||||
customer="_Test Customer 2",
|
||||
currency="EUR",
|
||||
@ -269,3 +330,5 @@ def make_credit_note(docname):
|
||||
is_return=1,
|
||||
return_against=docname,
|
||||
)
|
||||
|
||||
return credit_note
|
||||
|
@ -399,8 +399,9 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
|
||||
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
|
||||
`tabSales Invoice`.unrealized_profit_loss_account,
|
||||
`tabSales Invoice`.is_internal_customer,
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.remarks,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
|
||||
`tabSales Invoice Item`.project,
|
||||
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
|
||||
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
|
||||
|
@ -812,14 +812,14 @@ class TestDepreciationMethods(AssetSetup):
|
||||
number_of_depreciations_booked=1,
|
||||
opening_accumulated_depreciation=50000,
|
||||
expected_value_after_useful_life=10000,
|
||||
depreciation_start_date="2030-12-31",
|
||||
depreciation_start_date="2031-12-31",
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=12,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.status, "Draft")
|
||||
|
||||
expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]]
|
||||
expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
|
||||
|
||||
schedules = [
|
||||
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
|
||||
|
@ -10,6 +10,7 @@ from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
getdate,
|
||||
is_last_day_of_the_month,
|
||||
@ -271,8 +272,14 @@ class AssetDepreciationSchedule(Document):
|
||||
break
|
||||
|
||||
# For first row
|
||||
if n == 0 and has_pro_rata and not self.opening_accumulated_depreciation:
|
||||
from_date = add_days(asset_doc.available_for_use_date, -1)
|
||||
if (
|
||||
n == 0
|
||||
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
|
||||
and not self.opening_accumulated_depreciation
|
||||
):
|
||||
from_date = add_days(
|
||||
asset_doc.available_for_use_date, -1
|
||||
) # needed to calc depr amount for available_for_use_date too
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
@ -281,10 +288,18 @@ class AssetDepreciationSchedule(Document):
|
||||
has_wdv_or_dd_non_yearly_pro_rata,
|
||||
)
|
||||
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
|
||||
from_date = add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
)
|
||||
if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
|
||||
from_date = get_last_day(
|
||||
add_months(
|
||||
getdate(asset_doc.available_for_use_date),
|
||||
((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation),
|
||||
)
|
||||
)
|
||||
else:
|
||||
from_date = add_months(
|
||||
getdate(add_days(asset_doc.available_for_use_date, -1)),
|
||||
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
|
||||
)
|
||||
depreciation_amount, days, months = _get_pro_rata_amt(
|
||||
row,
|
||||
depreciation_amount,
|
||||
@ -702,3 +717,9 @@ def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
|
||||
["status", "=", status],
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def is_first_day_of_the_month(date):
|
||||
first_day_of_the_month = get_first_day(date)
|
||||
|
||||
return getdate(first_day_of_the_month) == getdate(date)
|
||||
|
@ -322,6 +322,7 @@
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Customer Mobile No",
|
||||
"options": "Phone",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@ -368,6 +369,7 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -1271,7 +1273,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-24 11:16:41.195340",
|
||||
"modified": "2023-06-03 16:19:45.710444",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
|
@ -230,6 +230,7 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -844,7 +845,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-14 16:43:41.714832",
|
||||
"modified": "2023-06-03 16:20:15.880114",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Quotation",
|
||||
|
@ -43,7 +43,6 @@ class SellingController(StockController):
|
||||
self.set_serial_and_batch_bundle(table_field)
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
|
||||
super(SellingController, self).set_missing_values(for_validate)
|
||||
|
||||
# set contact and address details for customer, if they are not mentioned
|
||||
@ -62,7 +61,7 @@ class SellingController(StockController):
|
||||
elif self.doctype == "Quotation" and self.party_name:
|
||||
if self.quotation_to == "Customer":
|
||||
customer = self.party_name
|
||||
else:
|
||||
elif self.quotation_to == "Lead":
|
||||
lead = self.party_name
|
||||
|
||||
if customer:
|
||||
|
@ -3,7 +3,10 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.contacts.address_and_contact import (
|
||||
delete_contact_and_address,
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
@ -40,9 +43,8 @@ class Lead(SellingController, CRMNote):
|
||||
self.update_prospect()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
frappe.db.set_value("Issue", {"lead": self.name}, "lead", None)
|
||||
delete_contact_and_address(self.doctype, self.name)
|
||||
self.remove_link_from_prospect()
|
||||
|
||||
def set_full_name(self):
|
||||
@ -119,27 +121,6 @@ class Lead(SellingController, CRMNote):
|
||||
)
|
||||
lead_row.db_update()
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": self.doctype, "link_name": self.name},
|
||||
fields=["parent", "parenttype"],
|
||||
)
|
||||
|
||||
for link in links:
|
||||
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
|
||||
|
||||
if len(linked_doc.get("links")) == 1:
|
||||
linked_doc.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in linked_doc.get("links"):
|
||||
if d.link_doctype == self.doctype and d.link_name == self.name:
|
||||
to_remove = d
|
||||
if to_remove:
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def remove_link_from_prospect(self):
|
||||
prospects = self.get_linked_prospects()
|
||||
|
||||
|
@ -2,7 +2,10 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.contacts.address_and_contact import (
|
||||
delete_contact_and_address,
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
@ -16,7 +19,7 @@ class Prospect(CRMNote):
|
||||
self.link_with_lead_contact_and_address()
|
||||
|
||||
def on_trash(self):
|
||||
self.unlink_dynamic_links()
|
||||
delete_contact_and_address(self.doctype, self.name)
|
||||
|
||||
def after_insert(self):
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
@ -54,27 +57,6 @@ class Prospect(CRMNote):
|
||||
linked_doc.append("links", {"link_doctype": self.doctype, "link_name": self.name})
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": self.doctype, "link_name": self.name},
|
||||
fields=["parent", "parenttype"],
|
||||
)
|
||||
|
||||
for link in links:
|
||||
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
|
||||
|
||||
if len(linked_doc.get("links")) == 1:
|
||||
linked_doc.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in linked_doc.get("links"):
|
||||
if d.link_doctype == self.doctype and d.link_name == self.name:
|
||||
to_remove = d
|
||||
if to_remove:
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name, target_doc=None):
|
||||
|
@ -78,9 +78,10 @@ erpnext.ProductList = class {
|
||||
let title_html = `<div style="display: flex; margin-left: -15px;">`;
|
||||
title_html += `
|
||||
<div class="col-8" style="margin-right: -15px;">
|
||||
<a class="" href="/${ item.route || '#' }"
|
||||
style="color: var(--gray-800); font-weight: 500;">
|
||||
<a href="/${ item.route || '#' }">
|
||||
<div class="product-title">
|
||||
${ title }
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
`;
|
||||
@ -201,4 +202,4 @@ erpnext.ProductList = class {
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
@ -160,4 +160,3 @@ class TestLoanDisbursement(unittest.TestCase):
|
||||
interest = per_day_interest * 15
|
||||
|
||||
self.assertEqual(amounts["pending_principal_amount"], 1500000)
|
||||
self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2))
|
||||
|
@ -22,7 +22,7 @@ class LoanInterestAccrual(AccountsController):
|
||||
frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
|
||||
|
||||
if not self.last_accrual_date:
|
||||
self.last_accrual_date = get_last_accrual_date(self.loan)
|
||||
self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date)
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
@ -274,14 +274,14 @@ def make_loan_interest_accrual_entry(args):
|
||||
|
||||
|
||||
def get_no_of_days_for_interest_accural(loan, posting_date):
|
||||
last_interest_accrual_date = get_last_accrual_date(loan.name)
|
||||
last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date)
|
||||
|
||||
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
|
||||
|
||||
return no_of_days
|
||||
|
||||
|
||||
def get_last_accrual_date(loan):
|
||||
def get_last_accrual_date(loan, posting_date):
|
||||
last_posting_date = frappe.db.sql(
|
||||
""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
|
||||
WHERE loan = %s and docstatus = 1""",
|
||||
@ -289,12 +289,30 @@ def get_last_accrual_date(loan):
|
||||
)
|
||||
|
||||
if last_posting_date[0][0]:
|
||||
last_interest_accrual_date = last_posting_date[0][0]
|
||||
# interest for last interest accrual date is already booked, so add 1 day
|
||||
return add_days(last_posting_date[0][0], 1)
|
||||
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
|
||||
|
||||
if last_disbursement_date and getdate(last_disbursement_date) > getdate(
|
||||
last_interest_accrual_date
|
||||
):
|
||||
last_interest_accrual_date = last_disbursement_date
|
||||
|
||||
return add_days(last_interest_accrual_date, 1)
|
||||
else:
|
||||
return frappe.db.get_value("Loan", loan, "disbursement_date")
|
||||
|
||||
|
||||
def get_last_disbursement_date(loan, posting_date):
|
||||
last_disbursement_date = frappe.db.get_value(
|
||||
"Loan Disbursement",
|
||||
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
|
||||
"MAX(posting_date)",
|
||||
)
|
||||
|
||||
return last_disbursement_date
|
||||
|
||||
|
||||
def days_in_year(year):
|
||||
days = 365
|
||||
|
||||
|
@ -101,7 +101,7 @@ class LoanRepayment(AccountsController):
|
||||
if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
|
||||
if not self.is_term_loan:
|
||||
# get last loan interest accrual date
|
||||
last_accrual_date = get_last_accrual_date(self.against_loan)
|
||||
last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date)
|
||||
|
||||
# get posting date upto which interest has to be accrued
|
||||
per_day_interest = get_per_day_interest(
|
||||
@ -725,7 +725,7 @@ def get_amounts(amounts, against_loan, posting_date):
|
||||
if due_date:
|
||||
pending_days = date_diff(posting_date, due_date) + 1
|
||||
else:
|
||||
last_accrual_date = get_last_accrual_date(against_loan_doc.name)
|
||||
last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date)
|
||||
pending_days = date_diff(posting_date, last_accrual_date) + 1
|
||||
|
||||
if pending_days > 0:
|
||||
|
@ -152,6 +152,7 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -160,6 +161,7 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -236,10 +238,11 @@
|
||||
"link_fieldname": "maintenance_schedule"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-27 16:05:10.746465",
|
||||
"modified": "2023-06-03 16:15:43.958072",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Maintenance",
|
||||
"name": "Maintenance Schedule",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -260,5 +263,6 @@
|
||||
"search_fields": "status,customer,customer_name",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer"
|
||||
}
|
@ -101,6 +101,7 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -108,6 +109,7 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -293,7 +295,7 @@
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-17 03:10:27.608112",
|
||||
"modified": "2023-06-03 16:19:07.902723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Maintenance",
|
||||
"name": "Maintenance Visit",
|
||||
@ -319,6 +321,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "customer_name"
|
||||
}
|
@ -88,12 +88,14 @@ class BOMUpdateLog(Document):
|
||||
boms=boms,
|
||||
timeout=40000,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
else:
|
||||
frappe.enqueue(
|
||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
|
||||
update_doc=self,
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
|
||||
|
@ -304,6 +304,7 @@ def set_tasks_as_overdue():
|
||||
@frappe.whitelist()
|
||||
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
|
||||
def set_missing_values(source, target):
|
||||
target.parent_project = source.project
|
||||
target.append(
|
||||
"time_logs",
|
||||
{
|
||||
|
@ -40,8 +40,8 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
|
||||
name: __("Date"),
|
||||
editable: false,
|
||||
width: 100,
|
||||
format: frappe.form.formatters.Date,
|
||||
},
|
||||
|
||||
{
|
||||
name: __("Party Type"),
|
||||
editable: false,
|
||||
@ -117,17 +117,13 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
|
||||
return [
|
||||
row["date"],
|
||||
row["party_type"],
|
||||
row["party"],
|
||||
frappe.form.formatters.Link(row["party"], {options: row["party_type"]}),
|
||||
row["description"],
|
||||
row["deposit"],
|
||||
row["withdrawal"],
|
||||
row["unallocated_amount"],
|
||||
row["reference_number"],
|
||||
`
|
||||
<Button class="btn btn-primary btn-xs center" data-name = ${row["name"]} >
|
||||
${__("Actions")}
|
||||
</a>
|
||||
`,
|
||||
`<button class="btn btn-primary btn-xs center" data-name="${row["name"]}">${__("Actions")}</button>`
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -76,30 +76,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
callback: (result) => {
|
||||
const data = result.message;
|
||||
|
||||
|
||||
if (data && data.length > 0) {
|
||||
const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
|
||||
proposals_wrapper.show();
|
||||
this.dialog.fields_dict.no_matching_vouchers.$wrapper.hide();
|
||||
this.data = [];
|
||||
data.forEach((row) => {
|
||||
const reference_date = row[5] ? row[5] : row[8];
|
||||
this.data.push([
|
||||
row[1],
|
||||
row[2],
|
||||
reference_date,
|
||||
format_currency(row[3], row[9]),
|
||||
row[4],
|
||||
row[6],
|
||||
]);
|
||||
});
|
||||
this.data = data.map((row) => this.format_row(row));
|
||||
this.get_dt_columns();
|
||||
this.get_datatable(proposals_wrapper);
|
||||
} else {
|
||||
const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
|
||||
proposals_wrapper.hide();
|
||||
this.dialog.fields_dict.no_matching_vouchers.$wrapper.show();
|
||||
|
||||
}
|
||||
this.dialog.show();
|
||||
},
|
||||
@ -122,6 +109,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
name: __("Reference Date"),
|
||||
editable: false,
|
||||
width: 120,
|
||||
format: frappe.form.formatters.Date,
|
||||
},
|
||||
{
|
||||
name: __("Remaining"),
|
||||
@ -141,6 +129,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
|
||||
];
|
||||
}
|
||||
|
||||
format_row(row) {
|
||||
return [
|
||||
row[1], // Document Type
|
||||
frappe.form.formatters.Link(row[2], {options: row[1]}), // Document Name
|
||||
row[5] || row[8], // Reference Date
|
||||
format_currency(row[3], row[9]), // Remaining
|
||||
row[4], // Reference Number
|
||||
row[6], // Party
|
||||
];
|
||||
}
|
||||
|
||||
get_datatable(proposals_wrapper) {
|
||||
if (!this.datatable) {
|
||||
const datatable_options = {
|
||||
|
@ -805,11 +805,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
);
|
||||
}
|
||||
|
||||
this.frm.doc.payments.find(pay => {
|
||||
if (pay.default) {
|
||||
pay.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
if(!this.frm.doc.is_return){
|
||||
this.frm.doc.payments.find(payment => {
|
||||
if (payment.default) {
|
||||
payment.amount = total_amount_to_pay;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
@ -16,8 +16,8 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
|| (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) {
|
||||
|
||||
let party_type = "Customer";
|
||||
if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") {
|
||||
party_type = "Lead";
|
||||
if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) {
|
||||
party_type = frm.doc.quotation_to;
|
||||
}
|
||||
|
||||
args = {
|
||||
|
@ -454,12 +454,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
|
||||
customer_outstanding += flt(extra_amount)
|
||||
|
||||
if credit_limit > 0 and flt(customer_outstanding) > credit_limit:
|
||||
msgprint(
|
||||
_("Credit limit has been crossed for customer {0} ({1}/{2})").format(
|
||||
customer, customer_outstanding, credit_limit
|
||||
)
|
||||
message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
|
||||
customer, customer_outstanding, credit_limit
|
||||
)
|
||||
|
||||
message += "<br><br>"
|
||||
|
||||
# If not authorized person raise exception
|
||||
credit_controller_role = frappe.db.get_single_value("Accounts Settings", "credit_controller")
|
||||
if not credit_controller_role or credit_controller_role not in frappe.get_roles():
|
||||
@ -480,7 +480,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
|
||||
"<li>".join(credit_controller_users_formatted)
|
||||
)
|
||||
|
||||
message = _(
|
||||
message += _(
|
||||
"Please contact any of the following users to extend the credit limits for {0}: {1}"
|
||||
).format(customer, user_list)
|
||||
|
||||
@ -488,7 +488,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
|
||||
# prompt them to send out an email to the controller users
|
||||
frappe.msgprint(
|
||||
message,
|
||||
title="Notify",
|
||||
title=_("Credit Limit Crossed"),
|
||||
raise_exception=1,
|
||||
primary_action={
|
||||
"label": "Send Email",
|
||||
@ -519,7 +519,6 @@ def get_customer_outstanding(
|
||||
customer, company, ignore_outstanding_sales_order=False, cost_center=None
|
||||
):
|
||||
# Outstanding based on GL Entries
|
||||
|
||||
cond = ""
|
||||
if cost_center:
|
||||
lft, rgt = frappe.get_cached_value("Cost Center", cost_center, ["lft", "rgt"])
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -13,7 +13,7 @@ frappe.ui.form.on('Quotation', {
|
||||
frm.set_query("quotation_to", function() {
|
||||
return{
|
||||
"filters": {
|
||||
"name": ["in", ["Customer", "Lead"]],
|
||||
"name": ["in", ["Customer", "Lead", "Prospect"]],
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -160,19 +160,16 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
}
|
||||
|
||||
set_dynamic_field_label(){
|
||||
if (this.frm.doc.quotation_to == "Customer")
|
||||
{
|
||||
if (this.frm.doc.quotation_to == "Customer") {
|
||||
this.frm.set_df_property("party_name", "label", "Customer");
|
||||
this.frm.fields_dict.party_name.get_query = null;
|
||||
}
|
||||
|
||||
if (this.frm.doc.quotation_to == "Lead")
|
||||
{
|
||||
} else if (this.frm.doc.quotation_to == "Lead") {
|
||||
this.frm.set_df_property("party_name", "label", "Lead");
|
||||
|
||||
this.frm.fields_dict.party_name.get_query = function() {
|
||||
return{ query: "erpnext.controllers.queries.lead_query" }
|
||||
}
|
||||
} else if (this.frm.doc.quotation_to == "Prospect") {
|
||||
this.frm.set_df_property("party_name", "label", "Prospect");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -291,6 +291,7 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -1072,7 +1073,7 @@
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-14 16:50:44.550098",
|
||||
"modified": "2023-06-03 16:21:04.980033",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
@ -398,6 +398,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -1475,6 +1476,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Phone",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -1643,7 +1645,7 @@
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-22 09:55:37.008190",
|
||||
"modified": "2023-06-03 16:16:23.411247",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
@ -230,6 +230,7 @@ class SalesOrder(SellingController):
|
||||
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
|
||||
|
||||
doc.set_status(update=True)
|
||||
doc.update_opportunity("Converted" if flag == "submit" else "Quotation")
|
||||
|
||||
def validate_drop_ship(self):
|
||||
for d in self.get("items"):
|
||||
|
@ -1772,7 +1772,14 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertEqual(pe.references[1].reference_name, so.name)
|
||||
self.assertEqual(pe.references[1].allocated_amount, 300)
|
||||
|
||||
@change_settings("Stock Settings", {"enable_stock_reservation": 1})
|
||||
@change_settings(
|
||||
"Stock Settings",
|
||||
{
|
||||
"enable_stock_reservation": 1,
|
||||
"auto_create_serial_and_batch_bundle_for_outward": 1,
|
||||
"pick_serial_and_batch_based_on": "FIFO",
|
||||
},
|
||||
)
|
||||
def test_stock_reservation_against_sales_order(self) -> None:
|
||||
from random import randint, uniform
|
||||
|
||||
|
@ -25,6 +25,7 @@ def after_install():
|
||||
create_default_success_action()
|
||||
create_default_energy_point_rules()
|
||||
create_incoterms()
|
||||
create_default_role_profiles()
|
||||
add_company_to_session_defaults()
|
||||
add_standard_navbar_items()
|
||||
add_app_name()
|
||||
@ -202,3 +203,42 @@ def setup_log_settings():
|
||||
def hide_workspaces():
|
||||
for ws in ["Integration", "Settings"]:
|
||||
frappe.db.set_value("Workspace", ws, "public", 0)
|
||||
|
||||
|
||||
def create_default_role_profiles():
|
||||
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
|
||||
role_profile = frappe.new_doc("Role Profile")
|
||||
role_profile.role_profile = role_profile_name
|
||||
for role in roles:
|
||||
role_profile.append("roles", {"role": role})
|
||||
|
||||
role_profile.insert(ignore_permissions=True)
|
||||
|
||||
|
||||
DEFAULT_ROLE_PROFILES = {
|
||||
"Inventory": [
|
||||
"Stock User",
|
||||
"Stock Manager",
|
||||
"Item Manager",
|
||||
],
|
||||
"Manufacturing": [
|
||||
"Stock User",
|
||||
"Manufacturing User",
|
||||
"Manufacturing Manager",
|
||||
],
|
||||
"Accounts": [
|
||||
"Accounts User",
|
||||
"Accounts Manager",
|
||||
],
|
||||
"Sales": [
|
||||
"Sales User",
|
||||
"Stock User",
|
||||
"Sales Manager",
|
||||
],
|
||||
"Purchase": [
|
||||
"Item Manager",
|
||||
"Stock User",
|
||||
"Purchase User",
|
||||
"Purchase Manager",
|
||||
],
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class ClosingStockBalance(Document):
|
||||
|
||||
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
|
||||
if self.get(fieldname):
|
||||
query = query.where(table.get(fieldname) == self.get(fieldname))
|
||||
query = query.where(table[fieldname] == self.get(fieldname))
|
||||
|
||||
query = query.run(as_dict=True)
|
||||
|
||||
|
@ -374,6 +374,7 @@
|
||||
"fieldtype": "Small Text",
|
||||
"hidden": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -1398,7 +1399,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-21 11:15:23.931084",
|
||||
"modified": "2023-06-03 16:13:25.011487",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
@ -1468,4 +1469,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
@ -772,12 +772,6 @@ $.extend(erpnext.item, {
|
||||
if (modal) {
|
||||
$(modal).removeClass("modal-dialog-scrollable");
|
||||
}
|
||||
})
|
||||
.on("awesomplete-close", () => {
|
||||
let modal = field.$input.parents('.modal-dialog')[0];
|
||||
if (modal) {
|
||||
$(modal).addClass("modal-dialog-scrollable");
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -714,6 +714,7 @@ class Item(Document):
|
||||
template=self,
|
||||
now=frappe.flags.in_test,
|
||||
timeout=600,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
def validate_has_variants(self):
|
||||
|
@ -326,6 +326,7 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -1239,7 +1240,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-07 20:18:25.458185",
|
||||
"modified": "2023-06-03 16:23:20.781368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
@ -127,6 +127,14 @@ frappe.ui.form.on('Serial and Batch Bundle', {
|
||||
},
|
||||
|
||||
toggle_fields(frm) {
|
||||
if (frm.doc.has_serial_no) {
|
||||
frm.doc.entries.forEach(row => {
|
||||
if (Math.abs(row.qty) !== 1) {
|
||||
frappe.model.set_value(row.doctype, row.name, "qty", 1);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
frm.fields_dict.entries.grid.update_docfield_property(
|
||||
'serial_no', 'read_only', !frm.doc.has_serial_no
|
||||
);
|
||||
@ -134,6 +142,10 @@ frappe.ui.form.on('Serial and Batch Bundle', {
|
||||
frm.fields_dict.entries.grid.update_docfield_property(
|
||||
'batch_no', 'read_only', !frm.doc.has_batch_no
|
||||
);
|
||||
|
||||
frm.fields_dict.entries.grid.update_docfield_property(
|
||||
'qty', 'read_only', frm.doc.has_serial_no
|
||||
);
|
||||
},
|
||||
|
||||
set_queries(frm) {
|
||||
@ -198,9 +210,9 @@ frappe.ui.form.on('Serial and Batch Bundle', {
|
||||
|
||||
|
||||
frappe.ui.form.on("Serial and Batch Entry", {
|
||||
ledgers_add(frm, cdt, cdn) {
|
||||
entries_add(frm, cdt, cdn) {
|
||||
if (frm.doc.warehouse) {
|
||||
locals[cdt][cdn].warehouse = frm.doc.warehouse;
|
||||
frappe.model.set_value(cdt, cdn, 'warehouse', frm.doc.warehouse);
|
||||
}
|
||||
},
|
||||
})
|
@ -133,7 +133,7 @@ class SerialandBatchBundle(Document):
|
||||
def calculate_total_qty(self, save=True):
|
||||
self.total_qty = 0.0
|
||||
for d in self.entries:
|
||||
d.qty = abs(d.qty) if d.qty else 0
|
||||
d.qty = 1 if self.has_serial_no and abs(d.qty) > 1 else abs(d.qty) if d.qty else 0
|
||||
d.stock_value_difference = abs(d.stock_value_difference) if d.stock_value_difference else 0
|
||||
if self.type_of_transaction == "Outward":
|
||||
d.qty *= -1
|
||||
|
@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.listview_settings["Serial and Batch Bundle"] = {
|
||||
add_fields: ["is_cancelled"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.is_cancelled) {
|
||||
return [__("Cancelled"), "red", "is_cancelled,=,1"];
|
||||
}
|
||||
},
|
||||
};
|
@ -94,6 +94,7 @@ class StockSettings(Document):
|
||||
frappe.enqueue(
|
||||
"erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions",
|
||||
now=frappe.flags.in_test,
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
|
@ -944,7 +944,7 @@ class update_entries_after(object):
|
||||
|
||||
for item in sr.items:
|
||||
# Skip for Serial and Batch Items
|
||||
if item.serial_no or item.batch_no:
|
||||
if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
|
||||
continue
|
||||
|
||||
previous_sle = get_previous_sle(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -205,6 +205,7 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -629,7 +630,7 @@
|
||||
"in_create": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-16 14:18:57.001239",
|
||||
"modified": "2023-06-03 16:18:39.088518",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Subcontracting",
|
||||
"name": "Subcontracting Receipt",
|
||||
|
@ -1,9 +1,11 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-01-10 16:34:30",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"naming_series",
|
||||
"status",
|
||||
@ -249,6 +251,7 @@
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Data",
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -362,10 +365,12 @@
|
||||
],
|
||||
"icon": "fa fa-bug",
|
||||
"idx": 1,
|
||||
"modified": "2021-11-09 17:26:09.703215",
|
||||
"links": [],
|
||||
"modified": "2023-06-03 16:17:07.694449",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Support",
|
||||
"name": "Warranty Claim",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -384,6 +389,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"timeline_field": "customer",
|
||||
"title_field": "customer_name"
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user