Merge branch 'develop' into payment-reco-company-field
This commit is contained in:
commit
3f383d81bd
@ -36,16 +36,16 @@
|
||||
}
|
||||
},
|
||||
"Fixed Assets": {
|
||||
"Capital Equipments": {
|
||||
"Capital Equipment": {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Electronic Equipments": {
|
||||
"Electronic Equipment": {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Furnitures and Fixtures": {
|
||||
"Furniture and Fixtures": {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Office Equipments": {
|
||||
"Office Equipment": {
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Plants and Machineries": {
|
||||
|
@ -23,13 +23,13 @@ def get():
|
||||
_("Tax Assets"): {"is_group": 1},
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipments"): {"account_type": "Fixed Asset"},
|
||||
_("Electronic Equipments"): {"account_type": "Fixed Asset"},
|
||||
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset"},
|
||||
_("Office Equipments"): {"account_type": "Fixed Asset"},
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset"},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset"},
|
||||
_("Softwares"): {"account_type": "Fixed Asset"},
|
||||
_("Software"): {"account_type": "Fixed Asset"},
|
||||
_("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
|
||||
_("CWIP Account"): {
|
||||
"account_type": "Capital Work in Progress",
|
||||
|
@ -36,13 +36,13 @@ def get():
|
||||
"account_number": "1100-1600",
|
||||
},
|
||||
_("Fixed Assets"): {
|
||||
_("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"},
|
||||
_("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"},
|
||||
_("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
|
||||
_("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"},
|
||||
_("Capital Equipment"): {"account_type": "Fixed Asset", "account_number": "1710"},
|
||||
_("Electronic Equipment"): {"account_type": "Fixed Asset", "account_number": "1720"},
|
||||
_("Furniture and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
|
||||
_("Office Equipment"): {"account_type": "Fixed Asset", "account_number": "1740"},
|
||||
_("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
|
||||
_("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
|
||||
_("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"},
|
||||
_("Software"): {"account_type": "Fixed Asset", "account_number": "1770"},
|
||||
_("Accumulated Depreciation"): {
|
||||
"account_type": "Accumulated Depreciation",
|
||||
"account_number": "1780",
|
||||
|
@ -119,7 +119,7 @@ class TestAccount(unittest.TestCase):
|
||||
InvalidAccountMergeError,
|
||||
merge_account,
|
||||
"Capital Stock - _TC",
|
||||
"Softwares - _TC",
|
||||
"Software - _TC",
|
||||
)
|
||||
|
||||
# Raise error as currency doesn't match
|
||||
|
@ -55,7 +55,7 @@ class BankAccount(Document):
|
||||
|
||||
def validate_company(self):
|
||||
if self.is_company_account and not self.company:
|
||||
frappe.throw(_("Company is manadatory for company account"))
|
||||
frappe.throw(_("Company is mandatory for company account"))
|
||||
|
||||
def validate_iban(self):
|
||||
"""
|
||||
|
@ -48,11 +48,11 @@ class BankGuarantee(Document):
|
||||
|
||||
def on_submit(self):
|
||||
if not self.bank_guarantee_number:
|
||||
frappe.throw(_("Enter the Bank Guarantee Number before submittting."))
|
||||
frappe.throw(_("Enter the Bank Guarantee Number before submitting."))
|
||||
if not self.name_of_beneficiary:
|
||||
frappe.throw(_("Enter the name of the Beneficiary before submittting."))
|
||||
frappe.throw(_("Enter the name of the Beneficiary before submitting."))
|
||||
if not self.bank:
|
||||
frappe.throw(_("Enter the name of the bank or lending institution before submittting."))
|
||||
frappe.throw(_("Enter the name of the bank or lending institution before submitting."))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -80,7 +80,7 @@
|
||||
{
|
||||
"fieldname": "valid_upto",
|
||||
"fieldtype": "Date",
|
||||
"label": "Valid Upto"
|
||||
"label": "Valid Up To"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.coupon_type == \"Promotional\"",
|
||||
@ -115,7 +115,7 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-19 14:48:14.602481",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Coupon Code",
|
||||
|
@ -82,11 +82,11 @@
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2020-11-05 12:16:53.081573",
|
||||
"modified": "2024-01-17 13:06:01.608953",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year",
|
||||
"owner": "Administrator",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
@ -118,6 +118,14 @@
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Employee"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Accounts Manager"
|
||||
},
|
||||
{
|
||||
"read": 1,
|
||||
"role": "Stock Manager"
|
||||
}
|
||||
],
|
||||
"show_name_in_global_search": 1,
|
||||
|
@ -154,7 +154,7 @@ frappe.ui.form.on('Invoice Discounting', {
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Get Invocies')
|
||||
primary_action_label: __('Get Invoices')
|
||||
});
|
||||
d.show();
|
||||
},
|
||||
|
@ -186,9 +186,12 @@ class JournalEntry(AccountsController):
|
||||
|
||||
def update_advance_paid(self):
|
||||
advance_paid = frappe._dict()
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
for d in self.get("accounts"):
|
||||
if d.is_advance:
|
||||
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.reference_type in advance_payment_doctypes:
|
||||
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
|
||||
|
||||
for voucher_type, order_list in advance_paid.items():
|
||||
|
@ -270,7 +270,7 @@ def start_import(invoices):
|
||||
errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>"
|
||||
),
|
||||
indicator="red",
|
||||
title=_("Error Occured"),
|
||||
title=_("Error Occurred"),
|
||||
)
|
||||
return names
|
||||
|
||||
|
@ -640,7 +640,7 @@ frappe.ui.form.on('Payment Entry', {
|
||||
|
||||
get_outstanding_invoices_or_orders: function(frm, get_outstanding_invoices, get_orders_to_be_billed) {
|
||||
const today = frappe.datetime.get_today();
|
||||
const fields = [
|
||||
let fields = [
|
||||
{fieldtype:"Section Break", label: __("Posting Date")},
|
||||
{fieldtype:"Date", label: __("From Date"),
|
||||
fieldname:"from_posting_date", default:frappe.datetime.add_days(today, -30)},
|
||||
@ -655,18 +655,29 @@ frappe.ui.form.on('Payment Entry', {
|
||||
fieldname:"outstanding_amt_greater_than", default: 0},
|
||||
{fieldtype:"Column Break"},
|
||||
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
|
||||
{fieldtype:"Section Break"},
|
||||
{fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {"company": frm.doc.company}
|
||||
}
|
||||
];
|
||||
|
||||
if (frm.dimension_filters) {
|
||||
let column_break_insertion_point = Math.ceil((frm.dimension_filters.length)/2);
|
||||
|
||||
fields.push({fieldtype:"Section Break"});
|
||||
frm.dimension_filters.map((elem, idx)=>{
|
||||
fields.push({
|
||||
fieldtype: "Link",
|
||||
label: elem.document_type == "Cost Center" ? "Cost Center" : elem.label,
|
||||
options: elem.document_type,
|
||||
fieldname: elem.fieldname || elem.document_type
|
||||
});
|
||||
if(idx+1 == column_break_insertion_point) {
|
||||
fields.push({fieldtype:"Column Break"});
|
||||
}
|
||||
},
|
||||
{fieldtype:"Column Break"},
|
||||
});
|
||||
}
|
||||
|
||||
fields = fields.concat([
|
||||
{fieldtype:"Section Break"},
|
||||
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
|
||||
];
|
||||
]);
|
||||
|
||||
let btn_text = "";
|
||||
|
||||
|
@ -13,6 +13,7 @@ from pypika import Case
|
||||
from pypika.functions import Coalesce, Sum
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.bank_account.bank_account import (
|
||||
get_bank_account_details,
|
||||
get_party_bank_account,
|
||||
@ -189,7 +190,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def set_liability_account(self):
|
||||
# Auto setting liability account should only be done during 'draft' status
|
||||
if self.docstatus > 0:
|
||||
if self.docstatus > 0 or self.payment_type == "Internal Transfer":
|
||||
return
|
||||
|
||||
if not frappe.db.get_value(
|
||||
@ -925,7 +926,10 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def calculate_base_allocated_amount_for_reference(self, d) -> float:
|
||||
base_allocated_amount = 0
|
||||
if d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
if d.reference_doctype in advance_payment_doctypes:
|
||||
# When referencing Sales/Purchase Order, use the source/target exchange rate depending on payment type.
|
||||
# This is so there are no Exchange Gain/Loss generated for such doctypes
|
||||
|
||||
@ -1423,8 +1427,11 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def update_advance_paid(self):
|
||||
if self.payment_type in ("Receive", "Pay") and self.party:
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
for d in self.get("references"):
|
||||
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
|
||||
if d.allocated_amount and d.reference_doctype in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
d.reference_doctype, d.reference_name, for_update=True
|
||||
).set_total_advance_paid()
|
||||
@ -1671,6 +1678,13 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if args.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, args.get(dim.fieldname))
|
||||
accounting_dimensions_filter.append(ple[dim.fieldname] == args.get(dim.fieldname))
|
||||
|
||||
date_fields_dict = {
|
||||
"posting_date": ["from_posting_date", "to_posting_date"],
|
||||
"due_date": ["from_due_date", "to_due_date"],
|
||||
@ -1904,6 +1918,12 @@ def get_orders_to_be_billed(
|
||||
if doc and hasattr(doc, "cost_center") and doc.cost_center:
|
||||
condition = " and cost_center='%s'" % cost_center
|
||||
|
||||
# dynamic dimension filters
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
if filters.get(dim.fieldname):
|
||||
condition += " and {0}='{1}'".format(dim.fieldname, filters.get(dim.fieldname))
|
||||
|
||||
if party_account_currency == company_currency:
|
||||
grand_total_field = "base_grand_total"
|
||||
rounded_total_field = "base_rounded_total"
|
||||
|
@ -95,6 +95,8 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.change_custom_button_type(__('Allocate'), null, 'default');
|
||||
}
|
||||
|
||||
this.frm.trigger("set_query_for_dimension_filters");
|
||||
|
||||
// check for any running reconciliation jobs
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
this.frm.call({
|
||||
@ -125,6 +127,25 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
|
||||
}
|
||||
set_query_for_dimension_filters() {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_reconciliation.payment_reconciliation.get_queries_for_dimension_filters",
|
||||
args: {
|
||||
company: this.frm.doc.company,
|
||||
},
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
r.message.forEach(x => {
|
||||
this.frm.set_query(x.fieldname, () => {
|
||||
return {
|
||||
'filters': x.filters
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
company() {
|
||||
this.frm.set_value('party', '');
|
||||
|
@ -25,7 +25,9 @@
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
@ -209,6 +211,18 @@
|
||||
"fieldname": "payment_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Payment"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval: doc.invoices.length == 0",
|
||||
"depends_on": "eval:doc.receivable_payable_account",
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions Filter"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
@ -10,6 +10,7 @@ from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_any_doc_running,
|
||||
)
|
||||
@ -70,6 +71,7 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions = []
|
||||
self.accounting_dimension_filter_conditions = []
|
||||
self.ple_posting_date_filter = []
|
||||
self.dimensions = get_dimensions()[0]
|
||||
|
||||
def load_from_db(self):
|
||||
# 'modified' attribute is required for `run_doc_method` to work properly.
|
||||
@ -172,6 +174,14 @@ class PaymentReconciliation(Document):
|
||||
if self.payment_name:
|
||||
condition.update({"name": self.payment_name})
|
||||
|
||||
# pass dynamic dimension filter values to query builder
|
||||
dimensions = {}
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
dimensions.update({dimension: self.get(dimension)})
|
||||
condition.update({"accounting_dimensions": dimensions})
|
||||
|
||||
payment_entries = get_advance_payment_entries_for_regional(
|
||||
self.party_type,
|
||||
self.party,
|
||||
@ -185,66 +195,67 @@ class PaymentReconciliation(Document):
|
||||
return payment_entries
|
||||
|
||||
def get_jv_entries(self):
|
||||
condition = self.get_conditions()
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
conditions = self.get_journal_filter_conditions()
|
||||
|
||||
# Dimension filters
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
conditions.append(jea[dimension] == self.get(dimension))
|
||||
|
||||
if self.payment_name:
|
||||
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
||||
conditions.append(je.name.like(f"%%{self.payment_name}%%"))
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
||||
conditions.append(jea.cost_center == self.cost_center)
|
||||
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
conditions.append(jea[dr_or_cr].gt(0))
|
||||
|
||||
bank_account_condition = (
|
||||
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
|
||||
if self.bank_cash_account:
|
||||
conditions.append(jea.against_account.like(f"%%{self.bank_cash_account}%%"))
|
||||
|
||||
journal_query = (
|
||||
qb.from_(je)
|
||||
.inner_join(jea)
|
||||
.on(jea.parent == je.name)
|
||||
.select(
|
||||
ConstantColumn("Journal Entry").as_("reference_type"),
|
||||
je.name.as_("reference_name"),
|
||||
je.posting_date,
|
||||
je.remark.as_("remarks"),
|
||||
jea.name.as_("reference_row"),
|
||||
jea[dr_or_cr].as_("amount"),
|
||||
jea.is_advance,
|
||||
jea.exchange_rate,
|
||||
jea.account_currency.as_("currency"),
|
||||
jea.cost_center.as_("cost_center"),
|
||||
)
|
||||
.where(
|
||||
(je.docstatus == 1)
|
||||
& (jea.party_type == self.party_type)
|
||||
& (jea.party == self.party)
|
||||
& (jea.account == self.receivable_payable_account)
|
||||
& (
|
||||
(jea.reference_type == "")
|
||||
| (jea.reference_type.isnull())
|
||||
| (jea.reference_type.isin(("Sales Order", "Purchase Order")))
|
||||
)
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.orderby(je.posting_date)
|
||||
)
|
||||
|
||||
limit = f"limit {self.payment_limit}" if self.payment_limit else " "
|
||||
if self.payment_limit:
|
||||
journal_query = journal_query.limit(self.payment_limit)
|
||||
|
||||
# nosemgrep
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
|
||||
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
|
||||
t2.account_currency as currency, t2.cost_center as cost_center
|
||||
from
|
||||
`tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where
|
||||
t1.name = t2.parent and t1.docstatus = 1 and t2.docstatus = 1
|
||||
and t2.party_type = %(party_type)s and t2.party = %(party)s
|
||||
and t2.account = %(account)s and {dr_or_cr} > 0 {condition}
|
||||
and (t2.reference_type is null or t2.reference_type = '' or
|
||||
(t2.reference_type in ('Sales Order', 'Purchase Order')
|
||||
and t2.reference_name is not null and t2.reference_name != ''))
|
||||
and (CASE
|
||||
WHEN t1.voucher_type in ('Debit Note', 'Credit Note')
|
||||
THEN 1=1
|
||||
ELSE {bank_account_condition}
|
||||
END)
|
||||
order by t1.posting_date
|
||||
{limit}
|
||||
""".format(
|
||||
**{
|
||||
"dr_or_cr": dr_or_cr,
|
||||
"bank_account_condition": bank_account_condition,
|
||||
"condition": condition,
|
||||
"limit": limit,
|
||||
}
|
||||
),
|
||||
{
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"account": self.receivable_payable_account,
|
||||
"bank_cash_account": "%%%s%%" % self.bank_cash_account,
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
journal_entries = journal_query.run(as_dict=True)
|
||||
|
||||
return list(journal_entries)
|
||||
|
||||
@ -298,6 +309,7 @@ class PaymentReconciliation(Document):
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
||||
get_payments=True,
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
)
|
||||
|
||||
for inv in return_outstanding:
|
||||
@ -447,8 +459,15 @@ class PaymentReconciliation(Document):
|
||||
row = self.append("allocation", {})
|
||||
row.update(entry)
|
||||
|
||||
def update_dimension_values_in_allocated_entries(self, res):
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
res[dimension] = self.get(dimension)
|
||||
return res
|
||||
|
||||
def get_allocated_entry(self, pay, inv, allocated_amount):
|
||||
return frappe._dict(
|
||||
res = frappe._dict(
|
||||
{
|
||||
"reference_type": pay.get("reference_type"),
|
||||
"reference_name": pay.get("reference_name"),
|
||||
@ -464,6 +483,9 @@ class PaymentReconciliation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
res = self.update_dimension_values_in_allocated_entries(res)
|
||||
return res
|
||||
|
||||
def reconcile_allocations(self, skip_ref_details_update_for_pe=False):
|
||||
adjust_allocations_for_taxes(self)
|
||||
dr_or_cr = (
|
||||
@ -486,10 +508,10 @@ class PaymentReconciliation(Document):
|
||||
reconciled_entry.append(payment_details)
|
||||
|
||||
if entry_list:
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe)
|
||||
reconcile_against_document(entry_list, skip_ref_details_update_for_pe, self.dimensions)
|
||||
|
||||
if dr_or_cr_notes:
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company)
|
||||
reconcile_dr_cr_note(dr_or_cr_notes, self.company, self.dimensions)
|
||||
|
||||
@frappe.whitelist()
|
||||
def reconcile(self):
|
||||
@ -518,7 +540,7 @@ class PaymentReconciliation(Document):
|
||||
self.get_unreconciled_entries()
|
||||
|
||||
def get_payment_details(self, row, dr_or_cr):
|
||||
return frappe._dict(
|
||||
payment_details = frappe._dict(
|
||||
{
|
||||
"voucher_type": row.get("reference_type"),
|
||||
"voucher_no": row.get("reference_name"),
|
||||
@ -541,6 +563,12 @@ class PaymentReconciliation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
for x in self.dimensions:
|
||||
if row.get(x.fieldname):
|
||||
payment_details[x.fieldname] = row.get(x.fieldname)
|
||||
|
||||
return payment_details
|
||||
|
||||
def check_mandatory_to_fetch(self):
|
||||
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
|
||||
if not self.get(fieldname):
|
||||
@ -648,6 +676,13 @@ class PaymentReconciliation(Document):
|
||||
if not invoices_to_reconcile:
|
||||
frappe.throw(_("No records found in Allocation table"))
|
||||
|
||||
def build_dimensions_filter_conditions(self):
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
for x in self.dimensions:
|
||||
dimension = x.fieldname
|
||||
if self.get(dimension):
|
||||
self.accounting_dimension_filter_conditions.append(ple[dimension] == self.get(dimension))
|
||||
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
self.common_filter_conditions.clear()
|
||||
self.accounting_dimension_filter_conditions.clear()
|
||||
@ -671,40 +706,30 @@ class PaymentReconciliation(Document):
|
||||
if self.to_payment_date:
|
||||
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
|
||||
|
||||
def get_conditions(self, get_payments=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
self.build_dimensions_filter_conditions()
|
||||
|
||||
if self.get("cost_center") and get_payments:
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
def get_journal_filter_conditions(self):
|
||||
conditions = []
|
||||
je = qb.DocType("Journal Entry")
|
||||
jea = qb.DocType("Journal Entry Account")
|
||||
conditions.append(je.company == self.company)
|
||||
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
if self.from_payment_date:
|
||||
conditions.append(je.posting_date.gte(self.from_payment_date))
|
||||
|
||||
if self.to_payment_date:
|
||||
conditions.append(je.posting_date.lte(self.to_payment_date))
|
||||
|
||||
if self.minimum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
||||
)
|
||||
conditions.append(je.total_debit.gte(self.minimum_payment_amount))
|
||||
|
||||
if self.maximum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
||||
)
|
||||
conditions.append(je.total_debit.lte(self.maximum_payment_amount))
|
||||
|
||||
return condition
|
||||
return conditions
|
||||
|
||||
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
for inv in dr_cr_notes:
|
||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||
|
||||
@ -754,6 +779,15 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
}
|
||||
)
|
||||
|
||||
# Credit Note(JE) will inherit the same dimension values as payment
|
||||
dimensions_dict = frappe._dict()
|
||||
if active_dimensions:
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = inv.get(dim.fieldname)
|
||||
|
||||
jv.accounts[0].update(dimensions_dict)
|
||||
jv.accounts[1].update(dimensions_dict)
|
||||
|
||||
jv.flags.ignore_mandatory = True
|
||||
jv.flags.ignore_exchange_rate = True
|
||||
jv.remark = None
|
||||
@ -787,9 +821,27 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
|
||||
inv.against_voucher,
|
||||
None,
|
||||
inv.cost_center,
|
||||
dimensions_dict,
|
||||
)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def adjust_allocations_for_taxes(doc):
|
||||
pass
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_queries_for_dimension_filters(company: str = None):
|
||||
dimensions_with_filters = []
|
||||
for d in get_dimensions()[0]:
|
||||
filters = {}
|
||||
meta = frappe.get_meta(d.document_type)
|
||||
if meta.has_field("company") and company:
|
||||
filters.update({"company": company})
|
||||
|
||||
if meta.is_tree:
|
||||
filters.update({"is_group": 0})
|
||||
|
||||
dimensions_with_filters.append({"fieldname": d.fieldname, "filters": filters})
|
||||
|
||||
return dimensions_with_filters
|
||||
|
@ -24,7 +24,9 @@
|
||||
"difference_account",
|
||||
"exchange_rate",
|
||||
"currency",
|
||||
"cost_center"
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -157,12 +159,21 @@
|
||||
"fieldname": "gain_loss_posting_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Difference Posting Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_virtual": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-11-17 17:33:38.612615",
|
||||
"modified": "2023-12-14 13:38:26.104150",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
@ -169,6 +169,13 @@ class PaymentRequest(Document):
|
||||
elif self.payment_channel == "Phone":
|
||||
self.request_phone_payment()
|
||||
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
# set advance payment status
|
||||
ref_doc.set_total_advance_paid()
|
||||
|
||||
def request_phone_payment(self):
|
||||
controller = _get_payment_gateway_controller(self.payment_gateway)
|
||||
request_amount = self.get_request_amount()
|
||||
@ -207,6 +214,14 @@ class PaymentRequest(Document):
|
||||
self.check_if_payment_entry_exists()
|
||||
self.set_as_cancelled()
|
||||
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
if self.reference_doctype in advance_payment_doctypes:
|
||||
# set advance payment status
|
||||
ref_doc.set_total_advance_paid()
|
||||
|
||||
def make_invoice(self):
|
||||
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
|
||||
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
|
||||
|
@ -371,7 +371,7 @@ class POSInvoice(SalesInvoice):
|
||||
if d.get("qty") > 0:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return."
|
||||
"Row #{}: You cannot add positive quantities in a return invoice. Please remove item {} to complete the return."
|
||||
).format(d.idx, frappe.bold(d.item_code)),
|
||||
title=_("Invalid Item"),
|
||||
)
|
||||
@ -793,7 +793,7 @@ def make_merge_log(invoices):
|
||||
invoices = json.loads(invoices)
|
||||
|
||||
if len(invoices) == 0:
|
||||
frappe.throw(_("Atleast one invoice has to be selected."))
|
||||
frappe.throw(_("At least one invoice has to be selected."))
|
||||
|
||||
merge_log = frappe.new_doc("POS Invoice Merge Log")
|
||||
merge_log.posting_date = getdate(nowdate())
|
||||
|
@ -132,7 +132,7 @@ class POSProfile(Document):
|
||||
|
||||
if len(customer_groups) != len(set(customer_groups)):
|
||||
frappe.throw(
|
||||
_("Duplicate customer group found in the cutomer group table"),
|
||||
_("Duplicate customer group found in the customer group table"),
|
||||
title=_("Duplicate Customer Group"),
|
||||
)
|
||||
|
||||
|
@ -339,7 +339,7 @@
|
||||
{
|
||||
"fieldname": "valid_upto",
|
||||
"fieldtype": "Date",
|
||||
"label": "Valid Upto"
|
||||
"label": "Valid Up To"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_break1",
|
||||
@ -608,7 +608,7 @@
|
||||
"icon": "fa fa-gift",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-14 04:53:34.887358",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Pricing Rule",
|
||||
|
@ -193,7 +193,7 @@ class PricingRule(Document):
|
||||
|
||||
def validate_applicable_for_selling_or_buying(self):
|
||||
if not self.selling and not self.buying:
|
||||
throw(_("Atleast one of the Selling or Buying must be selected"))
|
||||
throw(_("At least one of the Selling or Buying must be selected"))
|
||||
|
||||
if not self.selling and self.applicable_for in [
|
||||
"Customer",
|
||||
|
@ -232,7 +232,7 @@
|
||||
{
|
||||
"fieldname": "valid_upto",
|
||||
"fieldtype": "Date",
|
||||
"label": "Valid Upto"
|
||||
"label": "Valid Up To"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_26",
|
||||
@ -278,7 +278,7 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-05-06 16:20:22.039078",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Promotional Scheme",
|
||||
|
@ -64,6 +64,7 @@
|
||||
"warehouse",
|
||||
"from_warehouse",
|
||||
"quality_inspection",
|
||||
"add_serial_batch_bundle",
|
||||
"serial_and_batch_bundle",
|
||||
"serial_no",
|
||||
"col_br_wh",
|
||||
@ -913,12 +914,18 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Composite Asset",
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:parent.update_stock === 1",
|
||||
"fieldname": "add_serial_batch_bundle",
|
||||
"fieldtype": "Button",
|
||||
"label": "Add Serial / Batch No"
|
||||
}
|
||||
],
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-12-25 22:00:28.043555",
|
||||
"modified": "2024-01-21 19:46:25.537861",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
|
@ -51,7 +51,7 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
|
||||
"options": "\nTrialing\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -267,7 +267,7 @@
|
||||
"link_fieldname": "subscription"
|
||||
}
|
||||
],
|
||||
"modified": "2023-12-28 17:20:42.687789",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Subscription",
|
||||
|
@ -78,9 +78,7 @@ class Subscription(Document):
|
||||
purchase_tax_template: DF.Link | None
|
||||
sales_tax_template: DF.Link | None
|
||||
start_date: DF.Date | None
|
||||
status: DF.Literal[
|
||||
"", "Trialling", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"
|
||||
]
|
||||
status: DF.Literal["", "Trialing", "Active", "Past Due Date", "Cancelled", "Unpaid", "Completed"]
|
||||
submit_invoice: DF.Check
|
||||
trial_period_end: DF.Date | None
|
||||
trial_period_start: DF.Date | None
|
||||
@ -233,7 +231,7 @@ class Subscription(Document):
|
||||
Sets the status of the `Subscription`
|
||||
"""
|
||||
if self.is_trialling():
|
||||
self.status = "Trialling"
|
||||
self.status = "Trialing"
|
||||
elif (
|
||||
self.status == "Active" and self.end_date and getdate(posting_date) > getdate(self.end_date)
|
||||
):
|
||||
|
@ -1,7 +1,7 @@
|
||||
frappe.listview_settings['Subscription'] = {
|
||||
get_indicator: function(doc) {
|
||||
if(doc.status === 'Trialling') {
|
||||
return [__("Trialling"), "green"];
|
||||
if(doc.status === 'Trialing') {
|
||||
return [__("Trialing"), "green"];
|
||||
} else if(doc.status === 'Active') {
|
||||
return [__("Active"), "green"];
|
||||
} else if(doc.status === 'Completed') {
|
||||
|
@ -46,7 +46,7 @@ class TestSubscription(FrappeTestCase):
|
||||
get_date_str(subscription.current_invoice_end),
|
||||
)
|
||||
self.assertEqual(subscription.invoices, [])
|
||||
self.assertEqual(subscription.status, "Trialling")
|
||||
self.assertEqual(subscription.status, "Trialing")
|
||||
|
||||
def test_create_subscription_without_trial_with_correct_period(self):
|
||||
subscription = create_subscription()
|
||||
|
@ -4,7 +4,7 @@
|
||||
"doctype": "Form Tour",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-06-29 17:00:26.145996",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
@ -82,7 +82,7 @@
|
||||
"label": "Accounts Frozen Till Date",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Accounts Frozen Upto"
|
||||
"title": "Accounts Frozen Up To"
|
||||
},
|
||||
{
|
||||
"description": "Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.",
|
||||
|
@ -39,7 +39,7 @@ frappe.query_reports["Account Balance"] = {
|
||||
{ "value": "Asset Received But Not Billed", "label": __("Asset Received But Not Billed") },
|
||||
{ "value": "Bank", "label": __("Bank") },
|
||||
{ "value": "Cash", "label": __("Cash") },
|
||||
{ "value": "Chargeble", "label": __("Chargeble") },
|
||||
{ "value": "Chargeable", "label": __("Chargeable") },
|
||||
{ "value": "Capital Work in Progress", "label": __("Capital Work in Progress") },
|
||||
{ "value": "Cost of Goods Sold", "label": __("Cost of Goods Sold") },
|
||||
{ "value": "Depreciation", "label": __("Depreciation") },
|
||||
|
@ -10,10 +10,8 @@
|
||||
|
||||
<h2 class="text-center" style="margin-top:0">{%= __(report.report_name) %}</h2>
|
||||
<h4 class="text-center">
|
||||
{% if (filters.customer_name) { %}
|
||||
{%= filters.customer_name %}
|
||||
{% } else { %}
|
||||
{%= filters.customer || filters.supplier %}
|
||||
{% if (filters.party) { %}
|
||||
{%= __(filters.party) %}
|
||||
{% } %}
|
||||
</h4>
|
||||
<h6 class="text-center">
|
||||
@ -141,7 +139,7 @@
|
||||
<th style="width: 24%">{%= __("Reference") %}</th>
|
||||
{% } %}
|
||||
{% if(!filters.show_future_payments) { %}
|
||||
<th style="width: 20%">{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}</th>
|
||||
<th style="width: 20%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
|
||||
{% } %}
|
||||
<th style="width: 10%; text-align: right">{%= __("Invoiced Amount") %}</th>
|
||||
{% if(!filters.show_future_payments) { %}
|
||||
@ -158,7 +156,7 @@
|
||||
<th style="width: 10%">{%= __("Remaining Balance") %}</th>
|
||||
{% } %}
|
||||
{% } else { %}
|
||||
<th style="width: 40%">{%= (filters.customer || filters.supplier) ? __("Remarks"): __("Party") %}</th>
|
||||
<th style="width: 40%">{%= (filters.party) ? __("Remarks"): __("Party") %}</th>
|
||||
<th style="width: 15%">{%= __("Total Invoiced Amount") %}</th>
|
||||
<th style="width: 15%">{%= __("Total Paid Amount") %}</th>
|
||||
<th style="width: 15%">{%= report.report_name === "Accounts Receivable Summary" ? __('Credit Note Amount') : __('Debit Note Amount') %}</th>
|
||||
@ -187,7 +185,7 @@
|
||||
|
||||
{% if(!filters.show_future_payments) { %}
|
||||
<td>
|
||||
{% if(!(filters.customer || filters.supplier)) { %}
|
||||
{% if(!(filters.party)) { %}
|
||||
{%= data[i]["party"] %}
|
||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||
<br> {%= data[i]["customer_name"] %}
|
||||
@ -260,7 +258,7 @@
|
||||
{% if(data[i]["party"]|| " ") { %}
|
||||
{% if(!data[i]["is_total_row"]) { %}
|
||||
<td>
|
||||
{% if(!(filters.customer || filters.supplier)) { %}
|
||||
{% if(!(filters.party)) { %}
|
||||
{%= data[i]["party"] %}
|
||||
{% if(data[i]["customer_name"] && data[i]["customer_name"] != data[i]["party"]) { %}
|
||||
<br> {%= data[i]["customer_name"] %}
|
||||
|
@ -8,17 +8,7 @@ import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
getdate,
|
||||
today,
|
||||
)
|
||||
from frappe.utils import add_days, add_months, cint, cstr, flt, formatdate, get_first_day, getdate
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
@ -53,8 +43,6 @@ def get_period_list(
|
||||
year_start_date = getdate(period_start_date)
|
||||
year_end_date = getdate(period_end_date)
|
||||
|
||||
year_end_date = getdate(today()) if year_end_date > getdate(today()) else year_end_date
|
||||
|
||||
months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
|
||||
|
||||
period_list = []
|
||||
|
@ -46,12 +46,10 @@ def get_result(
|
||||
|
||||
out = []
|
||||
for name, details in gle_map.items():
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
bill_no, bill_date = "", ""
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
for entry in details:
|
||||
tax_amount, total_amount, grand_total, base_total = 0, 0, 0, 0
|
||||
tax_withholding_category, rate = None, None
|
||||
bill_no, bill_date = "", ""
|
||||
party = entry.party or entry.against
|
||||
posting_date = entry.posting_date
|
||||
voucher_type = entry.voucher_type
|
||||
@ -61,12 +59,19 @@ def get_result(
|
||||
if party_list:
|
||||
party = party_list[0]
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
if entry.account in tds_accounts:
|
||||
if entry.account in tds_accounts.keys():
|
||||
tax_amount += entry.credit - entry.debit
|
||||
# infer tax withholding category from the account if it's the single account for this category
|
||||
tax_withholding_category = tds_accounts.get(entry.account)
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
# or else the consolidated value from the voucher document
|
||||
if not tax_withholding_category:
|
||||
# or else from the party default
|
||||
tax_withholding_category = tax_category_map.get(name)
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category = party_map.get(party, {}).get("tax_withholding_category")
|
||||
rate = tax_rate_map.get(tax_withholding_category)
|
||||
|
||||
if net_total_map.get(name):
|
||||
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||
@ -80,41 +85,41 @@ def get_result(
|
||||
else:
|
||||
total_amount += entry.credit
|
||||
|
||||
if tax_amount:
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
party_name = "supplier_name"
|
||||
party_type = "supplier_type"
|
||||
else:
|
||||
party_name = "customer_name"
|
||||
party_type = "customer_type"
|
||||
if tax_amount:
|
||||
if party_map.get(party, {}).get("party_type") == "Supplier":
|
||||
party_name = "supplier_name"
|
||||
party_type = "supplier_type"
|
||||
else:
|
||||
party_name = "customer_name"
|
||||
party_type = "customer_type"
|
||||
|
||||
row = {
|
||||
"pan"
|
||||
if frappe.db.has_column(filters.party_type, "pan")
|
||||
else "tax_id": party_map.get(party, {}).get("pan"),
|
||||
"party": party_map.get(party, {}).get("name"),
|
||||
}
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
|
||||
row.update(
|
||||
{
|
||||
"section_code": tax_withholding_category or "",
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
"base_total": base_total,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
"transaction_type": voucher_type,
|
||||
"ref_no": name,
|
||||
"supplier_invoice_no": bill_no,
|
||||
"supplier_invoice_date": bill_date,
|
||||
row = {
|
||||
"pan"
|
||||
if frappe.db.has_column(filters.party_type, "pan")
|
||||
else "tax_id": party_map.get(party, {}).get("pan"),
|
||||
"party": party_map.get(party, {}).get("name"),
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
if filters.naming_series == "Naming Series":
|
||||
row.update({"party_name": party_map.get(party, {}).get(party_name)})
|
||||
|
||||
row.update(
|
||||
{
|
||||
"section_code": tax_withholding_category or "",
|
||||
"entity_type": party_map.get(party, {}).get(party_type),
|
||||
"rate": rate,
|
||||
"total_amount": total_amount,
|
||||
"grand_total": grand_total,
|
||||
"base_total": base_total,
|
||||
"tax_amount": tax_amount,
|
||||
"transaction_date": posting_date,
|
||||
"transaction_type": voucher_type,
|
||||
"ref_no": name,
|
||||
"supplier_invoice_no": bill_no,
|
||||
"supplier_invoice_date": bill_date,
|
||||
}
|
||||
)
|
||||
out.append(row)
|
||||
|
||||
out.sort(key=lambda x: x["section_code"])
|
||||
|
||||
@ -282,11 +287,20 @@ def get_tds_docs(filters):
|
||||
journal_entry_party_map = frappe._dict()
|
||||
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
|
||||
|
||||
tds_accounts = frappe.get_all(
|
||||
"Tax Withholding Account", {"company": filters.get("company")}, pluck="account"
|
||||
_tds_accounts = frappe.get_all(
|
||||
"Tax Withholding Account",
|
||||
{"company": filters.get("company")},
|
||||
["account", "parent"],
|
||||
)
|
||||
tds_accounts = {}
|
||||
for tds_acc in _tds_accounts:
|
||||
# if it turns out not to be the only tax withholding category, then don't include in the map
|
||||
if tds_accounts.get(tds_acc["account"]):
|
||||
tds_accounts[tds_acc["account"]] = None
|
||||
else:
|
||||
tds_accounts[tds_acc["account"]] = tds_acc["parent"]
|
||||
|
||||
tds_docs = get_tds_docs_query(filters, bank_accounts, tds_accounts).run(as_dict=True)
|
||||
tds_docs = get_tds_docs_query(filters, bank_accounts, list(tds_accounts.keys())).run(as_dict=True)
|
||||
|
||||
for d in tds_docs:
|
||||
if d.voucher_type == "Purchase Invoice":
|
||||
|
@ -453,7 +453,19 @@ def add_cc(args=None):
|
||||
return cc.name
|
||||
|
||||
|
||||
def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # nosemgrep
|
||||
def _build_dimensions_dict_for_exc_gain_loss(
|
||||
entry: dict | object = None, active_dimensions: list = None
|
||||
):
|
||||
dimensions_dict = frappe._dict()
|
||||
if entry and active_dimensions:
|
||||
for dim in active_dimensions:
|
||||
dimensions_dict[dim.fieldname] = entry.get(dim.fieldname)
|
||||
return dimensions_dict
|
||||
|
||||
|
||||
def reconcile_against_document(
|
||||
args, skip_ref_details_update_for_pe=False, active_dimensions=None
|
||||
): # nosemgrep
|
||||
"""
|
||||
Cancel PE or JV, Update against document, split if required and resubmit
|
||||
"""
|
||||
@ -482,6 +494,8 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
|
||||
check_if_advance_entry_modified(entry)
|
||||
validate_allocated_amount(entry)
|
||||
|
||||
dimensions_dict = _build_dimensions_dict_for_exc_gain_loss(entry, active_dimensions)
|
||||
|
||||
# update ref in advance entry
|
||||
if voucher_type == "Journal Entry":
|
||||
referenced_row = update_reference_in_journal_entry(entry, doc, do_not_save=False)
|
||||
@ -489,10 +503,14 @@ def reconcile_against_document(args, skip_ref_details_update_for_pe=False): # n
|
||||
# amount and account in args
|
||||
# referenced_row is used to deduplicate gain/loss journal
|
||||
entry.update({"referenced_row": referenced_row})
|
||||
doc.make_exchange_gain_loss_journal([entry])
|
||||
doc.make_exchange_gain_loss_journal([entry], dimensions_dict)
|
||||
else:
|
||||
referenced_row = update_reference_in_payment_entry(
|
||||
entry, doc, do_not_save=True, skip_ref_details_update_for_pe=skip_ref_details_update_for_pe
|
||||
entry,
|
||||
doc,
|
||||
do_not_save=True,
|
||||
skip_ref_details_update_for_pe=skip_ref_details_update_for_pe,
|
||||
dimensions_dict=dimensions_dict,
|
||||
)
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
@ -600,7 +618,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
if jv_detail.get("reference_type") in advance_payment_doctypes:
|
||||
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
|
||||
|
||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||
@ -654,7 +675,7 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
|
||||
def update_reference_in_payment_entry(
|
||||
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False
|
||||
d, payment_entry, do_not_save=False, skip_ref_details_update_for_pe=False, dimensions_dict=None
|
||||
):
|
||||
reference_details = {
|
||||
"reference_doctype": d.against_voucher_type,
|
||||
@ -667,13 +688,17 @@ def update_reference_in_payment_entry(
|
||||
else payment_entry.get_exchange_rate(),
|
||||
"exchange_gain_loss": d.difference_amount,
|
||||
"account": d.account,
|
||||
"dimensions": d.dimensions,
|
||||
}
|
||||
|
||||
if d.voucher_detail_no:
|
||||
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
# Update Advance Paid in SO/PO since they are getting unlinked
|
||||
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
|
||||
advance_payment_doctypes = frappe.get_hooks(
|
||||
"advance_payment_customer_doctypes"
|
||||
) + frappe.get_hooks("advance_payment_supplier_doctypes")
|
||||
if existing_row.get("reference_doctype") in advance_payment_doctypes:
|
||||
frappe.get_doc(
|
||||
existing_row.reference_doctype, existing_row.reference_name
|
||||
).set_total_advance_paid()
|
||||
@ -699,8 +724,9 @@ def update_reference_in_payment_entry(
|
||||
if not skip_ref_details_update_for_pe:
|
||||
payment_entry.set_missing_ref_details()
|
||||
payment_entry.set_amounts()
|
||||
|
||||
payment_entry.make_exchange_gain_loss_journal(
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date})
|
||||
frappe._dict({"difference_posting_date": d.difference_posting_date}), dimensions_dict
|
||||
)
|
||||
|
||||
if not do_not_save:
|
||||
@ -2042,6 +2068,7 @@ def create_gain_loss_journal(
|
||||
ref2_dn,
|
||||
ref2_detail_no,
|
||||
cost_center,
|
||||
dimensions,
|
||||
) -> str:
|
||||
journal_entry = frappe.new_doc("Journal Entry")
|
||||
journal_entry.voucher_type = "Exchange Gain Or Loss"
|
||||
@ -2075,7 +2102,8 @@ def create_gain_loss_journal(
|
||||
dr_or_cr + "_in_account_currency": 0,
|
||||
}
|
||||
)
|
||||
|
||||
if dimensions:
|
||||
journal_account.update(dimensions)
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_account = frappe._dict(
|
||||
@ -2091,7 +2119,8 @@ def create_gain_loss_journal(
|
||||
reverse_dr_or_cr: abs(exc_gain_loss),
|
||||
}
|
||||
)
|
||||
|
||||
if dimensions:
|
||||
journal_account.update(dimensions)
|
||||
journal_entry.append("accounts", journal_account)
|
||||
|
||||
journal_entry.save()
|
||||
|
@ -519,14 +519,11 @@ class Asset(AccountsController):
|
||||
movement.cancel()
|
||||
|
||||
def cancel_capitalization(self):
|
||||
asset_capitalization = frappe.db.get_value(
|
||||
"Asset Capitalization",
|
||||
{"target_asset": self.name, "docstatus": 1, "entry_type": "Capitalization"},
|
||||
)
|
||||
|
||||
if asset_capitalization:
|
||||
asset_capitalization = frappe.get_doc("Asset Capitalization", asset_capitalization)
|
||||
asset_capitalization.cancel()
|
||||
if self.capitalized_in:
|
||||
self.db_set("capitalized_in", None)
|
||||
asset_capitalization = frappe.get_doc("Asset Capitalization", self.capitalized_in)
|
||||
if asset_capitalization.docstatus == 1:
|
||||
asset_capitalization.cancel()
|
||||
|
||||
def delete_depreciation_entries(self):
|
||||
if self.calculate_depreciation:
|
||||
@ -1011,7 +1008,7 @@ def make_asset_movement(assets, purpose=None):
|
||||
assets = json.loads(assets)
|
||||
|
||||
if len(assets) == 0:
|
||||
frappe.throw(_("Atleast one asset has to be selected."))
|
||||
frappe.throw(_("At least one asset has to be selected."))
|
||||
|
||||
asset_movement = frappe.new_doc("Asset Movement")
|
||||
asset_movement.quantity = len(assets)
|
||||
|
@ -561,6 +561,8 @@ def modify_depreciation_schedule_for_asset_repairs(asset, notes):
|
||||
def reverse_depreciation_entry_made_after_disposal(asset, date):
|
||||
for row in asset.get("finance_books"):
|
||||
asset_depr_schedule_doc = get_asset_depr_schedule_doc(asset.name, "Active", row.finance_book)
|
||||
if not asset_depr_schedule_doc:
|
||||
continue
|
||||
|
||||
for schedule_idx, schedule in enumerate(asset_depr_schedule_doc.get("depreciation_schedule")):
|
||||
if schedule.schedule_date == date:
|
||||
|
@ -146,6 +146,7 @@ class AssetCapitalization(StockController):
|
||||
def cancel_target_asset(self):
|
||||
if self.entry_type == "Capitalization" and self.target_asset:
|
||||
asset_doc = frappe.get_doc("Asset", self.target_asset)
|
||||
frappe.db.set_value("Asset", self.target_asset, "capitalized_in", None)
|
||||
if asset_doc.docstatus == 1:
|
||||
asset_doc.cancel()
|
||||
|
||||
|
@ -40,7 +40,7 @@ class AssetMaintenance(Document):
|
||||
if getdate(task.next_due_date) < getdate(nowdate()):
|
||||
task.maintenance_status = "Overdue"
|
||||
if not task.assign_to and self.docstatus == 0:
|
||||
throw(_("Row #{}: Please asign task to a member.").format(task.idx))
|
||||
throw(_("Row #{}: Please assign task to a member.").format(task.idx))
|
||||
|
||||
def on_update(self):
|
||||
for task in self.get("asset_maintenance_tasks"):
|
||||
|
@ -2,14 +2,14 @@
|
||||
"action": "Show Form Tour",
|
||||
"action_label": "Let's review existing Asset Category",
|
||||
"creation": "2021-08-13 14:26:18.656303",
|
||||
"description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipments\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n",
|
||||
"description": "# Asset Category\n\nAn Asset Category classifies different assets of a Company.\n\nYou can create an Asset Category based on the type of assets. For example, all your desktops and laptops can be part of an Asset Category named \"Electronic Equipment\". Create a separate category for furniture. Also, you can update default properties for each category, like:\n - Depreciation type and duration\n - Fixed asset account\n - Depreciation account\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2021-11-23 10:02:03.242127",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Asset Category",
|
||||
"owner": "Administrator",
|
||||
|
@ -202,7 +202,7 @@ def prepare_chart_data(data, filters):
|
||||
"values": [flt(d.get("asset_value"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
{
|
||||
"name": _("Depreciatied Amount"),
|
||||
"name": _("Depreciated Amount"),
|
||||
"values": [flt(d.get("depreciated_amount"), 2) for d in labels_values_map.values()],
|
||||
},
|
||||
],
|
||||
|
@ -134,6 +134,7 @@
|
||||
"more_info_tab",
|
||||
"tracking_section",
|
||||
"status",
|
||||
"advance_payment_status",
|
||||
"column_break_75",
|
||||
"per_billed",
|
||||
"per_received",
|
||||
@ -1269,13 +1270,25 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_payment_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Advance Payment Status",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "Not Initiated\nInitiated\nPartially Paid\nFully Paid",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-01 20:58:07.851037",
|
||||
"modified": "2023-10-10 13:37:40.158761",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
@ -1330,4 +1343,4 @@
|
||||
"timeline_field": "supplier",
|
||||
"title_field": "supplier_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
@ -215,6 +215,10 @@ class PurchaseOrder(BuyingController):
|
||||
|
||||
self.validate_fg_item_for_subcontracting()
|
||||
self.set_received_qty_for_drop_ship_items()
|
||||
|
||||
if not self.advance_payment_status:
|
||||
self.advance_payment_status = "Not Initiated"
|
||||
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.supplier, self.company, self.inter_company_order_reference
|
||||
)
|
||||
|
@ -1,6 +1,6 @@
|
||||
frappe.listview_settings['Purchase Order'] = {
|
||||
add_fields: ["base_grand_total", "company", "currency", "supplier",
|
||||
"supplier_name", "per_received", "per_billed", "status"],
|
||||
"supplier_name", "per_received", "per_billed", "status", "advance_payment_status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status === "Closed") {
|
||||
return [__("Closed"), "green", "status,=,Closed"];
|
||||
@ -8,6 +8,8 @@ frappe.listview_settings['Purchase Order'] = {
|
||||
return [__("On Hold"), "orange", "status,=,On Hold"];
|
||||
} else if (doc.status === "Delivered") {
|
||||
return [__("Delivered"), "green", "status,=,Closed"];
|
||||
} else if (doc.advance_payment_status == "Initiated") {
|
||||
return [__("To Pay"), "gray", "advance_payment_status,=,Initiated"];
|
||||
} else if (flt(doc.per_received, 2) < 100 && doc.status !== "Closed") {
|
||||
if (flt(doc.per_billed, 2) < 100) {
|
||||
return [__("To Receive and Bill"), "orange",
|
||||
|
@ -1021,6 +1021,33 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
self.assertTrue(frappe.db.get_value("Subcontracting Order", {"purchase_order": po.name}))
|
||||
|
||||
def test_purchase_order_advance_payment_status(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
|
||||
po = create_purchase_order()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated"
|
||||
)
|
||||
|
||||
pr = make_payment_request(dt=po.doctype, dn=po.name, submit_doc=True, return_doc=True)
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
|
||||
|
||||
pe = get_payment_entry(po.doctype, po.name).save().submit()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Fully Paid"
|
||||
)
|
||||
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
self.assertEqual(frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Initiated")
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(po.doctype, po.name, "advance_payment_status"), "Not Initiated"
|
||||
)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
@ -54,7 +54,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
"fieldtype": "MultiSelectList",
|
||||
"width": "80",
|
||||
get_data: function(txt) {
|
||||
let status = ["To Bill", "To Receive", "To Receive and Bill", "Completed"]
|
||||
let status = ["To Pay", "To Bill", "To Receive", "To Receive and Bill", "Completed"]
|
||||
let options = []
|
||||
for (let option of status){
|
||||
options.push({
|
||||
|
@ -7,6 +7,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, bold, qb, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import (
|
||||
@ -21,12 +22,14 @@ from frappe.utils import (
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
nowdate,
|
||||
parse_json,
|
||||
today,
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
get_dimensions,
|
||||
)
|
||||
from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
apply_pricing_rule_for_free_items,
|
||||
@ -831,6 +834,37 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.extend("taxes", get_taxes_and_charges(tax_master_doctype, self.get("taxes_and_charges")))
|
||||
|
||||
def append_taxes_from_item_tax_template(self):
|
||||
if not frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template"):
|
||||
return
|
||||
|
||||
for row in self.items:
|
||||
item_tax_rate = row.get("item_tax_rate")
|
||||
if not item_tax_rate:
|
||||
continue
|
||||
|
||||
if isinstance(item_tax_rate, str):
|
||||
item_tax_rate = parse_json(item_tax_rate)
|
||||
|
||||
for account_head, rate in item_tax_rate.items():
|
||||
row = self.get_tax_row(account_head)
|
||||
|
||||
if not row:
|
||||
self.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": account_head,
|
||||
"rate": 0,
|
||||
"description": account_head,
|
||||
},
|
||||
)
|
||||
|
||||
def get_tax_row(self, account_head):
|
||||
for row in self.taxes:
|
||||
if row.account_head == account_head:
|
||||
return row
|
||||
|
||||
def set_other_charges(self):
|
||||
self.set("taxes", [])
|
||||
self.set_taxes()
|
||||
@ -1216,7 +1250,9 @@ class AccountsController(TransactionBase):
|
||||
return True
|
||||
return False
|
||||
|
||||
def make_exchange_gain_loss_journal(self, args: dict = None) -> None:
|
||||
def make_exchange_gain_loss_journal(
|
||||
self, args: dict = None, dimensions_dict: dict = None
|
||||
) -> None:
|
||||
"""
|
||||
Make Exchange Gain/Loss journal for Invoices and Payments
|
||||
"""
|
||||
@ -1271,6 +1307,7 @@ class AccountsController(TransactionBase):
|
||||
self.name,
|
||||
arg.get("referenced_row"),
|
||||
arg.get("cost_center"),
|
||||
dimensions_dict,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@ -1351,6 +1388,7 @@ class AccountsController(TransactionBase):
|
||||
self.name,
|
||||
d.idx,
|
||||
self.cost_center,
|
||||
dimensions_dict,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Exchange Gain/Loss amount has been booked through {0}").format(
|
||||
@ -1415,7 +1453,13 @@ class AccountsController(TransactionBase):
|
||||
if lst:
|
||||
from erpnext.accounts.utils import reconcile_against_document
|
||||
|
||||
reconcile_against_document(lst)
|
||||
# pass dimension values to utility method
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for x in lst:
|
||||
for dim in active_dimensions:
|
||||
if self.get(dim.fieldname):
|
||||
x.update({dim.fieldname: self.get(dim.fieldname)})
|
||||
reconcile_against_document(lst, active_dimensions=active_dimensions)
|
||||
|
||||
def on_cancel(self):
|
||||
from erpnext.accounts.doctype.bank_transaction.bank_transaction import (
|
||||
@ -1749,7 +1793,10 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def set_total_advance_paid(self):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
party = self.customer if self.doctype == "Sales Order" else self.supplier
|
||||
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
|
||||
party = self.customer
|
||||
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
|
||||
party = self.supplier
|
||||
advance = (
|
||||
frappe.qb.from_(ple)
|
||||
.select(ple.account_currency, Abs(Sum(ple.amount_in_account_currency)).as_("amount"))
|
||||
@ -1763,6 +1810,8 @@ class AccountsController(TransactionBase):
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
advance_paid, order_total = None, None
|
||||
|
||||
if advance:
|
||||
advance = advance[0]
|
||||
|
||||
@ -1791,7 +1840,38 @@ class AccountsController(TransactionBase):
|
||||
).format(formatted_advance_paid, self.name, formatted_order_total)
|
||||
)
|
||||
|
||||
frappe.db.set_value(self.doctype, self.name, "advance_paid", advance_paid)
|
||||
self.db_set("advance_paid", advance_paid)
|
||||
|
||||
self.set_advance_payment_status(advance_paid, order_total)
|
||||
|
||||
def set_advance_payment_status(
|
||||
self, advance_paid: float | None = None, order_total: float | None = None
|
||||
):
|
||||
new_status = None
|
||||
# if money is paid set the paid states
|
||||
if advance_paid:
|
||||
new_status = "Partially Paid" if advance_paid < order_total else "Fully Paid"
|
||||
|
||||
if not new_status:
|
||||
prs = frappe.db.count(
|
||||
"Payment Request",
|
||||
{
|
||||
"reference_doctype": self.doctype,
|
||||
"reference_name": self.name,
|
||||
"docstatus": 1,
|
||||
},
|
||||
)
|
||||
if self.doctype in frappe.get_hooks("advance_payment_customer_doctypes"):
|
||||
new_status = "Requested" if prs else "Not Requested"
|
||||
if self.doctype in frappe.get_hooks("advance_payment_supplier_doctypes"):
|
||||
new_status = "Initiated" if prs else "Not Initiated"
|
||||
|
||||
if new_status == self.advance_payment_status:
|
||||
return
|
||||
|
||||
self.db_set("advance_payment_status", new_status)
|
||||
self.set_status(update=True)
|
||||
self.notify_update()
|
||||
|
||||
@property
|
||||
def company_abbr(self):
|
||||
@ -2684,47 +2764,37 @@ def get_common_query(
|
||||
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
|
||||
|
||||
if condition:
|
||||
if condition.get("name", None):
|
||||
q = q.where(payment_entry.name.like(f"%{condition.get('name')}%"))
|
||||
# conditions should be built as an array and passed as Criterion
|
||||
common_filter_conditions = []
|
||||
|
||||
common_filter_conditions.append(payment_entry.company == condition["company"])
|
||||
if condition.get("name", None):
|
||||
common_filter_conditions.append(payment_entry.name.like(f"%{condition.get('name')}%"))
|
||||
|
||||
if condition.get("from_payment_date"):
|
||||
common_filter_conditions.append(payment_entry.posting_date.gte(condition["from_payment_date"]))
|
||||
|
||||
if condition.get("to_payment_date"):
|
||||
common_filter_conditions.append(payment_entry.posting_date.lte(condition["to_payment_date"]))
|
||||
|
||||
q = q.where(payment_entry.company == condition["company"])
|
||||
q = (
|
||||
q.where(payment_entry.posting_date >= condition["from_payment_date"])
|
||||
if condition.get("from_payment_date")
|
||||
else q
|
||||
)
|
||||
q = (
|
||||
q.where(payment_entry.posting_date <= condition["to_payment_date"])
|
||||
if condition.get("to_payment_date")
|
||||
else q
|
||||
)
|
||||
if condition.get("get_payments") == True:
|
||||
q = (
|
||||
q.where(payment_entry.cost_center == condition["cost_center"])
|
||||
if condition.get("cost_center")
|
||||
else q
|
||||
)
|
||||
q = (
|
||||
q.where(payment_entry.unallocated_amount >= condition["minimum_payment_amount"])
|
||||
if condition.get("minimum_payment_amount")
|
||||
else q
|
||||
)
|
||||
q = (
|
||||
q.where(payment_entry.unallocated_amount <= condition["maximum_payment_amount"])
|
||||
if condition.get("maximum_payment_amount")
|
||||
else q
|
||||
)
|
||||
else:
|
||||
q = (
|
||||
q.where(payment_entry.total_debit >= condition["minimum_payment_amount"])
|
||||
if condition.get("minimum_payment_amount")
|
||||
else q
|
||||
)
|
||||
q = (
|
||||
q.where(payment_entry.total_debit <= condition["maximum_payment_amount"])
|
||||
if condition.get("maximum_payment_amount")
|
||||
else q
|
||||
)
|
||||
if condition.get("cost_center"):
|
||||
common_filter_conditions.append(payment_entry.cost_center == condition["cost_center"])
|
||||
|
||||
if condition.get("accounting_dimensions"):
|
||||
for field, val in condition.get("accounting_dimensions").items():
|
||||
common_filter_conditions.append(payment_entry[field] == val)
|
||||
|
||||
if condition.get("minimum_payment_amount"):
|
||||
common_filter_conditions.append(
|
||||
payment_entry.unallocated_amount.gte(condition["minimum_payment_amount"])
|
||||
)
|
||||
|
||||
if condition.get("maximum_payment_amount"):
|
||||
common_filter_conditions.append(
|
||||
payment_entry.unallocated_amount.lte(condition["maximum_payment_amount"])
|
||||
)
|
||||
q = q.where(Criterion.all(common_filter_conditions))
|
||||
|
||||
q = q.orderby(payment_entry.posting_date)
|
||||
q = q.limit(limit) if limit else q
|
||||
|
@ -141,7 +141,7 @@ def validate_returned_items(doc):
|
||||
items_returned = True
|
||||
|
||||
if not items_returned:
|
||||
frappe.throw(_("Atleast one item should be entered with negative quantity in return document"))
|
||||
frappe.throw(_("At least one item should be entered with negative quantity in return document"))
|
||||
|
||||
|
||||
def validate_quantity(doc, args, ref, valid_items, already_returned_items):
|
||||
|
@ -53,6 +53,10 @@ status_map = {
|
||||
"To Deliver",
|
||||
"eval:self.per_delivered < 100 and self.per_billed >= 100 and self.docstatus == 1 and not self.skip_delivery_note",
|
||||
],
|
||||
[
|
||||
"To Pay",
|
||||
"eval:self.advance_payment_status == 'Requested' and self.docstatus == 1",
|
||||
],
|
||||
[
|
||||
"Completed",
|
||||
"eval:(self.per_delivered >= 100 or self.skip_delivery_note) and self.per_billed >= 100 and self.docstatus == 1",
|
||||
@ -63,15 +67,19 @@ status_map = {
|
||||
],
|
||||
"Purchase Order": [
|
||||
["Draft", None],
|
||||
[
|
||||
"To Receive and Bill",
|
||||
"eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1",
|
||||
],
|
||||
["To Bill", "eval:self.per_received >= 100 and self.per_billed < 100 and self.docstatus == 1"],
|
||||
[
|
||||
"To Receive",
|
||||
"eval:self.per_received < 100 and self.per_billed == 100 and self.docstatus == 1",
|
||||
],
|
||||
[
|
||||
"To Receive and Bill",
|
||||
"eval:self.per_received < 100 and self.per_billed < 100 and self.docstatus == 1",
|
||||
],
|
||||
[
|
||||
"To Pay",
|
||||
"eval:self.advance_payment_status == 'Initiated' and self.docstatus == 1",
|
||||
],
|
||||
[
|
||||
"Completed",
|
||||
"eval:self.per_received >= 100 and self.per_billed == 100 and self.docstatus == 1",
|
||||
|
@ -56,6 +56,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
20 series - Sales Invoice against Journals
|
||||
30 series - Sales Invoice against Credit Notes
|
||||
40 series - Company default Cost center is unset
|
||||
50 series - Dimension inheritence
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
@ -1255,3 +1256,214 @@ class TestAccountsController(FrappeTestCase):
|
||||
)
|
||||
|
||||
frappe.db.set_value("Company", self.company, "cost_center", cc)
|
||||
|
||||
def setup_dimensions(self):
|
||||
# create dimension
|
||||
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
|
||||
create_dimension,
|
||||
)
|
||||
|
||||
create_dimension()
|
||||
# make it non-mandatory
|
||||
loc = frappe.get_doc("Accounting Dimension", "Location")
|
||||
for x in loc.dimension_defaults:
|
||||
x.mandatory_for_bs = False
|
||||
x.mandatory_for_pl = False
|
||||
loc.save()
|
||||
|
||||
def test_50_dimensions_filter(self):
|
||||
"""
|
||||
Test workings of dimension filters
|
||||
"""
|
||||
self.setup_dimensions()
|
||||
rate_in_account_currency = 1
|
||||
|
||||
# Invoices
|
||||
si1 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||
si1.department = "Management"
|
||||
si1.save().submit()
|
||||
|
||||
si2 = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||
si2.department = "Operations"
|
||||
si2.save().submit()
|
||||
|
||||
# Payments
|
||||
cr_note1 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||
cr_note1.department = "Management"
|
||||
cr_note1.is_return = 1
|
||||
cr_note1.save().submit()
|
||||
|
||||
cr_note2 = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||
cr_note2.department = "Legal"
|
||||
cr_note2.is_return = 1
|
||||
cr_note2.save().submit()
|
||||
|
||||
pe1 = get_payment_entry(si1.doctype, si1.name)
|
||||
pe1.references = []
|
||||
pe1.department = "Research & Development"
|
||||
pe1.save().submit()
|
||||
|
||||
pe2 = get_payment_entry(si1.doctype, si1.name)
|
||||
pe2.references = []
|
||||
pe2.department = "Management"
|
||||
pe2.save().submit()
|
||||
|
||||
je1 = self.create_journal_entry(
|
||||
acc1=self.debit_usd,
|
||||
acc1_exc_rate=75,
|
||||
acc2=self.cash,
|
||||
acc1_amount=-1,
|
||||
acc2_amount=-75,
|
||||
acc2_exc_rate=1,
|
||||
)
|
||||
je1.accounts[0].party_type = "Customer"
|
||||
je1.accounts[0].party = self.customer
|
||||
je1.accounts[0].department = "Management"
|
||||
je1.save().submit()
|
||||
|
||||
# assert dimension filter's result
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 2)
|
||||
self.assertEqual(len(pr.payments), 5)
|
||||
|
||||
pr.department = "Legal"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
pr.department = "Management"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 3)
|
||||
|
||||
pr.department = "Research & Development"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
def test_51_cr_note_should_inherit_dimension(self):
|
||||
self.setup_dimensions()
|
||||
rate_in_account_currency = 1
|
||||
|
||||
# Invoice
|
||||
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_submit=True)
|
||||
si.department = "Management"
|
||||
si.save().submit()
|
||||
|
||||
# Payment
|
||||
cr_note = self.create_sales_invoice(qty=-1, conversion_rate=75, rate=1, do_not_save=True)
|
||||
cr_note.department = "Management"
|
||||
cr_note.is_return = 1
|
||||
cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.department = "Management"
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
# There should be 2 journals, JE(Cr Note) and JE(Exchange Gain/Loss)
|
||||
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||
exc_je_for_cr_note = self.get_journals_for(cr_note.doctype, cr_note.name)
|
||||
self.assertNotEqual(exc_je_for_si, [])
|
||||
self.assertEqual(len(exc_je_for_si), 2)
|
||||
self.assertEqual(len(exc_je_for_cr_note), 2)
|
||||
self.assertEqual(exc_je_for_si, exc_je_for_cr_note)
|
||||
|
||||
for x in exc_je_for_si + exc_je_for_cr_note:
|
||||
with self.subTest(x=x):
|
||||
self.assertEqual(
|
||||
[cr_note.department, cr_note.department],
|
||||
frappe.db.get_all("Journal Entry Account", filters={"parent": x.parent}, pluck="department"),
|
||||
)
|
||||
|
||||
def test_52_dimension_inhertiance_exc_gain_loss(self):
|
||||
# Sales Invoice in Foreign Currency
|
||||
self.setup_dimensions()
|
||||
rate = 80
|
||||
rate_in_account_currency = 1
|
||||
dpt = "Research & Development"
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=rate_in_account_currency, do_not_save=True)
|
||||
si.department = dpt
|
||||
si.save().submit()
|
||||
|
||||
pe = self.create_payment_entry(amount=1, source_exc_rate=82).save()
|
||||
pe.department = dpt
|
||||
pe = pe.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.department = dpt
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [x.as_dict() for x in pr.payments]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
self.assertEqual(len(pr.invoices), 0)
|
||||
self.assertEqual(len(pr.payments), 0)
|
||||
|
||||
# Exc Gain/Loss journals should inherit dimension from parent
|
||||
journals = self.get_journals_for(si.doctype, si.name)
|
||||
self.assertEqual(
|
||||
[dpt, dpt],
|
||||
frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"parent": ("in", [x.parent for x in journals])},
|
||||
pluck="department",
|
||||
),
|
||||
)
|
||||
|
||||
def test_53_dimension_inheritance_on_advance(self):
|
||||
self.setup_dimensions()
|
||||
dpt = "Research & Development"
|
||||
|
||||
adv = self.create_payment_entry(amount=1, source_exc_rate=85)
|
||||
adv.department = dpt
|
||||
adv.save().submit()
|
||||
adv.reload()
|
||||
|
||||
# Sales Invoices in different exchange rates
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=82, rate=1, do_not_submit=True)
|
||||
si.department = dpt
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(len(advances), 1)
|
||||
self.assertEqual(advances[0].reference_name, adv.name)
|
||||
si.append(
|
||||
"advances",
|
||||
{
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": advances[0].reference_type,
|
||||
"reference_name": advances[0].reference_name,
|
||||
"reference_row": advances[0].reference_row,
|
||||
"advance_amount": 1,
|
||||
"allocated_amount": 1,
|
||||
"ref_exchange_rate": advances[0].exchange_rate,
|
||||
"remarks": advances[0].remarks,
|
||||
},
|
||||
)
|
||||
si = si.save().submit()
|
||||
|
||||
# Outstanding in both currencies should be '0'
|
||||
adv.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||
|
||||
# Exc Gain/Loss journals should inherit dimension from parent
|
||||
journals = self.get_journals_for(si.doctype, si.name)
|
||||
self.assertEqual(
|
||||
[dpt, dpt],
|
||||
frappe.db.get_all(
|
||||
"Journal Entry Account",
|
||||
filters={"parent": ("in", [x.parent for x in journals])},
|
||||
pluck="department",
|
||||
),
|
||||
)
|
||||
|
@ -155,7 +155,7 @@ class TallyMigration(Document):
|
||||
except RecursionError:
|
||||
self.log(
|
||||
_(
|
||||
"Error occured while parsing Chart of Accounts: Please make sure that no two accounts have the same name"
|
||||
"Error occurred while parsing Chart of Accounts: Please make sure that no two accounts have the same name"
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -481,7 +481,8 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
|
||||
|
||||
communication_doctypes = ["Customer", "Supplier"]
|
||||
|
||||
advance_payment_doctypes = ["Sales Order", "Purchase Order"]
|
||||
advance_payment_customer_doctypes = ["Sales Order"]
|
||||
advance_payment_supplier_doctypes = ["Purchase Order"]
|
||||
|
||||
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]
|
||||
|
||||
@ -538,6 +539,8 @@ accounting_dimension_doctypes = [
|
||||
"Account Closing Balance",
|
||||
"Supplier Quotation",
|
||||
"Supplier Quotation Item",
|
||||
"Payment Reconciliation",
|
||||
"Payment Reconciliation Allocation",
|
||||
]
|
||||
|
||||
get_matching_queries = (
|
||||
|
@ -222,7 +222,7 @@ class MaintenanceSchedule(TransactionBase):
|
||||
|
||||
def validate_maintenance_detail(self):
|
||||
if not self.get("items"):
|
||||
throw(_("Please enter Maintaince Details first"))
|
||||
throw(_("Please enter Maintenance Details first"))
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.item_code:
|
||||
|
@ -998,12 +998,6 @@ class TestWorkOrder(FrappeTestCase):
|
||||
|
||||
make_job_card(wo_order.name, operations)
|
||||
job_card = frappe.db.get_value("Job Card", {"work_order": wo_order.name, "docstatus": 0}, "name")
|
||||
update_job_card(job_card, 10, 2)
|
||||
|
||||
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||
for row in stock_entry.items:
|
||||
if row.is_scrap_item:
|
||||
self.assertEqual(row.qty, 2)
|
||||
|
||||
def test_close_work_order(self):
|
||||
items = [
|
||||
|
@ -1520,7 +1520,7 @@ def validate_operation_data(row):
|
||||
|
||||
if row.get("qty") > row.get("pending_qty"):
|
||||
frappe.throw(
|
||||
_("For operation {0}: Quantity ({1}) can not be greter than pending quantity({2})").format(
|
||||
_("For operation {0}: Quantity ({1}) can not be greater than pending quantity({2})").format(
|
||||
frappe.bold(row.get("operation")),
|
||||
frappe.bold(row.get("qty")),
|
||||
frappe.bold(row.get("pending_qty")),
|
||||
|
@ -352,6 +352,7 @@ erpnext.patches.v14_0.update_zero_asset_quantity_field
|
||||
execute:frappe.db.set_single_value("Buying Settings", "project_update_frequency", "Each Transaction")
|
||||
execute:frappe.db.set_default("date_format", frappe.db.get_single_value("System Settings", "date_format"))
|
||||
erpnext.patches.v14_0.update_total_asset_cost_field
|
||||
erpnext.patches.v15_0.create_advance_payment_status
|
||||
# below migration patch should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20
|
||||
|
@ -188,4 +188,4 @@ def execute():
|
||||
raise err
|
||||
else:
|
||||
break
|
||||
print(f"{processed} records have been sucessfully migrated")
|
||||
print(f"{processed} records have been successfully migrated")
|
||||
|
54
erpnext/patches/v15_0/create_advance_payment_status.py
Normal file
54
erpnext/patches/v15_0/create_advance_payment_status.py
Normal file
@ -0,0 +1,54 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
"""
|
||||
Description:
|
||||
Calculate the new Advance Payment Statuse column in SO & PO
|
||||
"""
|
||||
|
||||
if frappe.reload_doc("selling", "doctype", "Sales Order"):
|
||||
so = frappe.qb.DocType("Sales Order")
|
||||
frappe.qb.update(so).set(so.advance_payment_status, "Not Requested").where(
|
||||
so.docstatus == 1
|
||||
).where(so.advance_paid == 0.0).run()
|
||||
|
||||
frappe.qb.update(so).set(so.advance_payment_status, "Partially Paid").where(
|
||||
so.docstatus == 1
|
||||
).where(so.advance_payment_status.isnull()).where(
|
||||
so.advance_paid < (so.rounded_total or so.grand_total)
|
||||
).run()
|
||||
|
||||
frappe.qb.update(so).set(so.advance_payment_status, "Fully Paid").where(so.docstatus == 1).where(
|
||||
so.advance_payment_status.isnull()
|
||||
).where(so.advance_paid == (so.rounded_total or so.grand_total)).run()
|
||||
|
||||
pr = frappe.qb.DocType("Payment Request")
|
||||
frappe.qb.update(so).join(pr).on(so.name == pr.reference_name).set(
|
||||
so.advance_payment_status, "Requested"
|
||||
).where(so.docstatus == 1).where(pr.docstatus == 1).where(
|
||||
so.advance_payment_status == "Not Requested"
|
||||
).run()
|
||||
|
||||
if frappe.reload_doc("buying", "doctype", "Purchase Order"):
|
||||
po = frappe.qb.DocType("Purchase Order")
|
||||
frappe.qb.update(po).set(po.advance_payment_status, "Not Initiated").where(
|
||||
po.docstatus == 1
|
||||
).where(po.advance_paid == 0.0).run()
|
||||
|
||||
frappe.qb.update(po).set(po.advance_payment_status, "Partially Paid").where(
|
||||
po.docstatus == 1
|
||||
).where(po.advance_payment_status.isnull()).where(
|
||||
po.advance_paid < (po.rounded_total or po.grand_total)
|
||||
).run()
|
||||
|
||||
frappe.qb.update(po).set(po.advance_payment_status, "Fully Paid").where(po.docstatus == 1).where(
|
||||
po.advance_payment_status.isnull()
|
||||
).where(po.advance_paid == (po.rounded_total or po.grand_total)).run()
|
||||
|
||||
pr = frappe.qb.DocType("Payment Request")
|
||||
frappe.qb.update(po).join(pr).on(po.name == pr.reference_name).set(
|
||||
po.advance_payment_status, "Initiated"
|
||||
).where(po.docstatus == 1).where(pr.docstatus == 1).where(
|
||||
po.advance_payment_status == "Not Initiated"
|
||||
).run()
|
@ -105,32 +105,47 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
this.frm.has_items = false;
|
||||
}
|
||||
|
||||
if (serial_no && this.is_duplicate_serial_no(row, item_code, serial_no)) {
|
||||
this.clean_up();
|
||||
reject();
|
||||
return;
|
||||
if (serial_no) {
|
||||
this.is_duplicate_serial_no(row, item_code, serial_no)
|
||||
.then((is_duplicate) => {
|
||||
if (!is_duplicate) {
|
||||
this.run_serially_tasks(row, data, resolve);
|
||||
} else {
|
||||
this.clean_up();
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.run_serially_tasks(row, data, resolve);
|
||||
}
|
||||
|
||||
frappe.run_serially([
|
||||
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
}),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.clean_up(),
|
||||
() => resolve(row),
|
||||
() => {
|
||||
if (row.serial_and_batch_bundle && !this.frm.is_new()) {
|
||||
this.frm.save();
|
||||
}
|
||||
|
||||
frappe.flags.trigger_from_barcode_scanner = false;
|
||||
}
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
run_serially_tasks(row, data, resolve) {
|
||||
const {item_code, barcode, batch_no, serial_no, uom} = data;
|
||||
|
||||
frappe.run_serially([
|
||||
() => this.set_serial_and_batch(row, item_code, serial_no, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.set_item(row, item_code, barcode, batch_no, serial_no).then(qty => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
}),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.clean_up(),
|
||||
() => {
|
||||
if (row.serial_and_batch_bundle && !this.frm.is_new()) {
|
||||
this.frm.save();
|
||||
}
|
||||
|
||||
frappe.flags.trigger_from_barcode_scanner = false;
|
||||
},
|
||||
() => resolve(row),
|
||||
]);
|
||||
}
|
||||
|
||||
set_item(row, item_code, barcode, batch_no, serial_no) {
|
||||
return new Promise(resolve => {
|
||||
const increment = async (value = 1) => {
|
||||
@ -475,26 +490,32 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
}
|
||||
}
|
||||
|
||||
is_duplicate_serial_no(row, item_code, serial_no) {
|
||||
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
|
||||
let is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
|
||||
if (is_duplicate) {
|
||||
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||
}
|
||||
|
||||
return is_duplicate;
|
||||
} else if (row.serial_and_batch_bundle) {
|
||||
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
|
||||
if (r.message) {
|
||||
async is_duplicate_serial_no(row, item_code, serial_no) {
|
||||
let is_duplicate = false;
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
if (this.frm.is_new() || !row.serial_and_batch_bundle) {
|
||||
is_duplicate = this.check_duplicate_serial_no_in_localstorage(item_code, serial_no);
|
||||
if (is_duplicate) {
|
||||
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||
}
|
||||
|
||||
return r.message;
|
||||
})
|
||||
}
|
||||
resolve(is_duplicate);
|
||||
} else if (row.serial_and_batch_bundle) {
|
||||
this.check_duplicate_serial_no_in_db(row, serial_no, (r) => {
|
||||
if (r.message) {
|
||||
this.show_alert(__("Serial No {0} is already added", [serial_no]), "orange");
|
||||
}
|
||||
|
||||
is_duplicate = r.message;
|
||||
resolve(is_duplicate);
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
return await promise;
|
||||
}
|
||||
|
||||
async check_duplicate_serial_no_in_db(row, serial_no, response) {
|
||||
check_duplicate_serial_no_in_db(row, serial_no, response) {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_duplicate_serial_no",
|
||||
args: {
|
||||
@ -504,7 +525,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
callback(r) {
|
||||
response(r);
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
check_duplicate_serial_no_in_localstorage(item_code, serial_no) {
|
||||
|
@ -25,6 +25,10 @@ erpnext.accounts.dimensions = {
|
||||
},
|
||||
|
||||
setup_filters(frm, doctype) {
|
||||
if (doctype == 'Payment Entry' && this.accounting_dimensions) {
|
||||
frm.dimension_filters = this.accounting_dimensions
|
||||
}
|
||||
|
||||
if (this.accounting_dimensions) {
|
||||
this.accounting_dimensions.forEach((dimension) => {
|
||||
frappe.model.with_doctype(dimension['document_type'], () => {
|
||||
|
@ -22,6 +22,15 @@ erpnext.sales_common = {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query('project', function(doc) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters: {
|
||||
'customer': doc.customer
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
@ -439,4 +448,4 @@ erpnext.pre_sales = {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,10 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
let warehouse = this.item?.type_of_transaction === "Outward" ?
|
||||
(this.item.warehouse || this.item.s_warehouse) : "";
|
||||
|
||||
if (!warehouse && this.frm.doc.doctype === 'Stock Reconciliation') {
|
||||
warehouse = this.get_warehouse();
|
||||
}
|
||||
|
||||
return {
|
||||
'item_code': this.item.item_code,
|
||||
'warehouse': ["=", warehouse]
|
||||
@ -135,7 +139,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
filters: this.get_serial_no_filters()
|
||||
};
|
||||
},
|
||||
onchange: () => this.update_serial_batch_no()
|
||||
onchange: () => this.scan_barcode_data()
|
||||
});
|
||||
}
|
||||
|
||||
@ -145,7 +149,7 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
options: 'Barcode',
|
||||
fieldname: 'scan_batch_no',
|
||||
label: __('Scan Batch No'),
|
||||
onchange: () => this.update_serial_batch_no()
|
||||
onchange: () => this.scan_barcode_data()
|
||||
});
|
||||
}
|
||||
|
||||
@ -179,11 +183,54 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
label = __('Serial Nos / Batch Nos');
|
||||
}
|
||||
|
||||
return [
|
||||
let fields = [
|
||||
{
|
||||
fieldtype: 'Section Break',
|
||||
label: __('{0} {1} via CSV File', [primary_label, label])
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
if (this.item?.has_serial_no) {
|
||||
fields = [...fields,
|
||||
{
|
||||
fieldtype: 'Check',
|
||||
label: __('Import Using CSV file'),
|
||||
fieldname: 'import_using_csv_file',
|
||||
default: 0,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Section Break',
|
||||
label: __('{0} {1} Manually', [primary_label, label]),
|
||||
depends_on: 'eval:doc.import_using_csv_file === 0',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Small Text',
|
||||
label: __('Enter Serial Nos'),
|
||||
fieldname: 'upload_serial_nos',
|
||||
depends_on: 'eval:doc.import_using_csv_file === 0',
|
||||
description: __('Enter each serial no in a new line'),
|
||||
},
|
||||
{
|
||||
fieldtype: 'Column Break',
|
||||
depends_on: 'eval:doc.import_using_csv_file === 0',
|
||||
},
|
||||
{
|
||||
fieldtype: 'Button',
|
||||
fieldname: 'make_serial_nos',
|
||||
label: __('Create Serial Nos'),
|
||||
depends_on: 'eval:doc.import_using_csv_file === 0',
|
||||
click: () => {
|
||||
this.create_serial_nos();
|
||||
}
|
||||
},
|
||||
{
|
||||
fieldtype: 'Section Break',
|
||||
depends_on: 'eval:doc.import_using_csv_file === 1',
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
fields = [...fields,
|
||||
{
|
||||
fieldtype: 'Button',
|
||||
fieldname: 'download_csv',
|
||||
@ -199,7 +246,32 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
label: __('Attach CSV File'),
|
||||
onchange: () => this.upload_csv_file()
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
create_serial_nos() {
|
||||
let {upload_serial_nos} = this.dialog.get_values();
|
||||
|
||||
if (!upload_serial_nos) {
|
||||
frappe.throw(__('Please enter Serial Nos'));
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.create_serial_nos',
|
||||
args: {
|
||||
item_code: this.item.item_code,
|
||||
serial_nos: upload_serial_nos
|
||||
},
|
||||
callback: (r) => {
|
||||
if (r.message) {
|
||||
this.dialog.fields_dict.entries.df.data = [];
|
||||
this.set_data(r.message);
|
||||
this.update_bundle_entries();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
download_csv_file() {
|
||||
@ -374,6 +446,26 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
}
|
||||
}
|
||||
|
||||
scan_barcode_data() {
|
||||
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
|
||||
|
||||
if (scan_serial_no || scan_batch_no) {
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle.is_serial_batch_no_exists',
|
||||
args: {
|
||||
item_code: this.item.item_code,
|
||||
type_of_transaction: this.item.type_of_transaction,
|
||||
serial_no: scan_serial_no,
|
||||
batch_no: scan_batch_no,
|
||||
},
|
||||
callback: (r) => {
|
||||
this.update_serial_batch_no();
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
update_serial_batch_no() {
|
||||
const { scan_serial_no, scan_batch_no } = this.dialog.get_values();
|
||||
|
||||
|
@ -64,7 +64,7 @@
|
||||
{
|
||||
"fieldname": "valid_upto",
|
||||
"fieldtype": "Date",
|
||||
"label": "Valid Upto",
|
||||
"label": "Valid Up To",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@ -135,7 +135,7 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2023-04-18 08:25:35.302081",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Regional",
|
||||
"name": "Lower Deduction Certificate",
|
||||
|
@ -37,7 +37,7 @@ class LowerDeductionCertificate(Document):
|
||||
|
||||
def validate_dates(self):
|
||||
if getdate(self.valid_upto) < getdate(self.valid_from):
|
||||
frappe.throw(_("Valid Upto date cannot be before Valid From date"))
|
||||
frappe.throw(_("Valid Up To date cannot be before Valid From date"))
|
||||
|
||||
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
|
||||
|
||||
@ -45,7 +45,7 @@ class LowerDeductionCertificate(Document):
|
||||
frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
|
||||
|
||||
if not (fiscal_year.year_start_date <= getdate(self.valid_upto) <= fiscal_year.year_end_date):
|
||||
frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
|
||||
frappe.throw(_("Valid Up To date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
|
||||
|
||||
def validate_supplier_against_tax_category(self):
|
||||
duplicate_certificate = frappe.db.get_value(
|
||||
|
@ -427,11 +427,11 @@ def create_internal_customer(
|
||||
if not allowed_to_interact_with:
|
||||
allowed_to_interact_with = represents_company
|
||||
|
||||
exisiting_representative = frappe.db.get_value(
|
||||
existing_representative = frappe.db.get_value(
|
||||
"Customer", {"represents_company": represents_company}
|
||||
)
|
||||
if exisiting_representative:
|
||||
return exisiting_representative
|
||||
if existing_representative:
|
||||
return existing_representative
|
||||
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
customer = frappe.get_doc(
|
||||
|
@ -127,7 +127,8 @@ class Quotation(SellingController):
|
||||
def validate(self):
|
||||
super(Quotation, self).validate()
|
||||
self.set_status()
|
||||
self.validate_uom_is_integer("stock_uom", "qty")
|
||||
self.validate_uom_is_integer("stock_uom", "stock_qty")
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_valid_till()
|
||||
self.set_customer_name()
|
||||
if self.items:
|
||||
|
@ -593,6 +593,77 @@ class TestQuotation(FrappeTestCase):
|
||||
quotation.reload()
|
||||
self.assertEqual(quotation.status, "Ordered")
|
||||
|
||||
def test_uom_validation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
item = "_Test Item FOR UOM Validation"
|
||||
make_item(item, {"is_stock_item": 1})
|
||||
|
||||
if not frappe.db.exists("UOM", "lbs"):
|
||||
frappe.get_doc({"doctype": "UOM", "uom_name": "lbs", "must_be_whole_number": 1}).insert()
|
||||
else:
|
||||
frappe.db.set_value("UOM", "lbs", "must_be_whole_number", 1)
|
||||
|
||||
quotation = make_quotation(item_code=item, qty=1, rate=100, do_not_submit=1)
|
||||
quotation.items[0].uom = "lbs"
|
||||
quotation.items[0].conversion_factor = 2.23
|
||||
self.assertRaises(frappe.ValidationError, quotation.save)
|
||||
|
||||
def test_item_tax_template_for_quotation(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists("Account", {"account_name": "_Test Vat", "company": "_Test Company"}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "_Test Vat",
|
||||
"company": "_Test Company",
|
||||
"account_type": "Tax",
|
||||
"root_type": "Asset",
|
||||
"is_group": 0,
|
||||
"parent_account": "Tax Assets - _TC",
|
||||
"tax_rate": 10,
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.exists("Item Tax Template", "Vat Template - _TC"):
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Tax Template",
|
||||
"name": "Vat Template",
|
||||
"title": "Vat Template",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": "_Test Vat - _TC",
|
||||
"tax_rate": 5,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
item_doc = make_item("_Test Item Tax Template QTN", {"is_stock_item": 1})
|
||||
if not frappe.db.exists(
|
||||
"Item Tax", {"parent": item_doc.name, "item_tax_template": "Vat Template - _TC"}
|
||||
):
|
||||
item_doc.append("taxes", {"item_tax_template": "Vat Template - _TC"})
|
||||
item_doc.save()
|
||||
|
||||
quotation = make_quotation(
|
||||
item_code="_Test Item Tax Template QTN", qty=1, rate=100, do_not_submit=1
|
||||
)
|
||||
self.assertFalse(quotation.taxes)
|
||||
|
||||
quotation.append_taxes_from_item_tax_template()
|
||||
quotation.save()
|
||||
self.assertTrue(quotation.taxes)
|
||||
for row in quotation.taxes:
|
||||
self.assertEqual(row.account_head, "_Test Vat - _TC")
|
||||
self.assertAlmostEqual(row.base_tax_amount, quotation.total * 5 / 100)
|
||||
|
||||
item_doc.taxes = []
|
||||
item_doc.save()
|
||||
|
||||
|
||||
test_records = frappe.get_test_records("Quotation")
|
||||
|
||||
|
@ -144,15 +144,6 @@ frappe.ui.form.on("Sales Order", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query('project', function(doc, cdt, cdn) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters: {
|
||||
'customer': doc.customer
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_query('warehouse', 'items', function(doc, cdt, cdn) {
|
||||
let row = locals[cdt][cdn];
|
||||
let query = {
|
||||
|
@ -131,6 +131,7 @@
|
||||
"per_billed",
|
||||
"per_picked",
|
||||
"billing_status",
|
||||
"advance_payment_status",
|
||||
"sales_team_section_break",
|
||||
"sales_partner",
|
||||
"column_break7",
|
||||
@ -1269,7 +1270,7 @@
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "status",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "\nDraft\nOn Hold\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed",
|
||||
"options": "\nDraft\nOn Hold\nTo Pay\nTo Deliver and Bill\nTo Bill\nTo Deliver\nCompleted\nCancelled\nClosed",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1,
|
||||
@ -1638,6 +1639,18 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "advance_payment_status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Advance Payment Status",
|
||||
"no_copy": 1,
|
||||
"options": "Not Requested\nRequested\nPartially Paid\nFully Paid",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@ -1722,4 +1735,4 @@
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
@ -223,6 +223,8 @@ class SalesOrder(SellingController):
|
||||
self.billing_status = "Not Billed"
|
||||
if not self.delivery_status:
|
||||
self.delivery_status = "Not Delivered"
|
||||
if not self.advance_payment_status:
|
||||
self.advance_payment_status = "Not Requested"
|
||||
|
||||
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
|
||||
@ -641,7 +643,7 @@ class SalesOrder(SellingController):
|
||||
if not frappe.get_cached_value("Item", item.item_code, "has_serial_no"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Item {0} has no Serial No. Only serilialized items can have delivery based on Serial No"
|
||||
"Item {0} has no Serial No. Only serialized items can have delivery based on Serial No"
|
||||
).format(item.item_code)
|
||||
)
|
||||
if not frappe.db.exists("BOM", {"item": item.item_code, "is_active": 1}):
|
||||
|
@ -1,6 +1,6 @@
|
||||
frappe.listview_settings['Sales Order'] = {
|
||||
add_fields: ["base_grand_total", "customer_name", "currency", "delivery_date",
|
||||
"per_delivered", "per_billed", "status", "order_type", "name", "skip_delivery_note"],
|
||||
"per_delivered", "per_billed", "status", "advance_payment_status", "order_type", "name", "skip_delivery_note"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status === "Closed") {
|
||||
// Closed
|
||||
@ -10,6 +10,8 @@ frappe.listview_settings['Sales Order'] = {
|
||||
return [__("On Hold"), "orange", "status,=,On Hold"];
|
||||
} else if (doc.status === "Completed") {
|
||||
return [__("Completed"), "green", "status,=,Completed"];
|
||||
} else if (doc.advance_payment_status === "Requested") {
|
||||
return [__("To Pay"), "gray", "advance_payment_status,=,Requested"];
|
||||
} else if (!doc.skip_delivery_note && flt(doc.per_delivered, 2) < 100) {
|
||||
if (frappe.datetime.get_diff(doc.delivery_date) < 0) {
|
||||
// not delivered & overdue
|
||||
|
@ -1996,6 +1996,33 @@ class TestSalesOrder(FrappeTestCase):
|
||||
self.assertEqual(so.items[0].rate, scenario.get("expected_rate"))
|
||||
self.assertEqual(so.packed_items[0].rate, scenario.get("expected_rate"))
|
||||
|
||||
def test_sales_order_advance_payment_status(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
|
||||
so = make_sales_order(qty=1, rate=100)
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||
)
|
||||
|
||||
pr = make_payment_request(dt=so.doctype, dn=so.name, submit_doc=True, return_doc=True)
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
||||
|
||||
pe = get_payment_entry(so.doctype, so.name).save().submit()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Fully Paid"
|
||||
)
|
||||
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
self.assertEqual(frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Requested")
|
||||
|
||||
pr.reload()
|
||||
pr.cancel()
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(so.doctype, so.name, "advance_payment_status"), "Not Requested"
|
||||
)
|
||||
|
||||
|
||||
def automatically_fetch_payment_terms(enable=1):
|
||||
accounts_settings = frappe.get_doc("Accounts Settings")
|
||||
|
@ -360,7 +360,7 @@ erpnext.PointOfSale.Controller = class {
|
||||
this.order_summary.load_summary_of(this.frm.doc, true);
|
||||
frappe.show_alert({
|
||||
indicator: 'green',
|
||||
message: __('POS invoice {0} created succesfully', [r.doc.name])
|
||||
message: __('POS invoice {0} created successfully', [r.doc.name])
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -209,8 +209,7 @@ def get_so_with_invoices(filters):
|
||||
)
|
||||
.where(
|
||||
(so.docstatus == 1)
|
||||
& (so.status.isin(["To Deliver and Bill", "To Bill"]))
|
||||
& (so.payment_terms_template != "NULL")
|
||||
& (so.status.isin(["To Deliver and Bill", "To Bill", "To Pay"]))
|
||||
& (so.company == conditions.company)
|
||||
& (so.transaction_date[conditions.start_date : conditions.end_date])
|
||||
)
|
||||
|
@ -56,7 +56,7 @@ frappe.query_reports["Sales Order Analysis"] = {
|
||||
"fieldtype": "MultiSelectList",
|
||||
"width": "80",
|
||||
get_data: function(txt) {
|
||||
let status = ["To Bill", "To Deliver", "To Deliver and Bill", "Completed"]
|
||||
let status = ["To Pay", "To Bill", "To Deliver", "To Deliver and Bill", "Completed"]
|
||||
let options = []
|
||||
for (let option of status){
|
||||
options.push({
|
||||
|
@ -54,7 +54,7 @@ class AuthorizationControl(TransactionBase):
|
||||
if not has_common(appr_roles, frappe.get_roles()) and not has_common(
|
||||
appr_users, [session["user"]]
|
||||
):
|
||||
frappe.msgprint(_("Not authroized since {0} exceeds limits").format(_(based_on)))
|
||||
frappe.msgprint(_("Not authorized since {0} exceeds limits").format(_(based_on)))
|
||||
frappe.throw(_("Can be approved by {0}").format(comma_or(appr_roles + appr_users)))
|
||||
|
||||
def validate_auth_rule(self, doctype_name, total, based_on, cond, company, master_name=""):
|
||||
|
@ -140,38 +140,48 @@ frappe.ui.form.on("Company", {
|
||||
},
|
||||
|
||||
delete_company_transactions: function(frm) {
|
||||
frappe.verify_password(function() {
|
||||
var d = frappe.prompt({
|
||||
fieldtype:"Data",
|
||||
fieldname: "company_name",
|
||||
label: __("Please enter the company name to confirm"),
|
||||
reqd: 1,
|
||||
description: __("Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.")
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.is_deletion_job_running",
|
||||
args: {
|
||||
company: frm.doc.name
|
||||
},
|
||||
function(data) {
|
||||
if(data.company_name !== frm.doc.name) {
|
||||
frappe.msgprint(__("Company name not same"));
|
||||
return;
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
frappe.verify_password(function() {
|
||||
var d = frappe.prompt({
|
||||
fieldtype:"Data",
|
||||
fieldname: "company_name",
|
||||
label: __("Please enter the company name to confirm"),
|
||||
reqd: 1,
|
||||
description: __("Please make sure you really want to delete all the transactions for this company. Your master data will remain as it is. This action cannot be undone.")
|
||||
},
|
||||
function(data) {
|
||||
if(data.company_name !== frm.doc.name) {
|
||||
frappe.msgprint(__("Company name not same"));
|
||||
return;
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request",
|
||||
args: {
|
||||
company: data.company_name
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r, rt) { },
|
||||
onerror: function() {
|
||||
frappe.msgprint(__("Wrong Password"));
|
||||
}
|
||||
});
|
||||
},
|
||||
__("Delete all the Transactions for this Company"), __("Delete")
|
||||
);
|
||||
d.get_primary_btn().addClass("btn-danger");
|
||||
});
|
||||
}
|
||||
frappe.call({
|
||||
method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request",
|
||||
args: {
|
||||
company: data.company_name
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r, rt) {
|
||||
if(!r.exc)
|
||||
frappe.msgprint(__("Successfully deleted all transactions related to this company!"));
|
||||
},
|
||||
onerror: function() {
|
||||
frappe.msgprint(__("Wrong Password"));
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
__("Delete all the Transactions for this Company"), __("Delete")
|
||||
);
|
||||
d.get_primary_btn().addClass("btn-danger");
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -11,7 +11,8 @@ from frappe.cache_manager import clear_defaults_cache
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import make_records
|
||||
from frappe.utils import cint, formatdate, get_timestamp, today
|
||||
from frappe.utils import cint, formatdate, get_link_to_form, get_timestamp, today
|
||||
from frappe.utils.background_jobs import get_job, is_job_enqueued
|
||||
from frappe.utils.nestedset import NestedSet, rebuild_tree
|
||||
|
||||
from erpnext.accounts.doctype.account.account import get_account_currency
|
||||
@ -900,8 +901,37 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad
|
||||
return None
|
||||
|
||||
|
||||
def generate_id_for_deletion_job(company):
|
||||
return "delete_company_transactions_" + company
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_deletion_job_running(company):
|
||||
job_id = generate_id_for_deletion_job(company)
|
||||
if is_job_enqueued(job_id):
|
||||
job_name = get_job(job_id).get_id() # job name will have site prefix
|
||||
frappe.throw(
|
||||
_("A Transaction Deletion Job: {0} is already running for {1}").format(
|
||||
frappe.bold(get_link_to_form("RQ Job", job_name)), frappe.bold(company)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_transaction_deletion_request(company):
|
||||
is_deletion_job_running(company)
|
||||
job_id = generate_id_for_deletion_job(company)
|
||||
|
||||
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
||||
tdr.insert()
|
||||
tdr.submit()
|
||||
|
||||
frappe.enqueue(
|
||||
"frappe.utils.background_jobs.run_doc_method",
|
||||
doctype=tdr.doctype,
|
||||
name=tdr.name,
|
||||
doc_method="submit",
|
||||
job_id=job_id,
|
||||
queue="long",
|
||||
enqueue_after_commit=True,
|
||||
)
|
||||
frappe.msgprint(_("A Transaction Deletion Job is triggered for {0}").format(frappe.bold(company)))
|
||||
|
@ -441,13 +441,13 @@
|
||||
{
|
||||
"fieldname": "prefered_contact_email",
|
||||
"fieldtype": "Select",
|
||||
"label": "Prefered Contact Email",
|
||||
"label": "Preferred Contact Email",
|
||||
"options": "\nCompany Email\nPersonal Email\nUser ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "prefered_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Prefered Email",
|
||||
"label": "Preferred Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
},
|
||||
@ -524,7 +524,7 @@
|
||||
{
|
||||
"fieldname": "valid_upto",
|
||||
"fieldtype": "Date",
|
||||
"label": "Valid Upto"
|
||||
"label": "Valid Up To"
|
||||
},
|
||||
{
|
||||
"fieldname": "place_of_issue",
|
||||
@ -824,7 +824,7 @@
|
||||
"image_field": "image",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-01-03 17:36:20.984421",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Employee",
|
||||
|
@ -4,9 +4,10 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestTransactionDeletionRecord(unittest.TestCase):
|
||||
class TestTransactionDeletionRecord(FrappeTestCase):
|
||||
def setUp(self):
|
||||
create_company("Dunder Mifflin Paper Co")
|
||||
|
||||
@ -14,7 +15,7 @@ class TestTransactionDeletionRecord(unittest.TestCase):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_doctypes_contain_company_field(self):
|
||||
tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co")
|
||||
tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
||||
for doctype in tdr.doctypes:
|
||||
contains_company = False
|
||||
doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()["fields"]
|
||||
@ -27,17 +28,27 @@ class TestTransactionDeletionRecord(unittest.TestCase):
|
||||
def test_no_of_docs_is_correct(self):
|
||||
for i in range(5):
|
||||
create_task("Dunder Mifflin Paper Co")
|
||||
tdr = create_transaction_deletion_request("Dunder Mifflin Paper Co")
|
||||
tdr = create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
||||
for doctype in tdr.doctypes:
|
||||
if doctype.doctype_name == "Task":
|
||||
self.assertEqual(doctype.no_of_docs, 5)
|
||||
|
||||
def test_deletion_is_successful(self):
|
||||
create_task("Dunder Mifflin Paper Co")
|
||||
create_transaction_deletion_request("Dunder Mifflin Paper Co")
|
||||
create_transaction_deletion_doc("Dunder Mifflin Paper Co")
|
||||
tasks_containing_company = frappe.get_all("Task", filters={"company": "Dunder Mifflin Paper Co"})
|
||||
self.assertEqual(tasks_containing_company, [])
|
||||
|
||||
def test_company_transaction_deletion_request(self):
|
||||
from erpnext.setup.doctype.company.company import create_transaction_deletion_request
|
||||
|
||||
# don't reuse below company for other test cases
|
||||
company = "Deep Space Exploration"
|
||||
create_company(company)
|
||||
|
||||
# below call should not raise any exceptions or throw errors
|
||||
create_transaction_deletion_request(company)
|
||||
|
||||
|
||||
def create_company(company_name):
|
||||
company = frappe.get_doc(
|
||||
@ -46,7 +57,7 @@ def create_company(company_name):
|
||||
company.insert(ignore_if_duplicate=True)
|
||||
|
||||
|
||||
def create_transaction_deletion_request(company):
|
||||
def create_transaction_deletion_doc(company):
|
||||
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
||||
tdr.insert()
|
||||
tdr.submit()
|
||||
|
@ -52,7 +52,7 @@ frappe.ui.form.on('Batch', {
|
||||
// sort by qty
|
||||
r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 });
|
||||
|
||||
var rows = $('<div></div>').appendTo(section);
|
||||
const rows = $('<div></div>').appendTo(section);
|
||||
|
||||
// show
|
||||
(r.message || []).forEach(function(d) {
|
||||
@ -76,7 +76,7 @@ frappe.ui.form.on('Batch', {
|
||||
|
||||
// move - ask for target warehouse and make stock entry
|
||||
rows.find('.btn-move').on('click', function() {
|
||||
var $btn = $(this);
|
||||
const $btn = $(this);
|
||||
const fields = [
|
||||
{
|
||||
fieldname: 'to_warehouse',
|
||||
@ -115,7 +115,7 @@ frappe.ui.form.on('Batch', {
|
||||
// split - ask for new qty and batch ID (optional)
|
||||
// and make stock entry via batch.batch_split
|
||||
rows.find('.btn-split').on('click', function() {
|
||||
var $btn = $(this);
|
||||
const $btn = $(this);
|
||||
frappe.prompt([{
|
||||
fieldname: 'qty',
|
||||
label: __('New Batch Qty'),
|
||||
@ -128,19 +128,16 @@ frappe.ui.form.on('Batch', {
|
||||
fieldtype: 'Data',
|
||||
}],
|
||||
(data) => {
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.doctype.batch.batch.split_batch',
|
||||
args: {
|
||||
frappe.xcall(
|
||||
'erpnext.stock.doctype.batch.batch.split_batch',
|
||||
{
|
||||
item_code: frm.doc.item,
|
||||
batch_no: frm.doc.name,
|
||||
qty: data.qty,
|
||||
warehouse: $btn.attr('data-warehouse'),
|
||||
new_batch_id: data.new_batch_id
|
||||
},
|
||||
callback: (r) => {
|
||||
frm.refresh();
|
||||
},
|
||||
});
|
||||
}
|
||||
).then(() => frm.reload_doc());
|
||||
},
|
||||
__('Split Batch'),
|
||||
__('Split')
|
||||
|
@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import make_autoname, revert_series_if_last
|
||||
from frappe.query_builder.functions import CurDate, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, nowtime, today
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
from frappe.utils.data import add_days
|
||||
from frappe.utils.jinja import render_template
|
||||
|
||||
@ -248,8 +248,9 @@ def get_batches_by_oldest(item_code, warehouse):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
|
||||
|
||||
def split_batch(
|
||||
batch_no: str, item_code: str, warehouse: str, qty: float, new_batch_id: str | None = None
|
||||
):
|
||||
"""Split the batch into a new batch"""
|
||||
batch = frappe.get_doc(dict(doctype="Batch", item=item_code, batch_id=new_batch_id)).insert()
|
||||
qty = flt(qty)
|
||||
@ -257,29 +258,21 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
|
||||
company = frappe.db.get_value("Warehouse", warehouse, "company")
|
||||
|
||||
from_bundle_id = make_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"batches": frappe._dict({batch_no: qty}),
|
||||
"company": company,
|
||||
"type_of_transaction": "Outward",
|
||||
"qty": qty,
|
||||
}
|
||||
)
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
batches=frappe._dict({batch_no: qty}),
|
||||
company=company,
|
||||
type_of_transaction="Outward",
|
||||
qty=qty,
|
||||
)
|
||||
|
||||
to_bundle_id = make_batch_bundle(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"batches": frappe._dict({batch.name: qty}),
|
||||
"company": company,
|
||||
"type_of_transaction": "Inward",
|
||||
"qty": qty,
|
||||
}
|
||||
)
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
batches=frappe._dict({batch.name: qty}),
|
||||
company=company,
|
||||
type_of_transaction="Inward",
|
||||
qty=qty,
|
||||
)
|
||||
|
||||
stock_entry = frappe.get_doc(
|
||||
@ -304,21 +297,30 @@ def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None):
|
||||
return batch.name
|
||||
|
||||
|
||||
def make_batch_bundle(kwargs):
|
||||
def make_batch_bundle(
|
||||
item_code: str,
|
||||
warehouse: str,
|
||||
batches: dict[str, float],
|
||||
company: str,
|
||||
type_of_transaction: str,
|
||||
qty: float,
|
||||
):
|
||||
from frappe.utils import nowtime, today
|
||||
|
||||
from erpnext.stock.serial_batch_bundle import SerialBatchCreation
|
||||
|
||||
return (
|
||||
SerialBatchCreation(
|
||||
{
|
||||
"item_code": kwargs.item_code,
|
||||
"warehouse": kwargs.warehouse,
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": today(),
|
||||
"posting_time": nowtime(),
|
||||
"voucher_type": "Stock Entry",
|
||||
"qty": flt(kwargs.qty),
|
||||
"type_of_transaction": kwargs.type_of_transaction,
|
||||
"company": kwargs.company,
|
||||
"batches": kwargs.batches,
|
||||
"qty": qty,
|
||||
"type_of_transaction": type_of_transaction,
|
||||
"company": company,
|
||||
"batches": batches,
|
||||
"do_not_submit": True,
|
||||
}
|
||||
)
|
||||
|
@ -31,15 +31,6 @@ frappe.ui.form.on("Delivery Note", {
|
||||
});
|
||||
erpnext.queries.setup_warehouse_query(frm);
|
||||
|
||||
frm.set_query('project', function(doc) {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.get_project_name",
|
||||
filters: {
|
||||
'customer': doc.customer
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
frm.set_query('transporter', function() {
|
||||
return {
|
||||
filters: {
|
||||
|
@ -796,36 +796,36 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
|
||||
|
||||
updated_dn = []
|
||||
for dnd in dn_details:
|
||||
billed_amt_agianst_dn = 0
|
||||
billed_amt_against_dn = 0
|
||||
|
||||
# If delivered against Sales Invoice
|
||||
if dnd.si_detail:
|
||||
billed_amt_agianst_dn = flt(dnd.amount)
|
||||
billed_against_so -= billed_amt_agianst_dn
|
||||
billed_amt_against_dn = flt(dnd.amount)
|
||||
billed_against_so -= billed_amt_against_dn
|
||||
else:
|
||||
# Get billed amount directly against Delivery Note
|
||||
billed_amt_agianst_dn = frappe.db.sql(
|
||||
billed_amt_against_dn = frappe.db.sql(
|
||||
"""select sum(amount) from `tabSales Invoice Item`
|
||||
where dn_detail=%s and docstatus=1""",
|
||||
dnd.name,
|
||||
)
|
||||
billed_amt_agianst_dn = billed_amt_agianst_dn and billed_amt_agianst_dn[0][0] or 0
|
||||
billed_amt_against_dn = billed_amt_against_dn and billed_amt_against_dn[0][0] or 0
|
||||
|
||||
# Distribute billed amount directly against SO between DNs based on FIFO
|
||||
if billed_against_so and billed_amt_agianst_dn < dnd.amount:
|
||||
pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn
|
||||
if billed_against_so and billed_amt_against_dn < dnd.amount:
|
||||
pending_to_bill = flt(dnd.amount) - billed_amt_against_dn
|
||||
if pending_to_bill <= billed_against_so:
|
||||
billed_amt_agianst_dn += pending_to_bill
|
||||
billed_amt_against_dn += pending_to_bill
|
||||
billed_against_so -= pending_to_bill
|
||||
else:
|
||||
billed_amt_agianst_dn += billed_against_so
|
||||
billed_amt_against_dn += billed_against_so
|
||||
billed_against_so = 0
|
||||
|
||||
frappe.db.set_value(
|
||||
"Delivery Note Item",
|
||||
dnd.name,
|
||||
"billed_amt",
|
||||
billed_amt_agianst_dn,
|
||||
billed_amt_against_dn,
|
||||
update_modified=update_modified,
|
||||
)
|
||||
|
||||
|
@ -191,7 +191,7 @@
|
||||
{
|
||||
"fieldname": "valid_upto",
|
||||
"fieldtype": "Date",
|
||||
"label": "Valid Upto"
|
||||
"label": "Valid Up To"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_24",
|
||||
@ -220,7 +220,7 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2022-11-15 08:26:04.041861",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Price",
|
||||
|
@ -59,7 +59,7 @@ class ItemPrice(Document):
|
||||
def validate_dates(self):
|
||||
if self.valid_from and self.valid_upto:
|
||||
if getdate(self.valid_from) > getdate(self.valid_upto):
|
||||
frappe.throw(_("Valid From Date must be lesser than Valid Upto Date."))
|
||||
frappe.throw(_("Valid From Date must be lesser than Valid Up To Date."))
|
||||
|
||||
def update_price_list_details(self):
|
||||
if self.price_list:
|
||||
|
@ -64,7 +64,7 @@ class TestItemPrice(FrappeTestCase):
|
||||
# Enter invalid dates valid_from >= valid_upto
|
||||
doc.valid_from = "2017-04-20"
|
||||
doc.valid_upto = "2017-04-17"
|
||||
# Valid Upto Date can not be less/equal than Valid From Date
|
||||
# Valid Up To Date can not be less/equal than Valid From Date
|
||||
self.assertRaises(frappe.ValidationError, doc.save)
|
||||
|
||||
def test_price_in_a_qty(self):
|
||||
|
@ -776,7 +776,7 @@ def raise_work_orders(material_request):
|
||||
)
|
||||
else:
|
||||
msgprint(
|
||||
_("The {0} {1} created sucessfully").format(frappe.bold(_("Work Order")), work_orders_list[0])
|
||||
_("The {0} {1} created successfully").format(frappe.bold(_("Work Order")), work_orders_list[0])
|
||||
)
|
||||
|
||||
if errors:
|
||||
|
@ -24,7 +24,7 @@ frappe.listview_settings['Material Request'] = {
|
||||
} else if (doc.material_request_type == "Purchase") {
|
||||
return [__("Ordered"), "green", "per_ordered,=,100"];
|
||||
} else if (doc.material_request_type == "Material Transfer") {
|
||||
return [__("Transfered"), "green", "per_ordered,=,100"];
|
||||
return [__("Transferred"), "green", "per_ordered,=,100"];
|
||||
} else if (doc.material_request_type == "Material Issue") {
|
||||
return [__("Issued"), "green", "per_ordered,=,100"];
|
||||
} else if (doc.material_request_type == "Customer Provided") {
|
||||
|
@ -774,6 +774,62 @@ class TestMaterialRequest(FrappeTestCase):
|
||||
self.assertEqual(mr.per_ordered, 100)
|
||||
self.assertEqual(existing_requested_qty, current_requested_qty)
|
||||
|
||||
def test_auto_email_users_with_company_user_permissions(self):
|
||||
from erpnext.stock.reorder_item import get_email_list
|
||||
|
||||
comapnywise_users = {
|
||||
"_Test Company": "test_auto_email_@example.com",
|
||||
"_Test Company 1": "test_auto_email_1@example.com",
|
||||
}
|
||||
|
||||
permissions = []
|
||||
|
||||
for company, user in comapnywise_users.items():
|
||||
if not frappe.db.exists("User", user):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"email": user,
|
||||
"first_name": user,
|
||||
"send_notifications": 0,
|
||||
"enabled": 1,
|
||||
"user_type": "System User",
|
||||
"roles": [{"role": "Purchase Manager"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
if not frappe.db.exists(
|
||||
"User Permission", {"user": user, "allow": "Company", "for_value": company}
|
||||
):
|
||||
perm_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User Permission",
|
||||
"user": user,
|
||||
"allow": "Company",
|
||||
"for_value": company,
|
||||
"apply_to_all_doctypes": 1,
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
permissions.append(perm_doc)
|
||||
|
||||
comapnywise_mr_list = frappe._dict({})
|
||||
mr1 = make_material_request()
|
||||
comapnywise_mr_list.setdefault(mr1.company, []).append(mr1.name)
|
||||
|
||||
mr2 = make_material_request(
|
||||
company="_Test Company 1", warehouse="Stores - _TC1", cost_center="Main - _TC1"
|
||||
)
|
||||
comapnywise_mr_list.setdefault(mr2.company, []).append(mr2.name)
|
||||
|
||||
for company, mr_list in comapnywise_mr_list.items():
|
||||
emails = get_email_list(company)
|
||||
|
||||
self.assertTrue(comapnywise_users[company] in emails)
|
||||
|
||||
for perm in permissions:
|
||||
perm.delete()
|
||||
|
||||
|
||||
def get_in_transit_warehouse(company):
|
||||
if not frappe.db.exists("Warehouse Type", "Transit"):
|
||||
|
@ -951,32 +951,32 @@ def update_billed_amount_based_on_po(po_details, update_modified=True, pr_doc=No
|
||||
billed_against_po = flt(po_billed_amt_details.get(pr_item.purchase_order_item))
|
||||
|
||||
# Get billed amount directly against Purchase Receipt
|
||||
billed_amt_agianst_pr = flt(pr_items_billed_amount.get(pr_item.name, 0))
|
||||
billed_amt_against_pr = flt(pr_items_billed_amount.get(pr_item.name, 0))
|
||||
|
||||
# Distribute billed amount directly against PO between PRs based on FIFO
|
||||
if billed_against_po and billed_amt_agianst_pr < pr_item.amount:
|
||||
pending_to_bill = flt(pr_item.amount) - billed_amt_agianst_pr
|
||||
if billed_against_po and billed_amt_against_pr < pr_item.amount:
|
||||
pending_to_bill = flt(pr_item.amount) - billed_amt_against_pr
|
||||
if pending_to_bill <= billed_against_po:
|
||||
billed_amt_agianst_pr += pending_to_bill
|
||||
billed_amt_against_pr += pending_to_bill
|
||||
billed_against_po -= pending_to_bill
|
||||
else:
|
||||
billed_amt_agianst_pr += billed_against_po
|
||||
billed_amt_against_pr += billed_against_po
|
||||
billed_against_po = 0
|
||||
|
||||
po_billed_amt_details[pr_item.purchase_order_item] = billed_against_po
|
||||
|
||||
if pr_item.billed_amt != billed_amt_agianst_pr:
|
||||
if pr_item.billed_amt != billed_amt_against_pr:
|
||||
# update existing doc if possible
|
||||
if pr_doc and pr_item.parent == pr_doc.name:
|
||||
pr_item = next((item for item in pr_doc.items if item.name == pr_item.name), None)
|
||||
pr_item.db_set("billed_amt", billed_amt_agianst_pr, update_modified=update_modified)
|
||||
pr_item.db_set("billed_amt", billed_amt_against_pr, update_modified=update_modified)
|
||||
|
||||
else:
|
||||
frappe.db.set_value(
|
||||
"Purchase Receipt Item",
|
||||
pr_item.name,
|
||||
"billed_amt",
|
||||
billed_amt_agianst_pr,
|
||||
billed_amt_against_pr,
|
||||
update_modified=update_modified,
|
||||
)
|
||||
|
||||
|
@ -74,7 +74,7 @@ frappe.ui.form.on('Serial and Batch Bundle', {
|
||||
|
||||
let fields = [
|
||||
{
|
||||
"label": __("Using CSV File"),
|
||||
"label": __("Import Using CSV file"),
|
||||
"fieldname": "using_csv_file",
|
||||
"default": 1,
|
||||
"fieldtype": "Check",
|
||||
|
@ -250,6 +250,7 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
for d in self.entries:
|
||||
available_qty = 0
|
||||
|
||||
if self.has_serial_no:
|
||||
d.incoming_rate = abs(sn_obj.serial_no_incoming_rate.get(d.serial_no, 0.0))
|
||||
else:
|
||||
@ -892,6 +893,13 @@ class SerialandBatchBundle(Document):
|
||||
elif batch_nos:
|
||||
self.set("entries", batch_nos)
|
||||
|
||||
def delete_serial_batch_entries(self):
|
||||
SBBE = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
frappe.qb.from_(SBBE).delete().where(SBBE.parent == self.name).run()
|
||||
|
||||
self.set("entries", [])
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def download_blank_csv_template(content):
|
||||
@ -999,9 +1007,25 @@ def get_serial_batch_from_data(item_code, kwargs):
|
||||
|
||||
make_serial_nos(item_code, serial_nos)
|
||||
|
||||
if kwargs.get("_has_serial_nos"):
|
||||
return serial_nos
|
||||
|
||||
return serial_nos, batch_nos
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_serial_nos(item_code, serial_nos):
|
||||
serial_nos = get_serial_batch_from_data(
|
||||
item_code,
|
||||
{
|
||||
"serial_nos": serial_nos,
|
||||
"_has_serial_nos": True,
|
||||
},
|
||||
)
|
||||
|
||||
return serial_nos
|
||||
|
||||
|
||||
def make_serial_nos(item_code, serial_nos):
|
||||
item = frappe.get_cached_value("Item", item_code, ["description", "item_code"], as_dict=1)
|
||||
|
||||
@ -1358,10 +1382,12 @@ def get_available_serial_nos(kwargs):
|
||||
elif kwargs.based_on == "Expiry":
|
||||
order_by = "amc_expiry_date asc"
|
||||
|
||||
filters = {"item_code": kwargs.item_code, "warehouse": ("is", "set")}
|
||||
filters = {"item_code": kwargs.item_code}
|
||||
|
||||
if kwargs.warehouse:
|
||||
filters["warehouse"] = kwargs.warehouse
|
||||
if not kwargs.get("ignore_warehouse"):
|
||||
filters["warehouse"] = ("is", "set")
|
||||
if kwargs.warehouse:
|
||||
filters["warehouse"] = kwargs.warehouse
|
||||
|
||||
# Since SLEs are not present against Reserved Stock [POS invoices, SRE], need to ignore reserved serial nos.
|
||||
ignore_serial_nos = get_reserved_serial_nos(kwargs)
|
||||
@ -2079,6 +2105,35 @@ def get_batch_no_from_serial_no(serial_no):
|
||||
return frappe.get_cached_value("Serial No", serial_no, "batch_no")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_serial_batch_no_exists(item_code, type_of_transaction, serial_no=None, batch_no=None):
|
||||
if serial_no and not frappe.db.exists("Serial No", serial_no):
|
||||
if type_of_transaction != "Inward":
|
||||
frappe.throw(_("Serial No {0} does not exists").format(serial_no))
|
||||
|
||||
make_serial_no(serial_no, item_code)
|
||||
|
||||
if batch_no and frappe.db.exists("Batch", batch_no):
|
||||
if type_of_transaction != "Inward":
|
||||
frappe.throw(_("Batch No {0} does not exists").format(batch_no))
|
||||
|
||||
make_batch_no(batch_no, item_code)
|
||||
|
||||
|
||||
def make_serial_no(serial_no, item_code):
|
||||
serial_no_doc = frappe.new_doc("Serial No")
|
||||
serial_no_doc.serial_no = serial_no
|
||||
serial_no_doc.item_code = item_code
|
||||
serial_no_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def make_batch_no(batch_no, item_code):
|
||||
batch_doc = frappe.new_doc("Batch")
|
||||
batch_doc.batch_id = batch_no
|
||||
batch_doc.item = item_code
|
||||
batch_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def is_duplicate_serial_no(bundle_id, serial_no):
|
||||
return frappe.db.exists("Serial and Batch Entry", {"parent": bundle_id, "serial_no": serial_no})
|
||||
|
@ -640,7 +640,7 @@ class StockEntry(StockController):
|
||||
frappe.throw(_("Source and target warehouse cannot be same for row {0}").format(d.idx))
|
||||
|
||||
if not (d.s_warehouse or d.t_warehouse):
|
||||
frappe.throw(_("Atleast one warehouse is mandatory"))
|
||||
frappe.throw(_("At least one warehouse is mandatory"))
|
||||
|
||||
def validate_work_order(self):
|
||||
if self.purpose in (
|
||||
|
@ -156,6 +156,7 @@ class StockReconciliation(StockController):
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"ignore_warehouse": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
@ -780,7 +781,20 @@ class StockReconciliation(StockController):
|
||||
|
||||
current_qty = 0.0
|
||||
if row.current_serial_and_batch_bundle:
|
||||
current_qty = self.get_qty_for_serial_and_batch_bundle(row)
|
||||
current_qty = self.get_current_qty_for_serial_or_batch(row)
|
||||
elif row.serial_no:
|
||||
item_dict = get_stock_balance_for(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.posting_date,
|
||||
self.posting_time,
|
||||
voucher_no=self.name,
|
||||
)
|
||||
|
||||
current_qty = item_dict.get("qty")
|
||||
row.current_serial_no = item_dict.get("serial_nos")
|
||||
row.current_valuation_rate = item_dict.get("rate")
|
||||
val_rate = item_dict.get("rate")
|
||||
elif row.batch_no:
|
||||
current_qty = get_batch_qty_for_stock_reco(
|
||||
row.item_code, row.warehouse, row.batch_no, self.posting_date, self.posting_time, self.name
|
||||
@ -788,15 +802,16 @@ class StockReconciliation(StockController):
|
||||
|
||||
precesion = row.precision("current_qty")
|
||||
if flt(current_qty, precesion) != flt(row.current_qty, precesion):
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
)
|
||||
if not row.serial_no:
|
||||
val_rate = get_valuation_rate(
|
||||
row.item_code,
|
||||
row.warehouse,
|
||||
self.doctype,
|
||||
self.name,
|
||||
company=self.company,
|
||||
batch_no=row.batch_no,
|
||||
serial_and_batch_bundle=row.current_serial_and_batch_bundle,
|
||||
)
|
||||
|
||||
row.current_valuation_rate = val_rate
|
||||
row.current_qty = current_qty
|
||||
@ -842,11 +857,56 @@ class StockReconciliation(StockController):
|
||||
|
||||
return allow_negative_stock
|
||||
|
||||
def get_qty_for_serial_and_batch_bundle(self, row):
|
||||
def get_current_qty_for_serial_or_batch(self, row):
|
||||
doc = frappe.get_doc("Serial and Batch Bundle", row.current_serial_and_batch_bundle)
|
||||
precision = doc.entries[0].precision("qty")
|
||||
current_qty = 0.0
|
||||
if doc.has_serial_no:
|
||||
current_qty = self.get_current_qty_for_serial_nos(doc)
|
||||
elif doc.has_batch_no:
|
||||
current_qty = self.get_current_qty_for_batch_nos(doc)
|
||||
|
||||
current_qty = 0
|
||||
return abs(current_qty)
|
||||
|
||||
def get_current_qty_for_serial_nos(self, doc):
|
||||
serial_nos_details = get_available_serial_nos(
|
||||
frappe._dict(
|
||||
{
|
||||
"item_code": doc.item_code,
|
||||
"warehouse": doc.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
"voucher_no": self.name,
|
||||
"ignore_warehouse": 1,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
if not serial_nos_details:
|
||||
return 0.0
|
||||
|
||||
doc.delete_serial_batch_entries()
|
||||
current_qty = 0.0
|
||||
for serial_no_row in serial_nos_details:
|
||||
current_qty += 1
|
||||
doc.append(
|
||||
"entries",
|
||||
{
|
||||
"serial_no": serial_no_row.serial_no,
|
||||
"qty": -1,
|
||||
"warehouse": doc.warehouse,
|
||||
"batch_no": serial_no_row.batch_no,
|
||||
},
|
||||
)
|
||||
|
||||
doc.set_incoming_rate(save=True)
|
||||
doc.calculate_qty_and_amount(save=True)
|
||||
doc.db_update_all()
|
||||
|
||||
return current_qty
|
||||
|
||||
def get_current_qty_for_batch_nos(self, doc):
|
||||
current_qty = 0.0
|
||||
precision = doc.entries[0].precision("qty")
|
||||
for d in doc.entries:
|
||||
qty = (
|
||||
get_batch_qty(
|
||||
@ -864,7 +924,7 @@ class StockReconciliation(StockController):
|
||||
|
||||
current_qty += qty
|
||||
|
||||
return abs(current_qty)
|
||||
return current_qty
|
||||
|
||||
|
||||
def get_batch_qty_for_stock_reco(
|
||||
|
@ -925,6 +925,74 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(len(serial_batch_bundle), 0)
|
||||
|
||||
def test_backdated_purchase_receipt_with_stock_reco(self):
|
||||
item_code = self.make_item(
|
||||
properties={
|
||||
"is_stock_item": 1,
|
||||
"has_serial_no": 1,
|
||||
"serial_no_series": "TEST-SERIAL-.###",
|
||||
}
|
||||
).name
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# Step - 1: Create a Backdated Purchase Receipt
|
||||
|
||||
pr1 = make_purchase_receipt(
|
||||
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
|
||||
)
|
||||
pr1.reload()
|
||||
|
||||
serial_nos = sorted(get_serial_nos_from_bundle(pr1.items[0].serial_and_batch_bundle))[:5]
|
||||
|
||||
# Step - 2: Create a Stock Reconciliation
|
||||
sr1 = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=5,
|
||||
serial_no=serial_nos,
|
||||
)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if d.actual_qty < 0:
|
||||
self.assertEqual(d.actual_qty, -10.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, -1000.0)
|
||||
else:
|
||||
self.assertEqual(d.actual_qty, 5.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||
|
||||
# Step - 3: Create a Purchase Receipt before the first Purchase Receipt
|
||||
make_purchase_receipt(
|
||||
item_code=item_code, warehouse=warehouse, qty=10, rate=200, posting_date=add_days(nowdate(), -5)
|
||||
)
|
||||
|
||||
data = frappe.get_all(
|
||||
"Stock Ledger Entry",
|
||||
fields=["serial_no", "actual_qty", "stock_value_difference"],
|
||||
filters={"voucher_no": sr1.name, "is_cancelled": 0},
|
||||
order_by="creation",
|
||||
)
|
||||
|
||||
for d in data:
|
||||
if d.actual_qty < 0:
|
||||
self.assertEqual(d.actual_qty, -20.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, -3000.0)
|
||||
else:
|
||||
self.assertEqual(d.actual_qty, 5.0)
|
||||
self.assertAlmostEqual(d.stock_value_difference, 500.0)
|
||||
|
||||
active_serial_no = frappe.get_all(
|
||||
"Serial No", filters={"status": "Active", "item_code": item_code}
|
||||
)
|
||||
self.assertEqual(len(active_serial_no), 5)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
@ -176,7 +176,7 @@
|
||||
"description": "No stock transactions can be created or modified before this date.",
|
||||
"fieldname": "stock_frozen_upto",
|
||||
"fieldtype": "Date",
|
||||
"label": "Stock Frozen Upto"
|
||||
"label": "Stock Frozen Up To"
|
||||
},
|
||||
{
|
||||
"description": "Stock transactions that are older than the mentioned days cannot be modified.",
|
||||
@ -427,7 +427,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-10-18 12:35:30.068799",
|
||||
"modified": "2024-01-24 02:20:26.145996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
@ -145,6 +145,7 @@ def create_material_request(material_requests):
|
||||
|
||||
mr.log_error("Unable to create material request")
|
||||
|
||||
company_wise_mr = frappe._dict({})
|
||||
for request_type in material_requests:
|
||||
for company in material_requests[request_type]:
|
||||
try:
|
||||
@ -206,17 +207,19 @@ def create_material_request(material_requests):
|
||||
mr.submit()
|
||||
mr_list.append(mr)
|
||||
|
||||
company_wise_mr.setdefault(company, []).append(mr)
|
||||
|
||||
except Exception:
|
||||
_log_exception(mr)
|
||||
|
||||
if mr_list:
|
||||
if company_wise_mr:
|
||||
if getattr(frappe.local, "reorder_email_notify", None) is None:
|
||||
frappe.local.reorder_email_notify = cint(
|
||||
frappe.db.get_single_value("Stock Settings", "reorder_email_notify")
|
||||
)
|
||||
|
||||
if frappe.local.reorder_email_notify:
|
||||
send_email_notification(mr_list)
|
||||
send_email_notification(company_wise_mr)
|
||||
|
||||
if exceptions_list:
|
||||
notify_errors(exceptions_list)
|
||||
@ -224,20 +227,56 @@ def create_material_request(material_requests):
|
||||
return mr_list
|
||||
|
||||
|
||||
def send_email_notification(mr_list):
|
||||
def send_email_notification(company_wise_mr):
|
||||
"""Notify user about auto creation of indent"""
|
||||
|
||||
email_list = frappe.db.sql_list(
|
||||
"""select distinct r.parent
|
||||
from `tabHas Role` r, tabUser p
|
||||
where p.name = r.parent and p.enabled = 1 and p.docstatus < 2
|
||||
and r.role in ('Purchase Manager','Stock Manager')
|
||||
and p.name not in ('Administrator', 'All', 'Guest')"""
|
||||
for company, mr_list in company_wise_mr.items():
|
||||
email_list = get_email_list(company)
|
||||
|
||||
if not email_list:
|
||||
continue
|
||||
|
||||
msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list})
|
||||
|
||||
frappe.sendmail(
|
||||
recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg
|
||||
)
|
||||
|
||||
|
||||
def get_email_list(company):
|
||||
users = get_comapny_wise_users(company)
|
||||
user_table = frappe.qb.DocType("User")
|
||||
role_table = frappe.qb.DocType("Has Role")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(user_table)
|
||||
.inner_join(role_table)
|
||||
.on(user_table.name == role_table.parent)
|
||||
.select(user_table.email)
|
||||
.where(
|
||||
(role_table.role.isin(["Purchase Manager", "Stock Manager"]))
|
||||
& (user_table.name.notin(["Administrator", "All", "Guest"]))
|
||||
& (user_table.enabled == 1)
|
||||
& (user_table.docstatus < 2)
|
||||
)
|
||||
)
|
||||
|
||||
msg = frappe.render_template("templates/emails/reorder_item.html", {"mr_list": mr_list})
|
||||
if users:
|
||||
query = query.where(user_table.name.isin(users))
|
||||
|
||||
frappe.sendmail(recipients=email_list, subject=_("Auto Material Requests Generated"), message=msg)
|
||||
emails = query.run(as_dict=True)
|
||||
|
||||
return list(set([email.email for email in emails]))
|
||||
|
||||
|
||||
def get_comapny_wise_users(company):
|
||||
users = frappe.get_all(
|
||||
"User Permission",
|
||||
filters={"allow": "Company", "for_value": company, "apply_to_all_doctypes": 1},
|
||||
fields=["user"],
|
||||
)
|
||||
|
||||
return [user.user for user in users]
|
||||
|
||||
|
||||
def notify_errors(exceptions_list):
|
||||
@ -246,7 +285,7 @@ def notify_errors(exceptions_list):
|
||||
_("Dear System Manager,")
|
||||
+ "<br>"
|
||||
+ _(
|
||||
"An error occured for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :"
|
||||
"An error occurred for certain Items while creating Material Requests based on Re-order level. Please rectify these issues :"
|
||||
)
|
||||
+ "<br>"
|
||||
)
|
||||
|
@ -22,9 +22,8 @@ def get_columns(filters):
|
||||
{"label": _("Posting Time"), "fieldtype": "Time", "fieldname": "posting_time", "width": 90},
|
||||
{
|
||||
"label": _("Voucher Type"),
|
||||
"fieldtype": "Link",
|
||||
"fieldtype": "Data",
|
||||
"fieldname": "voucher_type",
|
||||
"options": "DocType",
|
||||
"width": 160,
|
||||
},
|
||||
{
|
||||
|
@ -9,9 +9,18 @@ from typing import Optional, Set, Tuple
|
||||
import frappe
|
||||
from frappe import _, scrub
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import CombineDatetime, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, now, nowdate, nowtime, parse_json
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
cstr,
|
||||
flt,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
now,
|
||||
nowdate,
|
||||
nowtime,
|
||||
parse_json,
|
||||
)
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||
@ -439,7 +448,7 @@ def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
|
||||
reposting_data = get_reposting_data(doc.reposting_data_file)
|
||||
|
||||
if reposting_data and reposting_data.distinct_item_and_warehouse:
|
||||
return reposting_data.distinct_item_and_warehouse
|
||||
return parse_distinct_items_and_warehouses(reposting_data.distinct_item_and_warehouse)
|
||||
|
||||
distinct_item_warehouses = {}
|
||||
|
||||
@ -457,6 +466,16 @@ def get_distinct_item_warehouse(args=None, doc=None, reposting_data=None):
|
||||
return distinct_item_warehouses
|
||||
|
||||
|
||||
def parse_distinct_items_and_warehouses(distinct_items_and_warehouses):
|
||||
new_dict = frappe._dict({})
|
||||
|
||||
# convert string keys to tuple
|
||||
for k, v in distinct_items_and_warehouses.items():
|
||||
new_dict[frappe.safe_eval(k)] = frappe._dict(v)
|
||||
|
||||
return new_dict
|
||||
|
||||
|
||||
def get_affected_transactions(doc, reposting_data=None) -> Set[Tuple[str, str]]:
|
||||
if not reposting_data and doc and doc.reposting_data_file:
|
||||
reposting_data = get_reposting_data(doc.reposting_data_file)
|
||||
@ -702,11 +721,10 @@ class update_entries_after(object):
|
||||
|
||||
if (
|
||||
sle.voucher_type == "Stock Reconciliation"
|
||||
and (
|
||||
sle.batch_no or (sle.has_batch_no and sle.serial_and_batch_bundle and not sle.has_serial_no)
|
||||
)
|
||||
and (sle.batch_no or sle.serial_no or sle.serial_and_batch_bundle)
|
||||
and sle.voucher_detail_no
|
||||
and not self.args.get("sle_id")
|
||||
and sle.is_cancelled == 0
|
||||
):
|
||||
self.reset_actual_qty_for_stock_reco(sle)
|
||||
|
||||
@ -727,6 +745,23 @@ class update_entries_after(object):
|
||||
|
||||
if sle.serial_and_batch_bundle:
|
||||
self.calculate_valuation_for_serial_batch_bundle(sle)
|
||||
elif sle.serial_no and not self.args.get("sle_id"):
|
||||
# Only run in reposting
|
||||
self.get_serialized_values(sle)
|
||||
self.wh_data.qty_after_transaction += flt(sle.actual_qty)
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||
self.wh_data.qty_after_transaction = sle.qty_after_transaction
|
||||
|
||||
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(
|
||||
self.wh_data.valuation_rate
|
||||
)
|
||||
elif (
|
||||
sle.batch_no
|
||||
and frappe.db.get_value("Batch", sle.batch_no, "use_batchwise_valuation", cache=True)
|
||||
and not self.args.get("sle_id")
|
||||
):
|
||||
# Only run in reposting
|
||||
self.update_batched_values(sle)
|
||||
else:
|
||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no and not has_dimensions:
|
||||
# assert
|
||||
@ -772,6 +807,45 @@ class update_entries_after(object):
|
||||
):
|
||||
self.update_outgoing_rate_on_transaction(sle)
|
||||
|
||||
def get_serialized_values(self, sle):
|
||||
incoming_rate = flt(sle.incoming_rate)
|
||||
actual_qty = flt(sle.actual_qty)
|
||||
serial_nos = cstr(sle.serial_no).split("\n")
|
||||
|
||||
if incoming_rate < 0:
|
||||
# wrong incoming rate
|
||||
incoming_rate = self.wh_data.valuation_rate
|
||||
|
||||
stock_value_change = 0
|
||||
if actual_qty > 0:
|
||||
stock_value_change = actual_qty * incoming_rate
|
||||
else:
|
||||
# In case of delivery/stock issue, get average purchase rate
|
||||
# of serial nos of current entry
|
||||
if not sle.is_cancelled:
|
||||
outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
|
||||
stock_value_change = -1 * outgoing_value
|
||||
else:
|
||||
stock_value_change = actual_qty * sle.outgoing_rate
|
||||
|
||||
new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
|
||||
|
||||
if new_stock_qty > 0:
|
||||
new_stock_value = (
|
||||
self.wh_data.qty_after_transaction * self.wh_data.valuation_rate
|
||||
) + stock_value_change
|
||||
if new_stock_value >= 0:
|
||||
# calculate new valuation rate only if stock value is positive
|
||||
# else it remains the same as that of previous entry
|
||||
self.wh_data.valuation_rate = new_stock_value / new_stock_qty
|
||||
|
||||
if not self.wh_data.valuation_rate and sle.voucher_detail_no:
|
||||
allow_zero_rate = self.check_if_allow_zero_valuation_rate(
|
||||
sle.voucher_type, sle.voucher_detail_no
|
||||
)
|
||||
if not allow_zero_rate:
|
||||
self.wh_data.valuation_rate = self.get_fallback_rate(sle)
|
||||
|
||||
def reset_actual_qty_for_stock_reco(self, sle):
|
||||
doc = frappe.get_cached_doc("Stock Reconciliation", sle.voucher_no)
|
||||
doc.recalculate_current_qty(sle.voucher_detail_no, sle.creation, sle.actual_qty > 0)
|
||||
@ -785,6 +859,36 @@ class update_entries_after(object):
|
||||
if abs(sle.actual_qty) == 0.0:
|
||||
sle.is_cancelled = 1
|
||||
|
||||
if sle.serial_and_batch_bundle and frappe.get_cached_value(
|
||||
"Item", sle.item_code, "has_serial_no"
|
||||
):
|
||||
self.update_serial_no_status(sle)
|
||||
|
||||
def update_serial_no_status(self, sle):
|
||||
from erpnext.stock.serial_batch_bundle import get_serial_nos
|
||||
|
||||
serial_nos = get_serial_nos(sle.serial_and_batch_bundle)
|
||||
if not serial_nos:
|
||||
return
|
||||
|
||||
warehouse = None
|
||||
status = "Inactive"
|
||||
|
||||
if sle.actual_qty > 0:
|
||||
warehouse = sle.warehouse
|
||||
status = "Active"
|
||||
|
||||
sn_table = frappe.qb.DocType("Serial No")
|
||||
|
||||
query = (
|
||||
frappe.qb.update(sn_table)
|
||||
.set(sn_table.warehouse, warehouse)
|
||||
.set(sn_table.status, status)
|
||||
.where(sn_table.name.isin(serial_nos))
|
||||
)
|
||||
|
||||
query.run()
|
||||
|
||||
def calculate_valuation_for_serial_batch_bundle(self, sle):
|
||||
doc = frappe.get_cached_doc("Serial and Batch Bundle", sle.serial_and_batch_bundle)
|
||||
|
||||
@ -1161,11 +1265,12 @@ class update_entries_after(object):
|
||||
outgoing_rate = get_batch_incoming_rate(
|
||||
item_code=sle.item_code,
|
||||
warehouse=sle.warehouse,
|
||||
serial_and_batch_bundle=sle.serial_and_batch_bundle,
|
||||
batch_no=sle.batch_no,
|
||||
posting_date=sle.posting_date,
|
||||
posting_time=sle.posting_time,
|
||||
creation=sle.creation,
|
||||
)
|
||||
|
||||
if outgoing_rate is None:
|
||||
# This can *only* happen if qty available for the batch is zero.
|
||||
# in such case fall back various other rates.
|
||||
@ -1439,11 +1544,10 @@ def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
|
||||
|
||||
|
||||
def get_batch_incoming_rate(
|
||||
item_code, warehouse, serial_and_batch_bundle, posting_date, posting_time, creation=None
|
||||
item_code, warehouse, batch_no, posting_date, posting_time, creation=None
|
||||
):
|
||||
|
||||
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||
batch_ledger = frappe.qb.DocType("Serial and Batch Entry")
|
||||
|
||||
timestamp_condition = CombineDatetime(sle.posting_date, sle.posting_time) < CombineDatetime(
|
||||
posting_date, posting_time
|
||||
@ -1454,28 +1558,13 @@ def get_batch_incoming_rate(
|
||||
== CombineDatetime(posting_date, posting_time)
|
||||
) & (sle.creation < creation)
|
||||
|
||||
batches = frappe.get_all(
|
||||
"Serial and Batch Entry", fields=["batch_no"], filters={"parent": serial_and_batch_bundle}
|
||||
)
|
||||
|
||||
batch_details = (
|
||||
frappe.qb.from_(sle)
|
||||
.inner_join(batch_ledger)
|
||||
.on(sle.serial_and_batch_bundle == batch_ledger.parent)
|
||||
.select(
|
||||
Sum(
|
||||
Case()
|
||||
.when(sle.actual_qty > 0, batch_ledger.qty * batch_ledger.incoming_rate)
|
||||
.else_(batch_ledger.qty * batch_ledger.outgoing_rate * -1)
|
||||
).as_("batch_value"),
|
||||
Sum(Case().when(sle.actual_qty > 0, batch_ledger.qty).else_(batch_ledger.qty * -1)).as_(
|
||||
"batch_qty"
|
||||
),
|
||||
)
|
||||
.select(Sum(sle.stock_value_difference).as_("batch_value"), Sum(sle.actual_qty).as_("batch_qty"))
|
||||
.where(
|
||||
(sle.item_code == item_code)
|
||||
& (sle.warehouse == warehouse)
|
||||
& (batch_ledger.batch_no.isin([row.batch_no for row in batches]))
|
||||
& (sle.batch_no == batch_no)
|
||||
& (sle.is_cancelled == 0)
|
||||
)
|
||||
.where(timestamp_condition)
|
||||
|
@ -124,7 +124,7 @@ def get_help_messages():
|
||||
doctype="Timesheet",
|
||||
title=_("Add Timesheets"),
|
||||
description=_(
|
||||
"Timesheets help keep track of time, cost and billing for activites done by your team"
|
||||
"Timesheets help keep track of time, cost and billing for activities done by your team"
|
||||
),
|
||||
action=_("Create Timesheet"),
|
||||
route="List/Timesheet",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user