Merge branch 'develop' into github-issue-33344

This commit is contained in:
Sagar Sharma 2023-02-27 16:57:29 +05:30 committed by GitHub
commit ecd15d2a65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 538 additions and 1314 deletions

View File

@ -65,7 +65,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
## Contributing

View File

@ -29,6 +29,7 @@ def create_charts(
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
account_number = cstr(child.get("account_number")).strip()
@ -95,7 +96,17 @@ def identify_is_group(child):
is_group = child.get("is_group")
elif len(
set(child.keys())
- set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
- set(
[
"account_name",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_number",
"account_currency",
]
)
):
is_group = 1
else:
@ -185,6 +196,7 @@ def get_account_tree_from_existing_company(existing_company):
"root_type",
"tax_rate",
"account_number",
"account_currency",
],
order_by="lft, rgt",
)
@ -267,6 +279,7 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"root_type",
"is_group",
"tax_rate",
"account_currency",
]:
continue

View File

@ -36,7 +36,7 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
if no_of_columns > 8:
frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")),
@ -233,6 +233,7 @@ def build_forest(data):
is_group,
account_type,
root_type,
account_currency,
) = i
if not account_name:
@ -253,6 +254,8 @@ def build_forest(data):
charts_map[account_name]["account_type"] = account_type
if root_type:
charts_map[account_name]["root_type"] = root_type
if account_currency:
charts_map[account_name]["account_currency"] = account_currency
path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created
line_no += 1
@ -315,6 +318,7 @@ def get_template(template_type):
"Is Group",
"Account Type",
"Root Type",
"Account Currency",
]
writer = UnicodeWriter()
writer.writerow(fields)

View File

@ -21,8 +21,24 @@ class POSClosingEntry(StatusUpdater):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_transactions, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:

View File

@ -17,6 +17,22 @@ class POSInvoiceMergeLog(Document):
def validate(self):
self.validate_customer()
self.validate_pos_invoice_status()
self.validate_duplicate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_invoices, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_customer(self):
if self.merge_invoices_based_on == "Customer Group":
@ -425,6 +441,8 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
if type(error_message) == list:
error_message = frappe.json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise

View File

@ -5,6 +5,7 @@
import frappe
from frappe import _, throw
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate
import erpnext
@ -1465,19 +1466,16 @@ class PurchaseInvoice(BuyingController):
def update_billing_status_in_pr(self, update_modified=True):
updated_pr = []
po_details = []
pr_details_billed_amt = self.get_pr_details_billed_amt()
for d in self.get("items"):
if d.pr_detail:
billed_amt = frappe.db.sql(
"""select sum(amount) from `tabPurchase Invoice Item`
where pr_detail=%s and docstatus=1""",
d.pr_detail,
)
billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value(
"Purchase Receipt Item",
d.pr_detail,
"billed_amt",
billed_amt,
flt(pr_details_billed_amt.get(d.pr_detail)),
update_modified=update_modified,
)
updated_pr.append(d.purchase_receipt)
@ -1493,6 +1491,24 @@ class PurchaseInvoice(BuyingController):
pr_doc = frappe.get_doc("Purchase Receipt", pr)
update_billing_percentage(pr_doc, update_modified=update_modified)
def get_pr_details_billed_amt(self):
# Get billed amount based on purchase receipt item reference (pr_detail) in purchase invoice
pr_details_billed_amt = {}
pr_details = [d.get("pr_detail") for d in self.get("items") if d.get("pr_detail")]
if pr_details:
doctype = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(doctype)
.select(doctype.pr_detail, Sum(doctype.amount))
.where(doctype.pr_detail.isin(pr_details) & doctype.docstatus == 1)
.groupby(doctype.pr_detail)
)
pr_details_billed_amt = frappe._dict(query.run(as_list=1))
return pr_details_billed_amt
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None

View File

