mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 12:39:18 +00:00
Merge branch 'develop' into fix-22-23-05686
This commit is contained in:
1
.github/workflows/server-tests-mariadb.yml
vendored
1
.github/workflows/server-tests-mariadb.yml
vendored
@@ -7,7 +7,6 @@ on:
|
|||||||
- '**.css'
|
- '**.css'
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- '**.html'
|
- '**.html'
|
||||||
- '**.csv'
|
|
||||||
push:
|
push:
|
||||||
branches: [ develop ]
|
branches: [ develop ]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
|
|||||||
@@ -211,8 +211,7 @@ class ExchangeRateRevaluation(Document):
|
|||||||
# Handle Accounts with '0' balance in Account/Base Currency
|
# Handle Accounts with '0' balance in Account/Base Currency
|
||||||
for d in [x for x in account_details if x.zero_balance]:
|
for d in [x for x in account_details if x.zero_balance]:
|
||||||
|
|
||||||
# TODO: Set new balance in Base/Account currency
|
if d.balance != 0:
|
||||||
if d.balance > 0:
|
|
||||||
current_exchange_rate = new_exchange_rate = 0
|
current_exchange_rate = new_exchange_rate = 0
|
||||||
|
|
||||||
new_balance_in_account_currency = 0 # this will be '0'
|
new_balance_in_account_currency = 0 # this will be '0'
|
||||||
@@ -399,6 +398,9 @@ class ExchangeRateRevaluation(Document):
|
|||||||
|
|
||||||
journal_entry_accounts = []
|
journal_entry_accounts = []
|
||||||
for d in accounts:
|
for d in accounts:
|
||||||
|
if not flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")):
|
||||||
|
continue
|
||||||
|
|
||||||
dr_or_cr = (
|
dr_or_cr = (
|
||||||
"debit_in_account_currency"
|
"debit_in_account_currency"
|
||||||
if d.get("balance_in_account_currency") > 0
|
if d.get("balance_in_account_currency") > 0
|
||||||
@@ -448,7 +450,13 @@ class ExchangeRateRevaluation(Document):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
journal_entry_accounts.append(
|
journal_entry.set("accounts", journal_entry_accounts)
|
||||||
|
journal_entry.set_amounts_in_company_currency()
|
||||||
|
journal_entry.set_total_debit_credit()
|
||||||
|
|
||||||
|
self.gain_loss_unbooked += journal_entry.difference - self.gain_loss_unbooked
|
||||||
|
journal_entry.append(
|
||||||
|
"accounts",
|
||||||
{
|
{
|
||||||
"account": unrealized_exchange_gain_loss_account,
|
"account": unrealized_exchange_gain_loss_account,
|
||||||
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
|
||||||
@@ -460,10 +468,9 @@ class ExchangeRateRevaluation(Document):
|
|||||||
"exchange_rate": 1,
|
"exchange_rate": 1,
|
||||||
"reference_type": "Exchange Rate Revaluation",
|
"reference_type": "Exchange Rate Revaluation",
|
||||||
"reference_name": self.name,
|
"reference_name": self.name,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
journal_entry.set("accounts", journal_entry_accounts)
|
|
||||||
journal_entry.set_amounts_in_company_currency()
|
journal_entry.set_amounts_in_company_currency()
|
||||||
journal_entry.set_total_debit_credit()
|
journal_entry.set_total_debit_credit()
|
||||||
journal_entry.save()
|
journal_entry.save()
|
||||||
|
|||||||
@@ -137,7 +137,8 @@
|
|||||||
"fieldname": "finance_book",
|
"fieldname": "finance_book",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Finance Book",
|
"label": "Finance Book",
|
||||||
"options": "Finance Book"
|
"options": "Finance Book",
|
||||||
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "2_add_edit_gl_entries",
|
"fieldname": "2_add_edit_gl_entries",
|
||||||
@@ -538,7 +539,7 @@
|
|||||||
"idx": 176,
|
"idx": 176,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-01-17 12:53:53.280620",
|
"modified": "2023-03-01 14:58:59.286591",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Journal Entry",
|
"name": "Journal Entry",
|
||||||
|
|||||||
@@ -217,7 +217,6 @@ frappe.ui.form.on('Payment Entry', {
|
|||||||
frm.toggle_display("set_exchange_gain_loss",
|
frm.toggle_display("set_exchange_gain_loss",
|
||||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
|
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
|
||||||
|
|
||||||
frm.refresh_fields();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
set_dynamic_labels: function(frm) {
|
set_dynamic_labels: function(frm) {
|
||||||
|
|||||||
@@ -495,26 +495,22 @@ def get_amount(ref_doc, payment_account=None):
|
|||||||
"""get amount based on doctype"""
|
"""get amount based on doctype"""
|
||||||
dt = ref_doc.doctype
|
dt = ref_doc.doctype
|
||||||
if dt in ["Sales Order", "Purchase Order"]:
|
if dt in ["Sales Order", "Purchase Order"]:
|
||||||
grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid)
|
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
|
||||||
|
|
||||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||||
if ref_doc.party_account_currency == ref_doc.currency:
|
if ref_doc.party_account_currency == ref_doc.currency:
|
||||||
grand_total = flt(ref_doc.outstanding_amount)
|
grand_total = flt(ref_doc.outstanding_amount)
|
||||||
else:
|
else:
|
||||||
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate
|
||||||
|
|
||||||
elif dt == "POS Invoice":
|
elif dt == "POS Invoice":
|
||||||
for pay in ref_doc.payments:
|
for pay in ref_doc.payments:
|
||||||
if pay.type == "Phone" and pay.account == payment_account:
|
if pay.type == "Phone" and pay.account == payment_account:
|
||||||
grand_total = pay.amount
|
grand_total = pay.amount
|
||||||
break
|
break
|
||||||
|
|
||||||
elif dt == "Fees":
|
elif dt == "Fees":
|
||||||
grand_total = ref_doc.outstanding_amount
|
grand_total = ref_doc.outstanding_amount
|
||||||
|
|
||||||
if grand_total > 0:
|
if grand_total > 0:
|
||||||
return grand_total
|
return grand_total
|
||||||
|
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("Payment Entry is already created"))
|
frappe.throw(_("Payment Entry is already created"))
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,10 @@ class TestPaymentRequest(unittest.TestCase):
|
|||||||
frappe.get_doc(method).insert(ignore_permissions=True)
|
frappe.get_doc(method).insert(ignore_permissions=True)
|
||||||
|
|
||||||
def test_payment_request_linkings(self):
|
def test_payment_request_linkings(self):
|
||||||
so_inr = make_sales_order(currency="INR")
|
so_inr = make_sales_order(currency="INR", do_not_save=True)
|
||||||
|
so_inr.disable_rounded_total = 1
|
||||||
|
so_inr.save()
|
||||||
|
|
||||||
pr = make_payment_request(
|
pr = make_payment_request(
|
||||||
dt="Sales Order",
|
dt="Sales Order",
|
||||||
dn=so_inr.name,
|
dn=so_inr.name,
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
|
|||||||
for data in [asset_data, liability_data, equity_data]:
|
for data in [asset_data, liability_data, equity_data]:
|
||||||
if data:
|
if data:
|
||||||
account_name = get_root_account_name(data[0].root_type, company)
|
account_name = get_root_account_name(data[0].root_type, company)
|
||||||
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
if account_name:
|
||||||
|
opening_value += get_opening_balance(account_name, data, company) or 0.0
|
||||||
|
|
||||||
opening_balance[company] = opening_value
|
opening_balance[company] = opening_value
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ def get_opening_balance(account_name, data, company):
|
|||||||
|
|
||||||
|
|
||||||
def get_root_account_name(root_type, company):
|
def get_root_account_name(root_type, company):
|
||||||
return frappe.get_all(
|
root_account = frappe.get_all(
|
||||||
"Account",
|
"Account",
|
||||||
fields=["account_name"],
|
fields=["account_name"],
|
||||||
filters={
|
filters={
|
||||||
@@ -165,7 +166,10 @@ def get_root_account_name(root_type, company):
|
|||||||
"parent_account": ("is", "not set"),
|
"parent_account": ("is", "not set"),
|
||||||
},
|
},
|
||||||
as_list=1,
|
as_list=1,
|
||||||
)[0][0]
|
)
|
||||||
|
|
||||||
|
if root_account:
|
||||||
|
return root_account[0][0]
|
||||||
|
|
||||||
|
|
||||||
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
def get_profit_loss_data(fiscal_year, companies, columns, filters):
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ class SellingController(StockController):
|
|||||||
self.in_words = money_in_words(amount, self.currency)
|
self.in_words = money_in_words(amount, self.currency)
|
||||||
|
|
||||||
def calculate_commission(self):
|
def calculate_commission(self):
|
||||||
if not self.meta.get_field("commission_rate"):
|
if not self.meta.get_field("commission_rate") or self.docstatus.is_submitted():
|
||||||
return
|
return
|
||||||
|
|
||||||
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
|
self.round_floats_in(self, ("amount_eligible_for_commission", "commission_rate"))
|
||||||
|
|||||||
@@ -76,12 +76,9 @@ def get_transaction_list(
|
|||||||
ignore_permissions = False
|
ignore_permissions = False
|
||||||
|
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = []
|
filters = {}
|
||||||
|
|
||||||
if doctype in ["Supplier Quotation", "Purchase Invoice"]:
|
filters["docstatus"] = ["<", "2"] if doctype in ["Supplier Quotation", "Purchase Invoice"] else 1
|
||||||
filters.append((doctype, "docstatus", "<", 2))
|
|
||||||
else:
|
|
||||||
filters.append((doctype, "docstatus", "=", 1))
|
|
||||||
|
|
||||||
if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation":
|
if (user != "Guest" and is_website_user()) or doctype == "Request for Quotation":
|
||||||
parties_doctype = (
|
parties_doctype = (
|
||||||
@@ -92,12 +89,12 @@ def get_transaction_list(
|
|||||||
|
|
||||||
if customers:
|
if customers:
|
||||||
if doctype == "Quotation":
|
if doctype == "Quotation":
|
||||||
filters.append(("quotation_to", "=", "Customer"))
|
filters["quotation_to"] = "Customer"
|
||||||
filters.append(("party_name", "in", customers))
|
filters["party_name"] = ["in", customers]
|
||||||
else:
|
else:
|
||||||
filters.append(("customer", "in", customers))
|
filters["customer"] = ["in", customers]
|
||||||
elif suppliers:
|
elif suppliers:
|
||||||
filters.append(("supplier", "in", suppliers))
|
filters["supplier"] = ["in", suppliers]
|
||||||
elif not custom:
|
elif not custom:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -110,7 +107,7 @@ def get_transaction_list(
|
|||||||
|
|
||||||
if not customers and not suppliers and custom:
|
if not customers and not suppliers and custom:
|
||||||
ignore_permissions = False
|
ignore_permissions = False
|
||||||
filters = []
|
filters = {}
|
||||||
|
|
||||||
transactions = get_list_for_transactions(
|
transactions = get_list_for_transactions(
|
||||||
doctype,
|
doctype,
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (frm.doc.opportunity_from && frm.doc.party_name){
|
|
||||||
frm.trigger('set_contact_link');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
validate: function(frm) {
|
validate: function(frm) {
|
||||||
@@ -130,6 +126,10 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
} else {
|
} else {
|
||||||
frappe.contacts.clear_address_and_contact(frm);
|
frappe.contacts.clear_address_and_contact(frm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.opportunity_from && frm.doc.party_name) {
|
||||||
|
frm.trigger('set_contact_link');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
set_contact_link: function(frm) {
|
set_contact_link: function(frm) {
|
||||||
@@ -137,6 +137,8 @@ frappe.ui.form.on("Opportunity", {
|
|||||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
|
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Customer'}
|
||||||
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
|
} else if(frm.doc.opportunity_from == "Lead" && frm.doc.party_name) {
|
||||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'}
|
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Lead'}
|
||||||
|
} else if (frm.doc.opportunity_from == "Prospect" && frm.doc.party_name) {
|
||||||
|
frappe.dynamic_link = {doc: frm.doc, fieldname: 'party_name', doctype: 'Prospect'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ auto_cancel_exempted_doctypes = [
|
|||||||
|
|
||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
"cron": {
|
"cron": {
|
||||||
"0/5 * * * *": [
|
"0/15 * * * *": [
|
||||||
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
||||||
],
|
],
|
||||||
"0/30 * * * *": [
|
"0/30 * * * *": [
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ def resume_bom_cost_update_jobs():
|
|||||||
["name", "boms_updated", "status"],
|
["name", "boms_updated", "status"],
|
||||||
)
|
)
|
||||||
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
|
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
|
||||||
if not bom_batches or incomplete_level:
|
if not bom_batches or not incomplete_level:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Prep parent BOMs & updated processed BOMs for next level
|
# Prep parent BOMs & updated processed BOMs for next level
|
||||||
@@ -252,6 +252,9 @@ def get_processed_current_boms(
|
|||||||
current_boms = []
|
current_boms = []
|
||||||
|
|
||||||
for row in bom_batches:
|
for row in bom_batches:
|
||||||
|
if not row.boms_updated:
|
||||||
|
continue
|
||||||
|
|
||||||
boms_updated = json.loads(row.boms_updated)
|
boms_updated = json.loads(row.boms_updated)
|
||||||
current_boms.extend(boms_updated)
|
current_boms.extend(boms_updated)
|
||||||
boms_updated_dict = {bom: True for bom in boms_updated}
|
boms_updated_dict = {bom: True for bom in boms_updated}
|
||||||
|
|||||||
@@ -561,7 +561,34 @@ class JobCard(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def set_transferred_qty_in_job_card_item(self, ste_doc):
|
def set_transferred_qty_in_job_card_item(self, ste_doc):
|
||||||
from frappe.query_builder.functions import Sum
|
def _get_job_card_items_transferred_qty(ste_doc):
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
|
job_card_items_transferred_qty = {}
|
||||||
|
job_card_items = [
|
||||||
|
x.get("job_card_item") for x in ste_doc.get("items") if x.get("job_card_item")
|
||||||
|
]
|
||||||
|
|
||||||
|
if job_card_items:
|
||||||
|
se = frappe.qb.DocType("Stock Entry")
|
||||||
|
sed = frappe.qb.DocType("Stock Entry Detail")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(sed)
|
||||||
|
.join(se)
|
||||||
|
.on(sed.parent == se.name)
|
||||||
|
.select(sed.job_card_item, Sum(sed.qty))
|
||||||
|
.where(
|
||||||
|
(sed.job_card_item.isin(job_card_items))
|
||||||
|
& (se.docstatus == 1)
|
||||||
|
& (se.purpose == "Material Transfer for Manufacture")
|
||||||
|
)
|
||||||
|
.groupby(sed.job_card_item)
|
||||||
|
)
|
||||||
|
|
||||||
|
job_card_items_transferred_qty = frappe._dict(query.run(as_list=True))
|
||||||
|
|
||||||
|
return job_card_items_transferred_qty
|
||||||
|
|
||||||
def _validate_over_transfer(row, transferred_qty):
|
def _validate_over_transfer(row, transferred_qty):
|
||||||
"Block over transfer of items if not allowed in settings."
|
"Block over transfer of items if not allowed in settings."
|
||||||
@@ -578,29 +605,23 @@ class JobCard(Document):
|
|||||||
exc=JobCardOverTransferError,
|
exc=JobCardOverTransferError,
|
||||||
)
|
)
|
||||||
|
|
||||||
for row in ste_doc.items:
|
job_card_items_transferred_qty = _get_job_card_items_transferred_qty(ste_doc)
|
||||||
if not row.job_card_item:
|
|
||||||
continue
|
|
||||||
|
|
||||||
sed = frappe.qb.DocType("Stock Entry Detail")
|
|
||||||
se = frappe.qb.DocType("Stock Entry")
|
|
||||||
transferred_qty = (
|
|
||||||
frappe.qb.from_(sed)
|
|
||||||
.join(se)
|
|
||||||
.on(sed.parent == se.name)
|
|
||||||
.select(Sum(sed.qty))
|
|
||||||
.where(
|
|
||||||
(sed.job_card_item == row.job_card_item)
|
|
||||||
& (se.docstatus == 1)
|
|
||||||
& (se.purpose == "Material Transfer for Manufacture")
|
|
||||||
)
|
|
||||||
).run()[0][0]
|
|
||||||
|
|
||||||
|
if job_card_items_transferred_qty:
|
||||||
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
||||||
if not allow_excess:
|
|
||||||
_validate_over_transfer(row, transferred_qty)
|
|
||||||
|
|
||||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
|
for row in ste_doc.items:
|
||||||
|
if not row.job_card_item:
|
||||||
|
continue
|
||||||
|
|
||||||
|
transferred_qty = flt(job_card_items_transferred_qty.get(row.job_card_item))
|
||||||
|
|
||||||
|
if not allow_excess:
|
||||||
|
_validate_over_transfer(row, transferred_qty)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty)
|
||||||
|
)
|
||||||
|
|
||||||
def set_transferred_qty(self, update_status=False):
|
def set_transferred_qty(self, update_status=False):
|
||||||
"Set total FG Qty in Job Card for which RM was transferred."
|
"Set total FG Qty in Job Card for which RM was transferred."
|
||||||
|
|||||||
@@ -506,7 +506,7 @@ frappe.ui.form.on("Work Order Item", {
|
|||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
if (r.message) {
|
if (r.message) {
|
||||||
frappe.model.set_value(cdt, cdn, {
|
frappe.model.set_value(cdt, cdn, {
|
||||||
"required_qty": 1,
|
"required_qty": row.required_qty || 1,
|
||||||
"item_name": r.message.item_name,
|
"item_name": r.message.item_name,
|
||||||
"description": r.message.description,
|
"description": r.message.description,
|
||||||
"source_warehouse": r.message.default_warehouse,
|
"source_warehouse": r.message.default_warehouse,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Floor, Sum
|
||||||
|
from frappe.utils import cint
|
||||||
from pypika.terms import ExistsCriterion
|
from pypika.terms import ExistsCriterion
|
||||||
|
|
||||||
|
|
||||||
@@ -34,57 +35,55 @@ def get_columns():
|
|||||||
|
|
||||||
|
|
||||||
def get_bom_stock(filters):
|
def get_bom_stock(filters):
|
||||||
qty_to_produce = filters.get("qty_to_produce") or 1
|
qty_to_produce = filters.get("qty_to_produce")
|
||||||
if int(qty_to_produce) < 0:
|
if cint(qty_to_produce) <= 0:
|
||||||
frappe.throw(_("Quantity to Produce can not be less than Zero"))
|
frappe.throw(_("Quantity to Produce should be greater than zero."))
|
||||||
|
|
||||||
if filters.get("show_exploded_view"):
|
if filters.get("show_exploded_view"):
|
||||||
bom_item_table = "BOM Explosion Item"
|
bom_item_table = "BOM Explosion Item"
|
||||||
else:
|
else:
|
||||||
bom_item_table = "BOM Item"
|
bom_item_table = "BOM Item"
|
||||||
|
|
||||||
bin = frappe.qb.DocType("Bin")
|
warehouse_details = frappe.db.get_value(
|
||||||
bom = frappe.qb.DocType("BOM")
|
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
||||||
bom_item = frappe.qb.DocType(bom_item_table)
|
|
||||||
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(bom)
|
|
||||||
.inner_join(bom_item)
|
|
||||||
.on(bom.name == bom_item.parent)
|
|
||||||
.left_join(bin)
|
|
||||||
.on(bom_item.item_code == bin.item_code)
|
|
||||||
.select(
|
|
||||||
bom_item.item_code,
|
|
||||||
bom_item.description,
|
|
||||||
bom_item.stock_qty,
|
|
||||||
bom_item.stock_uom,
|
|
||||||
(bom_item.stock_qty / bom.quantity) * qty_to_produce,
|
|
||||||
Sum(bin.actual_qty),
|
|
||||||
Sum(bin.actual_qty) / (bom_item.stock_qty / bom.quantity),
|
|
||||||
)
|
|
||||||
.where((bom_item.parent == filters.get("bom")) & (bom_item.parenttype == "BOM"))
|
|
||||||
.groupby(bom_item.item_code)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if filters.get("warehouse"):
|
BOM = frappe.qb.DocType("BOM")
|
||||||
warehouse_details = frappe.db.get_value(
|
BOM_ITEM = frappe.qb.DocType(bom_item_table)
|
||||||
"Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1
|
BIN = frappe.qb.DocType("Bin")
|
||||||
)
|
WH = frappe.qb.DocType("Warehouse")
|
||||||
|
CONDITIONS = ()
|
||||||
|
|
||||||
if warehouse_details:
|
if warehouse_details:
|
||||||
wh = frappe.qb.DocType("Warehouse")
|
CONDITIONS = ExistsCriterion(
|
||||||
query = query.where(
|
frappe.qb.from_(WH)
|
||||||
ExistsCriterion(
|
.select(WH.name)
|
||||||
frappe.qb.from_(wh)
|
.where(
|
||||||
.select(wh.name)
|
(WH.lft >= warehouse_details.lft)
|
||||||
.where(
|
& (WH.rgt <= warehouse_details.rgt)
|
||||||
(wh.lft >= warehouse_details.lft)
|
& (BIN.warehouse == WH.name)
|
||||||
& (wh.rgt <= warehouse_details.rgt)
|
|
||||||
& (bin.warehouse == wh.name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
else:
|
)
|
||||||
query = query.where(bin.warehouse == filters.get("warehouse"))
|
else:
|
||||||
|
CONDITIONS = BIN.warehouse == filters.get("warehouse")
|
||||||
|
|
||||||
return query.run()
|
QUERY = (
|
||||||
|
frappe.qb.from_(BOM)
|
||||||
|
.inner_join(BOM_ITEM)
|
||||||
|
.on(BOM.name == BOM_ITEM.parent)
|
||||||
|
.left_join(BIN)
|
||||||
|
.on((BOM_ITEM.item_code == BIN.item_code) & (CONDITIONS))
|
||||||
|
.select(
|
||||||
|
BOM_ITEM.item_code,
|
||||||
|
BOM_ITEM.description,
|
||||||
|
BOM_ITEM.stock_qty,
|
||||||
|
BOM_ITEM.stock_uom,
|
||||||
|
BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity,
|
||||||
|
Sum(BIN.actual_qty).as_("actual_qty"),
|
||||||
|
Sum(Floor(BIN.actual_qty / (BOM_ITEM.stock_qty * qty_to_produce / BOM.quantity))),
|
||||||
|
)
|
||||||
|
.where((BOM_ITEM.parent == filters.get("bom")) & (BOM_ITEM.parenttype == "BOM"))
|
||||||
|
.groupby(BOM_ITEM.item_code)
|
||||||
|
)
|
||||||
|
|
||||||
|
return QUERY.run()
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.exceptions import ValidationError
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import floor
|
||||||
|
|
||||||
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
|
from erpnext.manufacturing.report.bom_stock_report.bom_stock_report import (
|
||||||
|
get_bom_stock as bom_stock_report,
|
||||||
|
)
|
||||||
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
|
||||||
|
class TestBomStockReport(FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.warehouse = "_Test Warehouse - _TC"
|
||||||
|
self.fg_item, self.rm_items = create_items()
|
||||||
|
make_stock_entry(target=self.warehouse, item_code=self.rm_items[0], qty=20, basic_rate=100)
|
||||||
|
make_stock_entry(target=self.warehouse, item_code=self.rm_items[1], qty=40, basic_rate=200)
|
||||||
|
self.bom = make_bom(item=self.fg_item, quantity=1, raw_materials=self.rm_items, rm_qty=10)
|
||||||
|
|
||||||
|
def test_bom_stock_report(self):
|
||||||
|
# Test 1: When `qty_to_produce` is 0.
|
||||||
|
filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"bom": self.bom.name,
|
||||||
|
"warehouse": "Stores - _TC",
|
||||||
|
"qty_to_produce": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertRaises(ValidationError, bom_stock_report, filters)
|
||||||
|
|
||||||
|
# Test 2: When stock is not available.
|
||||||
|
data = bom_stock_report(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"bom": self.bom.name,
|
||||||
|
"warehouse": "Stores - _TC",
|
||||||
|
"qty_to_produce": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_data = get_expected_data(self.bom, "Stores - _TC", 1)
|
||||||
|
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||||
|
|
||||||
|
# Test 3: When stock is available.
|
||||||
|
data = bom_stock_report(
|
||||||
|
frappe._dict(
|
||||||
|
{
|
||||||
|
"bom": self.bom.name,
|
||||||
|
"warehouse": self.warehouse,
|
||||||
|
"qty_to_produce": 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
expected_data = get_expected_data(self.bom, self.warehouse, 1)
|
||||||
|
self.assertSetEqual(set(tuple(x) for x in data), set(tuple(x) for x in expected_data))
|
||||||
|
|
||||||
|
|
||||||
|
def create_items():
|
||||||
|
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||||
|
rm_item1 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"standard_rate": 100,
|
||||||
|
"opening_stock": 100,
|
||||||
|
"last_purchase_rate": 100,
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
rm_item2 = make_item(
|
||||||
|
properties={
|
||||||
|
"is_stock_item": 1,
|
||||||
|
"standard_rate": 200,
|
||||||
|
"opening_stock": 200,
|
||||||
|
"last_purchase_rate": 200,
|
||||||
|
}
|
||||||
|
).name
|
||||||
|
|
||||||
|
return fg_item, [rm_item1, rm_item2]
|
||||||
|
|
||||||
|
|
||||||
|
def get_expected_data(bom, warehouse, qty_to_produce, show_exploded_view=False):
|
||||||
|
expected_data = []
|
||||||
|
|
||||||
|
for item in bom.get("exploded_items") if show_exploded_view else bom.get("items"):
|
||||||
|
in_stock_qty = frappe.get_cached_value(
|
||||||
|
"Bin", {"item_code": item.item_code, "warehouse": warehouse}, "actual_qty"
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_data.append(
|
||||||
|
[
|
||||||
|
item.item_code,
|
||||||
|
item.description,
|
||||||
|
item.stock_qty,
|
||||||
|
item.stock_uom,
|
||||||
|
item.stock_qty * qty_to_produce / bom.quantity,
|
||||||
|
in_stock_qty,
|
||||||
|
floor(in_stock_qty / (item.stock_qty * qty_to_produce / bom.quantity))
|
||||||
|
if in_stock_qty
|
||||||
|
else None,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return expected_data
|
||||||
@@ -323,6 +323,5 @@ erpnext.patches.v14_0.update_entry_type_for_journal_entry
|
|||||||
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
||||||
erpnext.patches.v14_0.set_pick_list_status
|
erpnext.patches.v14_0.set_pick_list_status
|
||||||
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
|
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
|
||||||
# below 2 migration patches should always run last
|
# below migration patches should always run last
|
||||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||||
erpnext.patches.v14_0.migrate_remarks_from_gl_to_payment_ledger
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.query_builder import Case, CustomFunction
|
from frappe.query_builder import CustomFunction
|
||||||
from frappe.query_builder.custom import ConstantColumn
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
from frappe.query_builder.functions import Count, IfNull
|
from frappe.query_builder.functions import Count, IfNull
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
@@ -18,9 +18,21 @@ def create_accounting_dimension_fields():
|
|||||||
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
|
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
|
||||||
|
|
||||||
|
|
||||||
def generate_name_for_payment_ledger_entries(gl_entries, start):
|
def generate_name_and_calculate_amount(gl_entries, start, receivable_accounts):
|
||||||
for index, entry in enumerate(gl_entries, 0):
|
for index, entry in enumerate(gl_entries, 0):
|
||||||
entry.name = start + index
|
entry.name = start + index
|
||||||
|
if entry.account in receivable_accounts:
|
||||||
|
entry.account_type = "Receivable"
|
||||||
|
entry.amount = entry.debit - entry.credit
|
||||||
|
entry.amount_in_account_currency = (
|
||||||
|
entry.debit_in_account_currency - entry.credit_in_account_currency
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
entry.account_type = "Payable"
|
||||||
|
entry.amount = entry.credit - entry.debit
|
||||||
|
entry.amount_in_account_currency = (
|
||||||
|
entry.credit_in_account_currency - entry.debit_in_account_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_columns():
|
def get_columns():
|
||||||
@@ -49,6 +61,9 @@ def get_columns():
|
|||||||
"finance_book",
|
"finance_book",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if frappe.db.has_column("Payment Ledger Entry", "remarks"):
|
||||||
|
columns.append("remarks")
|
||||||
|
|
||||||
dimensions_and_defaults = get_dimensions()
|
dimensions_and_defaults = get_dimensions()
|
||||||
if dimensions_and_defaults:
|
if dimensions_and_defaults:
|
||||||
for dimension in dimensions_and_defaults[0]:
|
for dimension in dimensions_and_defaults[0]:
|
||||||
@@ -99,12 +114,17 @@ def execute():
|
|||||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
||||||
|
|
||||||
# Get Records Count
|
# Get Records Count
|
||||||
accounts = (
|
relavant_accounts = (
|
||||||
qb.from_(account)
|
qb.from_(account)
|
||||||
.select(account.name)
|
.select(account.name, account.account_type)
|
||||||
.where((account.account_type == "Receivable") | (account.account_type == "Payable"))
|
.where((account.account_type == "Receivable") | (account.account_type == "Payable"))
|
||||||
.orderby(account.name)
|
.orderby(account.name)
|
||||||
|
.run(as_dict=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
receivable_accounts = [x.name for x in relavant_accounts if x.account_type == "Receivable"]
|
||||||
|
accounts = [x.name for x in relavant_accounts]
|
||||||
|
|
||||||
un_processed = (
|
un_processed = (
|
||||||
qb.from_(gl)
|
qb.from_(gl)
|
||||||
.select(Count(gl.name))
|
.select(Count(gl.name))
|
||||||
@@ -122,37 +142,21 @@ def execute():
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
if last_name:
|
if last_name:
|
||||||
where_clause = gl.name.gt(last_name) & (gl.is_cancelled == 0)
|
where_clause = gl.name.gt(last_name) & gl.account.isin(accounts) & gl.is_cancelled == 0
|
||||||
else:
|
else:
|
||||||
where_clause = gl.is_cancelled == 0
|
where_clause = gl.account.isin(accounts) & gl.is_cancelled == 0
|
||||||
|
|
||||||
gl_entries = (
|
gl_entries = (
|
||||||
qb.from_(gl)
|
qb.from_(gl)
|
||||||
.inner_join(account)
|
|
||||||
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
|
|
||||||
.select(
|
.select(
|
||||||
gl.star,
|
gl.star,
|
||||||
ConstantColumn(1).as_("docstatus"),
|
ConstantColumn(1).as_("docstatus"),
|
||||||
account.account_type.as_("account_type"),
|
|
||||||
IfNull(
|
IfNull(
|
||||||
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
|
ifelse(gl.against_voucher_type == "", None, gl.against_voucher_type), gl.voucher_type
|
||||||
).as_("against_voucher_type"),
|
).as_("against_voucher_type"),
|
||||||
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
|
IfNull(ifelse(gl.against_voucher == "", None, gl.against_voucher), gl.voucher_no).as_(
|
||||||
"against_voucher_no"
|
"against_voucher_no"
|
||||||
),
|
),
|
||||||
# convert debit/credit to amount
|
|
||||||
Case()
|
|
||||||
.when(account.account_type == "Receivable", gl.debit - gl.credit)
|
|
||||||
.else_(gl.credit - gl.debit)
|
|
||||||
.as_("amount"),
|
|
||||||
# convert debit/credit in account currency to amount in account currency
|
|
||||||
Case()
|
|
||||||
.when(
|
|
||||||
account.account_type == "Receivable",
|
|
||||||
gl.debit_in_account_currency - gl.credit_in_account_currency,
|
|
||||||
)
|
|
||||||
.else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
|
|
||||||
.as_("amount_in_account_currency"),
|
|
||||||
)
|
)
|
||||||
.where(where_clause)
|
.where(where_clause)
|
||||||
.orderby(gl.name)
|
.orderby(gl.name)
|
||||||
@@ -163,8 +167,8 @@ def execute():
|
|||||||
if gl_entries:
|
if gl_entries:
|
||||||
last_name = gl_entries[-1].name
|
last_name = gl_entries[-1].name
|
||||||
|
|
||||||
# primary key(name) for payment ledger records
|
# add primary key(name) and calculate based on debit and credit
|
||||||
generate_name_for_payment_ledger_entries(gl_entries, processed)
|
generate_name_and_calculate_amount(gl_entries, processed, receivable_accounts)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
insert_query = build_insert_query()
|
insert_query = build_insert_query()
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from frappe import qb
|
|
||||||
from frappe.query_builder import CustomFunction
|
|
||||||
from frappe.query_builder.functions import Count, IfNull
|
|
||||||
from frappe.utils import flt
|
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
"""
|
|
||||||
Migrate 'remarks' field from 'tabGL Entry' to 'tabPayment Ledger Entry'
|
|
||||||
"""
|
|
||||||
|
|
||||||
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
|
|
||||||
|
|
||||||
gle = qb.DocType("GL Entry")
|
|
||||||
ple = qb.DocType("Payment Ledger Entry")
|
|
||||||
|
|
||||||
# Get empty PLE records
|
|
||||||
un_processed = (
|
|
||||||
qb.from_(ple).select(Count(ple.name)).where((ple.remarks.isnull()) & (ple.delinked == 0)).run()
|
|
||||||
)[0][0]
|
|
||||||
|
|
||||||
if un_processed:
|
|
||||||
print(f"Remarks for {un_processed} Payment Ledger records will be updated from GL Entry")
|
|
||||||
|
|
||||||
ifelse = CustomFunction("IF", ["condition", "then", "else"])
|
|
||||||
|
|
||||||
processed = 0
|
|
||||||
last_percent_update = 0
|
|
||||||
batch_size = 1000
|
|
||||||
last_name = None
|
|
||||||
|
|
||||||
while True:
|
|
||||||
if last_name:
|
|
||||||
where_clause = (ple.name.gt(last_name)) & (ple.remarks.isnull()) & (ple.delinked == 0)
|
|
||||||
else:
|
|
||||||
where_clause = (ple.remarks.isnull()) & (ple.delinked == 0)
|
|
||||||
|
|
||||||
# results are deterministic
|
|
||||||
names = (
|
|
||||||
qb.from_(ple).select(ple.name).where(where_clause).orderby(ple.name).limit(batch_size).run()
|
|
||||||
)
|
|
||||||
|
|
||||||
if names:
|
|
||||||
last_name = names[-1][0]
|
|
||||||
|
|
||||||
pl_entries = (
|
|
||||||
qb.from_(ple)
|
|
||||||
.left_join(gle)
|
|
||||||
.on(
|
|
||||||
(ple.account == gle.account)
|
|
||||||
& (ple.party_type == gle.party_type)
|
|
||||||
& (ple.party == gle.party)
|
|
||||||
& (ple.voucher_type == gle.voucher_type)
|
|
||||||
& (ple.voucher_no == gle.voucher_no)
|
|
||||||
& (
|
|
||||||
ple.against_voucher_type
|
|
||||||
== IfNull(
|
|
||||||
ifelse(gle.against_voucher_type == "", None, gle.against_voucher_type), gle.voucher_type
|
|
||||||
)
|
|
||||||
)
|
|
||||||
& (
|
|
||||||
ple.against_voucher_no
|
|
||||||
== IfNull(ifelse(gle.against_voucher == "", None, gle.against_voucher), gle.voucher_no)
|
|
||||||
)
|
|
||||||
& (ple.company == gle.company)
|
|
||||||
& (
|
|
||||||
((ple.account_type == "Receivable") & (ple.amount == (gle.debit - gle.credit)))
|
|
||||||
| (ple.account_type == "Payable") & (ple.amount == (gle.credit - gle.debit))
|
|
||||||
)
|
|
||||||
& (gle.remarks.notnull())
|
|
||||||
& (gle.is_cancelled == 0)
|
|
||||||
)
|
|
||||||
.select(ple.name)
|
|
||||||
.distinct()
|
|
||||||
.select(
|
|
||||||
gle.remarks.as_("gle_remarks"),
|
|
||||||
)
|
|
||||||
.where(ple.name.isin(names))
|
|
||||||
.run(as_dict=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
if pl_entries:
|
|
||||||
for entry in pl_entries:
|
|
||||||
query = qb.update(ple).set(ple.remarks, entry.gle_remarks).where((ple.name == entry.name))
|
|
||||||
query.run()
|
|
||||||
|
|
||||||
frappe.db.commit()
|
|
||||||
|
|
||||||
processed += len(pl_entries)
|
|
||||||
percentage = flt((processed / un_processed) * 100, 2)
|
|
||||||
if percentage - last_percent_update > 1:
|
|
||||||
print(f"{percentage}% ({processed}) PLE records updated")
|
|
||||||
last_percent_update = percentage
|
|
||||||
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
print("Remarks succesfully migrated")
|
|
||||||
@@ -1884,11 +1884,13 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
|
|
||||||
get_advances() {
|
get_advances() {
|
||||||
if(!this.frm.is_return) {
|
if(!this.frm.is_return) {
|
||||||
|
var me = this;
|
||||||
return this.frm.call({
|
return this.frm.call({
|
||||||
method: "set_advances",
|
method: "set_advances",
|
||||||
doc: this.frm.doc,
|
doc: this.frm.doc,
|
||||||
callback: function(r, rt) {
|
callback: function(r, rt) {
|
||||||
refresh_field("advances");
|
refresh_field("advances");
|
||||||
|
me.frm.dirty();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import datetime
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils import add_days, nowdate
|
from frappe.utils import add_days, add_months, nowdate
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
@@ -15,9 +15,16 @@ test_dependencies = ["Sales Order", "Item", "Sales Invoice", "Payment Terms Temp
|
|||||||
|
|
||||||
|
|
||||||
class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.cleanup_old_entries()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def cleanup_old_entries(self):
|
||||||
|
frappe.db.delete("Sales Invoice", filters={"company": "_Test Company"})
|
||||||
|
frappe.db.delete("Sales Order", filters={"company": "_Test Company"})
|
||||||
|
|
||||||
def create_payment_terms_template(self):
|
def create_payment_terms_template(self):
|
||||||
# create template for 50-50 payments
|
# create template for 50-50 payments
|
||||||
template = None
|
template = None
|
||||||
@@ -348,7 +355,7 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
|||||||
item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
|
item = create_item(item_code="_Test Excavator 1", is_stock_item=0)
|
||||||
transaction_date = nowdate()
|
transaction_date = nowdate()
|
||||||
so = make_sales_order(
|
so = make_sales_order(
|
||||||
transaction_date=add_days(transaction_date, -30),
|
transaction_date=add_months(transaction_date, -1),
|
||||||
delivery_date=add_days(transaction_date, -15),
|
delivery_date=add_days(transaction_date, -15),
|
||||||
item=item.item_code,
|
item=item.item_code,
|
||||||
qty=10,
|
qty=10,
|
||||||
@@ -369,13 +376,15 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
|||||||
sinv.items[0].qty = 6
|
sinv.items[0].qty = 6
|
||||||
sinv.insert()
|
sinv.insert()
|
||||||
sinv.submit()
|
sinv.submit()
|
||||||
|
|
||||||
|
first_due_date = add_days(add_months(transaction_date, -1), 15)
|
||||||
columns, data, message, chart = execute(
|
columns, data, message, chart = execute(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
{
|
{
|
||||||
"company": "_Test Company",
|
"company": "_Test Company",
|
||||||
"item": item.item_code,
|
"item": item.item_code,
|
||||||
"from_due_date": add_days(transaction_date, -30),
|
"from_due_date": add_months(transaction_date, -1),
|
||||||
"to_due_date": add_days(transaction_date, -15),
|
"to_due_date": first_due_date,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -384,11 +393,11 @@ class TestPaymentTermsStatusForSalesOrder(FrappeTestCase):
|
|||||||
{
|
{
|
||||||
"name": so.name,
|
"name": so.name,
|
||||||
"customer": so.customer,
|
"customer": so.customer,
|
||||||
"submitted": datetime.date.fromisoformat(add_days(transaction_date, -30)),
|
"submitted": datetime.date.fromisoformat(add_months(transaction_date, -1)),
|
||||||
"status": "Completed",
|
"status": "Completed",
|
||||||
"payment_term": None,
|
"payment_term": None,
|
||||||
"description": "_Test 50-50",
|
"description": "_Test 50-50",
|
||||||
"due_date": datetime.date.fromisoformat(add_days(transaction_date, -15)),
|
"due_date": datetime.date.fromisoformat(first_due_date),
|
||||||
"invoice_portion": 50.0,
|
"invoice_portion": 50.0,
|
||||||
"currency": "INR",
|
"currency": "INR",
|
||||||
"base_payment_amount": 500000.0,
|
"base_payment_amount": 500000.0,
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
|
|||||||
}
|
}
|
||||||
|
|
||||||
calculate_commission() {
|
calculate_commission() {
|
||||||
if(!this.frm.fields_dict.commission_rate) return;
|
if(!this.frm.fields_dict.commission_rate || this.frm.doc.docstatus === 1) return;
|
||||||
|
|
||||||
if(this.frm.doc.commission_rate > 100) {
|
if(this.frm.doc.commission_rate > 100) {
|
||||||
this.frm.set_value("commission_rate", 100);
|
this.frm.set_value("commission_rate", 100);
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class ItemAlternative(Document):
|
|||||||
if not item_data.allow_alternative_item:
|
if not item_data.allow_alternative_item:
|
||||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
||||||
if self.two_way and not alternative_item_data.allow_alternative_item:
|
if self.two_way and not alternative_item_data.allow_alternative_item:
|
||||||
frappe.throw(alternate_item_check_msg.format(self.item_code))
|
frappe.throw(alternate_item_check_msg.format(self.alternative_item_code))
|
||||||
|
|
||||||
def validate_duplicate(self):
|
def validate_duplicate(self):
|
||||||
if frappe.db.get_value(
|
if frappe.db.get_value(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
|
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, new_line_sep, nowdate
|
||||||
|
|
||||||
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||||
@@ -180,6 +181,34 @@ class MaterialRequest(BuyingController):
|
|||||||
self.update_requested_qty()
|
self.update_requested_qty()
|
||||||
self.update_requested_qty_in_production_plan()
|
self.update_requested_qty_in_production_plan()
|
||||||
|
|
||||||
|
def get_mr_items_ordered_qty(self, mr_items):
|
||||||
|
mr_items_ordered_qty = {}
|
||||||
|
mr_items = [d.name for d in self.get("items") if d.name in mr_items]
|
||||||
|
|
||||||
|
doctype = qty_field = None
|
||||||
|
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
|
||||||
|
doctype = frappe.qb.DocType("Stock Entry Detail")
|
||||||
|
qty_field = doctype.transfer_qty
|
||||||
|
elif self.material_request_type == "Manufacture":
|
||||||
|
doctype = frappe.qb.DocType("Work Order")
|
||||||
|
qty_field = doctype.qty
|
||||||
|
|
||||||
|
if doctype and qty_field:
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(doctype)
|
||||||
|
.select(doctype.material_request_item, Sum(qty_field))
|
||||||
|
.where(
|
||||||
|
(doctype.material_request == self.name)
|
||||||
|
& (doctype.material_request_item.isin(mr_items))
|
||||||
|
& (doctype.docstatus == 1)
|
||||||
|
)
|
||||||
|
.groupby(doctype.material_request_item)
|
||||||
|
)
|
||||||
|
|
||||||
|
mr_items_ordered_qty = frappe._dict(query.run())
|
||||||
|
|
||||||
|
return mr_items_ordered_qty
|
||||||
|
|
||||||
def update_completed_qty(self, mr_items=None, update_modified=True):
|
def update_completed_qty(self, mr_items=None, update_modified=True):
|
||||||
if self.material_request_type == "Purchase":
|
if self.material_request_type == "Purchase":
|
||||||
return
|
return
|
||||||
@@ -187,18 +216,13 @@ class MaterialRequest(BuyingController):
|
|||||||
if not mr_items:
|
if not mr_items:
|
||||||
mr_items = [d.name for d in self.get("items")]
|
mr_items = [d.name for d in self.get("items")]
|
||||||
|
|
||||||
|
mr_items_ordered_qty = self.get_mr_items_ordered_qty(mr_items)
|
||||||
|
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.name in mr_items:
|
if d.name in mr_items:
|
||||||
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
|
if self.material_request_type in ("Material Issue", "Material Transfer", "Customer Provided"):
|
||||||
d.ordered_qty = flt(
|
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||||
frappe.db.sql(
|
|
||||||
"""select sum(transfer_qty)
|
|
||||||
from `tabStock Entry Detail` where material_request = %s
|
|
||||||
and material_request_item = %s and docstatus = 1""",
|
|
||||||
(self.name, d.name),
|
|
||||||
)[0][0]
|
|
||||||
)
|
|
||||||
mr_qty_allowance = frappe.db.get_single_value("Stock Settings", "mr_qty_allowance")
|
|
||||||
|
|
||||||
if mr_qty_allowance:
|
if mr_qty_allowance:
|
||||||
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
|
allowed_qty = d.qty + (d.qty * (mr_qty_allowance / 100))
|
||||||
@@ -217,14 +241,7 @@ class MaterialRequest(BuyingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif self.material_request_type == "Manufacture":
|
elif self.material_request_type == "Manufacture":
|
||||||
d.ordered_qty = flt(
|
d.ordered_qty = flt(mr_items_ordered_qty.get(d.name))
|
||||||
frappe.db.sql(
|
|
||||||
"""select sum(qty)
|
|
||||||
from `tabWork Order` where material_request = %s
|
|
||||||
and material_request_item = %s and docstatus = 1""",
|
|
||||||
(self.name, d.name),
|
|
||||||
)[0][0]
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
|
frappe.db.set_value(d.doctype, d.name, "ordered_qty", d.ordered_qty)
|
||||||
|
|
||||||
|
|||||||
@@ -397,6 +397,7 @@ class StockReconciliation(StockController):
|
|||||||
"voucher_type": self.doctype,
|
"voucher_type": self.doctype,
|
||||||
"voucher_no": self.name,
|
"voucher_no": self.name,
|
||||||
"voucher_detail_no": row.name,
|
"voucher_detail_no": row.name,
|
||||||
|
"actual_qty": 0,
|
||||||
"company": self.company,
|
"company": self.company,
|
||||||
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
|
"stock_uom": frappe.db.get_value("Item", row.item_code, "stock_uom"),
|
||||||
"is_cancelled": 1 if self.docstatus == 2 else 0,
|
"is_cancelled": 1 if self.docstatus == 2 else 0,
|
||||||
@@ -423,6 +424,8 @@ class StockReconciliation(StockController):
|
|||||||
data.valuation_rate = flt(row.valuation_rate)
|
data.valuation_rate = flt(row.valuation_rate)
|
||||||
data.stock_value_difference = -1 * flt(row.amount_difference)
|
data.stock_value_difference = -1 * flt(row.amount_difference)
|
||||||
|
|
||||||
|
self.update_inventory_dimensions(row, data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def make_sle_on_cancel(self):
|
def make_sle_on_cancel(self):
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class TestFIFOValuation(unittest.TestCase):
|
|||||||
total_qty = 0
|
total_qty = 0
|
||||||
|
|
||||||
for qty, rate in stock_queue:
|
for qty, rate in stock_queue:
|
||||||
if qty == 0:
|
if round_off_if_near_zero(qty) == 0:
|
||||||
continue
|
continue
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
self.queue.add_stock(qty, rate)
|
self.queue.add_stock(qty, rate)
|
||||||
@@ -154,7 +154,7 @@ class TestFIFOValuation(unittest.TestCase):
|
|||||||
|
|
||||||
for qty, rate in stock_queue:
|
for qty, rate in stock_queue:
|
||||||
# don't allow negative stock
|
# don't allow negative stock
|
||||||
if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||||
continue
|
continue
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
self.queue.add_stock(qty, rate)
|
self.queue.add_stock(qty, rate)
|
||||||
@@ -179,7 +179,7 @@ class TestFIFOValuation(unittest.TestCase):
|
|||||||
|
|
||||||
for qty, rate in stock_queue:
|
for qty, rate in stock_queue:
|
||||||
# don't allow negative stock
|
# don't allow negative stock
|
||||||
if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||||
continue
|
continue
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
self.queue.add_stock(qty, rate)
|
self.queue.add_stock(qty, rate)
|
||||||
@@ -282,7 +282,7 @@ class TestLIFOValuation(unittest.TestCase):
|
|||||||
total_qty = 0
|
total_qty = 0
|
||||||
|
|
||||||
for qty, rate in stock_stack:
|
for qty, rate in stock_stack:
|
||||||
if qty == 0:
|
if round_off_if_near_zero(qty) == 0:
|
||||||
continue
|
continue
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
self.stack.add_stock(qty, rate)
|
self.stack.add_stock(qty, rate)
|
||||||
@@ -304,7 +304,7 @@ class TestLIFOValuation(unittest.TestCase):
|
|||||||
|
|
||||||
for qty, rate in stock_stack:
|
for qty, rate in stock_stack:
|
||||||
# don't allow negative stock
|
# don't allow negative stock
|
||||||
if qty == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
if round_off_if_near_zero(qty) == 0 or total_qty + qty < 0 or abs(qty) < 0.1:
|
||||||
continue
|
continue
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
self.stack.add_stock(qty, rate)
|
self.stack.add_stock(qty, rate)
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ from frappe.utils import (
|
|||||||
get_datetime,
|
get_datetime,
|
||||||
get_datetime_str,
|
get_datetime_str,
|
||||||
get_link_to_form,
|
get_link_to_form,
|
||||||
|
get_system_timezone,
|
||||||
get_time,
|
get_time,
|
||||||
get_time_zone,
|
|
||||||
get_weekdays,
|
get_weekdays,
|
||||||
getdate,
|
getdate,
|
||||||
nowdate,
|
nowdate,
|
||||||
@@ -981,7 +981,7 @@ def convert_utc_to_user_timezone(utc_timestamp, user):
|
|||||||
|
|
||||||
|
|
||||||
def get_tz(user):
|
def get_tz(user):
|
||||||
return frappe.db.get_value("User", user, "time_zone") or get_time_zone()
|
return frappe.db.get_value("User", user, "time_zone") or get_system_timezone()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@ import pytz
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
|
from frappe.utils.data import get_system_timezone
|
||||||
from pyyoutube import Api
|
from pyyoutube import Api
|
||||||
|
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ def update_youtube_data():
|
|||||||
|
|
||||||
frequency = get_frequency(frequency)
|
frequency = get_frequency(frequency)
|
||||||
time = datetime.now()
|
time = datetime.now()
|
||||||
timezone = pytz.timezone(frappe.utils.get_time_zone())
|
timezone = pytz.timezone(get_system_timezone())
|
||||||
site_time = time.astimezone(timezone)
|
site_time = time.astimezone(timezone)
|
||||||
|
|
||||||
if frequency == 30:
|
if frequency == 30:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import frappe
|
import frappe
|
||||||
import pytz
|
import pytz
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.utils.data import get_system_timezone
|
||||||
|
|
||||||
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||||
|
|
||||||
@@ -125,7 +126,7 @@ def filter_timeslots(date, timeslots):
|
|||||||
|
|
||||||
def convert_to_guest_timezone(guest_tz, datetimeobject):
|
def convert_to_guest_timezone(guest_tz, datetimeobject):
|
||||||
guest_tz = pytz.timezone(guest_tz)
|
guest_tz = pytz.timezone(guest_tz)
|
||||||
local_timezone = pytz.timezone(frappe.utils.get_time_zone())
|
local_timezone = pytz.timezone(get_system_timezone())
|
||||||
datetimeobject = local_timezone.localize(datetimeobject)
|
datetimeobject = local_timezone.localize(datetimeobject)
|
||||||
datetimeobject = datetimeobject.astimezone(guest_tz)
|
datetimeobject = datetimeobject.astimezone(guest_tz)
|
||||||
return datetimeobject
|
return datetimeobject
|
||||||
@@ -134,7 +135,7 @@ def convert_to_guest_timezone(guest_tz, datetimeobject):
|
|||||||
def convert_to_system_timezone(guest_tz, datetimeobject):
|
def convert_to_system_timezone(guest_tz, datetimeobject):
|
||||||
guest_tz = pytz.timezone(guest_tz)
|
guest_tz = pytz.timezone(guest_tz)
|
||||||
datetimeobject = guest_tz.localize(datetimeobject)
|
datetimeobject = guest_tz.localize(datetimeobject)
|
||||||
system_tz = pytz.timezone(frappe.utils.get_time_zone())
|
system_tz = pytz.timezone(get_system_timezone())
|
||||||
datetimeobject = datetimeobject.astimezone(system_tz)
|
datetimeobject = datetimeobject.astimezone(system_tz)
|
||||||
return datetimeobject
|
return datetimeobject
|
||||||
|
|
||||||
|
|||||||
@@ -28,9 +28,6 @@ dependencies = [
|
|||||||
requires = ["flit_core >=3.4,<4"]
|
requires = ["flit_core >=3.4,<4"]
|
||||||
build-backend = "flit_core.buildapi"
|
build-backend = "flit_core.buildapi"
|
||||||
|
|
||||||
[tool.bench.dev-dependencies]
|
|
||||||
hypothesis = "~=6.31.0"
|
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 99
|
line-length = 99
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user