Merge branch 'develop' into payment-reco-company-field

This commit is contained in:
Gursheen Kaur Anand 2024-01-25 11:44:53 +05:30 committed by GitHub
commit 3f383d81bd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
101 changed files with 1757 additions and 538 deletions

View File

@ -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": {

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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):
"""

View File

@ -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()

View File

@ -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",

View File

@ -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,

View File

@ -154,7 +154,7 @@ frappe.ui.form.on('Invoice Discounting', {
}
});
},
primary_action_label: __('Get Invocies')
primary_action_label: __('Get Invoices')
});
d.show();
},

View File

@ -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():

View File

@ -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

View File

@ -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 = "";

View File

@ -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"

View File

@ -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', '');

View File

@ -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,

View File

@ -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

View File

@ -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",

View File

@ -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":

View File

@ -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())

View File

@ -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"),
)

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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)
):

View File

@ -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') {

View File

@ -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()

View File

@ -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.",

View File

@ -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") },

View File

@ -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"]|| "&nbsp;") { %}
{% 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"] %}

View File

@ -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 = []

View File

@ -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":

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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()

View File

@ -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"):

View File

@ -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",

View File

@ -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()],
},
],

View File

@ -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
}
}

View File

@ -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
)

View File

@ -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",

View File

@ -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

View File

@ -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({

View File

@ -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

View File

@ -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):

View File

@ -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",

View File

@ -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",
),
)

View File

@ -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"
)
)

View File

@ -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 = (

View File

@ -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:

View File

@ -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 = [

View File

@ -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")),

View File

@ -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

View File

@ -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")

View 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()

View File

@ -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) {

View File

@ -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'], () => {

View File

@ -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 = {
}
});
}
}
}

View File

@ -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();

View File

@ -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",

View File

@ -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(

View File

@ -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(

View File

@ -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:

View File

@ -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")

View File

@ -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 = {

View File

@ -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
}
}

View File

@ -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}):

View File

@ -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

View File

@ -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")

View File

@ -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])
});
});
}

View File

@ -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])
)

View File

@ -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({

View File

@ -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=""):

View File

@ -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");
});
}
});

View File

@ -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)))

View File

@ -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",

View File

@ -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()

View File

@ -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')

View File

@ -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,
}
)

View File

@ -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: {

View File

@ -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,
)

View File

@ -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",

View File

@ -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:

View File

@ -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):

View File

@ -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:

View File

@ -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") {

View File

@ -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"):

View File

@ -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,
)

View File

@ -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",

View File

@ -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})

View File

@ -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 (

View File

@ -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(

View File

@ -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)

View File

@ -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",

View File

@ -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>"
)

View File

@ -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,
},
{

View File

@ -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)

View File

@ -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