@ -278,7 +278,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
tax_amount = normal_round(tax_amount)
return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount
@ -603,3 +603,20 @@ def is_valid_certificate(
valid = True
return valid
def normal_round(number):
"""
Rounds a number to the nearest integer.
:param number: The number to round.
"""
decimal_part = number - int(number)
if decimal_part >= 0.5:
decimal_part = 1
else:
decimal_part = 0
number = int(number) + decimal_part
return number

View File

@ -395,6 +395,7 @@ def get_column_names():
class GrossProfitGenerator(object):
def __init__(self, filters=None):
self.sle = {}
self.data = []
self.average_buying_rate = {}
self.filters = frappe._dict(filters)
@ -404,7 +405,6 @@ class GrossProfitGenerator(object):
if filters.group_by == "Invoice":
self.group_items_by_invoice()
self.load_stock_ledger_entries()
self.load_product_bundle()
self.load_non_stock_items()
self.get_returned_invoice_items()
@ -633,7 +633,7 @@ class GrossProfitGenerator(object):
return flt(row.qty) * item_rate
else:
my_sle = self.sle.get((item_code, row.warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
if (row.update_stock or row.dn_detail) and my_sle:
parenttype, parent = row.parenttype, row.parent
if row.dn_detail:
@ -651,7 +651,7 @@ class GrossProfitGenerator(object):
dn["item_row"],
dn["warehouse"],
)
my_sle = self.sle.get((item_code, warehouse))
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
return self.calculate_buying_amount_from_sle(
row, my_sle, parenttype, parent, item_row, item_code
)
@ -667,15 +667,12 @@ class GrossProfitGenerator(object):
def get_buying_amount_from_so_dn(self, sales_order, so_detail, item_code):
from frappe.query_builder.functions import Sum
delivery_note = frappe.qb.DocType("Delivery Note")
delivery_note_item = frappe.qb.DocType("Delivery Note Item")
query = (
frappe.qb.from_(delivery_note)
.inner_join(delivery_note_item)
.on(delivery_note.name == delivery_note_item.parent)
frappe.qb.from_(delivery_note_item)
.select(Sum(delivery_note_item.incoming_rate * delivery_note_item.stock_qty))
.where(delivery_note.docstatus == 1)
.where(delivery_note_item.docstatus == 1)
.where(delivery_note_item.item_code == item_code)
.where(delivery_note_item.against_sales_order == sales_order)
.where(delivery_note_item.so_detail == so_detail)
@ -947,24 +944,36 @@ class GrossProfitGenerator(object):
"Item", item_code, ["item_name", "description", "item_group", "brand"]
)
def load_stock_ledger_entries(self):
res = frappe.db.sql(
"""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry`
where company=%(company)s and is_cancelled = 0
order by
item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""",
self.filters,
as_dict=True,
)
self.sle = {}
for r in res:
if (r.item_code, r.warehouse) not in self.sle:
self.sle[(r.item_code, r.warehouse)] = []
def get_stock_ledger_entries(self, item_code, warehouse):
if item_code and warehouse:
if (item_code, warehouse) not in self.sle:
sle = qb.DocType("Stock Ledger Entry")
res = (
qb.from_(sle)
.select(
sle.item_code,
sle.voucher_type,
sle.voucher_no,
sle.voucher_detail_no,
sle.stock_value,
sle.warehouse,
sle.actual_qty.as_("qty"),
)
.where(
(sle.company == self.filters.company)
& (sle.item_code == item_code)
& (sle.warehouse == warehouse)
& (sle.is_cancelled == 0)
)
.orderby(sle.item_code)
.orderby(sle.warehouse, sle.posting_date, sle.posting_time, sle.creation, order=Order.desc)
.run(as_dict=True)
)
self.sle[(r.item_code, r.warehouse)].append(r)
self.sle[(item_code, warehouse)] = res
return self.sle[(item_code, warehouse)]
return []
def load_product_bundle(self):
self.product_bundles = {}

View File

@ -43,9 +43,9 @@ erpnext.asset.set_accumulated_depreciation = function(frm) {
if(frm.doc.depreciation_method != "Manual") return;
var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation);
$.each(frm.doc.schedules || [], function(i, row) {
$.each(frm.doc.depreciation_schedule || [], function(i, row) {
accumulated_depreciation += flt(row.depreciation_amount);
frappe.model.set_value(row.doctype, row.name,
"accumulated_depreciation_amount", accumulated_depreciation);
frappe.model.set_value(row.doctype, row.name, "accumulated_depreciation_amount", accumulated_depreciation);
})
};

View File

@ -10,7 +10,9 @@
"asset",
"naming_series",
"column_break_2",
"gross_purchase_amount",
"opening_accumulated_depreciation",
"number_of_depreciations_booked",
"finance_book",
"finance_book_id",
"depreciation_details_section",
@ -148,18 +150,36 @@
"read_only": 1
},
{
"depends_on": "opening_accumulated_depreciation",
"fieldname": "opening_accumulated_depreciation",
"fieldtype": "Currency",
"hidden": 1,
"label": "Opening Accumulated Depreciation",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "gross_purchase_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Gross Purchase Amount",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "number_of_depreciations_booked",
"fieldtype": "Int",
"hidden": 1,
"label": "Number of Depreciations Booked",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2023-01-16 21:08:21.421260",
"modified": "2023-02-26 16:37:23.734806",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Depreciation Schedule",

View File

@ -4,7 +4,15 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_days, add_months, cint, flt, get_last_day, is_last_day_of_the_month
from frappe.utils import (
add_days,
add_months,
cint,
flt,
get_last_day,
getdate,
is_last_day_of_the_month,
)
class AssetDepreciationSchedule(Document):
@ -83,15 +91,58 @@ class AssetDepreciationSchedule(Document):
date_of_return=None,
update_asset_finance_book_row=True,
):
have_asset_details_been_modified = self.have_asset_details_been_modified(asset_doc)
not_manual_depr_or_have_manual_depr_details_been_modified = (
self.not_manual_depr_or_have_manual_depr_details_been_modified(row)
)
self.set_draft_asset_depr_schedule_details(asset_doc, row)
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
if self.should_prepare_depreciation_schedule(
have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
def have_asset_details_been_modified(self, asset_doc):
return (
asset_doc.gross_purchase_amount != self.gross_purchase_amount
or asset_doc.opening_accumulated_depreciation != self.opening_accumulated_depreciation
or asset_doc.number_of_depreciations_booked != self.number_of_depreciations_booked
)
def not_manual_depr_or_have_manual_depr_details_been_modified(self, row):
return (
self.depreciation_method != "Manual"
or row.total_number_of_depreciations != self.total_number_of_depreciations
or row.frequency_of_depreciation != self.frequency_of_depreciation
or getdate(row.depreciation_start_date) != self.get("depreciation_schedule")[0].schedule_date
or row.expected_value_after_useful_life != self.expected_value_after_useful_life
)
def should_prepare_depreciation_schedule(
self, have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
if not self.get("depreciation_schedule"):
return True
old_asset_depr_schedule_doc = self.get_doc_before_save()
if self.docstatus != 0 and not old_asset_depr_schedule_doc:
return True
if have_asset_details_been_modified or not_manual_depr_or_have_manual_depr_details_been_modified:
return True
return False
def set_draft_asset_depr_schedule_details(self, asset_doc, row):
self.asset = asset_doc.name
self.finance_book = row.finance_book
self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked
self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations
self.frequency_of_depreciation = row.frequency_of_depreciation
@ -102,7 +153,7 @@ class AssetDepreciationSchedule(Document):
def make_depr_schedule(
self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
):
if row.depreciation_method != "Manual" and not self.get("depreciation_schedule"):
if not self.get("depreciation_schedule"):
self.depreciation_schedule = []
if not asset_doc.available_for_use_date:
@ -293,7 +344,9 @@ class AssetDepreciationSchedule(Document):
ignore_booked_entry=False,
):
straight_line_idx = [
d.idx for d in self.get("depreciation_schedule") if d.depreciation_method == "Straight Line"
d.idx
for d in self.get("depreciation_schedule")
if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual"
]
accumulated_depreciation = flt(self.opening_accumulated_depreciation)

View File

@ -151,6 +151,7 @@ def prepare_chart_data(data, filters):
filters.filter_based_on,
"Monthly",
company=filters.company,
ignore_fiscal_year=True,
)
for d in period_list:

View File

@ -252,7 +252,6 @@ def get_already_returned_items(doc):
child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s
group by item_code
for update
""".format(
column, doc.doctype, doc.doctype
),

View File

@ -311,15 +311,10 @@ doc_events = {
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
"erpnext.regional.saudi_arabia.utils.create_qr_code",
],
"on_cancel": [
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file",
],
"on_cancel": ["erpnext.regional.italy.utils.sales_invoice_on_cancel"],
"on_trash": "erpnext.regional.check_deletion_permission",
},
"POS Invoice": {"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]},
"Purchase Invoice": {
"validate": [
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
@ -347,7 +342,6 @@ doc_events = {
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
},
"Company": {"on_trash": ["erpnext.regional.saudi_arabia.utils.delete_vat_settings_for_company"]},
"Integration Request": {
"validate": "erpnext.accounts.doctype.payment_request.payment_request.validate_payment"
},

View File

@ -64,8 +64,6 @@
"fieldtype": "Section Break"
},
{
"fetch_from": "prevdoc_detail_docname.sales_person",
"fetch_if_empty": 1,
"fieldname": "service_person",
"fieldtype": "Link",
"in_list_view": 1,
@ -110,13 +108,15 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-05-27 17:47:21.474282",
"modified": "2023-02-27 11:09:33.114458",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Visit Purpose",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -25,8 +25,9 @@ frappe.query_reports["BOM Stock Report"] = {
],
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (column.id == "item") {
if (data["enough_parts_to_build"] > 0) {
if (data["in_stock_qty"] >= data["required_qty"]) {
value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else {
value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;

View File

@ -250,18 +250,14 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.fetch_thumbnail_in_website_items
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
erpnext.patches.v13_0.non_profit_deprecation_warning
erpnext.patches.v13_0.enable_ksa_vat_docs #1
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
erpnext.patches.v13_0.reset_corrupt_defaults
@ -269,6 +265,8 @@ erpnext.patches.v13_0.create_accounting_dimensions_for_asset_repair
erpnext.patches.v15_0.delete_taxjar_doctypes
erpnext.patches.v15_0.create_asset_depreciation_schedules_from_assets
erpnext.patches.v14_0.update_reference_due_date_in_journal_entry
erpnext.patches.v15_0.saudi_depreciation_warning
erpnext.patches.v15_0.delete_saudi_doctypes
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')

View File

@ -1,11 +0,0 @@
import frappe
from erpnext.regional.saudi_arabia.setup import make_custom_fields
def execute():
company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
if not company:
return
make_custom_fields()

View File

@ -1,19 +0,0 @@
# Copyright (c) 2020, Wahni Green Technologies and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.regional.saudi_arabia.setup import add_print_formats
def execute():
company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
if company:
add_print_formats()
return
if frappe.db.exists("DocType", "Print Format"):
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
for d in ("KSA VAT Invoice", "KSA POS Invoice"):
frappe.db.set_value("Print Format", d, "disabled", 1)

View File

@ -1,12 +0,0 @@
import frappe
from erpnext.regional.saudi_arabia.setup import add_permissions, add_print_formats
def execute():
company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
if not company:
return
add_print_formats()
add_permissions()

View File

@ -1,36 +0,0 @@
# Copyright (c) 2020, Wahni Green Technologies and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.utils.rename_field import rename_field
def execute():
company = frappe.get_all("Company", filters={"country": "Saudi Arabia"})
if not company:
return
if frappe.db.exists("DocType", "Sales Invoice"):
frappe.reload_doc("accounts", "doctype", "sales_invoice", force=True)
# rename_field method assumes that the field already exists or the doc is synced
if not frappe.db.has_column("Sales Invoice", "ksa_einv_qr"):
create_custom_fields(
{
"Sales Invoice": [
dict(
fieldname="ksa_einv_qr",
label="KSA E-Invoicing QR",
fieldtype="Attach Image",
read_only=1,
no_copy=1,
hidden=1,
)
]
}
)
if frappe.db.has_column("Sales Invoice", "qr_code"):
rename_field("Sales Invoice", "qr_code", "ksa_einv_qr")
frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")

View File

@ -27,7 +27,13 @@ def get_details_of_draft_or_submitted_depreciable_assets():
records = (
frappe.qb.from_(asset)
.select(asset.name, asset.opening_accumulated_depreciation, asset.docstatus)
.select(
asset.name,
asset.opening_accumulated_depreciation,
asset.gross_purchase_amount,
asset.number_of_depreciations_booked,
asset.docstatus,
)
.where(asset.calculate_depreciation == 1)
.where(asset.docstatus < 2)
).run(as_dict=True)

View File

@ -0,0 +1,25 @@
import click
import frappe
def execute():
if "ksa" in frappe.get_installed_apps():
return
doctypes = ["KSA VAT Setting", "KSA VAT Purchase Account", "KSA VAT Sales Account"]
for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True)
print_formats = ["KSA POS Invoice", "KSA VAT Invoice"]
for print_format in print_formats:
frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True)
reports = ["KSA VAT"]
for report in reports:
frappe.delete_doc("Report", report, ignore_missing=True, force=True)
click.secho(
"Region Saudi Arabia(KSA) is moved to a separate app"
"Please install the app to continue using the module: https://github.com/8848digital/KSA",
fg="yellow",
)

View File

@ -0,0 +1,12 @@
import click
import frappe
def execute():
if "ksa" in frappe.get_installed_apps():
return
click.secho(
"Region Saudi Arabia(KSA) is moved to a separate app\n"
"Please install the app to continue using the KSA Features: https://github.com/8848digital/KSA",
fg="yellow",
)

View File

@ -221,9 +221,9 @@ $.extend(erpnext.utils, {
callback: function(r) {
if (r.message && r.message.length) {
r.message.forEach((dimension) => {
let found = filters.some(el => el.fieldname === dimension['fieldname']);
let existing_filter = filters.filter(el => el.fieldname === dimension['fieldname']);
if (!found) {
if (!existing_filter.length) {
filters.splice(index, 0, {
"fieldname": dimension["fieldname"],
"label": __(dimension["doctype"]),
@ -232,6 +232,11 @@ $.extend(erpnext.utils, {
return frappe.db.get_link_options(dimension["doctype"], txt);
},
});
} else {
existing_filter[0]['fieldtype'] = "MultiSelectList";
existing_filter[0]['get_data'] = function(txt) {
return frappe.db.get_link_options(dimension["doctype"], txt);
}
}
});
}

View File

@ -1,49 +0,0 @@
{
"actions": [],
"creation": "2021-07-13 09:17:09.862163",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:38.205597",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Purchase Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATPurchaseAccount(Document):
pass

View File

@ -1,8 +0,0 @@
// Copyright (c) 2021, Havenir Solutions and contributors
// For license information, please see license.txt
frappe.ui.form.on('KSA VAT Sales Account', {
// refresh: function(frm) {
// }
});

View File

@ -1,49 +0,0 @@
{
"actions": [],
"creation": "2021-07-13 08:46:33.820968",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:00.081407",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Sales Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATSalesAccount(Document):
pass

View File

@ -1,9 +0,0 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSalesAccount(unittest.TestCase):
pass

View File

@ -1,8 +0,0 @@
// Copyright (c) 2021, Havenir Solutions and contributors
// For license information, please see license.txt
frappe.ui.form.on('KSA VAT Setting', {
onload: function () {
frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting');
}
});

View File

@ -1,49 +0,0 @@
{
"actions": [],
"autoname": "field:company",
"creation": "2021-07-13 08:49:01.100356",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"ksa_vat_sales_accounts",
"ksa_vat_purchase_accounts"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1,
"unique": 1
},
{
"fieldname": "ksa_vat_sales_accounts",
"fieldtype": "Table",
"label": "KSA VAT Sales Accounts",
"options": "KSA VAT Sales Account",
"reqd": 1
},
{
"fieldname": "ksa_vat_purchase_accounts",
"fieldtype": "Table",
"label": "KSA VAT Purchase Accounts",
"options": "KSA VAT Purchase Account",
"reqd": 1
}
],
"links": [],
"modified": "2021-08-26 04:29:06.499378",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Setting",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "company",
"track_changes": 1
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATSetting(Document):
pass

View File

@ -1,5 +0,0 @@
frappe.listview_settings['KSA VAT Setting'] = {
onload () {
frappe.breadcrumbs.add('Accounts');
}
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSetting(unittest.TestCase):
pass

View File

@ -1,32 +0,0 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-12-07 13:25:05.424827",
"css": "",
"custom_format": 1,
"default_print_language": "en",
"disabled": 1,
"doc_type": "POS Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font_size": 0,
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2021-12-08 10:25:01.930885",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA POS Invoice",
"owner": "Administrator",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

File diff suppressed because one or more lines are too long

View File

@ -1,59 +0,0 @@
// Copyright (c) 2016, Havenir Solutions and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["KSA VAT"] = {
onload() {
frappe.breadcrumbs.add('Accounts');
},
"filters": [
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"reqd": 1,
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"reqd": 1,
"default": frappe.datetime.get_today()
}
],
"formatter": function(value, row, column, data, default_formatter) {
if (data
&& (data.title=='VAT on Sales' || data.title=='VAT on Purchases')
&& data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else if (data.title=='Grand Total'){
if (data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else{
value = default_formatter(value, row, column, data);
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}
}else{
value = default_formatter(value, row, column, data);
return value;
}
},
};

View File

@ -1,32 +0,0 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-07-13 08:54:38.000949",
"disable_prepared_report": 1,
"disabled": 1,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-08-26 04:14:37.202594",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT",
"owner": "Administrator",
"prepared_report": 1,
"ref_doctype": "GL Entry",
"report_name": "KSA VAT",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
]
}

View File

@ -1,231 +0,0 @@
# Copyright (c) 2013, Havenir Solutions and contributors
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.utils import get_url_to_list
def execute(filters=None):
columns = columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{
"fieldname": "title",
"label": _("Title"),
"fieldtype": "Data",
"width": 300,
},
{
"fieldname": "amount",
"label": _("Amount (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "adjustment_amount",
"label": _("Adjustment (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "vat_amount",
"label": _("VAT Amount (SAR)"),
"fieldtype": "Currency",
"options": "currency",
"width": 150,
},
{
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Currency",
"width": 150,
"hidden": 1,
},
]
def get_data(filters):
data = []
# Validate if vat settings exist
company = filters.get("company")
company_currency = frappe.get_cached_value("Company", company, "default_currency")
if frappe.db.exists("KSA VAT Setting", company) is None:
url = get_url_to_list("KSA VAT Setting")
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
return data
ksa_vat_setting = frappe.get_doc("KSA VAT Setting", company)
# Sales Heading
append_data(data, "VAT on Sales", "", "", "", company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts:
(
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Sales Invoice")
# Adding results to data
append_data(
data,
vat_setting.title,
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
company_currency,
)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Sales Grand Total
append_data(
data,
"Grand Total",
grand_total_taxable_amount,
grand_total_taxable_adjustment_amount,
grand_total_tax,
company_currency,
)
# Blank Line
append_data(data, "", "", "", "", company_currency)
# Purchase Heading
append_data(data, "VAT on Purchases", "", "", "", company_currency)
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts:
(
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
) = get_tax_data_for_each_vat_setting(vat_setting, filters, "Purchase Invoice")
# Adding results to data
append_data(
data,
vat_setting.title,
total_taxable_amount,
total_taxable_adjustment_amount,
total_tax,
company_currency,
)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Purchase Grand Total
append_data(
data,
"Grand Total",
grand_total_taxable_amount,
grand_total_taxable_adjustment_amount,
grand_total_tax,
company_currency,
)
return data
def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
"""
(KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n
calculates and returns \n
total_taxable_amount, total_taxable_adjustment_amount, total_tax"""
from_date = filters.get("from_date")
to_date = filters.get("to_date")
# Initiate variables
total_taxable_amount = 0
total_taxable_adjustment_amount = 0
total_tax = 0
# Fetch All Invoices
invoices = frappe.get_all(
doctype,
filters={"docstatus": 1, "posting_date": ["between", [from_date, to_date]]},
fields=["name", "is_return"],
)
for invoice in invoices:
invoice_items = frappe.get_all(
f"{doctype} Item",
filters={
"docstatus": 1,
"parent": invoice.name,
"item_tax_template": vat_setting.item_tax_template,
},
fields=["item_code", "net_amount"],
)
for item in invoice_items:
# Summing up total taxable amount
if invoice.is_return == 0:
total_taxable_amount += item.net_amount
if invoice.is_return == 1:
total_taxable_adjustment_amount += item.net_amount
# Summing up total tax
total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)
return total_taxable_amount, total_taxable_adjustment_amount, total_tax
def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency):
"""Returns data with appended value."""
data.append(
{
"title": _(title),
"amount": amount,
"adjustment_amount": adjustment_amount,
"vat_amount": vat_amount,
"currency": company_currency,
}
)
def get_tax_amount(item_code, account_head, doctype, parent):
if doctype == "Sales Invoice":
tax_doctype = "Sales Taxes and Charges"
elif doctype == "Purchase Invoice":
tax_doctype = "Purchase Taxes and Charges"
item_wise_tax_detail = frappe.get_value(
tax_doctype,
{"docstatus": 1, "parent": parent, "account_head": account_head},
"item_wise_tax_detail",
)
tax_amount = 0
if item_wise_tax_detail and len(item_wise_tax_detail) > 0:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
for key, value in item_wise_tax_detail.items():
if key == item_code:
tax_amount = value[1]
break
return tax_amount

View File

@ -1,173 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import (
create_ksa_vat_setting,
)
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup(company=None, patch=True):
add_print_formats()
add_permissions()
make_custom_fields()
def add_print_formats():
frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
for d in (
"Simplified Tax Invoice",
"Detailed Tax Invoice",
"Tax Invoice",
"KSA VAT Invoice",
"KSA POS Invoice",
):
frappe.db.set_value("Print Format", d, "disabled", 0)
def add_permissions():
"""Add Permissions for KSA VAT Setting."""
add_permission("KSA VAT Setting", "All", 0)
for role in ("Accounts Manager", "Accounts User", "System Manager"):
add_permission("KSA VAT Setting", role, 0)
update_permission_property("KSA VAT Setting", role, 0, "write", 1)
update_permission_property("KSA VAT Setting", role, 0, "create", 1)
"""Enable KSA VAT Report"""
frappe.db.set_value("Report", "KSA VAT", "disabled", 0)
def make_custom_fields():
"""Create Custom fields
- QR code Image file
- Company Name in Arabic
- Address in Arabic
"""
is_zero_rated = dict(
fieldname="is_zero_rated",
label="Is Zero Rated",
fieldtype="Check",
fetch_from="item_code.is_zero_rated",
insert_after="description",
print_hide=1,
)
is_exempt = dict(
fieldname="is_exempt",
label="Is Exempt",
fieldtype="Check",
fetch_from="item_code.is_exempt",
insert_after="is_zero_rated",
print_hide=1,
)
purchase_invoice_fields = [
dict(
fieldname="company_trn",
label="Company TRN",
fieldtype="Read Only",
insert_after="shipping_address",
fetch_from="company.tax_id",
print_hide=1,
),
dict(
fieldname="supplier_name_in_arabic",
label="Supplier Name in Arabic",
fieldtype="Read Only",
insert_after="supplier_name",
fetch_from="supplier.supplier_name_in_arabic",
print_hide=1,
),
]
sales_invoice_fields = [
dict(
fieldname="company_trn",
label="Company TRN",
fieldtype="Read Only",
insert_after="company_address",
fetch_from="company.tax_id",
print_hide=1,
),
dict(
fieldname="customer_name_in_arabic",
label="Customer Name in Arabic",
fieldtype="Read Only",
insert_after="customer_name",
fetch_from="customer.customer_name_in_arabic",
print_hide=1,
),
dict(
fieldname="ksa_einv_qr",
label="KSA E-Invoicing QR",
fieldtype="Attach Image",
read_only=1,
no_copy=1,
hidden=1,
),
]
custom_fields = {
"Item": [is_zero_rated, is_exempt],
"Customer": [
dict(
fieldname="customer_name_in_arabic",
label="Customer Name in Arabic",
fieldtype="Data",
insert_after="customer_name",
),
],
"Supplier": [
dict(
fieldname="supplier_name_in_arabic",
label="Supplier Name in Arabic",
fieldtype="Data",
insert_after="supplier_name",
),
],
"Purchase Invoice": purchase_invoice_fields,
"Purchase Order": purchase_invoice_fields,
"Purchase Receipt": purchase_invoice_fields,
"Sales Invoice": sales_invoice_fields,
"POS Invoice": sales_invoice_fields,
"Sales Order": sales_invoice_fields,
"Delivery Note": sales_invoice_fields,
"Sales Invoice Item": [is_zero_rated, is_exempt],
"POS Invoice Item": [is_zero_rated, is_exempt],
"Purchase Invoice Item": [is_zero_rated, is_exempt],
"Sales Order Item": [is_zero_rated, is_exempt],
"Delivery Note Item": [is_zero_rated, is_exempt],
"Quotation Item": [is_zero_rated, is_exempt],
"Purchase Order Item": [is_zero_rated, is_exempt],
"Purchase Receipt Item": [is_zero_rated, is_exempt],
"Supplier Quotation Item": [is_zero_rated, is_exempt],
"Address": [
dict(
fieldname="address_in_arabic",
label="Address in Arabic",
fieldtype="Data",
insert_after="address_line2",
)
],
"Company": [
dict(
fieldname="company_name_in_arabic",
label="Company Name In Arabic",
fieldtype="Data",
insert_after="company_name",
)
],
}
create_custom_fields(custom_fields, ignore_validate=True, update=True)
def update_regional_tax_settings(country, company):
create_ksa_vat_setting(company)

View File

@ -1,169 +0,0 @@
import io
import os
from base64 import b64encode
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create
from erpnext import get_region
def create_qr_code(doc, method=None):
region = get_region(doc.company)
if region not in ["Saudi Arabia"]:
return
# if QR Code field not present, create it. Invoices without QR are invalid as per law.
if not hasattr(doc, "ksa_einv_qr"):
create_custom_fields(
{
doc.doctype: [
dict(
fieldname="ksa_einv_qr",
label="KSA E-Invoicing QR",
fieldtype="Attach Image",
read_only=1,
no_copy=1,
hidden=1,
)
]
}
)
# Don't create QR Code if it already exists
qr_code = doc.get("ksa_einv_qr")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return
meta = frappe.get_meta(doc.doctype)
if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
"""TLV conversion for
1. Seller's Name
2. VAT Number
3. Time Stamp
4. Invoice Amount
5. VAT Amount
"""
tlv_array = []
# Sellers Name
seller_name = frappe.db.get_value("Company", doc.company, "company_name_in_arabic")
if not seller_name:
frappe.throw(_("Arabic name missing for {} in the company document").format(doc.company))
tag = bytes([1]).hex()
length = bytes([len(seller_name.encode("utf-8"))]).hex()
value = seller_name.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# VAT Number
tax_id = frappe.db.get_value("Company", doc.company, "tax_id")
if not tax_id:
frappe.throw(_("Tax ID missing for {} in the company document").format(doc.company))
tag = bytes([2]).hex()
length = bytes([len(tax_id)]).hex()
value = tax_id.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# Time Stamp
posting_date = getdate(doc.posting_date)
time = get_time(doc.posting_time)
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
time_stamp = add_to_date(posting_date, seconds=seconds)
time_stamp = time_stamp.strftime("%Y-%m-%dT%H:%M:%SZ")
tag = bytes([3]).hex()
length = bytes([len(time_stamp)]).hex()
value = time_stamp.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# Invoice Amount
invoice_amount = str(doc.base_grand_total)
tag = bytes([4]).hex()
length = bytes([len(invoice_amount)]).hex()
value = invoice_amount.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# VAT Amount
vat_amount = str(get_vat_amount(doc))
tag = bytes([5]).hex()
length = bytes([len(vat_amount)]).hex()
value = vat_amount.encode("utf-8").hex()
tlv_array.append("".join([tag, length, value]))
# Joining bytes into one
tlv_buff = "".join(tlv_array)
# base64 conversion for QR Code
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
qr_image = io.BytesIO()
url = qr_create(base64_string, error="L")
url.png(qr_image, scale=2, quiet_zone=1)
name = frappe.generate_hash(doc.name, 5)
# making file
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc(
{
"doctype": "File",
"file_name": filename,
"is_private": 0,
"content": qr_image.getvalue(),
"attached_to_doctype": doc.get("doctype"),
"attached_to_name": doc.get("name"),
"attached_to_field": "ksa_einv_qr",
}
)
_file.save()
# assigning to document
doc.db_set("ksa_einv_qr", _file.file_url)
doc.notify_update()
def get_vat_amount(doc):
vat_settings = frappe.db.get_value("KSA VAT Setting", {"company": doc.company})
vat_accounts = []
vat_amount = 0
if vat_settings:
vat_settings_doc = frappe.get_cached_doc("KSA VAT Setting", vat_settings)
for row in vat_settings_doc.get("ksa_vat_sales_accounts"):
vat_accounts.append(row.account)
for tax in doc.get("taxes"):
if tax.account_head in vat_accounts:
vat_amount += tax.base_tax_amount
return vat_amount
def delete_qr_code_file(doc, method=None):
region = get_region(doc.company)
if region not in ["Saudi Arabia"]:
return
if hasattr(doc, "ksa_einv_qr"):
if doc.get("ksa_einv_qr"):
file_doc = frappe.get_list("File", {"file_url": doc.get("ksa_einv_qr")})
if len(file_doc):
frappe.delete_doc("File", file_doc[0].name)
def delete_vat_settings_for_company(doc, method=None):
if doc.country != "Saudi Arabia":
return
if frappe.db.exists("KSA VAT Setting", doc.name):
frappe.delete_doc("KSA VAT Setting", doc.name)

View File

@ -1,47 +0,0 @@
[
{
"type": "Sales Account",
"accounts": [
{
"title": "Standard rated Sales",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Zero rated domestic sales",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted sales",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Exempted"
}
]
},
{
"type": "Purchase Account",
"accounts": [
{
"title": "Standard rated domestic purchases",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Imports subject to VAT paid at customs",
"item_tax_template": "KSA Excise 50%",
"account": "Excise 50%"
},
{
"title": "Zero rated purchases",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted purchases",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Exempted"
}
]
}
]

View File

@ -1,46 +0,0 @@
import json
import os
import frappe
def create_ksa_vat_setting(company):
"""On creation of first company. Creates KSA VAT Setting"""
company = frappe.get_doc("Company", company)
file_path = os.path.join(os.path.dirname(__file__), "..", "data", "ksa_vat_settings.json")
with open(file_path, "r") as json_file:
account_data = json.load(json_file)
# Creating KSA VAT Setting
ksa_vat_setting = frappe.get_doc({"doctype": "KSA VAT Setting", "company": company.name})
for data in account_data:
if data["type"] == "Sales Account":
for row in data["accounts"]:
item_tax_template = row["item_tax_template"]
account = row["account"]
ksa_vat_setting.append(
"ksa_vat_sales_accounts",
{
"title": row["title"],
"item_tax_template": f"{item_tax_template} - {company.abbr}",
"account": f"{account} - {company.abbr}",
},
)
elif data["type"] == "Purchase Account":
for row in data["accounts"]:
item_tax_template = row["item_tax_template"]
account = row["account"]
ksa_vat_setting.append(
"ksa_vat_purchase_accounts",
{
"title": row["title"],
"item_tax_template": f"{item_tax_template} - {company.abbr}",
"account": f"{account} - {company.abbr}",
},
)
ksa_vat_setting.save()

View File

@ -309,9 +309,12 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_work_order() {
var me = this;
this.frm.call({
doc: this.frm.doc,
method: 'get_work_order_items',
me.frm.call({
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
sales_order: this.frm.docname,
},
freeze: true,
callback: function(r) {
if(!r.message) {
frappe.msgprint({
@ -321,14 +324,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
});
return;
}
else if(!r.message) {
frappe.msgprint({
title: __('Work Order not created'),
message: __('Work Order already created for all items with BOM'),
indicator: 'orange'
});
return;
} else {
else {
const fields = [{
label: 'Items',
fieldtype: 'Table',
@ -429,9 +425,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
make_raw_material_request() {
var me = this;
this.frm.call({
doc: this.frm.doc,
method: 'get_work_order_items',
method: "erpnext.selling.doctype.sales_order.sales_order.get_work_order_items",
args: {
sales_order: this.frm.docname,
for_raw_material_request: 1
},
callback: function(r) {
@ -450,6 +446,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
}
make_raw_material_request_dialog(r) {
var me = this;
var fields = [
{fieldtype:'Check', fieldname:'include_exploded_items',
label: __('Include Exploded Items')},

View File

@ -6,11 +6,12 @@ import json
import frappe
import frappe.utils
from frappe import _
from frappe import _, qb
from frappe.contacts.doctype.address.address import get_company_address
from frappe.desk.notifications import clear_doctype_notifications
from frappe.model.mapper import get_mapped_doc
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import Sum
from frappe.utils import add_days, cint, cstr, flt, get_link_to_form, getdate, nowdate, strip_html
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
@ -414,51 +415,6 @@ class SalesOrder(SellingController):
self.indicator_color = "green"
self.indicator_title = _("Paid")
@frappe.whitelist()
def get_work_order_items(self, for_raw_material_request=0):
"""Returns items with BOM that already do not have a linked work order"""
items = []
item_codes = [i.item_code for i in self.items]
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
)
]
for table in [self.items, self.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request:
total_work_order_qty = flt(
frappe.db.sql(
"""select sum(qty) from `tabWork Order`
where production_item=%s and sales_order=%s and sales_order_item = %s and docstatus<2""",
(i.item_code, self.name, i.name),
)[0][0]
)
pending_qty = stock_qty - total_work_order_qty
else:
pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom=bom or "",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
)
return items
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
@ -1350,3 +1306,57 @@ def update_produced_qty_in_so_item(sales_order, sales_order_item):
return
frappe.db.set_value("Sales Order Item", sales_order_item, "produced_qty", total_produced_qty)
@frappe.whitelist()
def get_work_order_items(sales_order, for_raw_material_request=0):
"""Returns items with BOM that already do not have a linked work order"""
if sales_order:
so = frappe.get_doc("Sales Order", sales_order)
wo = qb.DocType("Work Order")
items = []
item_codes = [i.item_code for i in so.items]
product_bundle_parents = [
pb.new_item_code
for pb in frappe.get_all(
"Product Bundle", {"new_item_code": ["in", item_codes]}, ["new_item_code"]
)
]
for table in [so.items, so.packed_items]:
for i in table:
bom = get_default_bom(i.item_code)
stock_qty = i.qty if i.doctype == "Packed Item" else i.stock_qty
if not for_raw_material_request:
total_work_order_qty = flt(
qb.from_(wo)
.select(Sum(wo.qty))
.where(
(wo.production_item == i.item_code)
& (wo.sales_order == so.name) * (wo.sales_order_item == i.name)
& (wo.docstatus.lte(2))
)
.run()[0][0]
)
pending_qty = stock_qty - total_work_order_qty
else:
pending_qty = stock_qty
if pending_qty and i.item_code not in product_bundle_parents:
items.append(
dict(
name=i.name,
item_code=i.item_code,
description=i.description,
bom=bom or "",
warehouse=i.warehouse,
pending_qty=pending_qty,
required_qty=pending_qty if for_raw_material_request else 0,
sales_order_item=i.name,
)
)
return items

View File

@ -1217,6 +1217,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertTrue(si.get("payment_schedule"))
def test_make_work_order(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
# Make a new Sales Order
so = make_sales_order(
**{
@ -1230,7 +1232,7 @@ class TestSalesOrder(FrappeTestCase):
# Raise Work Orders
po_items = []
so_item_name = {}
for item in so.get_work_order_items():
for item in get_work_order_items(so.name):
po_items.append(
{
"warehouse": item.get("warehouse"),
@ -1448,6 +1450,7 @@ class TestSalesOrder(FrappeTestCase):
from erpnext.controllers.item_variant import create_variant
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
make_item( # template item
"Test-WO-Tshirt",
@ -1487,7 +1490,7 @@ class TestSalesOrder(FrappeTestCase):
]
}
)
wo_items = so.get_work_order_items()
wo_items = get_work_order_items(so.name)
self.assertEqual(wo_items[0].get("item_code"), "Test-WO-Tshirt-R")
self.assertEqual(wo_items[0].get("bom"), red_var_bom.name)
@ -1497,6 +1500,8 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(wo_items[1].get("bom"), template_bom.name)
def test_request_for_raw_materials(self):
from erpnext.selling.doctype.sales_order.sales_order import get_work_order_items
item = make_item(
"_Test Finished Item",
{
@ -1529,7 +1534,7 @@ class TestSalesOrder(FrappeTestCase):
so = make_sales_order(**{"item_list": [{"item_code": item.item_code, "qty": 1, "rate": 1000}]})
so.submit()
mr_dict = frappe._dict()
items = so.get_work_order_items(1)
items = get_work_order_items(so.name, 1)
mr_dict["items"] = items
mr_dict["include_exploded_items"] = 0
mr_dict["ignore_existing_ordered_qty"] = 1

View File

@ -216,7 +216,7 @@ def get_sales_order_details(company_list, filters):
)
if filters.get("item_group"):
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_group))
query = query.where(db_so_item.item_group == filters.item_group)
if filters.get("from_date"):
query = query.where(db_so.transaction_date >= filters.from_date)
@ -225,7 +225,7 @@ def get_sales_order_details(company_list, filters):
query = query.where(db_so.transaction_date <= filters.to_date)
if filters.get("item_code"):
query = query.where(db_so_item.item_group == frappe.db.escape(filters.item_code))
query = query.where(db_so_item.item_code == filters.item_code)
if filters.get("customer"):
query = query.where(db_so.customer == filters.customer)

View File

@ -418,8 +418,6 @@ erpnext.selling.SellingController = class SellingController extends erpnext.Tran
callback: function(r) {
if(r.message) {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
} else {
frappe.model.set_value(doc.doctype, doc.name, 'batch_no', r.message);
}
}
});

View File

@ -8,7 +8,6 @@ from frappe.permissions import (
get_doc_permissions,
has_permission,
remove_user_permission,
set_user_permission_if_allowed,
)
from frappe.utils import cstr, getdate, today, validate_email_address
from frappe.utils.nestedset import NestedSet
@ -96,7 +95,7 @@ class Employee(NestedSet):
return
add_user_permission("Employee", self.name, self.user_id)
set_user_permission_if_allowed("Company", self.company, self.user_id)
add_user_permission("Company", self.company, self.user_id)
def update_user(self):
# add employee role if missing

View File

@ -3,13 +3,17 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.desk.notifications import clear_notifications
from frappe.model.document import Document
from frappe.utils import cint
from frappe.utils import cint, create_batch
class TransactionDeletionRecord(Document):
def __init__(self, *args, **kwargs):
super(TransactionDeletionRecord, self).__init__(*args, **kwargs)
self.batch_size = 5000
def validate(self):
frappe.only_for("System Manager")
self.validate_doctypes_to_be_ignored()
@ -155,8 +159,9 @@ class TransactionDeletionRecord(Document):
"DocField", filters={"fieldtype": "Table", "parent": doctype}, pluck="options"
)
for table in child_tables:
frappe.db.delete(table, {"parent": ["in", parent_docs_to_be_deleted]})
for batch in create_batch(parent_docs_to_be_deleted, self.batch_size):
for table in child_tables:
frappe.db.delete(table, {"parent": ["in", batch]})
def delete_docs_linked_with_specified_company(self, doctype, company_fieldname):
frappe.db.delete(doctype, {company_fieldname: self.company})
@ -181,13 +186,16 @@ class TransactionDeletionRecord(Document):
frappe.db.sql("""update `tabSeries` set current = %s where name=%s""", (last, prefix))
def delete_version_log(self, doctype, company_fieldname):
frappe.db.sql(
"""delete from `tabVersion` where ref_doctype=%s and docname in
(select name from `tab{0}` where `{1}`=%s)""".format(
doctype, company_fieldname
),
(doctype, self.company),
)
dt = qb.DocType(doctype)
names = qb.from_(dt).select(dt.name).where(dt[company_fieldname] == self.company).run(as_list=1)
names = [x[0] for x in names]
if names:
versions = qb.DocType("Version")
for batch in create_batch(names, self.batch_size):
qb.from_(versions).delete().where(
(versions.ref_doctype == doctype) & (versions.docname.isin(batch))
).run()
def delete_communications(self, doctype, company_fieldname):
reference_docs = frappe.get_all(doctype, filters={company_fieldname: self.company})
@ -199,7 +207,8 @@ class TransactionDeletionRecord(Document):
)
communication_names = [c.name for c in communications]
frappe.delete_doc("Communication", communication_names, ignore_permissions=True)
for batch in create_batch(communication_names, self.batch_size):
frappe.delete_doc("Communication", batch, ignore_permissions=True)
@frappe.whitelist()

View File

@ -4015,34 +4015,6 @@
"tax_rate": 18.00
}
},
"Saudi Arabia": {
"KSA VAT 15%": {
"account_name": "VAT 15%",
"tax_rate": 15.00
},
"KSA VAT 5%": {
"account_name": "VAT 5%",
"tax_rate": 5.00
},
"KSA VAT Zero": {
"account_name": "VAT Zero",
"tax_rate": 0.00
},
"KSA VAT Exempted": {
"account_name": "VAT Exempted",
"tax_rate": 0.00
},
"KSA Excise 50%": {
"account_name": "Excise 50%",
"tax_rate": 50.00
},
"KSA Excise 100%": {
"account_name": "Excise 100%",
"tax_rate": 100.00
}
},
"Serbia": {
"Serbia Tax": {
"account_name": "VAT",

View File

@ -97,12 +97,12 @@ frappe.ui.form.on("Delivery Note", {
}
if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) {
let internal = me.frm.doc.is_internal_customer;
let internal = frm.doc.is_internal_customer;
if (internal) {
let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" :
let button_label = (frm.doc.company === frm.doc.represents_company) ? "Internal Purchase Receipt" :
"Inter Company Purchase Receipt";
me.frm.add_custom_button(button_label, function() {
frm.add_custom_button(__(button_label), function() {
frappe.model.open_mapped_doc({
method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt',
frm: frm,

View File

@ -33,6 +33,9 @@ frappe.ui.form.on("Item", {
'Material Request': () => {
open_form(frm, "Material Request", "Material Request Item", "items");
},
'Stock Entry': () => {
open_form(frm, "Stock Entry", "Stock Entry Detail", "items");
},
};
},
@ -893,6 +896,9 @@ function open_form(frm, doctype, child_doctype, parentfield) {
new_child_doc.item_name = frm.doc.item_name;
new_child_doc.uom = frm.doc.stock_uom;
new_child_doc.description = frm.doc.description;
if (!new_child_doc.qty) {
new_child_doc.qty = 1.0;
}
frappe.run_serially([
() => frappe.ui.form.make_quick_entry(doctype, null, null, new_doc),

View File

@ -2,7 +2,18 @@
// License: GNU General Public License v3. See license.txt
frappe.ui.form.on("Item Price", {
onload: function (frm) {
setup(frm) {
frm.set_query("item_code", function() {
return {
filters: {
"disabled": 0,
"has_variants": 0
}
};
});
},
onload(frm) {
// Fetch price list details
frm.add_fetch("price_list", "buying", "buying");
frm.add_fetch("price_list", "selling", "selling");

View File

@ -3,7 +3,7 @@
import frappe
from frappe import _
from frappe import _, bold
from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Cast_
@ -21,6 +21,7 @@ class ItemPrice(Document):
self.update_price_list_details()
self.update_item_details()
self.check_duplicates()
self.validate_item_template()
def validate_item(self):
if not frappe.db.exists("Item", self.item_code):
@ -49,6 +50,12 @@ class ItemPrice(Document):
"Item", self.item_code, ["item_name", "description"]
)
def validate_item_template(self):
if frappe.get_cached_value("Item", self.item_code, "has_variants"):
msg = f"Item Price cannot be created for the template item {bold(self.item_code)}"
frappe.throw(_(msg))
def check_duplicates(self):
item_price = frappe.qb.DocType("Item Price")

View File

@ -16,6 +16,28 @@ class TestItemPrice(FrappeTestCase):
frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True)
def test_template_item_price(self):
from erpnext.stock.doctype.item.test_item import make_item
item = make_item(
"Test Template Item 1",
{
"has_variants": 1,
"variant_based_on": "Manufacturer",
},
)
doc = frappe.get_doc(
{
"doctype": "Item Price",
"price_list": "_Test Price List",
"item_code": item.name,
"price_list_rate": 100,
}
)
self.assertRaises(frappe.ValidationError, doc.save)
def test_duplicate_item(self):
doc = frappe.copy_doc(test_records[0])
self.assertRaises(ItemPriceDuplicateItem, doc.save)

View File

@ -55,7 +55,6 @@ class LandedCostVoucher(Document):
self.get_items_from_purchase_receipts()
self.set_applicable_charges_on_item()
self.validate_applicable_charges_for_item()
def check_mandatory(self):
if not self.get("purchase_receipts"):
@ -115,6 +114,13 @@ class LandedCostVoucher(Document):
total_item_cost += item.get(based_on_field)
for item in self.get("items"):
if not total_item_cost and not item.get(based_on_field):
frappe.throw(
_(
"It's not possible to distribute charges equally when total amount is zero, please set 'Distribute Charges Based On' as 'Quantity'"
)
)
item.applicable_charges = flt(
flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
item.precision("applicable_charges"),
@ -162,6 +168,7 @@ class LandedCostVoucher(Document):
)
def on_submit(self):
self.validate_applicable_charges_for_item()
self.update_landed_cost()
def on_cancel(self):

View File

@ -175,6 +175,59 @@ class TestLandedCostVoucher(FrappeTestCase):
)
self.assertEqual(last_sle_after_landed_cost.stock_value - last_sle.stock_value, 50.0)
def test_landed_cost_voucher_for_zero_purchase_rate(self):
"Test impact of LCV on future stock balances."
from erpnext.stock.doctype.item.test_item import make_item
item = make_item("LCV Stock Item", {"is_stock_item": 1})
warehouse = "Stores - _TC"
pr = make_purchase_receipt(
item_code=item.name,
warehouse=warehouse,
qty=10,
rate=0,
posting_date=add_days(frappe.utils.nowdate(), -2),
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
0,
)
lcv = make_landed_cost_voucher(
company=pr.company,
receipt_document_type="Purchase Receipt",
receipt_document=pr.name,
charges=100,
distribute_charges_based_on="Distribute Manually",
do_not_save=True,
)
lcv.get_items_from_purchase_receipts()
lcv.items[0].applicable_charges = 100
lcv.save()
lcv.submit()
self.assertTrue(
frappe.db.exists(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
)
)
self.assertEqual(
frappe.db.get_value(
"Stock Ledger Entry",
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name, "is_cancelled": 0},
"stock_value_difference",
),
100,
)
def test_landed_cost_voucher_against_purchase_invoice(self):
pi = make_purchase_invoice(
@ -516,7 +569,7 @@ def make_landed_cost_voucher(**args):
lcv = frappe.new_doc("Landed Cost Voucher")
lcv.company = args.company or "_Test Company"
lcv.distribute_charges_based_on = "Amount"
lcv.distribute_charges_based_on = args.distribute_charges_based_on or "Amount"
lcv.set(
"purchase_receipts",

View File

@ -587,6 +587,9 @@ def make_stock_entry(source_name, target_doc=None):
def set_missing_values(source, target):
target.purpose = source.material_request_type
target.from_warehouse = source.set_from_warehouse
target.to_warehouse = source.set_warehouse
if source.job_card:
target.purpose = "Material Transfer for Manufacture"
@ -722,6 +725,7 @@ def create_pick_list(source_name, target_doc=None):
def make_in_transit_stock_entry(source_name, in_transit_warehouse):
ste_doc = make_stock_entry(source_name)
ste_doc.add_to_transit = 1
ste_doc.to_warehouse = in_transit_warehouse
for row in ste_doc.items:
row.t_warehouse = in_transit_warehouse

View File

@ -473,7 +473,7 @@ class PurchaseReceipt(BuyingController):
)
divisional_loss = flt(
valuation_amount_as_per_doc - stock_value_diff, d.precision("base_net_amount")
valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
)
if divisional_loss:
@ -887,18 +887,10 @@ def update_billing_percentage(pr_doc, update_modified=True):
# Update Billing % based on pending accepted qty
total_amount, total_billed_amount = 0, 0
for item in pr_doc.items:
return_data = frappe.get_all(
"Purchase Receipt",
fields=["sum(abs(`tabPurchase Receipt Item`.qty)) as qty"],
filters=[
["Purchase Receipt", "docstatus", "=", 1],
["Purchase Receipt", "is_return", "=", 1],
["Purchase Receipt Item", "purchase_receipt_item", "=", item.name],
],
)
item_wise_returned_qty = get_item_wise_returned_qty(pr_doc)
returned_qty = return_data[0].qty if return_data else 0
for item in pr_doc.items:
returned_qty = flt(item_wise_returned_qty.get(item.name))
returned_amount = flt(returned_qty) * flt(item.rate)
pending_amount = flt(item.amount) - returned_amount
total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
@ -915,6 +907,27 @@ def update_billing_percentage(pr_doc, update_modified=True):
pr_doc.notify_update()
def get_item_wise_returned_qty(pr_doc):
items = [d.name for d in pr_doc.items]
return frappe._dict(
frappe.get_all(
"Purchase Receipt",
fields=[
"`tabPurchase Receipt Item`.purchase_receipt_item",
"sum(abs(`tabPurchase Receipt Item`.qty)) as qty",
],
filters=[
["Purchase Receipt", "docstatus", "=", 1],
["Purchase Receipt", "is_return", "=", 1],
["Purchase Receipt Item", "purchase_receipt_item", "in", items],
],
group_by="`tabPurchase Receipt Item`.purchase_receipt_item",
as_list=1,
)
)
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
from erpnext.accounts.party import get_payment_terms_template
@ -1121,13 +1134,25 @@ def get_item_account_wise_additional_cost(purchase_document):
account.expense_account, {"amount": 0.0, "base_amount": 0.0}
)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
"amount"
] += (account.amount * item.get(based_on_field) / total_item_cost)
if total_item_cost > 0:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["amount"] += (
account.amount * item.get(based_on_field) / total_item_cost
)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account][
"base_amount"
] += (account.base_amount * item.get(based_on_field) / total_item_cost)
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += (
account.base_amount * item.get(based_on_field) / total_item_cost
)
else:
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["amount"] += item.applicable_charges
item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][
account.expense_account
]["base_amount"] += item.applicable_charges
return item_account_wise_cost

View File

@ -859,7 +859,8 @@
"label": "Purchase Receipt Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"collapsible": 1,
@ -974,7 +975,8 @@
"label": "Purchase Invoice Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"fieldname": "product_bundle",
@ -1010,7 +1012,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-11-02 12:49:28.746701",
"modified": "2023-01-18 15:48:58.114923",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@ -306,7 +306,7 @@ def get_stock_ledger_entries(filters, items):
query = query.where(sle.item_code.isin(items))
for field in ["voucher_no", "batch_no", "project", "company"]:
if filters.get(field):
if filters.get(field) and field not in inventory_dimension_fields:
query = query.where(sle[field] == filters.get(field))
query = apply_warehouse_filter(query, sle, filters)

View File

@ -9916,3 +9916,5 @@ Cost and Freight,Kosten und Fracht,
Delivered at Place,Geliefert benannter Ort,
Delivered at Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt,
Discount Validity,Frist für den Rabatt,
Discount Validity Based On,Frist für den Rabatt berechnet sich nach,

Can't render this file because it is too large.