Merge pull request #38383 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
commit
4bfdab93ad
@ -355,7 +355,9 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
|
||||
vouchers = json.loads(vouchers)
|
||||
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
transaction.add_payment_entries(vouchers)
|
||||
return frappe.get_doc("Bank Transaction", bank_transaction_name)
|
||||
transaction.save()
|
||||
|
||||
return transaction
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -13,6 +13,7 @@
|
||||
"status",
|
||||
"bank_account",
|
||||
"company",
|
||||
"amended_from",
|
||||
"section_break_4",
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
@ -25,10 +26,10 @@
|
||||
"transaction_id",
|
||||
"transaction_type",
|
||||
"section_break_14",
|
||||
"column_break_oufv",
|
||||
"payment_entries",
|
||||
"section_break_18",
|
||||
"allocated_amount",
|
||||
"amended_from",
|
||||
"column_break_17",
|
||||
"unallocated_amount",
|
||||
"party_section",
|
||||
@ -138,10 +139,12 @@
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "allocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Allocated Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "amended_from",
|
||||
@ -157,10 +160,12 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "unallocated_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Unallocated Amount",
|
||||
"options": "currency"
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "party_section",
|
||||
@ -225,11 +230,15 @@
|
||||
"fieldname": "bank_party_account_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Party Account No. (Bank Statement)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_oufv",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-06 13:58:12.821411",
|
||||
"modified": "2023-11-18 18:32:47.203694",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
@ -2,78 +2,73 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.controllers.status_updater import StatusUpdater
|
||||
|
||||
|
||||
class BankTransaction(StatusUpdater):
|
||||
def after_insert(self):
|
||||
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
def before_validate(self):
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_submit(self):
|
||||
self.clear_linked_payment_entries()
|
||||
def validate(self):
|
||||
self.validate_duplicate_references()
|
||||
|
||||
def validate_duplicate_references(self):
|
||||
"""Make sure the same voucher is not allocated twice within the same Bank Transaction"""
|
||||
if not self.payment_entries:
|
||||
return
|
||||
|
||||
pe = []
|
||||
for row in self.payment_entries:
|
||||
reference = (row.payment_document, row.payment_entry)
|
||||
if reference in pe:
|
||||
frappe.throw(
|
||||
_("{0} {1} is allocated twice in this Bank Transaction").format(
|
||||
row.payment_document, row.payment_entry
|
||||
)
|
||||
)
|
||||
pe.append(reference)
|
||||
|
||||
def update_allocated_amount(self):
|
||||
self.allocated_amount = (
|
||||
sum(p.allocated_amount for p in self.payment_entries) if self.payment_entries else 0.0
|
||||
)
|
||||
self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit)) - self.allocated_amount
|
||||
|
||||
def before_submit(self):
|
||||
self.allocate_payment_entries()
|
||||
self.set_status()
|
||||
|
||||
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
|
||||
self.auto_set_party()
|
||||
|
||||
_saving_flag = False
|
||||
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
|
||||
def on_update_after_submit(self):
|
||||
"Run on save(). Avoid recursion caused by multiple saves"
|
||||
if not self._saving_flag:
|
||||
self._saving_flag = True
|
||||
self.clear_linked_payment_entries()
|
||||
self.update_allocations()
|
||||
self._saving_flag = False
|
||||
def before_update_after_submit(self):
|
||||
self.validate_duplicate_references()
|
||||
self.allocate_payment_entries()
|
||||
self.update_allocated_amount()
|
||||
|
||||
def on_cancel(self):
|
||||
self.clear_linked_payment_entries(for_cancel=True)
|
||||
self.set_status(update=True)
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
|
||||
def update_allocations(self):
|
||||
"The doctype does not allow modifications after submission, so write to the db direct"
|
||||
if self.payment_entries:
|
||||
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
|
||||
else:
|
||||
allocated_amount = 0.0
|
||||
|
||||
amount = abs(flt(self.withdrawal) - flt(self.deposit))
|
||||
self.db_set("allocated_amount", flt(allocated_amount))
|
||||
self.db_set("unallocated_amount", amount - flt(allocated_amount))
|
||||
self.reload()
|
||||
self.set_status(update=True)
|
||||
|
||||
def add_payment_entries(self, vouchers):
|
||||
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
|
||||
if 0.0 >= self.unallocated_amount:
|
||||
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
frappe.throw(_("Bank Transaction {0} is already fully reconciled").format(self.name))
|
||||
|
||||
added = False
|
||||
for voucher in vouchers:
|
||||
# Can't add same voucher twice
|
||||
found = False
|
||||
for pe in self.payment_entries:
|
||||
if (
|
||||
pe.payment_document == voucher["payment_doctype"]
|
||||
and pe.payment_entry == voucher["payment_name"]
|
||||
):
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
pe = {
|
||||
self.append(
|
||||
"payment_entries",
|
||||
{
|
||||
"payment_document": voucher["payment_doctype"],
|
||||
"payment_entry": voucher["payment_name"],
|
||||
"allocated_amount": 0.0, # Temporary
|
||||
}
|
||||
child = self.append("payment_entries", pe)
|
||||
added = True
|
||||
|
||||
# runs on_update_after_submit
|
||||
if added:
|
||||
self.save()
|
||||
},
|
||||
)
|
||||
|
||||
def allocate_payment_entries(self):
|
||||
"""Refactored from bank reconciliation tool.
|
||||
@ -90,6 +85,7 @@ class BankTransaction(StatusUpdater):
|
||||
- clear means: set the latest transaction date as clearance date
|
||||
"""
|
||||
remaining_amount = self.unallocated_amount
|
||||
to_remove = []
|
||||
for payment_entry in self.payment_entries:
|
||||
if payment_entry.allocated_amount == 0.0:
|
||||
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
|
||||
@ -99,49 +95,39 @@ class BankTransaction(StatusUpdater):
|
||||
if 0.0 == unallocated_amount:
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif remaining_amount <= 0.0:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
to_remove.append(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", unallocated_amount)
|
||||
elif 0.0 < unallocated_amount <= remaining_amount:
|
||||
payment_entry.allocated_amount = unallocated_amount
|
||||
remaining_amount -= unallocated_amount
|
||||
if should_clear:
|
||||
latest_transaction.clear_linked_payment_entry(payment_entry)
|
||||
|
||||
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
|
||||
payment_entry.db_set("allocated_amount", remaining_amount)
|
||||
elif 0.0 < unallocated_amount:
|
||||
payment_entry.allocated_amount = remaining_amount
|
||||
remaining_amount = 0.0
|
||||
|
||||
elif 0.0 > unallocated_amount:
|
||||
self.db_delete_payment_entry(payment_entry)
|
||||
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
frappe.throw(_("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
|
||||
|
||||
self.reload()
|
||||
|
||||
def db_delete_payment_entry(self, payment_entry):
|
||||
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
|
||||
for payment_entry in to_remove:
|
||||
self.remove(to_remove)
|
||||
|
||||
@frappe.whitelist()
|
||||
def remove_payment_entries(self):
|
||||
for payment_entry in self.payment_entries:
|
||||
self.remove_payment_entry(payment_entry)
|
||||
# runs on_update_after_submit
|
||||
self.save()
|
||||
|
||||
self.save() # runs before_update_after_submit
|
||||
|
||||
def remove_payment_entry(self, payment_entry):
|
||||
"Clear payment entry and clearance"
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
|
||||
self.remove(payment_entry)
|
||||
|
||||
def clear_linked_payment_entries(self, for_cancel=False):
|
||||
if for_cancel:
|
||||
for payment_entry in self.payment_entries:
|
||||
self.clear_linked_payment_entry(payment_entry, for_cancel)
|
||||
else:
|
||||
self.allocate_payment_entries()
|
||||
|
||||
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
|
||||
clearance_date = None if for_cancel else self.date
|
||||
set_voucher_clearance(
|
||||
@ -162,11 +148,10 @@ class BankTransaction(StatusUpdater):
|
||||
deposit=self.deposit,
|
||||
).match()
|
||||
|
||||
if result:
|
||||
party_type, party = result
|
||||
frappe.db.set_value(
|
||||
"Bank Transaction", self.name, field={"party_type": party_type, "party": party}
|
||||
)
|
||||
if not result:
|
||||
return
|
||||
|
||||
self.party_type, self.party = result
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -198,9 +183,7 @@ def get_clearance_details(transaction, payment_entry):
|
||||
if gle["gl_account"] == gl_bank_account:
|
||||
if gle["amount"] <= 0.0:
|
||||
frappe.throw(
|
||||
frappe._("Voucher {0} value is broken: {1}").format(
|
||||
payment_entry.payment_entry, gle["amount"]
|
||||
)
|
||||
_("Voucher {0} value is broken: {1}").format(payment_entry.payment_entry, gle["amount"])
|
||||
)
|
||||
|
||||
unmatched_gles -= 1
|
||||
@ -221,7 +204,7 @@ def get_clearance_details(transaction, payment_entry):
|
||||
|
||||
def get_related_bank_gl_entries(doctype, docname):
|
||||
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
|
||||
result = frappe.db.sql(
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
|
||||
@ -239,7 +222,6 @@ def get_related_bank_gl_entries(doctype, docname):
|
||||
dict(doctype=doctype, docname=docname),
|
||||
as_dict=True,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def get_total_allocated_amount(doctype, docname):
|
||||
@ -365,6 +347,7 @@ def set_voucher_clearance(doctype, docname, clearance_date, self):
|
||||
if clearance_date:
|
||||
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
|
||||
bt.add_payment_entries(vouchers)
|
||||
bt.save()
|
||||
else:
|
||||
for pe in bt.payment_entries:
|
||||
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
|
||||
|
@ -113,7 +113,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
|
||||
if as_dict:
|
||||
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
|
||||
else:
|
||||
if not row[1]:
|
||||
if not row[1] and len(row) > 1:
|
||||
row[1] = row[0]
|
||||
row[3] = row[2]
|
||||
data.append(row)
|
||||
|
@ -51,7 +51,7 @@ frappe.ui.form.on("Journal Entry", {
|
||||
}, __('Make'));
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
},
|
||||
before_save: function(frm) {
|
||||
if ((frm.doc.docstatus == 0) && (!frm.doc.is_system_generated)) {
|
||||
|
@ -548,8 +548,16 @@
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-08-10 14:32:22.366895",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Bank Transaction Payments",
|
||||
"link_fieldname": "payment_entry",
|
||||
"parent_doctype": "Bank Transaction",
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:11:04.128015",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
@ -508,7 +508,7 @@ class JournalEntry(AccountsController):
|
||||
).format(d.reference_name, d.account)
|
||||
)
|
||||
else:
|
||||
dr_or_cr = "debit" if d.credit > 0 else "credit"
|
||||
dr_or_cr = "debit" if flt(d.credit) > 0 else "credit"
|
||||
valid = False
|
||||
for jvd in against_entries:
|
||||
if flt(jvd[dr_or_cr]) > 0:
|
||||
@ -868,7 +868,7 @@ class JournalEntry(AccountsController):
|
||||
party_account_currency = d.account_currency
|
||||
|
||||
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
|
||||
bank_amount += d.debit_in_account_currency or d.credit_in_account_currency
|
||||
bank_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
|
||||
bank_account_currency = d.account_currency
|
||||
|
||||
if party_type and pay_to_recd_from:
|
||||
|
@ -203,7 +203,8 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Reference Type",
|
||||
"no_copy": 1,
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry"
|
||||
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement\nPayment Entry",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_name",
|
||||
@ -211,7 +212,8 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Reference Name",
|
||||
"no_copy": 1,
|
||||
"options": "reference_type"
|
||||
"options": "reference_type",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
|
||||
@ -278,13 +280,14 @@
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Detail No",
|
||||
"no_copy": 1
|
||||
"no_copy": 1,
|
||||
"search_index": 1
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-16 14:11:13.507807",
|
||||
"modified": "2023-11-23 11:44:25.841187",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry Account",
|
||||
|
@ -9,7 +9,7 @@ erpnext.accounts.taxes.setup_tax_filters("Advance Taxes and Charges");
|
||||
|
||||
frappe.ui.form.on('Payment Entry', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payments', 'Unreconcile Payment Entries'];
|
||||
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', 'Repost Payment Ledger','Repost Accounting Ledger', 'Unreconcile Payment', 'Unreconcile Payment Entries'];
|
||||
|
||||
if(frm.doc.__islocal) {
|
||||
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
|
||||
@ -160,7 +160,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
}, __('Actions'));
|
||||
|
||||
}
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(frm);
|
||||
},
|
||||
|
||||
validate_company: (frm) => {
|
||||
|
@ -750,8 +750,16 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-08 21:51:03.482709",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Bank Transaction Payments",
|
||||
"link_fieldname": "payment_entry",
|
||||
"parent_doctype": "Bank Transaction",
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 12:07:20.887885",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
@ -148,7 +148,7 @@ class PaymentEntry(AccountsController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
)
|
||||
super(PaymentEntry, self).on_cancel()
|
||||
|
@ -1171,6 +1171,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
# Should not raise frappe.exceptions.ValidationError: Payment Entry has been modified after you pulled it. Please pull it again.
|
||||
pr.reconcile()
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.new_doc("Customer")
|
||||
|
@ -556,7 +556,7 @@ def get_stock_availability(item_code, warehouse):
|
||||
return bin_qty - pos_sales_qty, is_stock_item
|
||||
else:
|
||||
is_stock_item = True
|
||||
if frappe.db.exists("Product Bundle", item_code):
|
||||
if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}):
|
||||
return get_bundle_availability(item_code, warehouse), is_stock_item
|
||||
else:
|
||||
is_stock_item = False
|
||||
|
@ -17,11 +17,10 @@ class ProcessSubscription(Document):
|
||||
|
||||
|
||||
def create_subscription_process(
|
||||
subscription: str | None, posting_date: Union[str, datetime.date] | None
|
||||
subscription: str | None = None, posting_date: Union[str, datetime.date] | None = None
|
||||
):
|
||||
"""Create a new Process Subscription document"""
|
||||
doc = frappe.new_doc("Process Subscription")
|
||||
doc.subscription = subscription
|
||||
doc.posting_date = getdate(posting_date)
|
||||
doc.insert(ignore_permissions=True)
|
||||
doc.submit()
|
||||
|
@ -180,7 +180,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
|
||||
this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1);
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
unblock_invoice() {
|
||||
|
@ -13,6 +13,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
check_if_return_invoice_linked_with_payment_entry,
|
||||
@ -491,6 +492,7 @@ class PurchaseInvoice(BuyingController):
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_expense_account()
|
||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
|
||||
def on_submit(self):
|
||||
@ -525,7 +527,11 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.update_project()
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
|
||||
):
|
||||
self.update_project()
|
||||
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
self.update_advance_tax_references()
|
||||
|
||||
@ -1260,7 +1266,10 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
self.update_project()
|
||||
if (
|
||||
frappe.db.get_single_value("Buying Settings", "project_update_frequency") == "Each Transaction"
|
||||
):
|
||||
self.update_project()
|
||||
self.db_set("status", "Cancelled")
|
||||
|
||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
@ -1279,13 +1288,21 @@ class PurchaseInvoice(BuyingController):
|
||||
self.update_advance_tax_references(cancel=1)
|
||||
|
||||
def update_project(self):
|
||||
project_list = []
|
||||
projects = frappe._dict()
|
||||
for d in self.items:
|
||||
if d.project and d.project not in project_list:
|
||||
project = frappe.get_doc("Project", d.project)
|
||||
project.update_purchase_costing()
|
||||
project.db_update()
|
||||
project_list.append(d.project)
|
||||
if d.project:
|
||||
if self.docstatus == 1:
|
||||
projects[d.project] = projects.get(d.project, 0) + d.base_net_amount
|
||||
elif self.docstatus == 2:
|
||||
projects[d.project] = projects.get(d.project, 0) - d.base_net_amount
|
||||
|
||||
pj = frappe.qb.DocType("Project")
|
||||
for proj, value in projects.items():
|
||||
res = (
|
||||
frappe.qb.from_(pj).select(pj.total_purchase_cost).where(pj.name == proj).for_update().run()
|
||||
)
|
||||
current_purchase_cost = res and res[0][0] or 0
|
||||
frappe.db.set_value("Project", proj, "total_purchase_cost", current_purchase_cost + value)
|
||||
|
||||
def validate_supplier_invoice(self):
|
||||
if self.bill_date:
|
||||
|
@ -498,6 +498,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
@ -505,6 +506,7 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": ":Company",
|
||||
"depends_on": "eval:!doc.is_fixed_asset",
|
||||
"fieldname": "cost_center",
|
||||
|
@ -10,12 +10,7 @@ from frappe.utils.data import comma_and
|
||||
class RepostAccountingLedger(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(RepostAccountingLedger, self).__init__(*args, **kwargs)
|
||||
self._allowed_types = [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
self._allowed_types = get_allowed_types_from_settings()
|
||||
|
||||
def validate(self):
|
||||
self.validate_vouchers()
|
||||
@ -56,15 +51,7 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
def validate_vouchers(self):
|
||||
if self.vouchers:
|
||||
# Validate voucher types
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
if disallowed_types := voucher_types.difference(self._allowed_types):
|
||||
frappe.throw(
|
||||
_("{0} types are not allowed. Only {1} are.").format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
frappe.bold(comma_and(list(self._allowed_types))),
|
||||
)
|
||||
)
|
||||
validate_docs_for_voucher_types([x.voucher_type for x in self.vouchers])
|
||||
|
||||
def get_existing_ledger_entries(self):
|
||||
vouchers = [x.voucher_no for x in self.vouchers]
|
||||
@ -168,6 +155,15 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
return [
|
||||
x.document_type
|
||||
for x in frappe.db.get_all(
|
||||
"Repost Allowed Types", filters={"allowed": True}, fields=["distinct(document_type)"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
docs_with_deferred_revenue = frappe.db.get_all(
|
||||
"Sales Invoice Item",
|
||||
@ -191,6 +187,25 @@ def validate_docs_for_deferred_accounting(sales_docs, purchase_docs):
|
||||
)
|
||||
|
||||
|
||||
def validate_docs_for_voucher_types(doc_voucher_types):
|
||||
allowed_types = get_allowed_types_from_settings()
|
||||
# Validate voucher types
|
||||
voucher_types = set(doc_voucher_types)
|
||||
if disallowed_types := voucher_types.difference(allowed_types):
|
||||
message = "are" if len(disallowed_types) > 1 else "is"
|
||||
frappe.throw(
|
||||
_("{0} {1} not allowed to be reposted. Modify {2} to enable reposting.").format(
|
||||
frappe.bold(comma_and(list(disallowed_types))),
|
||||
message,
|
||||
frappe.bold(
|
||||
frappe.utils.get_link_to_form(
|
||||
"Repost Accounting Ledger Settings", "Repost Accounting Ledger Settings"
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_repost_allowed_types(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
@ -37,7 +37,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
super.onload();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log',
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payments", "Unreconcile Payment Entries"];
|
||||
'POS Closing Entry', 'Journal Entry', 'Payment Entry', "Repost Payment Ledger", "Repost Accounting Ledger", "Unreconcile Payment", "Unreconcile Payment Entries"];
|
||||
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
@ -184,7 +184,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}
|
||||
}
|
||||
|
||||
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(me.frm);
|
||||
erpnext.accounts.unreconcile_payment.add_unreconcile_btn(me.frm);
|
||||
}
|
||||
|
||||
make_maintenance_schedule() {
|
||||
|
@ -1615,7 +1615,8 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Inter Company Invoice Reference",
|
||||
"options": "Purchase Invoice",
|
||||
"read_only": 1
|
||||
"read_only": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_group",
|
||||
@ -2173,7 +2174,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-20 11:51:43.555197",
|
||||
"modified": "2023-11-23 16:56:29.679499",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -17,6 +17,7 @@ from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
|
||||
)
|
||||
from erpnext.accounts.doctype.repost_accounting_ledger.repost_accounting_ledger import (
|
||||
validate_docs_for_deferred_accounting,
|
||||
validate_docs_for_voucher_types,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
@ -172,6 +173,7 @@ class SalesInvoice(SellingController):
|
||||
self.validate_write_off_account()
|
||||
self.validate_account_for_change_amount()
|
||||
self.validate_income_account()
|
||||
validate_docs_for_voucher_types(["Sales Invoice"])
|
||||
validate_docs_for_deferred_accounting([self.name], [])
|
||||
|
||||
def validate_fixed_asset(self):
|
||||
@ -395,7 +397,7 @@ class SalesInvoice(SellingController):
|
||||
"Repost Payment Ledger Items",
|
||||
"Repost Accounting Ledger",
|
||||
"Repost Accounting Ledger Items",
|
||||
"Unreconcile Payments",
|
||||
"Unreconcile Payment",
|
||||
"Unreconcile Payment Entries",
|
||||
"Payment Ledger Entry",
|
||||
"Serial and Batch Bundle",
|
||||
|
@ -676,7 +676,7 @@ def get_prorata_factor(
|
||||
|
||||
|
||||
def process_all(
|
||||
subscription: str | None, posting_date: Optional["DateTimeLikeObject"] = None
|
||||
subscription: str | None = None, posting_date: Optional["DateTimeLikeObject"] = None
|
||||
) -> None:
|
||||
"""
|
||||
Task to updates the status of all `Subscription` apart from those that are cancelled
|
||||
|
@ -10,7 +10,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
|
||||
from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
|
||||
|
||||
|
||||
class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_customer()
|
||||
@ -73,7 +73,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
@ -138,7 +138,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
||||
@ -196,7 +196,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe.doctype,
|
||||
"voucher_no": pe.name,
|
||||
@ -281,7 +281,7 @@ class TestUnreconcilePayments(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
unreconcile = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Unreconcile Payments",
|
||||
"doctype": "Unreconcile Payment",
|
||||
"company": self.company,
|
||||
"voucher_type": pe2.doctype,
|
||||
"voucher_no": pe2.name,
|
@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("Unreconcile Payments", {
|
||||
frappe.ui.form.on("Unreconcile Payment", {
|
||||
refresh(frm) {
|
||||
frm.set_query("voucher_type", function() {
|
||||
return {
|
@ -21,7 +21,7 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Amended From",
|
||||
"no_copy": 1,
|
||||
"options": "Unreconcile Payments",
|
||||
"options": "Unreconcile Payment",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -61,7 +61,7 @@
|
||||
"modified": "2023-08-28 17:42:50.261377",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Unreconcile Payments",
|
||||
"name": "Unreconcile Payment",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
@ -15,7 +15,7 @@ from erpnext.accounts.utils import (
|
||||
)
|
||||
|
||||
|
||||
class UnreconcilePayments(Document):
|
||||
class UnreconcilePayment(Document):
|
||||
def validate(self):
|
||||
self.supported_types = ["Payment Entry", "Journal Entry"]
|
||||
if not self.voucher_type in self.supported_types:
|
||||
@ -142,7 +142,7 @@ def create_unreconcile_doc_for_selection(selections=None):
|
||||
selections = frappe.json.loads(selections)
|
||||
# assuming each row is a unique voucher
|
||||
for row in selections:
|
||||
unrecon = frappe.new_doc("Unreconcile Payments")
|
||||
unrecon = frappe.new_doc("Unreconcile Payment")
|
||||
unrecon.company = row.get("company")
|
||||
unrecon.voucher_type = row.get("voucher_type")
|
||||
unrecon.voucher_no = row.get("voucher_no")
|
@ -281,8 +281,8 @@ class ReceivablePayableReport(object):
|
||||
|
||||
must_consider = False
|
||||
if self.filters.get("for_revaluation_journals"):
|
||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) or (
|
||||
(abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision)
|
||||
if (abs(row.outstanding) > 0.0 / 10**self.currency_precision) or (
|
||||
(abs(row.outstanding_in_account_currency) > 0.0 / 10**self.currency_precision)
|
||||
):
|
||||
must_consider = True
|
||||
else:
|
||||
|
@ -1,7 +1,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import (
|
||||
from erpnext.accounts.report.tax_withholding_details.tax_withholding_details import (
|
||||
get_result,
|
||||
get_tds_docs,
|
||||
)
|
||||
|
@ -183,6 +183,7 @@ def get_balance_on(
|
||||
cost_center=None,
|
||||
ignore_account_permission=False,
|
||||
account_type=None,
|
||||
start_date=None,
|
||||
):
|
||||
if not account and frappe.form_dict.get("account"):
|
||||
account = frappe.form_dict.get("account")
|
||||
@ -196,6 +197,8 @@ def get_balance_on(
|
||||
cost_center = frappe.form_dict.get("cost_center")
|
||||
|
||||
cond = ["is_cancelled=0"]
|
||||
if start_date:
|
||||
cond.append("posting_date >= %s" % frappe.db.escape(cstr(start_date)))
|
||||
if date:
|
||||
cond.append("posting_date <= %s" % frappe.db.escape(cstr(date)))
|
||||
else:
|
||||
@ -1826,6 +1829,28 @@ class QueryPaymentLedger(object):
|
||||
Table("outstanding").amount_in_account_currency >= self.max_outstanding
|
||||
)
|
||||
|
||||
if self.limit and self.get_invoices:
|
||||
outstanding_vouchers = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_against_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
|
||||
.orderby(ple.posting_date, ple.voucher_no)
|
||||
.having(qb.Field("amount_in_account_currency") > 0)
|
||||
.limit(self.limit)
|
||||
.run()
|
||||
)
|
||||
if outstanding_vouchers:
|
||||
filter_on_voucher_no.append(ple.voucher_no.isin([x[0] for x in outstanding_vouchers]))
|
||||
filter_on_against_voucher_no.append(
|
||||
ple.against_voucher_no.isin([x[0] for x in outstanding_vouchers])
|
||||
)
|
||||
|
||||
# build query for voucher amount
|
||||
query_voucher_amount = (
|
||||
qb.from_(ple)
|
||||
|
@ -509,6 +509,9 @@ def restore_asset(asset_name):
|
||||
|
||||
|
||||
def depreciate_asset(asset_doc, date, notes):
|
||||
if not asset_doc.calculate_depreciation:
|
||||
return
|
||||
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
@ -521,6 +524,9 @@ def depreciate_asset(asset_doc, date, notes):
|
||||
|
||||
|
||||
def reset_depreciation_schedule(asset_doc, date, notes):
|
||||
if not asset_doc.calculate_depreciation:
|
||||
return
|
||||
|
||||
asset_doc.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
|
@ -17,6 +17,7 @@
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"blanket_order_allowance",
|
||||
"project_update_frequency",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
@ -172,6 +173,14 @@
|
||||
"fieldname": "blanket_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Blanket Order Allowance (%)"
|
||||
},
|
||||
{
|
||||
"default": "Each Transaction",
|
||||
"description": "How often should Project be updated of Total Purchase Cost ?",
|
||||
"fieldname": "project_update_frequency",
|
||||
"fieldtype": "Select",
|
||||
"label": "Update frequency of Project",
|
||||
"options": "Each Transaction\nManual"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -179,7 +188,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-25 14:03:32.520418",
|
||||
"modified": "2023-11-24 10:55:51.287327",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -16,7 +16,7 @@ from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_invoice as make_pi_from_po,
|
||||
)
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
|
||||
from erpnext.controllers.accounts_controller import update_child_qty_rate
|
||||
from erpnext.controllers.accounts_controller import InvalidQtyError, update_child_qty_rate
|
||||
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.material_request.material_request import make_purchase_order
|
||||
@ -27,6 +27,21 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
|
||||
|
||||
class TestPurchaseOrder(FrappeTestCase):
|
||||
def test_purchase_order_qty(self):
|
||||
po = create_purchase_order(qty=1, do_not_save=True)
|
||||
po.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"qty": -1,
|
||||
"rate": 10,
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.NonNegativeError, po.save)
|
||||
|
||||
po.items[1].qty = 0
|
||||
self.assertRaises(InvalidQtyError, po.save)
|
||||
|
||||
def test_make_purchase_receipt(self):
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
|
||||
|
@ -214,6 +214,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_width": "60px",
|
||||
@ -917,7 +918,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:34:27.267382",
|
||||
"modified": "2023-11-24 13:24:41.298416",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -165,16 +165,17 @@ class Supplier(TransactionBase):
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||
supplier = filters.get("supplier")
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
`tabContact`.name from `tabContact`,
|
||||
`tabDynamic Link`
|
||||
WHERE
|
||||
`tabContact`.name = `tabDynamic Link`.parent
|
||||
and `tabDynamic Link`.link_name = %(supplier)s
|
||||
and `tabDynamic Link`.link_doctype = 'Supplier'
|
||||
and `tabContact`.name like %(txt)s
|
||||
""",
|
||||
{"supplier": supplier, "txt": "%%%s%%" % txt},
|
||||
)
|
||||
contact = frappe.qb.DocType("Contact")
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(contact)
|
||||
.join(dynamic_link)
|
||||
.on(contact.name == dynamic_link.parent)
|
||||
.select(contact.name, contact.email_id)
|
||||
.where(
|
||||
(dynamic_link.link_name == supplier)
|
||||
& (dynamic_link.link_doctype == "Supplier")
|
||||
& (contact.name.like("%{0}%".format(txt)))
|
||||
)
|
||||
).run(as_dict=False)
|
||||
|
@ -71,6 +71,10 @@ class AccountMissingError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidQtyError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
force_item_fields = (
|
||||
"item_group",
|
||||
"brand",
|
||||
@ -239,7 +243,7 @@ class AccountsController(TransactionBase):
|
||||
references_map.setdefault(x.parent, []).append(x.name)
|
||||
|
||||
for doc, rows in references_map.items():
|
||||
unreconcile_doc = frappe.get_doc("Unreconcile Payments", doc)
|
||||
unreconcile_doc = frappe.get_doc("Unreconcile Payment", doc)
|
||||
for row in rows:
|
||||
unreconcile_doc.remove(unreconcile_doc.get("allocations", {"name": row})[0])
|
||||
|
||||
@ -248,9 +252,9 @@ class AccountsController(TransactionBase):
|
||||
unreconcile_doc.save(ignore_permissions=True)
|
||||
|
||||
# delete docs upon parent doc deletion
|
||||
unreconcile_docs = frappe.db.get_all("Unreconcile Payments", filters={"voucher_no": self.name})
|
||||
unreconcile_docs = frappe.db.get_all("Unreconcile Payment", filters={"voucher_no": self.name})
|
||||
for x in unreconcile_docs:
|
||||
_doc = frappe.get_doc("Unreconcile Payments", x.name)
|
||||
_doc = frappe.get_doc("Unreconcile Payment", x.name)
|
||||
if _doc.docstatus == 1:
|
||||
_doc.cancel()
|
||||
_doc.delete()
|
||||
@ -910,10 +914,16 @@ class AccountsController(TransactionBase):
|
||||
return flt(args.get(field, 0) / self.get("conversion_rate", 1))
|
||||
|
||||
def validate_qty_is_not_zero(self):
|
||||
if self.doctype != "Purchase Receipt":
|
||||
for item in self.items:
|
||||
if not item.qty:
|
||||
frappe.throw(_("Item quantity can not be zero"))
|
||||
if self.doctype == "Purchase Receipt":
|
||||
return
|
||||
|
||||
for item in self.items:
|
||||
if not flt(item.qty):
|
||||
frappe.throw(
|
||||
msg=_("Row #{0}: Item quantity cannot be zero").format(item.idx),
|
||||
title=_("Invalid Quantity"),
|
||||
exc=InvalidQtyError,
|
||||
)
|
||||
|
||||
def validate_account_currency(self, account, account_currency=None):
|
||||
valid_currency = [self.company_currency]
|
||||
@ -3139,16 +3149,19 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
qty_precision = child_item.precision("qty") or 2
|
||||
|
||||
if flt(child_item.billed_amt, rate_precision) > flt(
|
||||
flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision
|
||||
):
|
||||
# Amount cannot be lesser than billed amount, except for negative amounts
|
||||
row_rate = flt(d.get("rate"), rate_precision)
|
||||
amount_below_billed_amt = flt(child_item.billed_amt, rate_precision) > flt(
|
||||
row_rate * flt(d.get("qty"), qty_precision), rate_precision
|
||||
)
|
||||
if amount_below_billed_amt and row_rate > 0.0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.").format(
|
||||
child_item.idx, child_item.item_code
|
||||
)
|
||||
)
|
||||
else:
|
||||
child_item.rate = flt(d.get("rate"), rate_precision)
|
||||
child_item.rate = row_rate
|
||||
|
||||
if d.get("conversion_factor"):
|
||||
if child_item.stock_uom == child_item.uom:
|
||||
|
@ -350,11 +350,12 @@ class SellingController(StockController):
|
||||
return il
|
||||
|
||||
def has_product_bundle(self, item_code):
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabProduct Bundle`
|
||||
where new_item_code=%s and docstatus != 2""",
|
||||
item_code,
|
||||
)
|
||||
product_bundle = frappe.qb.DocType("Product Bundle")
|
||||
return (
|
||||
frappe.qb.from_(product_bundle)
|
||||
.select(product_bundle.name)
|
||||
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
|
||||
).run()
|
||||
|
||||
def get_already_delivered_qty(self, current_docname, so, so_detail):
|
||||
delivered_via_dn = frappe.db.sql(
|
||||
|
@ -29,8 +29,16 @@
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-21 12:43:59.106807",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Competitor Detail",
|
||||
"link_fieldname": "competitor",
|
||||
"parent_doctype": "Quotation",
|
||||
"table_fieldname": "competitors"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:33:54.284279",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Competitor",
|
||||
@ -64,5 +72,6 @@
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -34,6 +34,15 @@ class Lead(SellingController, CRMNote):
|
||||
def before_insert(self):
|
||||
self.contact_doc = None
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
if self.source == "Existing Customer" and self.customer:
|
||||
contact = frappe.db.get_value(
|
||||
"Dynamic Link",
|
||||
{"link_doctype": "Customer", "link_name": self.customer},
|
||||
"parent",
|
||||
)
|
||||
if contact:
|
||||
self.contact_doc = frappe.get_doc("Contact", contact)
|
||||
return
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
def after_insert(self):
|
||||
|
@ -419,7 +419,6 @@ scheduler_events = {
|
||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||
],
|
||||
"hourly_long": [
|
||||
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
|
||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries",
|
||||
"erpnext.utilities.bulk_transaction.retry",
|
||||
],
|
||||
@ -450,6 +449,7 @@ scheduler_events = {
|
||||
"erpnext.accounts.utils.auto_create_exchange_rate_revaluation_weekly",
|
||||
],
|
||||
"daily_long": [
|
||||
"erpnext.accounts.doctype.process_subscription.process_subscription.create_subscription_process",
|
||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.auto_update_latest_price_in_all_boms",
|
||||
"erpnext.crm.utils.open_leads_opportunities_based_on_todays_event",
|
||||
|
@ -185,7 +185,8 @@ class JobCard(Document):
|
||||
# override capacity for employee
|
||||
production_capacity = 1
|
||||
|
||||
if time_logs and production_capacity > len(time_logs):
|
||||
overlap_count = self.get_overlap_count(time_logs)
|
||||
if time_logs and production_capacity > overlap_count:
|
||||
return {}
|
||||
|
||||
if self.workstation_type and time_logs:
|
||||
@ -195,6 +196,37 @@ class JobCard(Document):
|
||||
|
||||
return time_logs[-1]
|
||||
|
||||
@staticmethod
|
||||
def get_overlap_count(time_logs):
|
||||
count = 1
|
||||
|
||||
# Check overlap exists or not between the overlapping time logs with the current Job Card
|
||||
for idx, row in enumerate(time_logs):
|
||||
next_idx = idx
|
||||
if idx + 1 < len(time_logs):
|
||||
next_idx = idx + 1
|
||||
next_row = time_logs[next_idx]
|
||||
if row.name == next_row.name:
|
||||
continue
|
||||
|
||||
if (
|
||||
(
|
||||
get_datetime(next_row.from_time) >= get_datetime(row.from_time)
|
||||
and get_datetime(next_row.from_time) <= get_datetime(row.to_time)
|
||||
)
|
||||
or (
|
||||
get_datetime(next_row.to_time) >= get_datetime(row.from_time)
|
||||
and get_datetime(next_row.to_time) <= get_datetime(row.to_time)
|
||||
)
|
||||
or (
|
||||
get_datetime(next_row.from_time) <= get_datetime(row.from_time)
|
||||
and get_datetime(next_row.to_time) >= get_datetime(row.to_time)
|
||||
)
|
||||
):
|
||||
count += 1
|
||||
|
||||
return count
|
||||
|
||||
def get_time_logs(self, args, doctype, check_next_available_slot=False):
|
||||
jc = frappe.qb.DocType("Job Card")
|
||||
jctl = frappe.qb.DocType(doctype)
|
||||
@ -211,7 +243,14 @@ class JobCard(Document):
|
||||
query = (
|
||||
frappe.qb.from_(jctl)
|
||||
.from_(jc)
|
||||
.select(jc.name.as_("name"), jctl.from_time, jctl.to_time, jc.workstation, jc.workstation_type)
|
||||
.select(
|
||||
jc.name.as_("name"),
|
||||
jctl.name.as_("row_name"),
|
||||
jctl.from_time,
|
||||
jctl.to_time,
|
||||
jc.workstation,
|
||||
jc.workstation_type,
|
||||
)
|
||||
.where(
|
||||
(jctl.parent == jc.name)
|
||||
& (Criterion.any(time_conditions))
|
||||
|
@ -920,6 +920,20 @@ class TestWorkOrder(FrappeTestCase):
|
||||
"Test RM Item 2 for Scrap Item Test",
|
||||
]
|
||||
|
||||
from_time = add_days(now(), -1)
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card Time Log",
|
||||
fields=["distinct parent as name", "docstatus"],
|
||||
filters={"from_time": (">", from_time)},
|
||||
order_by="creation asc",
|
||||
)
|
||||
|
||||
for job_card in job_cards:
|
||||
if job_card.docstatus == 1:
|
||||
frappe.get_doc("Job Card", job_card.name).cancel()
|
||||
|
||||
frappe.delete_doc("Job Card Time Log", job_card.name)
|
||||
|
||||
company = "_Test Company with perpetual inventory"
|
||||
for item_code in items:
|
||||
create_item(
|
||||
|
@ -351,5 +351,6 @@ erpnext.patches.v15_0.rename_depreciation_amount_based_on_num_days_in_month_to_d
|
||||
erpnext.patches.v15_0.set_reserved_stock_in_bin
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_in_supplier_quotation
|
||||
erpnext.patches.v14_0.update_zero_asset_quantity_field
|
||||
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
|
@ -21,6 +21,9 @@ def execute():
|
||||
params = set({x.casefold(): x for x in params}.values())
|
||||
|
||||
for parameter in params:
|
||||
if frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
continue
|
||||
|
||||
frappe.get_doc(
|
||||
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
|
||||
).insert(ignore_permissions=True)
|
||||
|
@ -68,6 +68,10 @@ frappe.ui.form.on("Project", {
|
||||
frm.events.create_duplicate(frm);
|
||||
}, __("Actions"));
|
||||
|
||||
frm.add_custom_button(__('Update Total Purchase Cost'), () => {
|
||||
frm.events.update_total_purchase_cost(frm);
|
||||
}, __("Actions"));
|
||||
|
||||
frm.trigger("set_project_status_button");
|
||||
|
||||
|
||||
@ -92,6 +96,22 @@ frappe.ui.form.on("Project", {
|
||||
|
||||
},
|
||||
|
||||
update_total_purchase_cost: function(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.projects.doctype.project.project.recalculate_project_total_purchase_cost",
|
||||
args: {project: frm.doc.name},
|
||||
freeze: true,
|
||||
freeze_message: __('Recalculating Purchase Cost against this Project...'),
|
||||
callback: function(r) {
|
||||
if (r && !r.exc) {
|
||||
frappe.msgprint(__('Total Purchase Cost has been updated'));
|
||||
frm.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
},
|
||||
|
||||
set_project_status_button: function(frm) {
|
||||
frm.add_custom_button(__('Set Project Status'), () => {
|
||||
let d = new frappe.ui.Dialog({
|
||||
|
@ -4,11 +4,11 @@
|
||||
|
||||
import frappe
|
||||
from email_reply_parser import EmailReplyParser
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Interval
|
||||
from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp
|
||||
from frappe.query_builder.functions import Count, CurDate, Date, Sum, UnixTimestamp
|
||||
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
@ -249,12 +249,7 @@ class Project(Document):
|
||||
self.per_gross_margin = (self.gross_margin / flt(self.total_billed_amount)) * 100
|
||||
|
||||
def update_purchase_costing(self):
|
||||
total_purchase_cost = frappe.db.sql(
|
||||
"""select sum(base_net_amount)
|
||||
from `tabPurchase Invoice Item` where project = %s and docstatus=1""",
|
||||
self.name,
|
||||
)
|
||||
|
||||
total_purchase_cost = calculate_total_purchase_cost(self.name)
|
||||
self.total_purchase_cost = total_purchase_cost and total_purchase_cost[0][0] or 0
|
||||
|
||||
def update_sales_amount(self):
|
||||
@ -695,3 +690,29 @@ def get_holiday_list(company=None):
|
||||
|
||||
def get_users_email(doc):
|
||||
return [d.email for d in doc.users if frappe.db.get_value("User", d.user, "enabled")]
|
||||
|
||||
|
||||
def calculate_total_purchase_cost(project: str | None = None):
|
||||
if project:
|
||||
pitem = qb.DocType("Purchase Invoice Item")
|
||||
frappe.qb.DocType("Purchase Invoice Item")
|
||||
total_purchase_cost = (
|
||||
qb.from_(pitem)
|
||||
.select(Sum(pitem.base_net_amount))
|
||||
.where((pitem.project == project) & (pitem.docstatus == 1))
|
||||
.run(as_list=True)
|
||||
)
|
||||
return total_purchase_cost
|
||||
return None
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def recalculate_project_total_purchase_cost(project: str | None = None):
|
||||
if project:
|
||||
total_purchase_cost = calculate_total_purchase_cost(project)
|
||||
frappe.db.set_value(
|
||||
"Project",
|
||||
project,
|
||||
"total_purchase_cost",
|
||||
(total_purchase_cost and total_purchase_cost[0][0] or 0),
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
frappe.provide('erpnext.accounts');
|
||||
|
||||
erpnext.accounts.unreconcile_payments = {
|
||||
erpnext.accounts.unreconcile_payment = {
|
||||
add_unreconcile_btn(frm) {
|
||||
if (frm.doc.docstatus == 1) {
|
||||
if(((frm.doc.doctype == "Journal Entry") && (frm.doc.voucher_type != "Journal Entry"))
|
||||
@ -10,7 +10,7 @@ erpnext.accounts.unreconcile_payments = {
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.doc_has_references",
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.doc_has_references",
|
||||
"args": {
|
||||
"doctype": frm.doc.doctype,
|
||||
"docname": frm.doc.name
|
||||
@ -18,7 +18,7 @@ erpnext.accounts.unreconcile_payments = {
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.add_custom_button(__("UnReconcile"), function() {
|
||||
erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
|
||||
erpnext.accounts.unreconcile_payment.build_unreconcile_dialog(frm);
|
||||
}, __('Actions'));
|
||||
}
|
||||
}
|
||||
@ -74,7 +74,7 @@ erpnext.accounts.unreconcile_payments = {
|
||||
|
||||
// get linked payments
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.get_linked_payments_for_doc",
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.get_linked_payments_for_doc",
|
||||
"args": {
|
||||
"company": frm.doc.company,
|
||||
"doctype": frm.doc.doctype,
|
||||
@ -96,8 +96,8 @@ erpnext.accounts.unreconcile_payments = {
|
||||
|
||||
let selected_allocations = values.allocations.filter(x=>x.__checked);
|
||||
if (selected_allocations.length > 0) {
|
||||
let selection_map = erpnext.accounts.unreconcile_payments.build_selection_map(frm, selected_allocations);
|
||||
erpnext.accounts.unreconcile_payments.create_unreconcile_docs(selection_map);
|
||||
let selection_map = erpnext.accounts.unreconcile_payment.build_selection_map(frm, selected_allocations);
|
||||
erpnext.accounts.unreconcile_payment.create_unreconcile_docs(selection_map);
|
||||
d.hide();
|
||||
|
||||
} else {
|
||||
@ -115,7 +115,7 @@ erpnext.accounts.unreconcile_payments = {
|
||||
|
||||
create_unreconcile_docs(selection_map) {
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payments.unreconcile_payments.create_unreconcile_doc_for_selection",
|
||||
"method": "erpnext.accounts.doctype.unreconcile_payment.unreconcile_payment.create_unreconcile_doc_for_selection",
|
||||
"args": {
|
||||
"selections": selection_map
|
||||
},
|
||||
|
@ -1,315 +1,119 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-06-20 11:53:21",
|
||||
"custom": 0,
|
||||
"description": "Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have \"Is Stock Item\" as \"No\" and \"Is Sales Item\" as \"Yes\".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"basic_section",
|
||||
"new_item_code",
|
||||
"description",
|
||||
"column_break_eonk",
|
||||
"disabled",
|
||||
"item_section",
|
||||
"items",
|
||||
"section_break_4",
|
||||
"about"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "basic_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "",
|
||||
"fieldname": "new_item_code",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Parent Item",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "new_item_code",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Item",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Description",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "List items that form the package.",
|
||||
"fieldname": "item_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Items",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"label": "Items"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Items",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "sales_bom_items",
|
||||
"oldfieldtype": "Table",
|
||||
"options": "Product Bundle Item",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_4",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "about",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"options": "<h3>About Product Bundle</h3>\n\n<p>Aggregate group of <b>Items</b> into another <b>Item</b>. This is useful if you are bundling a certain <b>Items</b> into a package and you maintain stock of the packed <b>Items</b> and not the aggregate <b>Item</b>.</p>\n<p>The package <b>Item</b> will have <code>Is Stock Item</code> as <b>No</b> and <code>Is Sales Item</code> as <b>Yes</b>.</p>\n<h4>Example:</h4>\n<p>If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.</p>"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_eonk",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-sitemap",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2020-09-18 17:26:09.703215",
|
||||
"links": [],
|
||||
"modified": "2023-11-22 15:20:46.805114",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Product Bundle",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Stock User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"role": "Stock User"
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
"states": []
|
||||
}
|
@ -59,10 +59,12 @@ class ProductBundle(Document):
|
||||
"""Validates, main Item is not a stock item"""
|
||||
if frappe.db.get_value("Item", self.new_item_code, "is_stock_item"):
|
||||
frappe.throw(_("Parent Item {0} must not be a Stock Item").format(self.new_item_code))
|
||||
if frappe.db.get_value("Item", self.new_item_code, "is_fixed_asset"):
|
||||
frappe.throw(_("Parent Item {0} must not be a Fixed Asset").format(self.new_item_code))
|
||||
|
||||
def validate_child_items(self):
|
||||
for item in self.items:
|
||||
if frappe.db.exists("Product Bundle", item.item_code):
|
||||
if frappe.db.exists("Product Bundle", {"name": item.item_code, "disabled": 0}):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Child Item should not be a Product Bundle. Please remove Item {1} and Save"
|
||||
@ -73,12 +75,20 @@ class ProductBundle(Document):
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
|
||||
from erpnext.controllers.queries import get_match_cond
|
||||
product_bundles = frappe.db.get_list("Product Bundle", {"disabled": 0}, pluck="name")
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select name, item_name, description from tabItem
|
||||
where is_stock_item=0 and name not in (select name from `tabProduct Bundle`)
|
||||
and %s like %s %s limit %s offset %s"""
|
||||
% (searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
|
||||
("%%%s%%" % txt, page_len, start),
|
||||
item = frappe.qb.DocType("Item")
|
||||
query = (
|
||||
frappe.qb.from_(item)
|
||||
.select(item.item_code, item.item_name)
|
||||
.where(
|
||||
(item.is_stock_item == 0) & (item.is_fixed_asset == 0) & (item[searchfield].like(f"%{txt}%"))
|
||||
)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
|
||||
if product_bundles:
|
||||
query = query.where(item.name.notin(product_bundles))
|
||||
|
||||
return query.run()
|
||||
|
@ -214,13 +214,12 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Items to Reserve"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: "name",
|
||||
fieldname: "sales_order_item",
|
||||
fieldtype: "Data",
|
||||
label: __("Name"),
|
||||
label: __("Sales Order Item"),
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
},
|
||||
@ -260,7 +259,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Reserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = {items: dialog.fields_dict.items.grid.get_selected_children()};
|
||||
var data = {items: dialog.fields_dict.items.grid.data};
|
||||
|
||||
if (data.items && data.items.length > 0) {
|
||||
frappe.call({
|
||||
@ -278,9 +277,6 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(__("Please select items to reserve."));
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
},
|
||||
@ -292,7 +288,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
|
||||
if (unreserved_qty > 0) {
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
'name': item.name,
|
||||
'sales_order_item': item.name,
|
||||
'item_code': item.item_code,
|
||||
'warehouse': item.warehouse,
|
||||
'qty_to_reserve': (unreserved_qty / flt(item.conversion_factor))
|
||||
@ -308,7 +304,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
cancel_stock_reservation_entries(frm) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Stock Unreservation"),
|
||||
size: "large",
|
||||
size: "extra-large",
|
||||
fields: [
|
||||
{
|
||||
fieldname: "sr_entries",
|
||||
@ -316,14 +312,13 @@ frappe.ui.form.on("Sales Order", {
|
||||
label: __("Reserved Stock"),
|
||||
allow_bulk_edit: false,
|
||||
cannot_add_rows: true,
|
||||
cannot_delete_rows: true,
|
||||
in_place_edit: true,
|
||||
data: [],
|
||||
fields: [
|
||||
{
|
||||
fieldname: "name",
|
||||
fieldname: "sre",
|
||||
fieldtype: "Link",
|
||||
label: __("SRE"),
|
||||
label: __("Stock Reservation Entry"),
|
||||
options: "Stock Reservation Entry",
|
||||
reqd: 1,
|
||||
read_only: 1,
|
||||
@ -360,14 +355,14 @@ frappe.ui.form.on("Sales Order", {
|
||||
],
|
||||
primary_action_label: __("Unreserve Stock"),
|
||||
primary_action: () => {
|
||||
var data = {sr_entries: dialog.fields_dict.sr_entries.grid.get_selected_children()};
|
||||
var data = {sr_entries: dialog.fields_dict.sr_entries.grid.data};
|
||||
|
||||
if (data.sr_entries && data.sr_entries.length > 0) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "cancel_stock_reservation_entries",
|
||||
args: {
|
||||
sre_list: data.sr_entries,
|
||||
sre_list: data.sr_entries.map(item => item.sre),
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Unreserving Stock...'),
|
||||
@ -377,9 +372,6 @@ frappe.ui.form.on("Sales Order", {
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(__("Please select items to unreserve."));
|
||||
}
|
||||
|
||||
dialog.hide();
|
||||
},
|
||||
@ -396,7 +388,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
r.message.forEach(sre => {
|
||||
if (flt(sre.reserved_qty) > flt(sre.delivered_qty)) {
|
||||
dialog.fields_dict.sr_entries.df.data.push({
|
||||
'name': sre.name,
|
||||
'sre': sre.name,
|
||||
'item_code': sre.item_code,
|
||||
'warehouse': sre.warehouse,
|
||||
'qty': (flt(sre.reserved_qty) - flt(sre.delivered_qty))
|
||||
|
@ -688,7 +688,9 @@ def make_material_request(source_name, target_doc=None):
|
||||
"Sales Order Item": {
|
||||
"doctype": "Material Request Item",
|
||||
"field_map": {"name": "sales_order_item", "parent": "sales_order"},
|
||||
"condition": lambda item: not frappe.db.exists("Product Bundle", item.item_code)
|
||||
"condition": lambda item: not frappe.db.exists(
|
||||
"Product Bundle", {"name": item.item_code, "disabled": 0}
|
||||
)
|
||||
and get_remaining_qty(item) > 0,
|
||||
"postprocess": update_item,
|
||||
},
|
||||
@ -1309,7 +1311,7 @@ def set_delivery_date(items, sales_order):
|
||||
|
||||
|
||||
def is_product_bundle(item_code):
|
||||
return frappe.db.exists("Product Bundle", item_code)
|
||||
return frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0})
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -1521,7 +1523,7 @@ def get_work_order_items(sales_order, for_raw_material_request=0):
|
||||
product_bundle_parents = [
|
||||
pb.new_item_code
|
||||
for pb in frappe.get_all(
|
||||
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
|
||||
"Product Bundle", {"new_item_code": ["in", item_codes], "disabled": 0}, ["new_item_code"]
|
||||
)
|
||||
]
|
||||
|
||||
|
@ -51,6 +51,35 @@ class TestSalesOrder(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_sales_order_with_negative_rate(self):
|
||||
"""
|
||||
Test if negative rate is allowed in Sales Order via doc submission and update items
|
||||
"""
|
||||
so = make_sales_order(qty=1, rate=100, do_not_save=True)
|
||||
so.append("items", {"item_code": "_Test Item", "qty": 1, "rate": -10})
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
first_item = so.get("items")[0]
|
||||
second_item = so.get("items")[1]
|
||||
trans_item = json.dumps(
|
||||
[
|
||||
{
|
||||
"item_code": first_item.item_code,
|
||||
"rate": first_item.rate,
|
||||
"qty": first_item.qty,
|
||||
"docname": first_item.name,
|
||||
},
|
||||
{
|
||||
"item_code": second_item.item_code,
|
||||
"rate": -20,
|
||||
"qty": second_item.qty,
|
||||
"docname": second_item.name,
|
||||
},
|
||||
]
|
||||
)
|
||||
update_child_qty_rate("Sales Order", trans_item, so.name)
|
||||
|
||||
def test_make_material_request(self):
|
||||
so = make_sales_order(do_not_submit=True)
|
||||
|
||||
|
@ -200,6 +200,7 @@
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity",
|
||||
"non_negative": 1,
|
||||
"oldfieldname": "qty",
|
||||
"oldfieldtype": "Currency",
|
||||
"print_width": "100px",
|
||||
@ -895,7 +896,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-14 18:37:12.787893",
|
||||
"modified": "2023-11-24 13:24:55.756320",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order Item",
|
||||
|
0
erpnext/selling/report/lost_quotations/__init__.py
Normal file
0
erpnext/selling/report/lost_quotations/__init__.py
Normal file
40
erpnext/selling/report/lost_quotations/lost_quotations.js
Normal file
40
erpnext/selling/report/lost_quotations/lost_quotations.js
Normal file
@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Lost Quotations"] = {
|
||||
filters: [
|
||||
{
|
||||
fieldname: "company",
|
||||
label: __("Company"),
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
},
|
||||
{
|
||||
label: "Timespan",
|
||||
fieldtype: "Select",
|
||||
fieldname: "timespan",
|
||||
options: [
|
||||
"Last Week",
|
||||
"Last Month",
|
||||
"Last Quarter",
|
||||
"Last 6 months",
|
||||
"Last Year",
|
||||
"This Week",
|
||||
"This Month",
|
||||
"This Quarter",
|
||||
"This Year",
|
||||
],
|
||||
default: "This Year",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "group_by",
|
||||
label: __("Group By"),
|
||||
fieldtype: "Select",
|
||||
options: ["Lost Reason", "Competitor"],
|
||||
default: "Lost Reason",
|
||||
reqd: 1,
|
||||
},
|
||||
],
|
||||
};
|
30
erpnext/selling/report/lost_quotations/lost_quotations.json
Normal file
30
erpnext/selling/report/lost_quotations/lost_quotations.json
Normal file
@ -0,0 +1,30 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-11-23 18:00:19.141922",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": null,
|
||||
"letterhead": null,
|
||||
"modified": "2023-11-23 19:27:28.854108",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Lost Quotations",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Quotation",
|
||||
"report_name": "Lost Quotations",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Sales User"
|
||||
},
|
||||
{
|
||||
"role": "Sales Manager"
|
||||
}
|
||||
]
|
||||
}
|
98
erpnext/selling/report/lost_quotations/lost_quotations.py
Normal file
98
erpnext/selling/report/lost_quotations/lost_quotations.py
Normal file
@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from typing import Literal
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.docstatus import DocStatus
|
||||
from frappe.query_builder.functions import Coalesce, Count, Round, Sum
|
||||
from frappe.utils.data import get_timespan_date_range
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns = get_columns(filters.get("group_by"))
|
||||
from_date, to_date = get_timespan_date_range(filters.get("timespan").lower())
|
||||
data = get_data(filters.get("company"), from_date, to_date, filters.get("group_by"))
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns(group_by: Literal["Lost Reason", "Competitor"]):
|
||||
return [
|
||||
{
|
||||
"fieldname": "lost_reason" if group_by == "Lost Reason" else "competitor",
|
||||
"label": _("Lost Reason") if group_by == "Lost Reason" else _("Competitor"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Quotation Lost Reason" if group_by == "Lost Reason" else "Competitor",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"filedname": "lost_quotations",
|
||||
"label": _("Lost Quotations"),
|
||||
"fieldtype": "Int",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"filedname": "lost_quotations_pct",
|
||||
"label": _("Lost Quotations %"),
|
||||
"fieldtype": "Percent",
|
||||
"width": 200,
|
||||
},
|
||||
{
|
||||
"fieldname": "lost_value",
|
||||
"label": _("Lost Value"),
|
||||
"fieldtype": "Currency",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"filedname": "lost_value_pct",
|
||||
"label": _("Lost Value %"),
|
||||
"fieldtype": "Percent",
|
||||
"width": 200,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_data(
|
||||
company: str, from_date: str, to_date: str, group_by: Literal["Lost Reason", "Competitor"]
|
||||
):
|
||||
"""Return quotation value grouped by lost reason or competitor"""
|
||||
if group_by == "Lost Reason":
|
||||
fieldname = "lost_reason"
|
||||
dimension = frappe.qb.DocType("Quotation Lost Reason Detail")
|
||||
elif group_by == "Competitor":
|
||||
fieldname = "competitor"
|
||||
dimension = frappe.qb.DocType("Competitor Detail")
|
||||
else:
|
||||
frappe.throw(_("Invalid Group By"))
|
||||
|
||||
q = frappe.qb.DocType("Quotation")
|
||||
|
||||
lost_quotation_condition = (
|
||||
(q.status == "Lost")
|
||||
& (q.docstatus == DocStatus.submitted())
|
||||
& (q.transaction_date >= from_date)
|
||||
& (q.transaction_date <= to_date)
|
||||
& (q.company == company)
|
||||
)
|
||||
|
||||
from_lost_quotations = frappe.qb.from_(q).where(lost_quotation_condition)
|
||||
total_quotations = from_lost_quotations.select(Count(q.name))
|
||||
total_value = from_lost_quotations.select(Sum(q.base_net_total))
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(q)
|
||||
.select(
|
||||
Coalesce(dimension[fieldname], _("Not Specified")),
|
||||
Count(q.name).distinct(),
|
||||
Round((Count(q.name).distinct() / total_quotations * 100), 2),
|
||||
Sum(q.base_net_total),
|
||||
Round((Sum(q.base_net_total) / total_value * 100), 2),
|
||||
)
|
||||
.left_join(dimension)
|
||||
.on(dimension.parent == q.name)
|
||||
.where(lost_quotation_condition)
|
||||
.groupby(dimension[fieldname])
|
||||
)
|
||||
|
||||
return query.run()
|
@ -382,9 +382,10 @@ class EmailDigest(Document):
|
||||
"""Get income to date"""
|
||||
balance = 0.0
|
||||
count = 0
|
||||
fy_start_date = get_fiscal_year(self.future_to_date)[1]
|
||||
|
||||
for account in self.get_root_type_accounts(root_type):
|
||||
balance += get_balance_on(account, date=self.future_to_date)
|
||||
balance += get_balance_on(account, date=self.future_to_date, start_date=fy_start_date)
|
||||
count += get_count_on(account, fieldname, date=self.future_to_date)
|
||||
|
||||
if fieldname == "income":
|
||||
|
@ -1,83 +1,58 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 0,
|
||||
"autoname": "field:order_lost_reason",
|
||||
"beta": 0,
|
||||
"creation": "2013-01-10 16:34:24",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 0,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"order_lost_reason"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "order_lost_reason",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Quotation Lost Reason",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"oldfieldname": "order_lost_reason",
|
||||
"oldfieldtype": "Data",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-flag",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-07-25 05:24:25.533953",
|
||||
"links": [
|
||||
{
|
||||
"is_child_table": 1,
|
||||
"link_doctype": "Quotation Lost Reason Detail",
|
||||
"link_fieldname": "lost_reason",
|
||||
"parent_doctype": "Quotation",
|
||||
"table_fieldname": "lost_reasons"
|
||||
}
|
||||
],
|
||||
"modified": "2023-11-23 19:31:02.743353",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Quotation Lost Reason",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Master Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"track_seen": 0
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -615,7 +615,7 @@ class DeliveryNote(SellingController):
|
||||
items_list = [item.item_code for item in self.items]
|
||||
return frappe.db.get_all(
|
||||
"Product Bundle",
|
||||
filters={"new_item_code": ["in", items_list]},
|
||||
filters={"new_item_code": ["in", items_list], "disabled": 0},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
@ -938,7 +938,7 @@ def make_packing_slip(source_name, target_doc=None):
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda item: (
|
||||
not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code})
|
||||
not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code, "disabled": 0})
|
||||
and flt(item.packed_qty) < flt(item.qty)
|
||||
),
|
||||
},
|
||||
|
@ -1247,6 +1247,25 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
dn.reload()
|
||||
self.assertFalse(dn.items[0].target_warehouse)
|
||||
|
||||
def test_serial_no_status(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
item = make_item(
|
||||
"Test Serial Item For Status",
|
||||
{"has_serial_no": 1, "is_stock_item": 1, "serial_no_series": "TESTSERIAL.#####"},
|
||||
)
|
||||
|
||||
item_code = item.name
|
||||
pi = make_purchase_receipt(qty=1, item_code=item.name)
|
||||
pi.reload()
|
||||
serial_no = get_serial_nos_from_bundle(pi.items[0].serial_and_batch_bundle)
|
||||
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active")
|
||||
|
||||
dn = create_delivery_note(qty=1, item_code=item_code, serial_no=serial_no)
|
||||
dn.reload()
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Delivered")
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
@ -512,8 +512,12 @@ class Item(Document):
|
||||
|
||||
def validate_duplicate_product_bundles_before_merge(self, old_name, new_name):
|
||||
"Block merge if both old and new items have product bundles."
|
||||
old_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": old_name})
|
||||
new_bundle = frappe.get_value("Product Bundle", filters={"new_item_code": new_name})
|
||||
old_bundle = frappe.get_value(
|
||||
"Product Bundle", filters={"new_item_code": old_name, "disabled": 0}
|
||||
)
|
||||
new_bundle = frappe.get_value(
|
||||
"Product Bundle", filters={"new_item_code": new_name, "disabled": 0}
|
||||
)
|
||||
|
||||
if old_bundle and new_bundle:
|
||||
bundle_link = get_link_to_form("Product Bundle", old_bundle)
|
||||
|
@ -55,7 +55,7 @@ def make_packing_list(doc):
|
||||
|
||||
|
||||
def is_product_bundle(item_code: str) -> bool:
|
||||
return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code}))
|
||||
return bool(frappe.db.exists("Product Bundle", {"new_item_code": item_code, "disabled": 0}))
|
||||
|
||||
|
||||
def get_indexed_packed_items_table(doc):
|
||||
@ -111,7 +111,7 @@ def get_product_bundle_items(item_code):
|
||||
product_bundle_item.uom,
|
||||
product_bundle_item.description,
|
||||
)
|
||||
.where(product_bundle.new_item_code == item_code)
|
||||
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
|
||||
.orderby(product_bundle_item.idx)
|
||||
)
|
||||
return query.run(as_dict=True)
|
||||
|
@ -233,7 +233,7 @@ class PickList(Document):
|
||||
for location in self.locations:
|
||||
if location.warehouse and location.sales_order and location.sales_order_item:
|
||||
item_details = {
|
||||
"name": location.sales_order_item,
|
||||
"sales_order_item": location.sales_order_item,
|
||||
"item_code": location.item_code,
|
||||
"warehouse": location.warehouse,
|
||||
"qty_to_reserve": (flt(location.picked_qty) - flt(location.stock_reserved_qty)),
|
||||
@ -368,7 +368,9 @@ class PickList(Document):
|
||||
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
|
||||
if not cint(
|
||||
frappe.get_cached_value("Item", item.item_code, "is_stock_item")
|
||||
) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
|
||||
) and not frappe.db.exists(
|
||||
"Product Bundle", {"new_item_code": item.item_code, "disabled": 0}
|
||||
):
|
||||
continue
|
||||
item_code = item.item_code
|
||||
reference = item.sales_order_item or item.material_request_item
|
||||
@ -507,7 +509,9 @@ class PickList(Document):
|
||||
# bundle_item_code: Dict[component, qty]
|
||||
product_bundle_qty_map = {}
|
||||
for bundle_item_code in bundles:
|
||||
bundle = frappe.get_last_doc("Product Bundle", {"new_item_code": bundle_item_code})
|
||||
bundle = frappe.get_last_doc(
|
||||
"Product Bundle", {"new_item_code": bundle_item_code, "disabled": 0}
|
||||
)
|
||||
product_bundle_qty_map[bundle_item_code] = {item.item_code: item.qty for item in bundle.items}
|
||||
return product_bundle_qty_map
|
||||
|
||||
|
@ -781,7 +781,7 @@ class PurchaseReceipt(BuyingController):
|
||||
for item in self.items:
|
||||
if item.sales_order and item.sales_order_item:
|
||||
item_details = {
|
||||
"name": item.sales_order_item,
|
||||
"sales_order_item": item.sales_order_item,
|
||||
"item_code": item.item_code,
|
||||
"warehouse": item.warehouse,
|
||||
"qty_to_reserve": item.stock_qty,
|
||||
|
@ -18,3 +18,22 @@ cur_frm.cscript.onload = function() {
|
||||
frappe.ui.form.on("Serial No", "refresh", function(frm) {
|
||||
frm.toggle_enable("item_code", frm.doc.__islocal);
|
||||
});
|
||||
|
||||
|
||||
frappe.ui.form.on("Serial No", {
|
||||
refresh(frm) {
|
||||
frm.trigger("view_ledgers")
|
||||
},
|
||||
|
||||
view_ledgers(frm) {
|
||||
frm.add_custom_button(__("View Ledgers"), () => {
|
||||
frappe.route_options = {
|
||||
"item_code": frm.doc.item_code,
|
||||
"serial_no": frm.doc.name,
|
||||
"posting_date": frappe.datetime.now_date(),
|
||||
"posting_time": frappe.datetime.now_time()
|
||||
};
|
||||
frappe.set_route("query-report", "Serial No Ledger");
|
||||
}).addClass('btn-primary');
|
||||
}
|
||||
})
|
@ -269,7 +269,7 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "\nActive\nInactive\nExpired",
|
||||
"options": "\nActive\nInactive\nDelivered\nExpired",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -280,7 +280,7 @@
|
||||
"icon": "fa fa-barcode",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-16 15:58:46.139887",
|
||||
"modified": "2023-11-28 15:37:59.489945",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Serial No",
|
||||
|
@ -869,7 +869,7 @@ def create_stock_reservation_entries_for_so_items(
|
||||
items = []
|
||||
if items_details:
|
||||
for item in items_details:
|
||||
so_item = frappe.get_doc("Sales Order Item", item.get("name"))
|
||||
so_item = frappe.get_doc("Sales Order Item", item.get("sales_order_item"))
|
||||
so_item.warehouse = item.get("warehouse")
|
||||
so_item.qty_to_reserve = (
|
||||
flt(item.get("qty_to_reserve"))
|
||||
@ -1053,12 +1053,14 @@ def cancel_stock_reservation_entries(
|
||||
from_voucher_type: Literal["Pick List", "Purchase Receipt"] = None,
|
||||
from_voucher_no: str = None,
|
||||
from_voucher_detail_no: str = None,
|
||||
sre_list: list[dict] = None,
|
||||
sre_list: list = None,
|
||||
notify: bool = True,
|
||||
) -> None:
|
||||
"""Cancel Stock Reservation Entries."""
|
||||
|
||||
if not sre_list:
|
||||
sre_list = {}
|
||||
|
||||
if voucher_type and voucher_no:
|
||||
sre_list = get_stock_reservation_entries_for_voucher(
|
||||
voucher_type, voucher_no, voucher_detail_no, fields=["name"]
|
||||
@ -1082,9 +1084,11 @@ def cancel_stock_reservation_entries(
|
||||
|
||||
sre_list = query.run(as_dict=True)
|
||||
|
||||
sre_list = [d.name for d in sre_list]
|
||||
|
||||
if sre_list:
|
||||
for sre in sre_list:
|
||||
frappe.get_doc("Stock Reservation Entry", sre["name"]).cancel()
|
||||
frappe.get_doc("Stock Reservation Entry", sre).cancel()
|
||||
|
||||
if notify:
|
||||
msg = _("Stock Reservation Entries Cancelled")
|
||||
|
@ -149,7 +149,7 @@ def remove_standard_fields(details):
|
||||
|
||||
|
||||
def set_valuation_rate(out, args):
|
||||
if frappe.db.exists("Product Bundle", args.item_code, cache=True):
|
||||
if frappe.db.exists("Product Bundle", {"name": args.item_code, "disabled": 0}, cache=True):
|
||||
valuation_rate = 0.0
|
||||
bundled_items = frappe.get_doc("Product Bundle", args.item_code)
|
||||
|
||||
|
@ -36,21 +36,27 @@ def get_columns(filters):
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "company",
|
||||
"options": "Company",
|
||||
"width": 150,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Warehouse"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "warehouse",
|
||||
"options": "Warehouse",
|
||||
"width": 150,
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Status"),
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "status",
|
||||
"width": 120,
|
||||
},
|
||||
{
|
||||
"label": _("Serial No"),
|
||||
"fieldtype": "Link",
|
||||
"fieldname": "serial_no",
|
||||
"options": "Serial No",
|
||||
"width": 150,
|
||||
"width": 130,
|
||||
},
|
||||
{
|
||||
"label": _("Valuation Rate"),
|
||||
@ -58,6 +64,12 @@ def get_columns(filters):
|
||||
"fieldname": "valuation_rate",
|
||||
"width": 150,
|
||||
},
|
||||
{
|
||||
"label": _("Qty"),
|
||||
"fieldtype": "Float",
|
||||
"fieldname": "qty",
|
||||
"width": 150,
|
||||
},
|
||||
]
|
||||
|
||||
return columns
|
||||
@ -83,12 +95,16 @@ def get_data(filters):
|
||||
"posting_time": row.posting_time,
|
||||
"voucher_type": row.voucher_type,
|
||||
"voucher_no": row.voucher_no,
|
||||
"status": "Active" if row.actual_qty > 0 else "Delivered",
|
||||
"company": row.company,
|
||||
"warehouse": row.warehouse,
|
||||
"qty": 1 if row.actual_qty > 0 else -1,
|
||||
}
|
||||
)
|
||||
|
||||
serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
|
||||
serial_nos = [{"serial_no": row.serial_no, "valuation_rate": row.valuation_rate}]
|
||||
if row.serial_and_batch_bundle:
|
||||
serial_nos = bundle_wise_serial_nos.get(row.serial_and_batch_bundle, [])
|
||||
|
||||
for index, bundle_data in enumerate(serial_nos):
|
||||
if index == 0:
|
||||
|
@ -166,4 +166,4 @@ def create_reposting_entries(rows, company):
|
||||
|
||||
if entries:
|
||||
entries = ", ".join(entries)
|
||||
frappe.msgprint(_(f"Reposting entries created: {entries}"))
|
||||
frappe.msgprint(_("Reposting entries created: {0}").format(entries))
|
||||
|
@ -255,11 +255,15 @@ class SerialBatchBundle:
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
status = "Inactive"
|
||||
if self.sle.actual_qty < 0:
|
||||
status = "Delivered"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
(
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warehouse, warehouse)
|
||||
.set(sn_table.status, "Active" if warehouse else "Inactive")
|
||||
.set(sn_table.status, "Active" if warehouse else status)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
).run()
|
||||
|
||||
|
@ -98,6 +98,7 @@ class TransactionBase(StatusUpdater):
|
||||
"Selling Settings", "None", ["maintain_same_rate_action", "role_to_override_stop_action"]
|
||||
)
|
||||
|
||||
stop_actions = []
|
||||
for ref_dt, ref_dn_field, ref_link_field in ref_details:
|
||||
reference_names = [d.get(ref_link_field) for d in self.get("items") if d.get(ref_link_field)]
|
||||
reference_details = self.get_reference_details(reference_names, ref_dt + " Item")
|
||||
@ -108,7 +109,7 @@ class TransactionBase(StatusUpdater):
|
||||
if abs(flt(d.rate - ref_rate, d.precision("rate"))) >= 0.01:
|
||||
if action == "Stop":
|
||||
if role_allowed_to_override not in frappe.get_roles():
|
||||
frappe.throw(
|
||||
stop_actions.append(
|
||||
_("Row #{0}: Rate must be same as {1}: {2} ({3} / {4})").format(
|
||||
d.idx, ref_dt, d.get(ref_dn_field), d.rate, ref_rate
|
||||
)
|
||||
@ -121,6 +122,8 @@ class TransactionBase(StatusUpdater):
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
if stop_actions:
|
||||
frappe.throw(stop_actions, as_list=True)
|
||||
|
||||
def get_reference_details(self, reference_names, reference_doctype):
|
||||
return frappe._dict(
|
||||
|
Loading…
x
Reference in New Issue
Block a user